From 5288e9ef77161cbdd402ba44805b45db22d44986 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Mon, 23 Feb 2026 13:12:11 -0800 Subject: [PATCH] feat: Submit Pipeline Tags on Runs --- src/components/Home/RunSection/RunRow.tsx | 19 +++++++ src/components/Home/RunSection/RunSection.tsx | 50 +++++++++++-------- src/components/PipelineRun/RunDetails.tsx | 14 +++++- src/components/PipelineRun/RunNotesEditor.tsx | 8 ++- src/components/shared/AnnouncementBanners.tsx | 3 +- .../Submitters/Oasis/OasisSubmitter.tsx | 26 +++++++++- src/components/shared/Tags/TagList.tsx | 9 ++++ src/services/pipelineRunService.ts | 22 ++++---- src/utils/annotations.ts | 16 ++++++ 9 files changed, 129 insertions(+), 38 deletions(-) diff --git a/src/components/Home/RunSection/RunRow.tsx b/src/components/Home/RunSection/RunRow.tsx index 291479ccd..ff6fd1297 100644 --- a/src/components/Home/RunSection/RunRow.tsx +++ b/src/components/Home/RunSection/RunRow.tsx @@ -1,8 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { type MouseEvent } from "react"; import type { PipelineRunResponse } from "@/api/types.gen"; import { StatusBar, StatusIcon } from "@/components/shared/Status"; +import { TagList } from "@/components/shared/Tags/TagList"; import { Button } from "@/components/ui/button"; import { InlineStack } from "@/components/ui/layout"; import { TableCell, TableRow } from "@/components/ui/table"; @@ -13,17 +15,31 @@ import { } from "@/components/ui/tooltip"; import { Paragraph } from "@/components/ui/typography"; import useToastNotification from "@/hooks/useToastNotification"; +import { useBackend } from "@/providers/BackendProvider"; import { APP_ROUTES } from "@/routes/router"; +import { fetchRunAnnotations } from "@/services/pipelineRunService"; +import { getPipelineTagsFromAnnotations } from "@/utils/annotations"; +import { TWENTY_FOUR_HOURS_IN_MS } from "@/utils/constants"; import { formatDate } from "@/utils/date"; import { getOverallExecutionStatusFromStats } from "@/utils/executionStatus"; const RunRow = ({ run }: { run: PipelineRunResponse }) => { const navigate = useNavigate(); const notify = useToastNotification(); + const { backendUrl } = useBackend(); const runId = `${run.id}`; + const { data: annotations } = useQuery({ + queryKey: ["pipeline-run-annotations", runId], + queryFn: () => fetchRunAnnotations(runId, backendUrl), + enabled: !!runId, + refetchOnWindowFocus: false, + staleTime: TWENTY_FOUR_HOURS_IN_MS, + }); + const name = run.pipeline_name ?? "Unknown pipeline"; + const tags = getPipelineTagsFromAnnotations(annotations); const createdBy = run.created_by ?? "Unknown user"; const truncatedCreatedBy = truncateMiddle(createdBy); @@ -101,6 +117,9 @@ const RunRow = ({ run }: { run: PipelineRunResponse }) => { {isTruncated ? createdByButtonWithTooltip : createdByButton} + + {tags && tags.length > 0 && } + ); }; diff --git a/src/components/Home/RunSection/RunSection.tsx b/src/components/Home/RunSection/RunSection.tsx index 7ac3891e7..72b96efcf 100644 --- a/src/components/Home/RunSection/RunSection.tsx +++ b/src/components/Home/RunSection/RunSection.tsx @@ -1,15 +1,15 @@ import { useQuery } from "@tanstack/react-query"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; -import { ChevronFirst, ChevronLeft, ChevronRight } from "lucide-react"; import { useEffect, useRef, useState } from "react"; 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 { Icon } from "@/components/ui/icon"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { InlineStack } from "@/components/ui/layout"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { @@ -19,6 +19,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { Text } from "@/components/ui/typography"; import { useBackend } from "@/providers/BackendProvider"; import { getBackendStatusString } from "@/utils/backend"; import { fetchWithErrorHandling } from "@/utils/fetchWithErrorHandling"; @@ -198,9 +199,9 @@ export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => { if (isLoading || isFetching || !ready) { return ( -
+ Loading... -
+ ); } @@ -259,29 +260,30 @@ export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => { if (!data?.pipeline_runs || data?.pipeline_runs?.length === 0) { return ( -
+ {searchMarkup} {createdByValue ? ( -
+ No runs found for user: {createdByValue}. -
+ ) : ( -
No runs found. Run a pipeline to see it here.
+ No runs found. Run a pipeline to see it here. )} -
+ ); } return ( -
+ {searchMarkup} - Name - Status - Date - Initiated By + Name + Status + Date + Initiated By + Tags @@ -292,34 +294,38 @@ export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => {
{(data.next_page_token || previousPageTokens.length > 0) && ( -
-
+ + -
+ -
+ )} -
+ ); }; diff --git a/src/components/PipelineRun/RunDetails.tsx b/src/components/PipelineRun/RunDetails.tsx index faf33f624..2dccf4556 100644 --- a/src/components/PipelineRun/RunDetails.tsx +++ b/src/components/PipelineRun/RunDetails.tsx @@ -15,7 +15,9 @@ import { useExecutionData } from "@/providers/ExecutionDataProvider"; import { FLEX_NODES_ANNOTATION, getAnnotationValue, + getPipelineTagsFromSpec, PIPELINE_NOTES_ANNOTATION, + PIPELINE_TAGS_ANNOTATION, } from "@/utils/annotations"; import { flattenExecutionStatusStats, @@ -23,9 +25,14 @@ import { getOverallExecutionStatusFromStats, } from "@/utils/executionStatus"; +import { TagList } from "../shared/Tags/TagList"; import { RunNotesEditor } from "./RunNotesEditor"; -const EXCLUDED_ANNOTATIONS = [PIPELINE_NOTES_ANNOTATION, FLEX_NODES_ANNOTATION]; +const EXCLUDED_ANNOTATIONS = [ + PIPELINE_NOTES_ANNOTATION, + FLEX_NODES_ANNOTATION, + PIPELINE_TAGS_ANNOTATION, +]; export const RunDetails = () => { const { configured } = useBackend(); @@ -76,6 +83,7 @@ export const RunDetails = () => { pipelineAnnotations, PIPELINE_NOTES_ANNOTATION, ); + const tags = getPipelineTagsFromSpec(componentSpec); const displayedAnnotations = Object.entries(pipelineAnnotations) .filter(([key]) => !EXCLUDED_ANNOTATIONS.includes(key)) @@ -142,6 +150,10 @@ export const RunDetails = () => { )} + + + + ); }; diff --git a/src/components/PipelineRun/RunNotesEditor.tsx b/src/components/PipelineRun/RunNotesEditor.tsx index 17b2de026..11107553b 100644 --- a/src/components/PipelineRun/RunNotesEditor.tsx +++ b/src/components/PipelineRun/RunNotesEditor.tsx @@ -6,7 +6,7 @@ import { Paragraph } from "@/components/ui/typography"; import { useBackend } from "@/providers/BackendProvider"; import { fetchRunAnnotations, - updateRunNotes, + updateRunAnnotation, } from "@/services/pipelineRunService"; import { getAnnotationValue, @@ -35,7 +35,11 @@ export const RunNotesEditor = ({ runId, readOnly }: RunNotesEditorProps) => { }); const { mutate: saveRunNotes, isPending } = useMutation({ - mutationFn: (runId: string) => updateRunNotes(runId, backendUrl, value), + mutationFn: (runId: string) => + updateRunAnnotation(runId, backendUrl, { + key: PIPELINE_RUN_NOTES_ANNOTATION, + value: value, + }), onSuccess: () => { refetch(); }, diff --git a/src/components/shared/AnnouncementBanners.tsx b/src/components/shared/AnnouncementBanners.tsx index 529d1e11e..152f24a63 100644 --- a/src/components/shared/AnnouncementBanners.tsx +++ b/src/components/shared/AnnouncementBanners.tsx @@ -1,8 +1,9 @@ +import "@/config/announcements"; + import { useState } from "react"; import { InfoBox } from "@/components/shared/InfoBox"; import { BlockStack } from "@/components/ui/layout"; -import "@/config/announcements"; import { getStorage } from "@/utils/typedStorage"; interface DismissedAnnouncementsStorage { diff --git a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx index f7d47228b..72e37879e 100644 --- a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx +++ b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx @@ -13,8 +13,13 @@ import useToastNotification from "@/hooks/useToastNotification"; import { cn } from "@/lib/utils"; import { useBackend } from "@/providers/BackendProvider"; import { APP_ROUTES } from "@/routes/router"; -import { updateRunNotes } from "@/services/pipelineRunService"; +import { updateRunAnnotation } from "@/services/pipelineRunService"; import type { PipelineRun } from "@/types/pipelineRun"; +import { + getPipelineTagsFromSpec, + PIPELINE_RUN_NOTES_ANNOTATION, + PIPELINE_TAGS_ANNOTATION, +} from "@/utils/annotations"; import { type ArgumentType, type ComponentSpec, @@ -110,7 +115,18 @@ const OasisSubmitter = ({ const { mutate: saveNotes } = useMutation({ mutationFn: (runId: string) => - updateRunNotes(runId, backendUrl, runNotes.current), + updateRunAnnotation(runId, backendUrl, { + key: PIPELINE_RUN_NOTES_ANNOTATION, + value: runNotes.current, + }), + }); + + const { mutate: saveTags } = useMutation({ + mutationFn: (runId: string) => + updateRunAnnotation(runId, backendUrl, { + key: PIPELINE_TAGS_ANNOTATION, + value: getPipelineTagsFromSpec(componentSpec).join(","), + }), }); const handleError = (message: string) => { @@ -147,6 +163,12 @@ const OasisSubmitter = ({ if (runNotes.current.trim() !== "") { saveNotes(response.id.toString()); } + + const tags = getPipelineTagsFromSpec(componentSpec); + if (tags.length > 0) { + saveTags(response.id.toString()); + } + setSubmitSuccess(true); setCooldownTime(3); onSubmitComplete?.(); diff --git a/src/components/shared/Tags/TagList.tsx b/src/components/shared/Tags/TagList.tsx index 11e7dfc17..8ff99a876 100644 --- a/src/components/shared/Tags/TagList.tsx +++ b/src/components/shared/Tags/TagList.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import { InlineStack } from "@/components/ui/layout"; +import { Paragraph } from "@/components/ui/typography"; interface TagListProps { tags: string[]; @@ -15,6 +16,14 @@ export const TagList = ({ tags }: TagListProps) => { const hasMoreTags = tags.length > MAX_VISIBLE_TAGS; const visibleTags = showAllTags ? tags : tags.slice(0, MAX_VISIBLE_TAGS); + if (visibleTags.length === 0) { + return ( + + None + + ); + } + return ( {visibleTags.map((tag) => ( diff --git a/src/services/pipelineRunService.ts b/src/services/pipelineRunService.ts index f6d97c9ee..adc9e7abd 100644 --- a/src/services/pipelineRunService.ts +++ b/src/services/pipelineRunService.ts @@ -6,7 +6,6 @@ import type { } from "@/api/types.gen"; import { APP_ROUTES } from "@/routes/router"; import type { PipelineRun } from "@/types/pipelineRun"; -import { PIPELINE_RUN_NOTES_ANNOTATION } from "@/utils/annotations"; import { removeCachingStrategyFromSpec } from "@/utils/cache"; import { type ComponentSpec, @@ -229,17 +228,20 @@ export const fetchRunAnnotations = async ( return fetchWithErrorHandling(url); }; -export const updateRunNotes = async ( +export const updateRunAnnotation = async ( runId: string, backendUrl: string, - notes: string, + annotation: { + key: string; + value: string; + }, ) => { - await fetchWithErrorHandling( - `${backendUrl}/api/pipeline_runs/${runId}/annotations/${PIPELINE_RUN_NOTES_ANNOTATION}?value=${encodeURIComponent( - notes, - )}`, - { - method: "PUT", - }, + const url = new URL( + `${backendUrl}/api/pipeline_runs/${runId}/annotations/${annotation.key}`, ); + url.searchParams.append("value", annotation.value); + + await fetchWithErrorHandling(url.toString(), { + method: "PUT", + }); }; diff --git a/src/utils/annotations.ts b/src/utils/annotations.ts index cfb146946..b28ccc758 100644 --- a/src/utils/annotations.ts +++ b/src/utils/annotations.ts @@ -276,6 +276,22 @@ export function getPipelineTagsFromSpec( } const annotations = componentSpec.metadata?.annotations; + + return getPipelineTagsFromAnnotations(annotations); +} + +/** + * Extract the pipeline tags from annotations. + * @param annotations - The annotations object + * @returns The pipeline tags as an array of strings + */ +export function getPipelineTagsFromAnnotations( + annotations?: Annotations, +): string[] { + if (!annotations) { + return []; + } + const tagsString = getAnnotationValue(annotations, PIPELINE_TAGS_ANNOTATION) ?? "";