Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/app/layouts/AppLayout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,20 @@ export function Sidebar() {
const isDark = theme === "dark";

useEffect(() => {
if (!projects.length) return;
if (isLoading) return;
if (!projects.length) {
if (selectedProject) {
setSelectedProject(null);
setUserRole(null);
}
return;
}
const selectedExists = selectedProject ? projects.some((project) => project.id === selectedProject.id) : false;
if (!selectedExists) {
setSelectedProject({ id: projects[0].id, name: projects[0].name });
setUserRole(getProjectUserRole(projects[0], user?.id));
}
}, [projects, selectedProject, setSelectedProject]);
}, [isLoading, projects, selectedProject, setSelectedProject, setUserRole, user?.id]);

const selectPlaceholder = isLoading
? "Loading projects..."
Expand Down
2 changes: 1 addition & 1 deletion src/features/dashboard/api/dashboardService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Proprietary and confidential. Unauthorized use is strictly prohibited.
// See LICENSE file in the project root for full license information.

import type { ProjectDashboardResponse } from "@coveritlabs/contracts";
import { apiClient } from "@shared/api/client";
import type { ProjectDashboardResponse } from "../model/types/dashboard.types";

export const dashboardService = {
async getProjectDashboard(projectId: string, versionId?: string): Promise<ProjectDashboardResponse> {
Expand Down
10 changes: 10 additions & 0 deletions src/features/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@

export { dashboardService } from "./api/dashboardService";
export { useProjectDashboard } from "./model/queries/useProjectDashboard";
export type {
ProjectActivity,
ProjectCoverageSummary,
ProjectDashboardResponse,
ProjectDashboardVersionRef,
ProjectLatestCrawlSession,
ProjectLatestRun,
ProjectLatestTestFlow,
ProjectRunStatistics,
} from "./model/types/dashboard.types";
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// See LICENSE file in the project root for full license information.

import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { ProjectDashboardResponse } from "@coveritlabs/contracts";
import { queryKeys } from "@shared/config/queryKeys";
import { dashboardService } from "../../api/dashboardService";
import type { ProjectDashboardResponse } from "../types/dashboard.types";

export function useProjectDashboard(projectId: string | null, versionId?: string) {
const queryClient = useQueryClient();
Expand Down
94 changes: 94 additions & 0 deletions src/features/dashboard/model/types/dashboard.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2026 CoverIt Labs. All Rights Reserved.
// Proprietary and confidential. Unauthorized use is strictly prohibited.
// See LICENSE file in the project root for full license information.

export interface ProjectDashboardVersionRef {
id: string;
version: string;
applicationId: string;
applicationName: string;
}

export interface ProjectCoverageSummary {
percentage: number;
coveredTransitions: number;
totalTransitions: number;
crawlSessionId?: string;
calculatedAt?: string;
}

export interface ProjectRunStatistics {
passedCount: number;
warningCount: number;
failedCount: number;
reportedWarningCount: number;
reportedFailedCount: number;
totalRuns: number;
}

export interface ProjectLatestRun {
id: string;
runId: string;
displayName: string;
status: string;
applicationId: string;
applicationName: string;
versionId?: string;
version?: string;
passedCount: number;
warningCount: number;
failedCount: number;
createdAt: string;
}

export interface ProjectLatestCrawlSession {
id: string;
status: string;
triggerType: string;
applicationId: string;
applicationName: string;
versionId: string;
version: string;
stateCount: number;
transitionCount: number;
createdAt: string;
startedAt?: string;
finishedAt?: string;
}

export interface ProjectLatestTestFlow {
id: string;
crawlSessionId: string;
applicationId: string;
applicationName: string;
versionId: string;
version: string;
checkpointStateHash: string;
checkpointUrl: string;
isClipped: boolean;
stepCount: number;
createdAt: string;
}

export interface ProjectActivity {
id: string;
projectId: string;
eventType: string;
entityType: string;
entityId?: string;
message: string;
actorUserId?: string;
actorName?: string;
actorEmail?: string;
createdAt: string;
}

export interface ProjectDashboardResponse {
selectedVersion?: ProjectDashboardVersionRef;
coverage: ProjectCoverageSummary;
runStatistics: ProjectRunStatistics;
latestRuns: ProjectLatestRun[];
latestCrawlSessions: ProjectLatestCrawlSession[];
latestTestFlows: ProjectLatestTestFlow[];
recentActivities: ProjectActivity[];
}
18 changes: 18 additions & 0 deletions src/features/regression-runs/ui/RegressionRuns.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,24 @@
white-space: nowrap;
}

.refreshButton {
min-height: 1.9rem;
}

.spinIcon {
animation: regression-runs-spin 1s linear infinite;
}

@keyframes regression-runs-spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

.runTimestamp {
color: var(--text-secondary);
font-size: 0.84rem;
Expand Down
31 changes: 31 additions & 0 deletions src/features/regression-runs/ui/RegressionRuns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function RegressionRuns() {
data: applications = [],
isLoading: applicationsLoading,
isError: applicationsError,
isFetching: applicationsFetching,
refetch: refetchApplications,
} = useTargetApplications(selectedProject?.id ?? null);
const [searchParams, setSearchParams] = useSearchParams();
const [searchText, setSearchText] = useState("");
Expand Down Expand Up @@ -318,6 +320,33 @@ function RegressionRuns() {
);

const latestRunAt = runs[0]?.createdAt;
const isRefreshing =
applicationsFetching ||
runsQuery.isFetching ||
runDetailsQuery.isFetching ||
scenariosQuery.isFetching ||
scenarioQuery.isFetching ||
runArtifactsQuery.isFetching ||
scenarioEventsQuery.isFetching ||
scenarioArtifactsQuery.isFetching ||
jiraStatusQuery.isFetching;

const handleRefresh = () => {
const refreshes: Array<Promise<unknown>> = [refetchApplications(), runsQuery.refetch(), jiraStatusQuery.refetch()];

if (runId) {
refreshes.push(runDetailsQuery.refetch(), scenariosQuery.refetch());
if (runTab === "artifacts") refreshes.push(runArtifactsQuery.refetch());
}

if (runId && scenarioId && runTab === "scenarios") {
refreshes.push(scenarioQuery.refetch());
if (scenarioTab === "events") refreshes.push(scenarioEventsQuery.refetch());
if (scenarioTab === "artifacts") refreshes.push(scenarioArtifactsQuery.refetch());
}

void Promise.all(refreshes);
};

const clearFilters = () => {
setSearchText("");
Expand Down Expand Up @@ -368,6 +397,8 @@ function RegressionRuns() {
applicationName={activeApplication?.name ?? null}
latestRunAt={latestRunAt}
runCount={filteredRuns.length}
isRefreshing={isRefreshing}
onRefresh={handleRefresh}
/>

<div className={styles.topLevelTabs}>
Expand Down
19 changes: 18 additions & 1 deletion src/features/regression-runs/ui/components/filters/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
// Proprietary and confidential. Unauthorized use is strictly prohibited.
// See LICENSE file in the project root for full license information.

import { AppWindow, Clock3 } from "lucide-react";
import { AppWindow, Clock3, RefreshCw } from "lucide-react";
import { Button } from "@shared/ui";
import { cn } from "@shared/utils/cn";
import styles from "../../RegressionRuns.module.scss";
import { formatDateTime } from "../../../lib/formatters";

export function RegressionRunsHeader({
applicationName,
latestRunAt,
runCount,
isRefreshing,
onRefresh,
}: {
applicationName: string | null;
latestRunAt?: string;
runCount: number;
isRefreshing: boolean;
onRefresh: () => void;
}) {
return (
<div className={styles.header}>
Expand All @@ -26,6 +32,17 @@ export function RegressionRunsHeader({
</div>

<div className={styles.headerMeta}>
<Button
size="sm"
variant="ghost"
className={styles.refreshButton}
onClick={onRefresh}
disabled={isRefreshing}
aria-label="Refresh regression runs"
title="Refresh"
>
<RefreshCw size={14} className={cn(isRefreshing && styles.spinIcon)} />
</Button>
{applicationName && (
<div className={styles.headerMetaItem}>
<AppWindow size={16} />
Expand Down
17 changes: 15 additions & 2 deletions src/features/target-applications/api/applicationDetailsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ function getStats(sessions: CrawlSession[]): ApplicationDetailStats {
const statesDiscovered = crawledSessions.reduce((total, session) => total + (session.statesDiscovered ?? 0), 0);

return {
versionCount: 0,
crawledCount: crawledSessions.length,
statesDiscovered: statesDiscovered,
lastCrawlDate: lastSession?.startedAt.slice(0, 10) ?? "—",
Expand Down Expand Up @@ -420,6 +421,18 @@ export const applicationDetailsService = {
return response.data;
},

async deleteCrawlSession(params: {
projectId: string;
applicationId: string;
versionId: string;
sessionId: string;
}): Promise<MessageResponse> {
const response = await apiClient.delete<MessageResponse>(
`projects/${params.projectId}/target-applications/${params.applicationId}/versions/${params.versionId}/crawl-sessions/${params.sessionId}`,
);
return response.data;
},

async connectManualSession(params: {
projectId: string;
applicationId: string;
Expand Down Expand Up @@ -455,11 +468,11 @@ export const applicationDetailsService = {
applicationId: string;
config: RegressionCodebaseConfig;
}): Promise<RegressionCodebaseConfig> {
const body = {
const body: { frameworkName: string; repositoryUrl: string; apiKey?: string } = {
frameworkName: "Playwright",
repositoryUrl: params.config.repositoryUrl,
apiKey: params.config.apiKey,
};
if (params.config.apiKey) body.apiKey = params.config.apiKey;

if (params.config.id) {
await apiClient.put<MessageResponse>(
Expand Down
1 change: 1 addition & 0 deletions src/features/target-applications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
useCreateCrawlSession,
useConnectManualSession,
useDeleteCrawlSchedule,
useDeleteCrawlSession,
useReattachManualSession,
useSaveCrawlSchedule,
useSaveRegressionCodebaseConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function invalidateApplicationDetails(
queryClient.invalidateQueries({
queryKey: queryKeys.targetApplications.crawlSessions(projectId, applicationId, versionId),
});
queryClient.invalidateQueries({
queryKey: queryKeys.dashboard.all,
});
}
}

Expand Down Expand Up @@ -114,6 +117,34 @@ export function useStartCrawlSession() {
});
}

export function useDeleteCrawlSession() {
const queryClient = useQueryClient();

return useMutation<
unknown,
Error,
{ projectId: string; applicationId: string; versionId: string; sessionId: string }
>({
mutationFn: ({ projectId, applicationId, versionId, sessionId }) =>
applicationDetailsService.deleteCrawlSession({ projectId, applicationId, versionId, sessionId }),
onSuccess: (_data, variables) => {
toast.success("Crawl session deleted");
invalidateApplicationDetails(queryClient, variables.projectId, variables.applicationId, variables.versionId);
queryClient.removeQueries({
queryKey: queryKeys.targetApplications.crawlSession(
variables.projectId,
variables.applicationId,
variables.versionId,
variables.sessionId,
),
});
},
onError: (error) => {
toast.error("Failed to delete crawl session", error.message);
},
});
}

export function useConnectManualSession() {
return useMutation<
ManualSessionConnectResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ApplicationDetailTab = "crawl-sessions" | "schedules";
export type ScheduleFrequency = "daily" | "weekly" | "monthly";

export interface ApplicationDetailStats {
versionCount: number;
crawledCount: number;
statesDiscovered: number;
lastCrawlDate: string;
Expand Down
Loading
Loading