Skip to content
Closed
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
59 changes: 59 additions & 0 deletions src/components/Home/RunSection/RunBulkActionsBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useNavigate } from "@tanstack/react-router";

import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { InlineStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
import { APP_ROUTES } from "@/routes/appRoutes";
import { pluralize } from "@/utils/string";
import { tracking } from "@/utils/tracking";

interface RunBulkActionsBarProps {
selectedRuns: string[];
onClearSelection: () => void;
}

const RunBulkActionsBar = ({
selectedRuns,
onClearSelection,
}: RunBulkActionsBarProps) => {
const navigate = useNavigate();

const canCompare = selectedRuns.length === 2;

const handleCompare = () => {
if (!canCompare) return;
const [a, b] = selectedRuns;
navigate({ to: APP_ROUTES.COMPARE, search: { a, b } });
};

return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 transform rounded-lg border border-border bg-background p-4 shadow-lg z-50">
<InlineStack gap="4" blockAlign="center">
<Text size="sm" weight="semibold">
{selectedRuns.length} {pluralize(selectedRuns.length, "run")} selected
</Text>

<InlineStack gap="2" blockAlign="center">
<Button
variant="default"
size="sm"
onClick={handleCompare}
disabled={!canCompare}
title={canCompare ? undefined : "Select exactly 2 runs to compare"}
{...tracking("compare_runs.dashboard.compare_selected")}
>
<Icon name="GitCompare" />
Compare
</Button>

<Button variant="ghost" size="sm" onClick={onClearSelection}>
<Icon name="X" />
</Button>
</InlineStack>
</InlineStack>
</div>
);
};

export default RunBulkActionsBar;
26 changes: 25 additions & 1 deletion src/components/Home/RunSection/RunRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RunSourceIcon } from "@/components/shared/RunSource";
import { StatusBar, StatusIcon } from "@/components/shared/Status";
import { TagList } from "@/components/shared/Tags/TagList";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { InlineStack } from "@/components/ui/layout";
import { TableCell, TableRow } from "@/components/ui/table";
import {
Expand All @@ -32,9 +33,18 @@ import { getOverallExecutionStatusFromStats } from "@/utils/executionStatus";
interface RunRowProps {
run: PipelineRunResponse;
onFilterByUser?: (createdBy: string) => void;
selectable?: boolean;
isSelected?: boolean;
onToggleSelected?: (runId: string) => void;
}

const RunRow = ({ run, onFilterByUser }: RunRowProps) => {
const RunRow = ({
run,
onFilterByUser,
selectable = false,
isSelected = false,
onToggleSelected,
}: RunRowProps) => {
const navigate = useNavigate();
const { backendUrl } = useBackend();

Expand Down Expand Up @@ -111,6 +121,20 @@ const RunRow = ({ run, onFilterByUser }: RunRowProps) => {
onClick={handleRowClick}
className="cursor-pointer text-muted-foreground text-xs h-10"
>
{selectable && (
<TableCell className="w-8">
<div
onClick={(e) => e.stopPropagation()}
className="flex items-center"
>
<Checkbox
checked={isSelected}
onCheckedChange={() => onToggleSelected?.(runId)}
aria-label={`Select run ${runId}`}
/>
</div>
</TableCell>
)}
<TableCell>
<InlineStack gap="2" blockAlign="center" wrap="nowrap">
<StatusIcon status={overallStatus} />
Expand Down
68 changes: 63 additions & 5 deletions src/components/Home/RunSection/RunSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ListPipelineJobsResponse } from "@/api/types.gen";
import { InfoBox } from "@/components/shared/InfoBox";
import { useFlagValue } from "@/components/shared/Settings/useFlags";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@/components/ui/icon";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
Expand All @@ -29,6 +30,7 @@ import {
parseFilterParam,
} from "@/utils/pipelineRunFilterUtils";

import RunBulkActionsBar from "./RunBulkActionsBar";
import RunRow from "./RunRow";

const PIPELINE_RUNS_QUERY_URL = "/api/pipeline_runs/";
Expand Down Expand Up @@ -58,9 +60,24 @@ export const RunSection = ({
const { pathname } = useLocation();
const search = useSearch({ strict: false }) as RunSectionSearch;
const isCreatedByMeDefault = useFlagValue("created-by-me-default");
const compareEnabled = useFlagValue("compare-runs");
const { setFilter } = useRunSearchParams();
const dataVersion = useRef(0);

const [selectedRuns, setSelectedRuns] = useState<Set<string>>(new Set());

const toggleRun = (runId: string) => {
setSelectedRuns((prev) => {
const next = new Set(prev);
if (next.has(runId)) {
next.delete(runId);
} else {
next.add(runId);
}
return next;
});
};

const onFilterByUser = forcedFilter
? undefined
: (createdBy: string) => setFilter("created_by", createdBy);
Expand Down Expand Up @@ -279,12 +296,42 @@ export const RunSection = ({
);
}

const pageRuns = maxItems
? data.pipeline_runs?.slice(0, maxItems)
: data.pipeline_runs;

const allPageRunsSelected =
!!pageRuns?.length &&
pageRuns.every((run) => selectedRuns.has(`${run.id}`));

const toggleSelectAll = () => {
setSelectedRuns((prev) => {
const next = new Set(prev);
const pageRunIds = pageRuns?.map((run) => `${run.id}`) ?? [];
if (allPageRunsSelected) {
pageRunIds.forEach((id) => next.delete(id));
} else {
pageRunIds.forEach((id) => next.add(id));
}
return next;
});
};

return (
<BlockStack gap="4">
{searchMarkup}
<Table>
<TableHeader>
<TableRow className="text-xs">
{compareEnabled && (
<TableHead className="w-8">
<Checkbox
checked={allPageRunsSelected}
onCheckedChange={toggleSelectAll}
aria-label="Select all runs on this page"
/>
</TableHead>
)}
<TableHead className="w-1/4">Name</TableHead>
<TableHead className="w-1/4">Status</TableHead>
<TableHead className="w-3/20">Date</TableHead>
Expand All @@ -293,15 +340,26 @@ export const RunSection = ({
</TableRow>
</TableHeader>
<TableBody>
{(maxItems
? data.pipeline_runs?.slice(0, maxItems)
: data.pipeline_runs
)?.map((run) => (
<RunRow key={run.id} run={run} onFilterByUser={onFilterByUser} />
{pageRuns?.map((run) => (
<RunRow
key={run.id}
run={run}
onFilterByUser={onFilterByUser}
selectable={compareEnabled}
isSelected={selectedRuns.has(`${run.id}`)}
onToggleSelected={toggleRun}
/>
))}
</TableBody>
</Table>

{compareEnabled && selectedRuns.size > 0 && (
<RunBulkActionsBar
selectedRuns={Array.from(selectedRuns)}
onClearSelection={() => setSelectedRuns(new Set())}
/>
)}

{(data.next_page_token || previousPageTokens.length > 0) && (
<InlineStack
align="space-between"
Expand Down
8 changes: 8 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,12 @@ export const ExistingFlags: ConfigFlags = {
default: false,
category: "beta",
},

["compare-runs"]: {
name: "Compare runs",
description:
"Select two runs to compare their pipeline structure and results side by side.",
default: false,
category: "beta",
},
};
26 changes: 1 addition & 25 deletions src/providers/ExecutionDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import type { BreadcrumbSegment } from "@/hooks/useSubgraphBreadcrumbs";
import { useSubgraphBreadcrumbs } from "@/hooks/useSubgraphBreadcrumbs";
import { useFetchPipelineRunMetadata } from "@/services/executionService";
import { getOverallExecutionStatusFromStats } from "@/utils/executionStatus";
import { buildTaskExecutionStatusMap } from "@/utils/executionStatus";

import { useComponentSpec } from "./ComponentSpecProvider";

Expand Down Expand Up @@ -51,30 +51,6 @@ const isAtRootLevel = (path: string[]) => path.length <= 1;

const buildPathKey = (path: string[]) => path.join(PATH_DELIMITER);

const buildTaskExecutionStatusMap = (
details?: GetExecutionInfoResponse,
state?: GetGraphExecutionStateResponse,
): Map<string, string> => {
const taskExecutionStatusMap = new Map<string, string>();

if (!details?.child_task_execution_ids) {
return taskExecutionStatusMap;
}

Object.entries(details.child_task_execution_ids).forEach(
([taskId, executionId]) => {
const statusStats = state?.child_execution_status_stats?.[executionId];
const aggregated = getOverallExecutionStatusFromStats(statusStats);

if (aggregated) {
taskExecutionStatusMap.set(taskId, aggregated);
}
},
);

return taskExecutionStatusMap;
};

const findExecutionIdAtPath = (
path: string[],
runId: string | null | undefined,
Expand Down
1 change: 1 addition & 0 deletions src/routes/appRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export const APP_ROUTES = {
PIPELINE_FOLDERS: "/pipeline-folders",
PLAYGROUND: "/playground",
ARTIFACT_PREVIEW: "/artifact/$artifactId",
COMPARE: "/compare",
} as const;
13 changes: 13 additions & 0 deletions src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { BetaFeaturesSettings } from "./Settings/sections/BetaFeaturesSettings";
import { PreferencesSettings } from "./Settings/sections/PreferencesSettings";
import { SecretsSettings } from "./Settings/sections/SecretsSettings";
import { SettingsLayout } from "./Settings/SettingsLayout";
import { CompareView } from "./v2/pages/CompareView/CompareView";
import { EditorV2 } from "./v2/pages/Editor/EditorV2";
import { PipelineFoldersPage } from "./v2/pages/PipelineFolders/PipelineFoldersPage";
import { RunViewV2 } from "./v2/pages/RunView/RunViewV2";
Expand Down Expand Up @@ -363,6 +364,17 @@ const runV2WithSubgraphRoute = createRoute({
},
});

const compareRoute = createRoute({
getParentRoute: () => mainLayout,
path: APP_ROUTES.COMPARE,
component: CompareView,
beforeLoad: () => {
if (!isFlagEnabled("compare-runs")) {
throw redirect({ to: APP_ROUTES.DASHBOARD_RUNS });
}
},
});

const pipelineFoldersRoute = createRoute({
getParentRoute: () => mainLayout,
path: APP_ROUTES.PIPELINE_FOLDERS,
Expand Down Expand Up @@ -403,6 +415,7 @@ const appRouteTree = mainLayout.addChildren([
editorV2PipelineRoute,
runV2Route,
runV2WithSubgraphRoute,
compareRoute,
pipelineFoldersRoute,
artifactPreviewRoute,
tourRoute,
Expand Down
Loading
Loading