From ae17703dc2d35d97994f26b9208369aca396e959 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Mon, 15 Apr 2024 21:26:55 -0500 Subject: [PATCH] Refactor pipelines context and add useSafePipelines hook --- .../pipelines/apiHooks/usePipelines.ts | 24 ++- .../pipelines/context/PipelinesContext.tsx | 3 +- .../overview/trainModels/PipelinesCard.tsx | 155 ++--------------- .../trainModels/PipelinesCardMetrics.tsx | 163 ++++++++++++++++++ .../trainModels/PipelinesOverviewCard.tsx | 30 ++++ 5 files changed, 230 insertions(+), 145 deletions(-) create mode 100644 frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesCardMetrics.tsx create mode 100644 frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesOverviewCard.tsx diff --git a/frontend/src/concepts/pipelines/apiHooks/usePipelines.ts b/frontend/src/concepts/pipelines/apiHooks/usePipelines.ts index a10510ee55..aef8bddf99 100644 --- a/frontend/src/concepts/pipelines/apiHooks/usePipelines.ts +++ b/frontend/src/concepts/pipelines/apiHooks/usePipelines.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FetchState } from '~/utilities/useFetchState'; +import { FetchState, NotReadyError } from '~/utilities/useFetchState'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; import usePipelineQuery from '~/concepts/pipelines/apiHooks/usePipelineQuery'; import { PipelineListPaged, PipelineOptions } from '~/concepts/pipelines/types'; @@ -21,4 +21,26 @@ const usePipelines = ( ); }; +export const useSafePipelines = ( + options?: PipelineOptions, + refreshRate?: number, +): FetchState> => { + const { api, pipelinesServer, apiAvailable } = usePipelinesAPI(); + return usePipelineQuery( + React.useCallback( + (opts, params) => { + if (!apiAvailable || !pipelinesServer.compatible) { + throw new NotReadyError('Pipelines is not available'); + } + return api + .listPipelines(opts, params) + .then((result) => ({ ...result, items: result.pipelines })); + }, + [api, apiAvailable, pipelinesServer.compatible], + ), + options, + refreshRate, + ); +}; + export default usePipelines; diff --git a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx index db13e927a1..83af9ea8f0 100644 --- a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx +++ b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx @@ -90,7 +90,8 @@ export const PipelineContextProvider = conditionalArea { const { pipelinesServer } = usePipelinesAPI(); const { - currentProject, notebooks: { data: notebooks, loaded: notebooksLoaded, error: notebooksError }, } = React.useContext(ProjectDetailsContext); - const [{ items: pipelines, totalSize: pipelinesCount }, pipelinesLoaded, pipelinesError] = - usePipelines({ - pageSize: 5, - }); - const [{ totalSize: activeRunsCount }, activeRunsLoaded, activeRunsError] = usePipelineActiveRuns( - { - pageSize: 1, - }, - ); - const [{ totalSize: archivedRunsCount }, archivedRunsLoaded, archivedRunsError] = - usePipelineArchivedRuns({ - pageSize: 1, - }); - const [{ totalSize: scheduledCount }, scheduledLoaded, scheduledError] = usePipelineRunJobs({ - pageSize: 1, - }); - const [{ totalSize: experimentsCount }, experimentsLoaded, experimentsError] = useExperiments({ + const [{ totalSize: pipelinesCount }] = useSafePipelines({ pageSize: 1, }); - const loaded = - !pipelinesServer.initializing && - pipelinesLoaded && - activeRunsLoaded && - archivedRunsLoaded && - scheduledLoaded && - experimentsLoaded; - - const loadError = - pipelinesError || activeRunsError || archivedRunsError || scheduledError || experimentsError; - - const triggeredCount = activeRunsCount + archivedRunsCount; - - const statistics = React.useMemo( - () => [ - { - count: pipelinesCount, - text: pipelinesCount === 1 ? 'Pipeline' : 'Pipelines', - }, - { - count: scheduledCount, - text: scheduledCount === 1 ? 'Schedule' : 'Schedules', - }, - { - count: triggeredCount, - text: triggeredCount === 1 ? 'Run' : 'Runs', - }, - { - count: experimentsCount, - text: experimentsCount === 1 ? 'Experiment' : 'Experiments', - }, - ], - [experimentsCount, pipelinesCount, scheduledCount, triggeredCount], - ); - const renderContent = () => { - if (loadError) { - return ( - - ( - - )} - /> - } - headingLevel="h3" - /> - {loadError.message} - - ); - } - - if (!loaded) { + if (pipelinesServer.initializing) { return ( { ); } - if (!pipelinesServer.installed) { return ( <> @@ -167,61 +81,16 @@ const PipelinesCard: React.FC = () => { } return ( - - {pipelinesCount ? ( - <> - } - createText="Import pipeline" - statistics={statistics} - listItems={ - - } - /> - - ) : ( - <> - - - - Pipelines are platforms for building and deploying portable and scalable - machine-learning (ML) workflows. You can import a pipeline or create one in a - workbench. - - - - - - - - )} - + + + + + ); }; return ( - - {renderContent()} - + {renderContent()} ); }; diff --git a/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesCardMetrics.tsx b/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesCardMetrics.tsx new file mode 100644 index 0000000000..58a43957c0 --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesCardMetrics.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import { + CardBody, + CardFooter, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateIcon, + Spinner, + Text, + TextContent, +} from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; +import usePipelineRunJobs from '~/concepts/pipelines/apiHooks/usePipelineRunJobs'; +import { + usePipelineActiveRuns, + usePipelineArchivedRuns, +} from '~/concepts/pipelines/apiHooks/usePipelineRuns'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import useExperiments from '~/concepts/pipelines/apiHooks/useExperiments'; +import usePipelines from '~/concepts/pipelines/apiHooks/usePipelines'; +import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; +import ImportPipelineButton from '~/concepts/pipelines/content/import/ImportPipelineButton'; +import PipelinesCardItems from '~/pages/projects/screens/detail/overview/trainModels/PipelinesCardItems'; +import MetricsContents from './MetricsContents'; + +const PipelinesCardMetrics: React.FC = () => { + const { pipelinesServer } = usePipelinesAPI(); + const { currentProject } = React.useContext(ProjectDetailsContext); + + const [{ items: pipelines, totalSize: pipelinesCount }, pipelinesLoaded, pipelinesError] = + usePipelines({ + pageSize: 5, + }); + const [{ totalSize: activeRunsCount }, activeRunsLoaded, activeRunsError] = usePipelineActiveRuns( + { + pageSize: 1, + }, + ); + const [{ totalSize: archivedRunsCount }, archivedRunsLoaded, archivedRunsError] = + usePipelineArchivedRuns({ + pageSize: 1, + }); + const [{ totalSize: scheduledCount }, scheduledLoaded, scheduledError] = usePipelineRunJobs({ + pageSize: 1, + }); + const [{ totalSize: experimentsCount }, experimentsLoaded, experimentsError] = useExperiments({ + pageSize: 1, + }); + + const loaded = + !pipelinesServer.initializing && + pipelinesLoaded && + activeRunsLoaded && + archivedRunsLoaded && + scheduledLoaded && + experimentsLoaded; + + const loadError = + pipelinesError || activeRunsError || archivedRunsError || scheduledError || experimentsError; + + const triggeredCount = activeRunsCount + archivedRunsCount; + + const statistics = React.useMemo( + () => [ + { + count: pipelinesCount, + text: pipelinesCount === 1 ? 'Pipeline' : 'Pipelines', + }, + { + count: scheduledCount, + text: scheduledCount === 1 ? 'Schedule' : 'Schedules', + }, + { + count: triggeredCount, + text: triggeredCount === 1 ? 'Run' : 'Runs', + }, + { + count: experimentsCount, + text: experimentsCount === 1 ? 'Experiment' : 'Experiments', + }, + ], + [experimentsCount, pipelinesCount, scheduledCount, triggeredCount], + ); + + if (loadError) { + return ( + + ( + + )} + /> + } + headingLevel="h3" + /> + {loadError.message} + + ); + } + + if (!loaded) { + return ( + + } />} + headingLevel="h3" + /> + Loading... + + ); + } + + return ( + + {pipelinesCount ? ( + <> + } + createText="Import pipeline" + statistics={statistics} + listItems={ + + } + /> + + ) : ( + <> + + + + Pipelines are platforms for building and deploying portable and scalable + machine-learning (ML) workflows. You can import a pipeline or create one in a + workbench. + + + + + + + + )} + + ); +}; + +export default PipelinesCardMetrics; diff --git a/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesOverviewCard.tsx b/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesOverviewCard.tsx new file mode 100644 index 0000000000..4fc5f5b648 --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/overview/trainModels/PipelinesOverviewCard.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { ProjectObjectType, SectionType } from '~/concepts/design/utils'; +import OverviewCard from '~/pages/projects/screens/detail/overview/components/OverviewCard'; + +type PipelinesOverviewCardProps = { + children: React.ReactNode; + pipelinesCount: number; +}; + +const PipelinesOverviewCard: React.FC = ({ + children, + pipelinesCount, +}) => ( + + {children} + +); +export default PipelinesOverviewCard;