diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index 2bd44d93c9..a085876382 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -6,6 +6,7 @@ import UnauthorizedError from '~/pages/UnauthorizedError'; import { useUser } from '~/redux/selectors'; import { globArtifactsAll, + globExecutionsAll, globExperimentsAll, globPipelineRunsAll, globPipelinesAll, @@ -38,6 +39,9 @@ const GlobalPipelineRunsRoutes = React.lazy( const GlobalPipelineExperimentRoutes = React.lazy( () => import('../pages/pipelines/GlobalPipelineExperimentsRoutes'), ); +const GlobalPipelineExecutionsRoutes = React.lazy( + () => import('../pages/pipelines/GlobalPipelineExecutionsRoutes'), +); const GlobalArtifactsRoutes = React.lazy(() => import('../pages/pipelines/GlobalArtifactsRoutes')); @@ -111,6 +115,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useExecutionsFromMlmdContext.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useExecutionsFromMlmdContext.ts index 454fd19dd3..36654f5bda 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/useExecutionsFromMlmdContext.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useExecutionsFromMlmdContext.ts @@ -11,10 +11,10 @@ import useFetchState, { export const useExecutionsFromMlmdContext = ( context: MlmdContext | null, refreshRate?: number, -): FetchState => { +): FetchState => { const { metadataStoreServiceClient } = usePipelinesAPI(); - const call = React.useCallback>(async () => { + const call = React.useCallback>(async () => { if (!context) { return Promise.reject(new NotReadyError('No context')); } @@ -25,7 +25,7 @@ export const useExecutionsFromMlmdContext = ( return res.getExecutionsList(); }, [metadataStoreServiceClient, context]); - return useFetchState(call, null, { + return useFetchState(call, [], { refreshRate, }); }; diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList.ts new file mode 100644 index 0000000000..095c64552a --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList.ts @@ -0,0 +1,41 @@ +import React from 'react'; +import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context'; +import { Execution, GetExecutionsRequest } from '~/third_party/mlmd'; +import { ListOperationOptions } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_pb'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +export interface ExecutionsListResponse { + executions: Execution[]; + nextPageToken: string; +} + +export const useGetExecutionsList = (): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + const { pageToken, maxResultSize, filterQuery } = useMlmdListContext(); + + const call = React.useCallback>(async () => { + const request = new GetExecutionsRequest(); + const listOperationOptions = new ListOperationOptions(); + listOperationOptions.setOrderByField( + new ListOperationOptions.OrderByField().setField(ListOperationOptions.OrderByField.Field.ID), + ); + + if (filterQuery) { + listOperationOptions.setFilterQuery(filterQuery); + } + if (pageToken) { + listOperationOptions.setNextPageToken(pageToken); + } + + listOperationOptions.setMaxResultSize(maxResultSize); + request.setOptions(listOperationOptions); + + const response = await metadataStoreServiceClient.getExecutions(request); + const nextPageToken = response.getNextPageToken(); + listOperationOptions.setNextPageToken(nextPageToken); + + return { executions: response.getExecutionsList(), nextPageToken }; + }, [filterQuery, maxResultSize, metadataStoreServiceClient, pageToken]); + + return useFetchState(call, null); +}; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun.ts b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun.ts index f8e6bad3ea..dd17675577 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun.ts +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun.ts @@ -7,7 +7,7 @@ import { FAST_POLL_INTERVAL } from '~/utilities/const'; const useExecutionsForPipelineRun = ( run: PipelineRunKFv2 | null, -): [executions: Execution[] | null, loaded: boolean, error?: Error] => { +): [executions: Execution[], loaded: boolean, error?: Error] => { const isFinished = isPipelineRunFinished(run); const refreshRate = isFinished ? 0 : FAST_POLL_INTERVAL; // contextError means mlmd service is not available, no need to check executions diff --git a/frontend/src/concepts/pipelines/kfTypes.ts b/frontend/src/concepts/pipelines/kfTypes.ts index 782628dc0a..8d5ac602de 100644 --- a/frontend/src/concepts/pipelines/kfTypes.ts +++ b/frontend/src/concepts/pipelines/kfTypes.ts @@ -59,6 +59,21 @@ export enum ArtifactType { MARKDOWN = 'system.Markdown', } +export enum ExecutionType { + CONTAINER_EXECUTION = 'system.ContainerExecution', + DAG_EXECUTION = 'system.DAGExecution', +} + +export enum ExecutionStatus { + UNKNOWN = 'Unknown', + NEW = 'New', + RUNNING = 'Running', + COMPLETE = 'Complete', + FAILED = 'Failed', + CACHED = 'Cached', + CANCELED = 'Canceled', +} + /** @deprecated resource type is no longer a concept in v2 */ export enum ResourceTypeKF { UNKNOWN_RESOURCE_TYPE = 'UNKNOWN_RESOURCE_TYPE', diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index a44cea3e36..daf8e5d7b7 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -18,7 +18,7 @@ import { KubeFlowTaskTopology } from './pipelineTaskTypes'; export const usePipelineTaskTopology = ( spec?: PipelineSpecVariable, runDetails?: RunDetailsKF, - executions?: Execution[] | null, + executions?: Execution[], ): KubeFlowTaskTopology => { if (!spec) { return { taskMap: {}, nodes: [] }; @@ -50,9 +50,10 @@ export const usePipelineTaskTopology = ( const executorLabel = component?.executorLabel; const executor = executorLabel ? executors[executorLabel] : undefined; - const status = executions - ? parseRuntimeInfoFromExecutions(taskId, executions) - : parseRuntimeInfoFromRunDetails(taskId, runDetails); + const status = + executions && executions.length !== 0 + ? parseRuntimeInfoFromExecutions(taskId, executions) + : parseRuntimeInfoFromRunDetails(taskId, runDetails); const runAfter: string[] = taskValue.dependentTasks ?? []; diff --git a/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx b/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx new file mode 100644 index 0000000000..9a9d4dc8b3 --- /dev/null +++ b/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { Navigate, Route } from 'react-router-dom'; +import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; +import GlobalPipelineCoreLoader from '~/pages/pipelines/global/GlobalPipelineCoreLoader'; +import { executionsBaseRoute } from '~/routes'; +import { + executionsPageDescription, + executionsPageTitle, +} from '~/pages/pipelines/global/experiments/executions/const'; +import GlobalExecutions from '~/pages/pipelines/global/experiments/executions/GlobalExecutions'; + +const GlobalPipelineExecutionsRoutes: React.FC = () => ( + + + } + > + } /> + } /> + + +); + +export default GlobalPipelineExecutionsRoutes; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsList.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsList.tsx new file mode 100644 index 0000000000..c2ceb3d0a8 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsList.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateIcon, + Spinner, +} from '@patternfly/react-core'; +import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import { useGetExecutionsList } from '~/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList'; +import ExecutionsTable from '~/pages/pipelines/global/experiments/executions/ExecutionsTable'; +import { useMlmdListContext } from '~/concepts/pipelines/context'; + +const ExecutionsList: React.FC = () => { + const { filterQuery } = useMlmdListContext(); + const [executionsResponse, isExecutionsLoaded, executionsError] = useGetExecutionsList(); + const { executions, nextPageToken } = executionsResponse || { executions: [] }; + const filterQueryRef = React.useRef(filterQuery); + + if (executionsError) { + return ( + + + } + headingLevel="h2" + /> + {executionsError.message} + + + ); + } + + if (!isExecutionsLoaded) { + return ( + + + + ); + } + + if (!executions.length && !filterQuery && filterQueryRef.current === filterQuery) { + return ( + + } + headingLevel="h4" + /> + + No experiments have been executed within this project. Select a different project, or + execute an experiment from the Experiments and runs page. + + + ); + } + + return ( + + ); +}; +export default ExecutionsList; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTable.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTable.tsx new file mode 100644 index 0000000000..c30ab40b0c --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTable.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { TableBase } from '~/components/table'; +import { Execution } from '~/third_party/mlmd'; +import ExecutionsTableRow from '~/pages/pipelines/global/experiments/executions/ExecutionsTableRow'; +import { executionColumns } from '~/pages/pipelines/global/experiments/executions/columns'; +import { useMlmdListContext } from '~/concepts/pipelines/context'; +import { initialFilterData } from '~/pages/pipelines/global/experiments/executions/const'; +import ExecutionsTableToolbar from '~/pages/pipelines/global/experiments/executions/ExecutionsTableToolbar'; +import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; + +interface ExecutionsTableProps { + executions: Execution[]; + nextPageToken: string | undefined; + isLoaded: boolean; +} + +const ExecutionsTable: React.FC = ({ + executions, + nextPageToken, + isLoaded, +}) => { + const { + maxResultSize, + setPageToken: setRequestToken, + setMaxResultSize, + } = useMlmdListContext(nextPageToken); + + const [page, setPage] = React.useState(1); + const [filterData, setFilterData] = React.useState(initialFilterData); + const [pageTokens, setPageTokens] = React.useState>({}); + + const onClearFilters = React.useCallback(() => setFilterData(initialFilterData), [setFilterData]); + + const onNextPageClick = React.useCallback( + (_: React.SyntheticEvent, nextPage: number) => { + if (nextPageToken) { + setPageTokens((prevTokens) => ({ ...prevTokens, [nextPage]: nextPageToken })); + setRequestToken(nextPageToken); + setPage(nextPage); + } + }, + [nextPageToken, setRequestToken], + ); + + const onPrevPageClick = React.useCallback( + (_: React.SyntheticEvent, prevPage: number) => { + if (pageTokens[prevPage]) { + setRequestToken(pageTokens[prevPage]); + setPage(prevPage); + } else { + setRequestToken(undefined); + } + }, + [pageTokens, setRequestToken], + ); + + return ( + } + toggleTemplate={() => <>{maxResultSize} per page } + toolbarContent={ + + } + page={page} + perPage={maxResultSize} + disableItemCount + onNextClick={onNextPageClick} + onPreviousClick={onPrevPageClick} + onPerPageSelect={(_, newSize) => { + setMaxResultSize(newSize); + }} + onSetPage={(_, newPage) => { + if (newPage < page || !isLoaded) { + setPage(newPage); + } + }} + emptyTableView={} + id="executions-list-table" + /> + ); +}; + +export default ExecutionsTable; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx new file mode 100644 index 0000000000..2e02ee0991 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Td, Tr } from '@patternfly/react-table'; +import { Execution } from '~/third_party/mlmd'; +import ExecutionsTableRowStatusIcon from '~/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon'; + +type ExecutionsTableRowProps = { + obj: Execution; +}; + +const ExecutionsTableRow: React.FC = ({ obj }) => ( + + + {obj.getCustomPropertiesMap().get('task_name')?.getStringValue() || '(No name)'} + + + + + {obj.getId()} + {obj.getType()} + +); + +export default ExecutionsTableRow; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx new file mode 100644 index 0000000000..a69e941c5d --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Icon, Tooltip } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + OutlinedWindowRestoreIcon, + QuestionCircleIcon, + TimesCircleIcon, +} from '@patternfly/react-icons'; +import { Execution } from '~/third_party/mlmd'; + +type ExecutionsTableRowStatusIconProps = { + status: Execution.State; +}; + +const ExecutionsTableRowStatusIcon: React.FC = ({ status }) => { + let tooltip; + let icon; + switch (status) { + case Execution.State.COMPLETE: + icon = ( + + + + ); + tooltip = 'Complete'; + break; + case Execution.State.CACHED: + icon = ( + + + + ); + tooltip = 'Cached'; + break; + case Execution.State.CANCELED: + icon = ( + + + + ); + tooltip = 'Canceled'; + break; + case Execution.State.FAILED: + icon = ( + + + + ); + tooltip = 'Failed'; + break; + case Execution.State.RUNNING: + icon = ; + tooltip = 'Running'; + break; + // TODO: change the icon here + case Execution.State.NEW: + icon = ( + + + + ); + tooltip = 'New'; + break; + default: + icon = ( + + + + ); + tooltip = 'Unknown'; + } + + return {icon}; +}; + +export default ExecutionsTableRowStatusIcon; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableToolbar.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableToolbar.tsx new file mode 100644 index 0000000000..25fe607eb2 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableToolbar.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { TextInput } from '@patternfly/react-core'; +import { FilterToolbar } from '~/concepts/pipelines/content/tables/PipelineFilterBar'; +import { + FilterOptions, + getMlmdExecutionState, + options, +} from '~/pages/pipelines/global/experiments/executions/const'; +import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; +import { ExecutionStatus, ExecutionType } from '~/concepts/pipelines/kfTypes'; +import { useMlmdListContext } from '~/concepts/pipelines/context'; + +type ExecutionsTableToolbarProps = { + filterData: Record; + setFilterData: React.Dispatch>>; + onClearFilters: () => void; +}; + +const ExecutionsTableToolbar: React.FC = ({ + filterData, + setFilterData, + onClearFilters, +}) => { + const { setFilterQuery } = useMlmdListContext(); + const onFilterUpdate = React.useCallback( + (key: string, value: string | { label: string; value: string } | undefined) => + setFilterData((prevValues) => ({ ...prevValues, [key]: value })), + [setFilterData], + ); + + React.useEffect(() => { + if (Object.values(filterData).some((filterOption) => !!filterOption)) { + let filterQuery = ''; + + if (filterData[FilterOptions.Execution]) { + const executionNameQuery = `custom_properties.display_name.string_value LIKE '%${ + filterData[FilterOptions.Execution] + }%'`; + filterQuery += filterQuery.length ? ` AND ${executionNameQuery}` : executionNameQuery; + } + + if (filterData[FilterOptions.Id]) { + const executionIdQuery = `id = cast(${filterData[FilterOptions.Id]} as int64)`; + filterQuery += filterQuery.length ? ` AND ${executionIdQuery}` : executionIdQuery; + } + + if (filterData[FilterOptions.Type]) { + const executionTypeQuery = `type LIKE '%${filterData[FilterOptions.Type]}%'`; + filterQuery += filterQuery.length ? ` AND ${executionTypeQuery}` : executionTypeQuery; + } + if (filterData[FilterOptions.Status]) { + const executionStatusQuery = `cast(last_known_state as int64) = ${getMlmdExecutionState( + filterData[FilterOptions.Status], + )}`; + filterQuery += filterQuery.length ? ` AND ${executionStatusQuery}` : executionStatusQuery; + } + + setFilterQuery(filterQuery); + } else { + setFilterQuery(''); + } + }, [filterData, setFilterQuery]); + + return ( + + filterOptions={options} + filterOptionRenders={{ + [FilterOptions.Execution]: ({ onChange, ...props }) => ( + onChange(value)} + /> + ), + [FilterOptions.Id]: ({ onChange, ...props }) => ( + onChange(value)} + /> + ), + [FilterOptions.Type]: ({ value, onChange, ...props }) => ( + ({ + key: v, + label: v, + }))} + onChange={(v) => onChange(v)} + /> + ), + [FilterOptions.Status]: ({ value, onChange, ...props }) => ( + ({ + key: v, + label: v, + }))} + onChange={(v) => onChange(v)} + /> + ), + }} + filterData={filterData} + onClearFilters={onClearFilters} + onFilterUpdate={onFilterUpdate} + /> + ); +}; + +export default ExecutionsTableToolbar; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/GlobalExecutions.tsx b/frontend/src/pages/pipelines/global/experiments/executions/GlobalExecutions.tsx new file mode 100644 index 0000000000..bfa87e3074 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/GlobalExecutions.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { PageSection } from '@patternfly/react-core'; +import { MlmdListContextProvider, usePipelinesAPI } from '~/concepts/pipelines/context'; +import PipelineServerActions from '~/concepts/pipelines/content/PipelineServerActions'; +import PipelineCoreApplicationPage from '~/pages/pipelines/global/PipelineCoreApplicationPage'; +import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; +import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; +import { executionsBaseRoute } from '~/routes'; +import { + executionsPageDescription, + executionsPageTitle, +} from '~/pages/pipelines/global/experiments/executions/const'; +import ExecutionsList from '~/pages/pipelines/global/experiments/executions/ExecutionsList'; + +const GlobalExecutions: React.FC = () => { + const pipelinesAPI = usePipelinesAPI(); + + return ( + } + getRedirectPath={executionsBaseRoute} + overrideChildPadding + > + + + + + + + + + + + ); +}; + +export default GlobalExecutions; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/columns.ts b/frontend/src/pages/pipelines/global/experiments/executions/columns.ts new file mode 100644 index 0000000000..c70fd5d2e3 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/columns.ts @@ -0,0 +1,28 @@ +import { SortableData } from '~/components/table'; +import { Execution } from '~/third_party/mlmd'; + +export const executionColumns: SortableData[] = [ + { + label: 'Executions', + field: 'name', + sortable: false, + }, + { + label: 'Status', + field: 'status', + sortable: false, + width: 15, + }, + { + label: 'ID', + field: 'execution_id', + sortable: false, + width: 15, + }, + { + label: 'Type', + field: 'type', + sortable: false, + width: 40, + }, +]; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/const.ts b/frontend/src/pages/pipelines/global/experiments/executions/const.ts new file mode 100644 index 0000000000..acff4a6ea4 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/const.ts @@ -0,0 +1,45 @@ +import { ExecutionStatus } from '~/concepts/pipelines/kfTypes'; +import { Execution as MlmdExecution } from '~/third_party/mlmd'; + +export const executionsPageTitle = 'Executions'; +export const executionsPageDescription = 'View execution metadata.'; + +export enum FilterOptions { + Execution = 'name', + Id = 'id', + Type = 'type', + Status = 'status', +} + +export const options = { + [FilterOptions.Execution]: 'Execution', + [FilterOptions.Id]: 'ID', + [FilterOptions.Type]: 'Type', + [FilterOptions.Status]: 'Status', +}; + +export const initialFilterData: Record = { + [FilterOptions.Execution]: '', + [FilterOptions.Id]: '', + [FilterOptions.Type]: undefined, + [FilterOptions.Status]: '', +}; + +export const getMlmdExecutionState = (status: string): MlmdExecution.State => { + switch (status) { + case ExecutionStatus.NEW: + return MlmdExecution.State.NEW; + case ExecutionStatus.CACHED: + return MlmdExecution.State.CACHED; + case ExecutionStatus.CANCELED: + return MlmdExecution.State.CANCELED; + case ExecutionStatus.COMPLETE: + return MlmdExecution.State.COMPLETE; + case ExecutionStatus.FAILED: + return MlmdExecution.State.FAILED; + case ExecutionStatus.RUNNING: + return MlmdExecution.State.RUNNING; + default: + return MlmdExecution.State.UNKNOWN; + } +}; diff --git a/frontend/src/routes/pipelines/executions.ts b/frontend/src/routes/pipelines/executions.ts new file mode 100644 index 0000000000..66a549a2af --- /dev/null +++ b/frontend/src/routes/pipelines/executions.ts @@ -0,0 +1,5 @@ +export const executionsRootPath = '/executions'; +export const globExecutionsAll = `${executionsRootPath}/*`; + +export const executionsBaseRoute = (namespace: string | undefined): string => + !namespace ? executionsRootPath : `${executionsRootPath}/${namespace}`; diff --git a/frontend/src/routes/pipelines/index.ts b/frontend/src/routes/pipelines/index.ts index 1dda88e0e3..67212914a0 100644 --- a/frontend/src/routes/pipelines/index.ts +++ b/frontend/src/routes/pipelines/index.ts @@ -3,3 +3,4 @@ export * from './project'; export * from './experiments'; export * from './artifacts'; export * from './runs'; +export * from './executions'; diff --git a/frontend/src/utilities/NavData.tsx b/frontend/src/utilities/NavData.tsx index 1c1681a2bd..82dbc9f26e 100644 --- a/frontend/src/utilities/NavData.tsx +++ b/frontend/src/utilities/NavData.tsx @@ -3,6 +3,7 @@ import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { useUser } from '~/redux/selectors'; import { artifactsRootPath, + executionsRootPath, experimentsRootPath, routePipelineRuns, routePipelines, @@ -86,6 +87,11 @@ const useDSPipelinesNav = (): NavDataItem[] => { label: 'Experiments and runs', href: experimentsRootPath, }, + { + id: 'executions', + label: 'Executions', + href: executionsRootPath, + }, { id: 'artifacts', label: 'Artifacts',