diff --git a/frontend/src/__mocks__/mockPipelineVersionsProxy.ts b/frontend/src/__mocks__/mockPipelineVersionsProxy.ts index 18a0c3a2b2..3b5a26fe82 100644 --- a/frontend/src/__mocks__/mockPipelineVersionsProxy.ts +++ b/frontend/src/__mocks__/mockPipelineVersionsProxy.ts @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import { - InputDefParamType, + ArtifactType, + InputDefinitionParameterType, PipelineVersionKF, PipelineVersionKFv2, RelationshipKF, @@ -210,7 +211,7 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ artifacts: { iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, @@ -223,17 +224,17 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ artifacts: { input_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, }, parameters: { min_max_scaler: { - parameterType: 'BOOLEAN', + parameterType: InputDefinitionParameterType.BOOLEAN, }, standard_scaler: { - parameterType: 'BOOLEAN', + parameterType: InputDefinitionParameterType.BOOLEAN, }, }, }, @@ -241,7 +242,7 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ artifacts: { normalized_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, @@ -254,14 +255,14 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ artifacts: { normalized_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, }, parameters: { n_neighbors: { - parameterType: 'NUMBER_INTEGER', + parameterType: InputDefinitionParameterType.INTEGER, }, }, }, @@ -269,7 +270,7 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ artifacts: { model: { artifactType: { - schemaTitle: 'system.Model', + schemaTitle: ArtifactType.MODEL, schemaVersion: '0.0.1', }, }, @@ -409,13 +410,13 @@ export const mockPipelineVersionsListV2: PipelineVersionKFv2[] = [ inputDefinitions: { parameters: { min_max_scaler: { - parameterType: InputDefParamType.Boolean, + parameterType: InputDefinitionParameterType.BOOLEAN, }, neighbors: { - parameterType: InputDefParamType.NumberInteger, + parameterType: InputDefinitionParameterType.INTEGER, }, standard_scaler: { - parameterType: InputDefParamType.Boolean, + parameterType: InputDefinitionParameterType.BOOLEAN, }, }, }, @@ -458,7 +459,7 @@ export const buildMockPipelineVersionV2 = ( artifacts: { iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, @@ -471,17 +472,17 @@ export const buildMockPipelineVersionV2 = ( artifacts: { input_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, }, parameters: { min_max_scaler: { - parameterType: 'BOOLEAN', + parameterType: InputDefinitionParameterType.BOOLEAN, }, standard_scaler: { - parameterType: 'STRING', + parameterType: InputDefinitionParameterType.STRING, }, }, }, @@ -489,7 +490,7 @@ export const buildMockPipelineVersionV2 = ( artifacts: { normalized_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, @@ -502,14 +503,14 @@ export const buildMockPipelineVersionV2 = ( artifacts: { normalized_iris_dataset: { artifactType: { - schemaTitle: 'system.Dataset', + schemaTitle: ArtifactType.DATASET, schemaVersion: '0.0.1', }, }, }, parameters: { n_neighbors: { - parameterType: 'NUMBER_INTEGER', + parameterType: InputDefinitionParameterType.INTEGER, }, }, }, @@ -517,7 +518,7 @@ export const buildMockPipelineVersionV2 = ( artifacts: { model: { artifactType: { - schemaTitle: 'system.Model', + schemaTitle: ArtifactType.MODEL, schemaVersion: '0.0.1', }, }, @@ -657,13 +658,13 @@ export const buildMockPipelineVersionV2 = ( inputDefinitions: { parameters: { min_max_scaler: { - parameterType: InputDefParamType.Boolean, + parameterType: InputDefinitionParameterType.BOOLEAN, }, neighbors: { - parameterType: InputDefParamType.NumberInteger, + parameterType: InputDefinitionParameterType.INTEGER, }, standard_scaler: { - parameterType: InputDefParamType.String, + parameterType: InputDefinitionParameterType.STRING, }, }, }, diff --git a/frontend/src/__mocks__/mockRunKF.ts b/frontend/src/__mocks__/mockRunKF.ts index 2582dff3be..6d38407e56 100644 --- a/frontend/src/__mocks__/mockRunKF.ts +++ b/frontend/src/__mocks__/mockRunKF.ts @@ -13,8 +13,8 @@ export const buildMockRunKF = (run?: Partial): PipelineRunKFv2 runtime_config: { parameters: { min_max_scaler: false, - neighbors: 0, - standard_scaler: 'yes', + neighbors: 1, + standard_scaler: false, }, }, service_account: 'pipeline-runner-dspa', diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts index da5030f30d..5da88d9474 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { - InputDefParamType, + InputDefinitionParameterType, PipelineRunJobKFv2, PipelineRunKFv2, } from '~/concepts/pipelines/kfTypes'; @@ -243,25 +243,26 @@ describe('Pipeline create runs', () => { pipeline_spec: { ...mockPipelineVersion.pipeline_spec, root: { + dag: { tasks: {} }, inputDefinitions: { parameters: { string_param: { - parameterType: InputDefParamType.String, + parameterType: InputDefinitionParameterType.STRING, }, double_param: { - parameterType: InputDefParamType.NumberDouble, + parameterType: InputDefinitionParameterType.DOUBLE, }, int_param: { - parameterType: InputDefParamType.NumberInteger, + parameterType: InputDefinitionParameterType.INTEGER, }, struct_param: { - parameterType: InputDefParamType.Struct, + parameterType: InputDefinitionParameterType.STRUCT, }, list_param: { - parameterType: InputDefParamType.List, + parameterType: InputDefinitionParameterType.LIST, }, bool_param: { - parameterType: InputDefParamType.Boolean, + parameterType: InputDefinitionParameterType.BOOLEAN, }, }, }, diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts index 43296f3b58..9eb7c40e5f 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts @@ -13,7 +13,6 @@ import { mockPodLogs } from '~/__mocks__/mockPodLogs'; import { pipelineDetails, pipelineRunJobDetails, - pipelinesTopology, pipelineRunDetails, } from '~/__tests__/cypress/cypress/pages/pipelines'; import { buildMockRunKF } from '~/__mocks__/mockRunKF'; @@ -148,6 +147,13 @@ const initIntercepts = () => { }, { runs: [mockRun] }, ); + cy.intercept( + { + method: 'POST', + pathname: `/api/proxy/apis/v2beta1/pipelines/${mockPipeline.pipeline_id}/versions`, + }, + [mockVersion], + ); cy.intercept( { method: 'POST', @@ -175,23 +181,6 @@ const initIntercepts = () => { describe('Pipeline topology', () => { describe('Pipeline details', () => { - // TODO, remove skip after https://issues.redhat.com/browse/RHOAIENG-2282 - it.skip('Test pipeline topology renders', () => { - initIntercepts(); - - pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); - - pipelinesTopology.findTaskNode('print-msg').click(); - pipelinesTopology - .findTaskDrawer() - .findByText('$(tasks.random-num.results.Output)') - .should('exist'); - pipelinesTopology.findCloseDrawerButton().click(); - - pipelinesTopology.findTaskNode('flip-coin').click(); - pipelinesTopology.findTaskDrawer().findByText('/tmp/outputs/Output/data').should('exist'); - }); - describe('Navigation', () => { beforeEach(() => { initIntercepts(); @@ -386,16 +375,14 @@ describe('Pipeline topology', () => { beforeEach(() => { initIntercepts(); pipelineRunDetails.visit(projectId, mockRun.run_id); - pipelineRunDetails.findTaskNode('flip-coin').click(); + pipelineRunDetails.findTaskNode('create-dataset').click(); pipelineRunDetails.findRightDrawer().findRightDrawerDetailsTab().should('be.visible'); - pipelineRunDetails.findRightDrawer().findRightDrawerVolumesTab().should('be.visible'); pipelineRunDetails.findRightDrawer().findRightDrawerLogsTab().should('be.visible'); pipelineRunDetails.findRightDrawer().findRightDrawerLogsTab().click(); pipelineRunDetails.findLogsSuccessAlert().should('be.visible'); }); - // TODO, remove skip after https://issues.redhat.com/browse/RHOAIENG-2282 - it.skip('test whether the logs load in Logs tab', () => { + it.only('test whether the logs load in Logs tab', () => { pipelineRunDetails .findLogs() .contains( @@ -413,8 +400,7 @@ describe('Pipeline topology', () => { pipelineRunDetails.findRawLogs().should('not.exist'); }); - // TODO, remove skip after https://issues.redhat.com/browse/RHOAIENG-2282 - it.skip('test logs of another step', () => { + it('test logs of another step', () => { pipelineRunDetails.findStepSelect().should('not.be.disabled'); pipelineRunDetails.selectStepByName('step-copy-artifacts'); pipelineRunDetails.findLogs().contains('No logs available'); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 92236f64d0..70419f16e5 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -7,7 +7,6 @@ class PipelinesTopology { } protected wait() { - cy.findByTestId('topology'); cy.testA11y(); } diff --git a/frontend/src/concepts/pipelines/content/PipelineJobReferenceName.tsx b/frontend/src/concepts/pipelines/content/PipelineJobReferenceName.tsx index 701a3d4a2e..19afea5c00 100644 --- a/frontend/src/concepts/pipelines/content/PipelineJobReferenceName.tsx +++ b/frontend/src/concepts/pipelines/content/PipelineJobReferenceName.tsx @@ -1,16 +1,19 @@ import React from 'react'; import { Text, TextVariants } from '@patternfly/react-core'; import { getPipelineJobExecutionCount } from '~/concepts/pipelines/content/tables/utils'; -import { PipelineRunJobKFv2 } from '~/concepts/pipelines/kfTypes'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; type PipelineJobReferenceNameProps = { - resource: PipelineRunJobKFv2; + runName: string; + recurringRunId?: string; }; -const PipelineJobReferenceName: React.FC = ({ resource }) => { +const PipelineJobReferenceName: React.FC = ({ + runName, + recurringRunId, +}) => { const { getJobInformation } = usePipelinesAPI(); - const { data, loading } = getJobInformation(resource); + const { data, loading } = getJobInformation(recurringRunId); return ( <> @@ -18,7 +21,7 @@ const PipelineJobReferenceName: React.FC = ({ res 'loading...' ) : data ? ( - Run {getPipelineJobExecutionCount(resource.display_name)} of {data.display_name} + Run {getPipelineJobExecutionCount(runName)} of {data.display_name} ) : ( '' diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineServerActions.tsx b/frontend/src/concepts/pipelines/content/PipelineServerActions.tsx similarity index 100% rename from frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineServerActions.tsx rename to frontend/src/concepts/pipelines/content/PipelineServerActions.tsx diff --git a/frontend/src/concepts/pipelines/content/__tests__/utils.spec.tsx b/frontend/src/concepts/pipelines/content/__tests__/utils.spec.tsx index 1a83de0d8e..a1633c2e06 100644 --- a/frontend/src/concepts/pipelines/content/__tests__/utils.spec.tsx +++ b/frontend/src/concepts/pipelines/content/__tests__/utils.spec.tsx @@ -19,12 +19,6 @@ import { computeRunStatus } from '~/concepts/pipelines/content/utils'; const run: PipelineRunKFv2 = { created_at: '2023-09-05T16:23:25Z', storage_state: StorageStateKF.AVAILABLE, - pipeline_spec: { - runtime_config: { - parameters: {}, - pipeline_root: '', - }, - }, pipeline_version_id: 'version-id', finished_at: '2023-09-05T16:24:34Z', run_id: 'dc66a214-4df2-4d4d-a302-0d02d8bba0e7', diff --git a/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/ParamsSection.tsx b/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/ParamsSection.tsx index 35af9877f8..4c4f2a7ded 100644 --- a/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/ParamsSection.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/ParamsSection.tsx @@ -5,7 +5,7 @@ import { runPageSectionTitles, } from '~/concepts/pipelines/content/createRun/const'; import { - InputDefParamType, + InputDefinitionParameterType, PipelineVersionKFv2, RuntimeConfigParameters, } from '~/concepts/pipelines/kfTypes'; @@ -56,20 +56,20 @@ export const ParamsSection: React.FC = ({ let input: React.ReactNode; switch (parameterType) { - case InputDefParamType.NumberInteger: + case InputDefinitionParameterType.INTEGER: input = ; break; - case InputDefParamType.Boolean: + case InputDefinitionParameterType.BOOLEAN: input = ; break; - case InputDefParamType.List: - case InputDefParamType.Struct: + case InputDefinitionParameterType.LIST: + case InputDefinitionParameterType.STRUCT: input = ; break; - case InputDefParamType.NumberDouble: + case InputDefinitionParameterType.DOUBLE: input = ; break; - case InputDefParamType.String: + case InputDefinitionParameterType.STRING: input = ; } diff --git a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts index 25dc8a2b65..fbf8c8582c 100644 --- a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts +++ b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts @@ -9,7 +9,7 @@ import { CreatePipelineRunJobKFData, CreatePipelineRunKFData, DateTimeKF, - InputDefParamType, + InputDefinitionParameterType, PipelineVersionKFv2, RecurringRunMode, RuntimeConfigParameters, @@ -139,14 +139,14 @@ const normalizeInputParams = ( const paramType = inputDefinitionParams?.[paramKey].parameterType; switch (paramType) { - case InputDefParamType.NumberInteger: + case InputDefinitionParameterType.INTEGER: acc[paramKey] = parseInt(String(paramValue)); break; - case InputDefParamType.NumberDouble: + case InputDefinitionParameterType.DOUBLE: acc[paramKey] = parseFloat(String(paramValue)); break; - case InputDefParamType.Struct: - case InputDefParamType.List: + case InputDefinitionParameterType.STRUCT: + case InputDefinitionParameterType.LIST: acc[paramKey] = JSON.parse( typeof paramValue !== 'string' ? JSON.stringify(paramValue) : paramValue, ); diff --git a/frontend/src/concepts/pipelines/content/createRun/utils.ts b/frontend/src/concepts/pipelines/content/createRun/utils.ts index bddc516b45..24be4ff00c 100644 --- a/frontend/src/concepts/pipelines/content/createRun/utils.ts +++ b/frontend/src/concepts/pipelines/content/createRun/utils.ts @@ -5,7 +5,8 @@ import { SafeRunFormData, ScheduledType, } from '~/concepts/pipelines/content/createRun/types'; -import { PipelineInputParameters, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; +import { ParametersKF, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; +import { getCorePipelineSpec } from '~/concepts/pipelines/utils'; const runTypeSafeData = (runType: RunFormData['runType']): boolean => runType.type !== RunTypeOption.SCHEDULED || @@ -51,4 +52,5 @@ export const isFilledRunFormDataExperiment = (formData: RunFormData): formData i export const getInputDefinitionParams = ( version: PipelineVersionKFv2 | null | undefined, -): PipelineInputParameters | undefined => version?.pipeline_spec.root.inputDefinitions.parameters; +): ParametersKF | undefined => + getCorePipelineSpec(version?.pipeline_spec)?.root.inputDefinitions?.parameters; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index e1a74761fa..8dec7379b5 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx @@ -26,6 +26,7 @@ import usePipelineById from '~/concepts/pipelines/apiHooks/usePipelineById'; import PipelineVersionSelector from '~/concepts/pipelines/content/pipelineSelector/PipelineVersionSelector'; import DeletePipelinesModal from '~/concepts/pipelines/content/DeletePipelinesModal'; import { routePipelineDetailsNamespace, routePipelinesNamespace } from '~/routes'; +import { getCorePipelineSpec } from '~/concepts/pipelines/utils'; import PipelineDetailsActions from './PipelineDetailsActions'; import SelectedTaskDrawerContent from './SelectedTaskDrawerContent'; import PipelineNotFound from './PipelineNotFound'; @@ -198,7 +199,10 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = style={{ height: '100%' }} > diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineTaskDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineTaskDetails.tsx index 5479e0841d..d115c44e99 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineTaskDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineTaskDetails.tsx @@ -1,72 +1,75 @@ import * as React from 'react'; -import { Stack, StackItem, Title } from '@patternfly/react-core'; -import { Divider } from '@patternfly/react-core/components'; -import { PipelineRunTask } from '~/k8sTypes'; +import { Alert, Stack, StackItem } from '@patternfly/react-core'; import TaskDetailsSection from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsSection'; import TaskDetailsCodeBlock from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsCodeBlock'; -import TaskDetailsInputParams from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputParams'; -import TaskDetailsOutputResults from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsOutputResults'; -import TaskDetailsVolumeMounts from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsVolumeMounts'; +import TaskDetailsInputOutput from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput'; +import { PipelineTask } from '~/concepts/pipelines/topology'; type TaskDetailsProps = { - task: PipelineRunTask; + task: PipelineTask; }; -const PipelineTaskDetails: React.FC = ({ task }) => ( - - {task.params && ( - - - - )} - {task.taskSpec.results && ( - - ({ - name, - value: description ?? '', - }))} - /> - - )} - {task.taskSpec.steps.map((step) => ( - - {task.taskSpec.steps.length > 1 && ( - - - Step {step.name} - - - - )} - {step.args && ( - - - - - - )} - {step.command && ( - - - - - - )} - {step.image && ( +const PipelineTaskDetails: React.FC = ({ task }) => { + let groupAlert: React.ReactNode | null = null; + if (task.type === 'groupTask') { + // TODO: remove when we support group details + groupAlert = ; + } + + if (!task.inputs && !task.outputs && !task.steps) { + return ( + + {groupAlert && {groupAlert}} + No content + + ); + } + + return ( + + {groupAlert && {groupAlert}} + {task.inputs && ( + + ({ label: a.label, value: a.type }))} + params={task.inputs.params?.map((p) => ({ label: p.label, value: p.value ?? p.type }))} + /> + + )} + {task.outputs && ( + + ({ label: a.label, value: a.type }))} + params={task.outputs.params?.map((p) => ({ label: p.label, value: p.value ?? p.type }))} + /> + + )} + {task.steps?.map((step, i) => ( + {step.image} - )} - - ))} - {task.taskSpec.stepTemplate?.volumeMounts && ( - - - - )} -   - -); + {step.command && ( + + + + + + )} + {step.args && ( + + + + + + )} + + ))} +   + + ); +}; export default PipelineTaskDetails; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/SelectedTaskDrawerContent.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/SelectedTaskDrawerContent.tsx index d73b859d0b..4cc4eaa75d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/SelectedTaskDrawerContent.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/SelectedTaskDrawerContent.tsx @@ -7,11 +7,11 @@ import { DrawerPanelContent, Title, } from '@patternfly/react-core'; -import { PipelineRunTask } from '~/k8sTypes'; +import { PipelineTask } from '~/concepts/pipelines/topology'; import PipelineTaskDetails from './PipelineTaskDetails'; type SelectedTaskDrawerContentProps = { - task?: PipelineRunTask; + task?: PipelineTask; onClose: () => void; }; @@ -29,8 +29,7 @@ const SelectedTaskDrawerContent: React.FC = ({ t > - {task.taskSpec.metadata?.annotations?.['pipelines.kubeflow.org/task_display_name'] || - task.name} + {task.name} {task.type === 'artifact' ? 'artifact details' : ''} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index 612ec129d4..c5b6a52338 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -30,21 +30,19 @@ import { import DeletePipelineRunsModal from '~/concepts/pipelines/content/DeletePipelineRunsModal'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import PipelineDetailsTitle from '~/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle'; -import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobReferenceName'; import { PipelineTopology, PipelineTopologyEmpty } from '~/concepts/topology'; import usePipelineVersionById from '~/concepts/pipelines/apiHooks/usePipelineVersionById'; import { usePipelineTaskTopology } from '~/concepts/pipelines/topology'; -import usePipelineRunJobById from '~/concepts/pipelines/apiHooks/usePipelineRunJobById'; import { PipelineRunType } from '~/pages/pipelines/global/runs/types'; import { routePipelineRunsNamespace } from '~/routes'; +import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobReferenceName'; const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, contextPath }) => { const { pipelineRunId } = useParams(); const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); - const [runResource, loaded, error] = usePipelineRunById(pipelineRunId, true); - const [job] = usePipelineRunJobById(runResource?.recurring_run_id); - const [version] = usePipelineVersionById( + const [runResource, runLoaded, runError] = usePipelineRunById(pipelineRunId, true); + const [version, versionLoaded, versionError] = usePipelineVersionById( runResource?.pipeline_version_reference.pipeline_id, runResource?.pipeline_version_reference.pipeline_version_id, ); @@ -53,8 +51,13 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, RunDetailsTabs.DETAILS, ); const [selectedId, setSelectedId] = React.useState(null); - const { taskMap, nodes } = usePipelineTaskTopology(version?.pipeline_spec); + const { taskMap, nodes } = usePipelineTaskTopology( + version?.pipeline_spec, + runResource ?? undefined, + ); + const loaded = versionLoaded && runLoaded; + const error = versionError || runError; if (!loaded && !error) { return ( @@ -83,9 +86,6 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, panelContent={ setSelectedId(null)} /> } @@ -101,9 +101,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, setSelectedId(null); }} pipelineRunDetails={ - runResource && version?.pipeline_spec - ? { kf: runResource, kind: version.pipeline_spec } - : undefined + runResource && version?.pipeline_spec ? runResource : undefined } /> } @@ -116,9 +114,16 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, 'Error loading run' ) } - jobReferenceName={job && } + subtext={ + runResource && ( + + ) + } description={ - runResource ? ( + runResource?.description ? ( ) : ( '' diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerBottomTabs.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerBottomTabs.tsx index 8c27aa454f..d1fcc6d22d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerBottomTabs.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerBottomTabs.tsx @@ -23,11 +23,7 @@ export type RunDetailsTabSelection = RunDetailsTabs | null; type PipelineRunBottomDrawerProps = { selection: RunDetailsTabSelection; onSelection: (id: RunDetailsTabs) => void; - pipelineRunDetails?: { - // TODO need to get pipeline runtime for v2. https://issues.redhat.com/browse/RHOAIENG-2297 - kind: unknown; - kf: PipelineRunKFv2 | PipelineRunJobKFv2; - }; + pipelineRunDetails?: PipelineRunKFv2 | PipelineRunJobKFv2; }; export const PipelineRunDrawerBottomTabs: React.FC = ({ @@ -35,7 +31,7 @@ export const PipelineRunDrawerBottomTabs: React.FC onSelection, pipelineRunDetails, }) => { - const isJob = pipelineRunDetails?.kf && isPipelineRunJob(pipelineRunDetails.kf); + const isJob = pipelineRunDetails && isPipelineRunJob(pipelineRunDetails); return ( <> @@ -66,9 +62,8 @@ export const PipelineRunDrawerBottomTabs: React.FC hidden={RunDetailsTabs.DETAILS !== selection} > activeKey={selection} hidden={RunDetailsTabs.PARAMETERS !== selection} > - + {!isJob && ( // do not include yaml tab for jobs style={{ height: '100%' }} > void; }; const PipelineRunDrawerRightContent: React.FC = ({ task, - taskReferences, - parameters, onClose, }) => { if (!task) { @@ -39,20 +34,15 @@ const PipelineRunDrawerRightContent: React.FC - {task.taskSpec.metadata?.annotations?.['pipelines.kubeflow.org/task_display_name'] || - task.name} + {task.name} {task.type === 'artifact' ? 'artifact details' : ''} - {task.runDetails && {task.runDetails.status?.podName}} + {task.status?.podName && {task.status.podName}} - + ); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightTabs.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightTabs.tsx index c07933a156..045dcb09f4 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightTabs.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightTabs.tsx @@ -1,18 +1,16 @@ import * as React from 'react'; import { DrawerPanelBody, Tab, TabContent, Tabs } from '@patternfly/react-core'; import SelectedNodeDetailsTab from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeDetailsTab'; -import { PipelineRunTaskDetails, TaskReferenceMap } from '~/concepts/pipelines/content/types'; import SelectedNodeInputOutputTab from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab'; -import SelectedNodeVolumeMountsTab from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeVolumeMountsTab'; -import { PipelineRunTaskParam } from '~/k8sTypes'; import LogsTab from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTab'; import './PipelineRunDrawer.scss'; +import { PipelineTask } from '~/concepts/pipelines/topology'; enum PipelineRunNodeTabs { INPUT_OUTPUT = 'inputoutput', // VISUALIZATIONS = 'Visualizations', DETAILS = 'details', - VOLUMES = 'volumes', + // VOLUMES = 'volumes', LOGS = 'logs', // POD = 'Pod', // EVENTS = 'Events', @@ -22,34 +20,21 @@ enum PipelineRunNodeTabs { const PipelineRunNodeTabsTitles = { [PipelineRunNodeTabs.INPUT_OUTPUT]: 'Input / Output', [PipelineRunNodeTabs.DETAILS]: 'Details', - [PipelineRunNodeTabs.VOLUMES]: 'Volumes', [PipelineRunNodeTabs.LOGS]: 'Logs', }; type PipelineRunDrawerRightTabsProps = { - task: PipelineRunTaskDetails; - taskReferences: TaskReferenceMap; - parameters?: PipelineRunTaskParam[]; + task: PipelineTask; }; -const PipelineRunDrawerRightTabs: React.FC = ({ - task, - taskReferences, - parameters, -}) => { +const PipelineRunDrawerRightTabs: React.FC = ({ task }) => { const [selection, setSelection] = React.useState(PipelineRunNodeTabs.INPUT_OUTPUT); const tabContents: Record = { - [PipelineRunNodeTabs.INPUT_OUTPUT]: ( - - ), + [PipelineRunNodeTabs.INPUT_OUTPUT]: , // [PipelineRunNodeTabs.VISUALIZATIONS]: <>TBD 2, [PipelineRunNodeTabs.DETAILS]: , - [PipelineRunNodeTabs.VOLUMES]: , + // [PipelineRunNodeTabs.VOLUMES]: , [PipelineRunNodeTabs.LOGS]: , // [PipelineRunNodeTabs.POD]: <>TBD 6, // [PipelineRunNodeTabs.EVENTS]: <>TBD 7, diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeDetailsTab.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeDetailsTab.tsx index 079abb0099..80930fdc4a 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeDetailsTab.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeDetailsTab.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; -import { Tooltip } from '@patternfly/react-core'; import { asTimestamp, DetailItem, renderDetailItems, } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils'; -import { PipelineRunTaskDetails } from '~/concepts/pipelines/content/types'; import { relativeDuration } from '~/utilities/time'; +import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; +import { PipelineTask } from '~/concepts/pipelines/topology'; type SelectedNodeDetailsTabProps = { - task: PipelineRunTaskDetails; + task: PipelineTask; }; const SelectedNodeDetailsTab: React.FC = ({ task }) => { @@ -17,47 +17,41 @@ const SelectedNodeDetailsTab: React.FC = ({ task }) const taskName = { key: 'Task name', - value: - task.taskSpec.metadata?.annotations?.['pipelines.kubeflow.org/task_display_name'] || - task.name, + value: task.name, }; - if (task.skipped) { - details = [taskName, { key: 'Status', value: 'Skipped' }]; - } else if (task.runDetails) { - const startDate = - task.runDetails.status?.startTime && new Date(task.runDetails.status.startTime); - const endDate = - task.runDetails.status?.completionTime && new Date(task.runDetails.status.completionTime); - const statusCondition = task.runDetails.status?.conditions?.find((c) => c.type === 'Succeeded'); + if (task.status) { + const { startTime, completeTime, state } = task.status; + const skipped = state === RuntimeStateKF.SKIPPED; - details = [ - { key: 'Task ID', value: task.runDetails.runID || '-' }, - taskName, - { - key: 'Status', - value: statusCondition ? ( - -
{statusCondition.reason}
-
- ) : ( - '-' - ), - }, - { - key: 'Started at', - value: startDate ? asTimestamp(startDate) : '-', - }, - { - key: 'Finished at', - value: endDate ? asTimestamp(endDate) : '-', - }, - { - key: 'Duration', - value: - startDate && endDate ? relativeDuration(endDate.getTime() - startDate.getTime()) : '-', - }, - ]; + if (skipped) { + details = [taskName, { key: 'Status', value: 'Skipped' }]; + } else { + const startDate = startTime && new Date(startTime); + const endDate = completeTime && new Date(completeTime); + + details = [ + { key: 'Task ID', value: task.status.taskId || '-' }, + taskName, + { + key: 'Status', + value: state ?? '-', + }, + { + key: 'Started at', + value: startDate ? asTimestamp(startDate) : '-', + }, + { + key: 'Finished at', + value: endDate ? asTimestamp(endDate) : '-', + }, + { + key: 'Duration', + value: + startDate && endDate ? relativeDuration(endDate.getTime() - startDate.getTime()) : '-', + }, + ]; + } } else { details = [taskName]; } diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx index 95b2f22c4c..76f755e789 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx @@ -1,64 +1,38 @@ import * as React from 'react'; import { Stack, StackItem } from '@patternfly/react-core'; -import { PipelineRunTaskDetails, TaskReferenceMap } from '~/concepts/pipelines/content/types'; -import { - getNameAndPathFromTaskRef, - getValue, - getParamName, -} from '~/concepts/pipelines/topology/pipelineUtils'; -import TaskDetailsInputParams from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputParams'; -import TaskDetailsOutputResults from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsOutputResults'; -import { PipelineRunTaskParam } from '~/k8sTypes'; +import TaskDetailsInputOutput from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput'; +import { PipelineTask } from '~/concepts/pipelines/topology'; type SelectedNodeInputOutputTabProps = { - task: PipelineRunTaskDetails; - taskReferences: TaskReferenceMap; - parameters?: PipelineRunTaskParam[]; + task: PipelineTask; }; -const SelectedNodeInputOutputTab: React.FC = ({ - task, - taskReferences, - parameters, -}) => { - const params = - task.params?.map((p) => { - const paramName = getParamName(p.value); - if (paramName && parameters) { - const paramFromParameter = - parameters.find((result) => result.name === paramName)?.value ?? p.value; - return { ...p, value: paramFromParameter }; - } - const ref = getNameAndPathFromTaskRef(p.value); - if (!ref) { - return p; - } - const [name, path] = ref; - const refTask = taskReferences[name]; - const value = (refTask ? getValue(refTask, path) : null) ?? p.value; - return { ...p, value }; - }) ?? []; - - const results = task.runDetails?.status?.taskResults ?? []; - - if (params.length === 0 && results.length === 0) { +const SelectedNodeInputOutputTab: React.FC = ({ task }) => { + if (!task.inputs && !task.outputs) { return <>No content; } return ( - {params.length > 0 && ( + {task.inputs && ( - + ({ label: a.label, value: a.type }))} + params={task.inputs.params?.map((p) => ({ label: p.label, value: p.value ?? p.type }))} + /> )} - {results.length > 0 && ( + {task.outputs && ( - + ({ label: a.label, value: a.type }))} + params={task.outputs.params?.map((p) => ({ label: p.label, value: p.value ?? p.type }))} + /> )} ); }; - export default SelectedNodeInputOutputTab; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeVolumeMountsTab.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeVolumeMountsTab.tsx deleted file mode 100644 index 1fa1c34267..0000000000 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeVolumeMountsTab.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; -import { PipelineRunTaskDetails } from '~/concepts/pipelines/content/types'; -import { PipelineRunTaskVolumeMount } from '~/k8sTypes'; -import { renderDetailItems } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils'; - -type SelectedNodeVolumeMountsTabProps = { - task: PipelineRunTaskDetails; -}; - -const SelectedNodeVolumeMountsTab: React.FC = ({ task }) => { - const items = - task.runDetails?.status?.taskSpec?.steps?.reduce>( - (acc, step) => { - step.volumeMounts?.forEach((mountPath) => { - acc[mountPath.name] = mountPath; - }); - - return acc; - }, - {}, - ) ?? null; - - if (!items || Object.keys(items).length === 0) { - return <>No content; - } - - return ( - <> - {renderDetailItems( - Object.values(items).map(({ name, mountPath }) => ({ key: mountPath, value: name })), - )} - - ); -}; - -export default SelectedNodeVolumeMountsTab; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTab.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTab.tsx index c689b7c9ac..0adb424e84 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTab.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTab.tsx @@ -1,16 +1,16 @@ import React, { useEffect } from 'react'; import { Button, - Tooltip, + DropdownList, + Icon, Spinner, Stack, StackItem, - ToolbarContent, Toolbar, - ToolbarItem, + ToolbarContent, ToolbarGroup, - DropdownList, - Icon, + ToolbarItem, + Tooltip, } from '@patternfly/react-core'; import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/deprecated'; import { OutlinedPlayCircleIcon } from '@patternfly/react-icons/dist/esm/icons/outlined-play-circle-icon'; @@ -27,25 +27,24 @@ import { OutlinedWindowRestoreIcon, } from '@patternfly/react-icons'; import DashboardLogViewer from '~/concepts/dashboard/DashboardLogViewer'; -import { PipelineRunTaskDetails } from '~/concepts/pipelines/content/types'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import useFetchLogs from '~/concepts/k8s/pods/useFetchLogs'; import usePodContainerLogState from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/usePodContainerLogState'; import LogsTabStatus from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/LogsTabStatus'; import { LOG_TAIL_LINES } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/const'; -import { downloadCurrentStepLog, downloadAllStepLogs } from '~/concepts/k8s/pods/utils'; +import { downloadAllStepLogs, downloadCurrentStepLog } from '~/concepts/k8s/pods/utils'; import usePodStepsStates from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/usePodStepsStates'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import DownloadDropdown from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/runLogs/DownloadDropdown'; import { PodStepStateType } from '~/types'; import useDebounceCallback from '~/utilities/useDebounceCallback'; +import { PipelineTask } from '~/concepts/pipelines/topology'; +import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; // TODO: If this gets large enough we should look to make this its own component file -const LogsTab: React.FC<{ task: PipelineRunTaskDetails }> = ({ task }) => { - const podName = task.runDetails?.status?.podName; - const isFailedPod = !!task.runDetails?.status?.conditions?.find((c) => - c.reason?.includes('Failed'), - ); +const LogsTab: React.FC<{ task: PipelineTask }> = ({ task }) => { + const podName = task.status?.podName; + const isFailedPod = task.status?.state === RuntimeStateKF.FAILED; if (!podName) { return <>No content; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx index e8f6d913ee..510fbac0db 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx @@ -29,10 +29,10 @@ import { import DeletePipelineRunsModal from '~/concepts/pipelines/content/DeletePipelineRunsModal'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import usePipelineRunJobById from '~/concepts/pipelines/apiHooks/usePipelineRunJobById'; -import PipelineRunDrawerRightContent from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent'; import usePipelineVersionById from '~/concepts/pipelines/apiHooks/usePipelineVersionById'; import { PipelineRunType } from '~/pages/pipelines/global/runs'; import { routePipelineRunsNamespace } from '~/routes'; +import SelectedTaskDrawerContent from '~/concepts/pipelines/content/pipelinesDetails/pipeline/SelectedTaskDrawerContent'; import PipelineRunJobDetailsActions from './PipelineRunJobDetailsActions'; const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ @@ -42,8 +42,8 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ const { pipelineRunJobId } = useParams(); const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); - const [job, loaded, error] = usePipelineRunJobById(pipelineRunJobId); - const [version] = usePipelineVersionById( + const [job, jobLoaded, jobError] = usePipelineRunJobById(pipelineRunJobId); + const [version, versionLoaded, versionError] = usePipelineVersionById( job?.pipeline_version_reference.pipeline_id, job?.pipeline_version_reference.pipeline_version_id, ); @@ -53,9 +53,11 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ ); const [selectedId, setSelectedId] = React.useState(null); - // TODO need to get pipeline runtime for v2. but should jobs really have a graph view? https://issues.redhat.com/browse/RHOAIENG-2297 const { taskMap, nodes } = usePipelineTaskTopology(version?.pipeline_spec); + const loaded = versionLoaded && jobLoaded; + const error = versionError || jobError; + if (!loaded && !error) { return ( @@ -81,11 +83,8 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ setSelectedId(null)} /> } @@ -100,11 +99,7 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ setDetailsTab(selection); setSelectedId(null); }} - pipelineRunDetails={ - job && version?.pipeline_spec - ? { kf: job, kind: version.pipeline_spec } - : undefined - } + pipelineRunDetails={job && version?.pipeline_spec ? job : undefined} /> } > diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx new file mode 100644 index 0000000000..664cc1d0cc --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import TaskDetailsSection from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsSection'; +import TaskDetailsPrintKeyValues from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues'; + +type TaskDetailsInputOutputProps = { + type: 'Input' | 'Output'; + artifacts?: React.ComponentProps['items']; + params?: React.ComponentProps['items']; +}; + +const TaskDetailsInputOutput: React.FC = ({ + artifacts, + params, + type, +}) => { + if (!params && !artifacts) { + return null; + } + + return ( + + {artifacts && ( + + + + + + )} + {params && ( + + + + + + )} + + ); +}; + +export default TaskDetailsInputOutput; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputParams.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputParams.tsx deleted file mode 100644 index 5fa58e4ee2..0000000000 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputParams.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import TaskDetailsSection from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsSection'; -import { PipelineRunTaskParam } from '~/k8sTypes'; -import TaskDetailsPrintKeyValues from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues'; - -type TaskDetailsInputParamsProps = { - params: PipelineRunTaskParam[]; -}; - -const TaskDetailsInputParams: React.FC = ({ params }) => ( - - - -); - -export default TaskDetailsInputParams; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsOutputResults.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsOutputResults.tsx deleted file mode 100644 index c227dfe6d5..0000000000 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsOutputResults.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import TaskDetailsSection from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsSection'; -import TaskDetailsPrintKeyValues from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues'; - -type TaskDetailsOutputResultsProps = { - results: { name: string; value: string }[]; -}; - -const TaskDetailsOutputResults: React.FC = ({ results }) => ( - - - -); - -export default TaskDetailsOutputResults; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx index 1e53303134..33b7be52ac 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx @@ -1,18 +1,20 @@ import * as React from 'react'; -import { Grid, GridItem } from '@patternfly/react-core'; +import { Grid, GridItem, Truncate } from '@patternfly/react-core'; type TaskDetailsPrintKeyValuesProps = { - items: { name: string; value: string }[]; + items: { label: string; value: string }[]; }; const TaskDetailsPrintKeyValues: React.FC = ({ items }) => ( {items.map((result, i) => ( - - {result.name} + + + + - {result.value} + {result.value} ))} diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowTitle.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowTitle.tsx index 9782390400..ce00f2c857 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowTitle.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowTitle.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Label, Tooltip } from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import { TableText } from '@patternfly/react-table'; import { TableRowTitleDescription } from '~/components/table'; import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import { PipelineRunLabels } from '~/concepts/pipelines/content/tables/utils'; import { routePipelineRunDetailsNamespace } from '~/routes'; +import PipelineRunTypeLabel from '~/concepts/pipelines/content/PipelineRunTypeLabel'; +import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobReferenceName'; type PipelineRunTableRowTitleProps = { run: PipelineRunKFv2; @@ -22,15 +22,15 @@ const PipelineRunTableRowTitle: React.FC = ({ run {run.display_name} } + subtitle={ + + } description={run.description} descriptionAsMarkdown - label={ - - - - } + label={} /> ); }; diff --git a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx index cfd13d8706..eb627290f5 100644 --- a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx +++ b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx @@ -134,7 +134,7 @@ export const RunJobScheduled: RunJobUtil = ({ job }) => { } break; case ScheduledState.UNBOUNDED_END: - return <>On-going; + return <>No end; default: } diff --git a/frontend/src/concepts/pipelines/content/types.ts b/frontend/src/concepts/pipelines/content/types.ts index b3e1a80b03..0db3a3d417 100644 --- a/frontend/src/concepts/pipelines/content/types.ts +++ b/frontend/src/concepts/pipelines/content/types.ts @@ -1,7 +1,5 @@ import * as React from 'react'; import { BreadcrumbItem } from '@patternfly/react-core'; -import { PipelineRunTask, PipelineRunTaskRunStatus } from '~/k8sTypes'; -import { createNode } from '~/concepts/topology'; export type PathProps = { breadcrumbPath: React.ReactElement[]; @@ -10,23 +8,6 @@ export type PathProps = { export type PipelineCoreDetailsPageComponent = React.FC; -export type PipelineRunTaskRunDetails = { - runID: string; -} & PipelineRunTaskRunStatus; - -export type PipelineRunTaskDetails = { - runDetails?: PipelineRunTaskRunDetails; - skipped: boolean; -} & PipelineRunTask; - -/** [Task.name]: Task */ -export type TaskReferenceMap = Record; - -export type KubeFlowTaskTopology = { - taskMap: TaskReferenceMap; - nodes: ReturnType[]; -}; - export enum PipelineRunSearchParam { RunType = 'runType', TriggerType = 'triggerType', diff --git a/frontend/src/concepts/pipelines/context/useJobRelatedInformation.ts b/frontend/src/concepts/pipelines/context/useJobRelatedInformation.ts index 21b808a229..8f87b237eb 100644 --- a/frontend/src/concepts/pipelines/context/useJobRelatedInformation.ts +++ b/frontend/src/concepts/pipelines/context/useJobRelatedInformation.ts @@ -7,7 +7,7 @@ type JobStatus = { data: PipelineRunJobKFv2 | null; }; -export type GetJobInformation = (resource?: PipelineRunJobKFv2) => JobStatus; +export type GetJobInformation = (recurringRunId?: string) => JobStatus; const useJobRelatedInformation = ( apiState: PipelineAPIState, @@ -19,14 +19,14 @@ const useJobRelatedInformation = ( return { getJobInformation: React.useCallback( - (resource) => { + (recurringRunId) => { if (!apiState.apiAvailable) { return { loading: false, data: null }; } - if (!resource) { + if (!recurringRunId) { return { loading: false, data: null }; } - const jobId = resource.recurring_run_id; + const jobId = recurringRunId; if (jobStorage?.[jobId]) { return jobStorage[jobId]; } diff --git a/frontend/src/concepts/pipelines/kfTypes.ts b/frontend/src/concepts/pipelines/kfTypes.ts index 5a874a72b6..9344b06ce8 100644 --- a/frontend/src/concepts/pipelines/kfTypes.ts +++ b/frontend/src/concepts/pipelines/kfTypes.ts @@ -1,4 +1,4 @@ -import { ExactlyOne } from '~/typeHelpers'; +import { EitherNotBoth, ExactlyOne } from '~/typeHelpers'; /* Types pulled from https://www.kubeflow.org/docs/components/pipelines/v1/reference/api/kubeflow-pipeline-api-spec */ // TODO: Determine what is optional and what is not @@ -48,6 +48,17 @@ export type PipelineKFCallCommon = { // Note: if Kubeflow backend determines no results, it doesn't even give you your structure, you get an empty object } & Partial; +export enum ArtifactType { + ARTIFACT = 'system.Artifact', + DATASET = 'system.Dataset', + MODEL = 'system.Model', + METRICS = 'system.Metrics', + CLASSIFICATION_METRICS = 'system.ClassificationMetrics', + SLICED_CLASSIFICATION_METRICS = 'system.SlicedClassificationMetrics', + HTML = 'system.HTML', + MARKDOWN = 'system.Markdown', +} + /** @deprecated resource type is no longer a concept in v2 */ export enum ResourceTypeKF { UNKNOWN_RESOURCE_TYPE = 'UNKNOWN_RESOURCE_TYPE', @@ -66,15 +77,6 @@ export enum RelationshipKF { CREATOR = 'CREATOR', } -/** - * @deprecated - * Replace with RunStorageStateKFv2 - */ -export enum RunStorageStateKF { - AVAILABLE = 'STORAGESTATE_AVAILABLE', - ARCHIVED = 'STORAGESTATE_ARCHIVED', -} - export enum RunMetricFormatKF { UNSPECIFIED = 'UNSPECIFIED', RAW = 'RAW', @@ -121,6 +123,7 @@ export enum StorageStateKF { ARCHIVED = 'ARCHIVED', } +/** @deprecated */ export type ParameterKF = { name: string; value: string; @@ -142,35 +145,181 @@ export type PipelineVersionKF = { }; // https://github.com/kubeflow/pipelines/blob/0b1553eb05ea44fdf720efdc91ef71cc5ac557ea/api/v2alpha1/pipeline_spec.proto#L416 -export enum InputDefParamType { - NumberDouble = 'NUMBER_DOUBLE', - NumberInteger = 'NUMBER_INTEGER', - String = 'STRING', - Boolean = 'BOOLEAN', - List = 'LIST', - Struct = 'STRUCT', +export enum InputDefinitionParameterType { + DOUBLE = 'NUMBER_DOUBLE', + INTEGER = 'NUMBER_INTEGER', + BOOLEAN = 'BOOLEAN', + STRING = 'STRING', + LIST = 'LIST', + STRUCT = 'STRUCT', } -export type PipelineInputParameters = Record; +export type InputOutputArtifactType = { + schemaTitle: ArtifactType; + schemaVersion: string; +}; + +export type InputOutputDefinition = { + artifacts?: Record< + string, + { + artifactType: InputOutputArtifactType; + } + >; + parameters?: ParametersKF; +}; -// https://www.kubeflow.org/docs/components/pipelines/v2/reference/api/kubeflow-pipeline-api-spec/#/definitions/v2beta1PipelineVersion -export type PipelineSpec = Record & { +type GroupNodeComponent = { + dag: DAG; +}; + +type SingleNodeComponent = { + /** @see PipelineExecutorsKF */ + executorLabel: string; +}; + +export type PipelineComponentKF = EitherNotBoth & { + inputDefinitions?: InputOutputDefinition; + outputDefinitions?: InputOutputDefinition; +}; + +/** + * Component definitions, mapped by the name of the component. + * @see TaskKF.componentRef.name + */ +export type PipelineComponentsKF = Record; + +/** + * High-level concept of a part of the pipeline. + * + * These are the items that will convert into nodes that at run have statuses. + * Artifacts nodes do not have statuses and are not tasks, but they are bi-products of what tasks do. + */ +export type TaskKF = { + cachingOptions: { + enableCache: boolean; + }; + taskInfo: { + /** Node name */ + name: string; + }; + componentRef: { + /** @see PipelineComponentsKF */ + name: string; + }; + /** + * References the name of the task id to run after + * aka 'runAfter' from topology + * @see DAG.tasks + */ + dependentTasks?: string[]; + inputs?: { + artifacts?: Record< + string, + { + taskOutputArtifact?: { + /** Artifact node name */ + outputArtifactKey: string; + /** + * The task string for runAfter + * @see DAG.tasks + */ + producerTask: string; + }; + } + >; + parameters?: Record< + string, + { + /** @see PipelineSpec.root.inputDefinitions.parameters */ + componentInputParameter?: string; + runtimeValue?: { + constant: boolean; + }; + } + >; + }; +}; + +export type DAG = { + tasks: Record; + // TODO: determine if there are more properties +}; + +export type ParameterKFV2 = { + parameterType: InputDefinitionParameterType; +}; + +export type ParametersKF = Record; + +export type PipelineExecutorKF = { + container: { + args?: string[]; + command?: string[]; + image: string; + }; +}; + +export type PipelineExecutorsKF = Record< + /** + * Relationship with executorLabel under PipelineComponentKF + * @see SingleNodeComponent + */ + string, + PipelineExecutorKF | undefined +>; + +/** + * IR Templates + * + * To read the flow: + * - root.dag.; aka (task) + * - (task).componentRef.name => components.; aka (component) + * - (component)s can be a looped dag, aka drilling + * - (component).executorLabel => deploymentSpec.executors. + */ +export type PipelineSpec = { + /** Internal details about each node */ + components: PipelineComponentsKF; + /** Infrastructure & execution information */ + deploymentSpec: { + /** Details about the functionality of the execution; aka "steps" */ + executors: PipelineExecutorsKF; + }; pipelineInfo: { name: string; }; - root: Record & { - inputDefinitions: { - parameters: PipelineInputParameters; + root: { + dag: DAG; + inputDefinitions?: { + parameters: ParametersKF; }; }; schemaVersion: string; sdkVersion: string; }; +export type PlatformSpec = { + platforms: { + kubernetes?: { + deploymentSpec: { + executors: PipelineExecutorsKF; + }; + }; + }; +}; + +export type PipelineSpecVolume = { + pipeline_spec: PipelineSpec; + platmform_spec: PlatformSpec; +}; + +export type PipelineSpecVariable = EitherNotBoth; + export type PipelineVersionKFv2 = PipelineCoreResourceKFv2 & { pipeline_id: string; pipeline_version_id: string; - pipeline_spec: PipelineSpec; + pipeline_spec: PipelineSpecVariable; code_source_url?: string; package_url?: UrlKF; error?: GoogleRpcStatusKF; @@ -312,19 +461,18 @@ export const runtimeStateLabels = { export type TaskDetailKF = { run_id: string; task_id: string; - display_name: string; - create_time: string; - start_time: string; - end_time: string; - executor_detail?: PipelineTaskExecutorDetailKF; - state: RuntimeStateKF; + create_time: DateTimeKF; + start_time: DateTimeKF; + end_time: DateTimeKF; + display_name?: string; execution_id?: string; + executor_detail?: PipelineTaskExecutorDetailKF; + state?: RuntimeStateKF; error?: GoogleRpcStatusKF; inputs?: ArtifactListKF; outputs?: ArtifactListKF; parent_task_id?: string; - state_history: RuntimeStatusKF[]; - pod_name?: string; + state_history?: RuntimeStatusKF[]; child_tasks?: PipelineTaskDetailChildTask[]; }; @@ -345,7 +493,7 @@ type PipelineTaskExecutorDetailKF = { }; export type RuntimeStatusKF = { - update_time: string; + update_time: DateTimeKF; state: RuntimeStateKF; error?: GoogleRpcStatusKF; }; @@ -360,7 +508,7 @@ export type PipelineRunKFv2 = PipelineCoreResourceKFv2 & { run_id: string; storage_state: StorageStateKF; pipeline_version_id?: string; - pipeline_spec?: object; + pipeline_spec?: PipelineSpecVariable; pipeline_version_reference: PipelineVersionReferenceKF; runtime_config: PipelineSpecRuntimeConfig; service_account: string; diff --git a/frontend/src/concepts/pipelines/topology/__tests__/parseUtils.spec.ts b/frontend/src/concepts/pipelines/topology/__tests__/parseUtils.spec.ts new file mode 100644 index 0000000000..7561ba207b --- /dev/null +++ b/frontend/src/concepts/pipelines/topology/__tests__/parseUtils.spec.ts @@ -0,0 +1,17 @@ +describe('pipeline topology parseUtils', () => { + it('need tests!', () => { + expect(true).toBe(true); + }); + + describe('parseInputOutput', () => { + // TODO + }); + + describe('parseRuntimeInfo', () => { + // TODO + }); + + describe('translateStatusForNode', () => { + // TODO + }); +}); diff --git a/frontend/src/concepts/pipelines/topology/__tests__/pipelineUtils.spec.ts b/frontend/src/concepts/pipelines/topology/__tests__/pipelineUtils.spec.ts deleted file mode 100644 index bf3e08cebe..0000000000 --- a/frontend/src/concepts/pipelines/topology/__tests__/pipelineUtils.spec.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { RunStatus } from '@patternfly/react-topology'; -import { - getNameAndPathFromTaskRef, - getNameFromTaskRef, - getRunStatus, - getValue, - paramAsRunAfter, - whenAsRunAfter, -} from '~/concepts/pipelines/topology/pipelineUtils'; -import { - PipelineRunTaskParam, - PipelineRunTaskRunStatusProperties, - PipelineRunTaskWhen, -} from '~/k8sTypes'; -import { PipelineRunTaskDetails } from '~/concepts/pipelines/content/types'; - -const taskRef = '$(tasks.random-num.results.Output)'; - -const param: PipelineRunTaskParam = { - name: 'operator', - value: '5', -}; - -const when: PipelineRunTaskWhen = { - input: '', - operator: 'in', - values: ['true'], -}; - -const status: PipelineRunTaskRunStatusProperties = { - conditions: [ - { - type: 'Succeeded', - status: 'True', - lastTransitionTime: '2023-10-20T07:32:06Z', - reason: 'Succeeded', - message: 'All Steps have completed executing', - }, - ], - podName: 'conditional-execution-pipeline-5fe97-condition-2-pod', - startTime: '2023-10-20T07:32:00Z', -}; - -const task: PipelineRunTaskDetails = { - name: 'flip-coin', - runDetails: { - pipelineTaskName: 'flip-coin', - runID: 'conditional-execution-pipeline-6a55f-flip-coin', - status: { - conditions: [ - { - type: 'Succeeded', - status: 'True', - lastTransitionTime: '2023-10-21T22:11:11Z', - reason: 'Succeeded', - message: 'All Steps have completed executing', - }, - ], - podName: 'conditional-execution-pipeline-6a55f-flip-coin-pod', - startTime: '2023-10-21T22:11:02Z', - taskSpec: { - results: [ - { - name: 'Output', - type: 'string', - }, - ], - steps: [{}], - }, - taskResults: [ - { - name: 'Output', - type: 'string', - value: 'heads', - }, - ], - }, - }, - skipped: true, - taskSpec: { - steps: [ - { - name: 'main', - image: 'python:alpine3.6', - command: [ - 'sh', - '-ec', - 'program_path=$(mktemp)\nprintf "%s" "$0" > "$program_path"\npython3 -u "$program_path" "$@"\n', - ], - }, - ], - results: [ - { - name: 'Output', - type: 'string', - }, - ], - }, -}; - -describe('getNameAndPathFromTaskRef', () => { - it("should return null when name and task from path ref doesn't match", () => { - const taskRefMock = 'random-string'; - const nameandTaskfromPath = getNameAndPathFromTaskRef(taskRefMock); - expect(nameandTaskfromPath).toBe(null); - }); - - it('should check for name and task from path ref', () => { - const nameandTaskfromPath = getNameAndPathFromTaskRef(taskRef); - expect(nameandTaskfromPath?.[0]).toBe('random-num'); - expect(nameandTaskfromPath?.[1]).toBe('results.Output'); - }); -}); - -describe('getNameFromTaskRef', () => { - it('should get null from task ref', () => { - const taskRefMock = 'random-string'; - const nameFromTaskRef = getNameFromTaskRef(taskRefMock); - expect(nameFromTaskRef).toBe(null); - }); - - it('should get name from task ref', () => { - const nameFromTaskRef = getNameFromTaskRef(taskRef); - expect(nameFromTaskRef).toBe('random-num'); - }); -}); - -describe('paramAsRunAfter', () => { - it('should return null for param as run after', () => { - const paramasRunAfter = paramAsRunAfter(param); - expect(paramasRunAfter).toBe(null); - }); - - it('should return name for param as run after', () => { - param.value = '$(tasks.random-num.results.Output)'; - const paramasRunAfter = paramAsRunAfter(param); - expect(paramasRunAfter).toBe('random-num'); - }); -}); - -describe('whenAsRunAfter', () => { - it('should return null for when as run after', () => { - const whenasRunAfter = whenAsRunAfter(when); - expect(whenasRunAfter).toBe(null); - }); - - it('should return name for when as run after', () => { - when.input = '$(tasks.condition-1.results.outcome)'; - const whenasRunAfter = whenAsRunAfter(when); - expect(whenasRunAfter).toBe('condition-1'); - }); -}); - -describe('getRunStatus', () => { - it('should get run status as Succeeded', () => { - const getrunStatus = getRunStatus(status); - expect(getrunStatus).toBe(RunStatus.Succeeded); - }); - - it('should get run status as failed', () => { - (status.conditions || [])[0].status = 'False'; - const getrunStatus = getRunStatus(status); - expect(getrunStatus).toBe(RunStatus.Failed); - }); - - it('should get run status as running', () => { - (status.conditions || [])[0].status = 'Unkonown'; - const getrunStatus = getRunStatus(status); - expect(getrunStatus).toBe(RunStatus.Running); - }); - - it('should get run status as idle', () => { - (status.conditions || [])[0].type = 'Failed'; - const getrunStatus = getRunStatus(status); - expect(getrunStatus).toBe(RunStatus.Idle); - }); -}); - -describe('getValue', () => { - it('should get value as null when runDetails is undefined', () => { - const taskMock: PipelineRunTaskDetails = { - name: 'flip-coin', - runDetails: undefined, - skipped: true, - taskSpec: { - steps: [ - { - name: 'main', - image: 'python:alpine3.6', - command: ['sh', '-ec'], - }, - ], - results: [ - { - name: 'Output', - type: 'string', - }, - ], - }, - }; - const path = 'results.Output'; - const getvalue = getValue(taskMock, path); - expect(getvalue).toBe(null); - }); - - it('should get value as heads', () => { - const path = 'results.Output'; - const getvalue = getValue(task, path); - expect(getvalue).toBe('heads'); - }); - - it("should get value as null when path doesn't match", () => { - const path = 'random.string'; - const getvalue = getValue(task, path); - expect(getvalue).toBe(null); - }); -}); diff --git a/frontend/src/concepts/pipelines/topology/index.ts b/frontend/src/concepts/pipelines/topology/index.ts index f1a31617a8..4bb3813535 100644 --- a/frontend/src/concepts/pipelines/topology/index.ts +++ b/frontend/src/concepts/pipelines/topology/index.ts @@ -1 +1,2 @@ export { usePipelineTaskTopology } from './usePipelineTaskTopology'; +export * from './pipelineTaskTypes'; diff --git a/frontend/src/concepts/pipelines/topology/parseUtils.ts b/frontend/src/concepts/pipelines/topology/parseUtils.ts new file mode 100644 index 0000000000..fe247ae4fe --- /dev/null +++ b/frontend/src/concepts/pipelines/topology/parseUtils.ts @@ -0,0 +1,186 @@ +import { RunStatus } from '@patternfly/react-topology'; +import { + DAG, + InputOutputArtifactType, + InputOutputDefinition, + PipelineComponentsKF, + RunDetailsKF, + RuntimeStateKF, + TaskDetailKF, +} from '~/concepts/pipelines/kfTypes'; +import { PipelineTaskInputOutput, PipelineTaskRunStatus } from './pipelineTaskTypes'; + +export const composeArtifactType = (data: InputOutputArtifactType): string => + `${data.schemaTitle} (${data.schemaVersion})`; + +export type ComponentArtifactMap = { + [componentName: string]: { [artifactId: string]: InputOutputArtifactType } | undefined; +}; +export const parseComponentsForArtifactRelationship = ( + components: PipelineComponentsKF, +): ComponentArtifactMap => + Object.entries(components).reduce( + (map, [componentId, componentValue]) => + Object.entries(componentValue?.outputDefinitions?.artifacts ?? {}).reduce( + (artifactItems, [artifactId, value]) => { + const { artifactType } = value; + + return { + ...artifactItems, + [componentId]: { + ...artifactItems[componentId], + [artifactId]: artifactType, + }, + }; + }, + map, + ), + {}, + ); + +export type TaskArtifactMap = { + [taskName: string]: { outputArtifactKey: string; artifactId: string }[] | undefined; +}; +export const parseTasksForArtifactRelationship = (tasks: DAG['tasks']): TaskArtifactMap => + Object.values(tasks).reduce( + (map, taskValue) => + Object.entries(taskValue.inputs?.artifacts ?? {}).reduce( + (artifactItems, [artifactId, value]) => { + const { producerTask: taskId, outputArtifactKey } = value.taskOutputArtifact || {}; + if (!taskId || !outputArtifactKey) { + // eslint-disable-next-line no-console + console.warn('Issue constructing artifact node', value); + return artifactItems; + } + + return { + ...artifactItems, + [taskId]: [ + ...(artifactItems[taskId] ?? []), + { + outputArtifactKey, + artifactId, + }, + ], + }; + }, + map, + ), + {}, + ); + +export const parseInputOutput = ( + definition?: InputOutputDefinition, +): PipelineTaskInputOutput | undefined => { + let data: PipelineTaskInputOutput | undefined; + if (definition) { + const { artifacts, parameters } = definition; + data = {}; + + if (parameters) { + data = { + ...data, + params: Object.entries(parameters).map(([paramLabel, { parameterType }]) => ({ + label: paramLabel, + type: parameterType, + // TODO: support value + })), + }; + } + + if (artifacts) { + data = { + ...data, + artifacts: Object.entries(artifacts).map(([paramLabel, { artifactType }]) => ({ + label: paramLabel, + type: composeArtifactType(artifactType), + // TODO: support value + })), + }; + } + } + + return data; +}; + +export const lowestProgress = (details: TaskDetailKF[]): PipelineTaskRunStatus['state'] => { + const statusWeight = (status?: RuntimeStateKF) => { + switch (status) { + case RuntimeStateKF.PENDING: + return 10; + case RuntimeStateKF.RUNNING: + return 20; + case RuntimeStateKF.SKIPPED: + return 30; + case RuntimeStateKF.PAUSED: + return 40; + case RuntimeStateKF.CANCELING: + return 59; + case RuntimeStateKF.CANCELED: + return 51; + case RuntimeStateKF.SUCCEEDED: + return 60; + case RuntimeStateKF.FAILED: + return 70; + case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: + default: + return 0; + } + }; + + return details.sort( + ({ state: stateA }, { state: stateB }) => statusWeight(stateB) - statusWeight(stateA), + )[0].state; +}; + +export const parseRuntimeInfo = ( + taskId: string, + runDetails?: RunDetailsKF, +): PipelineTaskRunStatus | undefined => { + if (!runDetails) { + return undefined; + } + + const { task_details: taskDetails } = runDetails; + + // taskId should always be first, as it's the most direct item, but it may not drive the entire details + const nameVariants = [taskId, `${taskId}-driver`]; + const thisTaskDetail = taskDetails.filter(({ display_name: name, execution_id: executionId }) => + nameVariants.includes(name ?? executionId ?? ''), + ); + if (thisTaskDetail.length === 0) { + // No details yet + return undefined; + } + + return { + startTime: thisTaskDetail[0].start_time, + completeTime: thisTaskDetail[0].end_time, + state: lowestProgress(thisTaskDetail), + taskId: thisTaskDetail[0].task_id, + podName: thisTaskDetail[0].child_tasks?.find((o) => o.pod_name)?.pod_name, + }; +}; + +export const translateStatusForNode = (stateKF?: RuntimeStateKF): RunStatus | undefined => { + switch (stateKF) { + case RuntimeStateKF.CANCELED: + case RuntimeStateKF.CANCELING: + return RunStatus.Cancelled; + case RuntimeStateKF.PAUSED: + return RunStatus.Pending; + case RuntimeStateKF.FAILED: + return RunStatus.Failed; + case RuntimeStateKF.PENDING: + return RunStatus.Pending; + case RuntimeStateKF.RUNNING: + return RunStatus.InProgress; + case RuntimeStateKF.SUCCEEDED: + return RunStatus.Succeeded; + case RuntimeStateKF.SKIPPED: + return RunStatus.Skipped; + case RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED: + default: + return undefined; + } +}; diff --git a/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts b/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts new file mode 100644 index 0000000000..241be81453 --- /dev/null +++ b/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts @@ -0,0 +1,57 @@ +import { InputDefinitionParameterType, RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; +import { createNode } from '~/concepts/topology'; + +export type PipelineTaskParam = { + label: string; + type: InputDefinitionParameterType; + value?: string; +}; + +export type PipelineTaskArtifact = { + label: string; + type: string; +}; + +export type PipelineTaskStep = { + image: string; + args?: string[]; + command?: string[]; + volume?: { + mountPath: string; + }; +}; + +export type PipelineTaskInputOutput = { + artifacts?: PipelineTaskArtifact[]; + params?: PipelineTaskParam[]; +}; + +export type PipelineTaskRunStatus = { + startTime: string; + completeTime?: string; + podName?: string; + state?: RuntimeStateKF; + taskId?: string; +}; + +export type PipelineTask = { + type: 'artifact' | 'task' | 'groupTask'; + name: string; + steps?: PipelineTaskStep[]; + inputs?: PipelineTaskInputOutput; + outputs?: PipelineTaskInputOutput; + /** Run Status */ + status?: PipelineTaskRunStatus; +}; + +export type KubeFlowTaskTopology = { + /** + * Details of a selected node. + * [Task.name]: Task + */ + taskMap: Record; + /** + * Nodes to render in topology. + */ + nodes: ReturnType[]; +}; diff --git a/frontend/src/concepts/pipelines/topology/pipelineUtils.ts b/frontend/src/concepts/pipelines/topology/pipelineUtils.ts deleted file mode 100644 index 25f2efcaa2..0000000000 --- a/frontend/src/concepts/pipelines/topology/pipelineUtils.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { RunStatus } from '@patternfly/react-topology'; -import { - PipelineRunTaskParam, - PipelineRunTaskRunStatusProperties, - PipelineRunTaskWhen, -} from '~/k8sTypes'; -import { PipelineRunTaskDetails } from '~/concepts/pipelines/content/types'; - -export const getParamName = (param: string): string | null => { - const paramValueMatch = param.match(/^\$\((params\.([a-zA-Z0-9-_]+))\)/); - if (!paramValueMatch) { - return null; - } - return paramValueMatch[1].split('.')[1]; -}; - -export const getNameAndPathFromTaskRef = (taskRef: string): [string, string] | null => { - const match = taskRef.match(/\$\(tasks\.([a-z0-9-]+)\.(.+)\)/); - if (!match) { - return null; - } - - return [match[1], match[2]]; -}; - -export const getNameFromTaskRef = (taskRef: string): string | null => - getNameAndPathFromTaskRef(taskRef)?.[0] ?? null; - -type AsRunAfter = (item: T) => string | null; - -export const paramAsRunAfter: AsRunAfter = (param) => { - if (param.value) { - return getNameFromTaskRef(param.value); - } - return null; -}; -export const whenAsRunAfter: AsRunAfter = (when) => { - if (when.input) { - return getNameFromTaskRef(when.input); - } - return null; -}; - -export const getRunStatus = (status?: PipelineRunTaskRunStatusProperties): RunStatus => { - const successCondition = status?.conditions?.find((s) => s.type === 'Succeeded'); - // const cancelledCondition = status.conditions.find((s) => s.status === 'Cancelled'); - - if (!successCondition || !successCondition.status) { - return RunStatus.Idle; - } - - let runStatus: RunStatus | undefined; - if (successCondition.status === 'True') { - runStatus = RunStatus.Succeeded; - } else if (successCondition.status === 'False') { - runStatus = RunStatus.Failed; - } else { - runStatus = RunStatus.Running; - } - - return runStatus; -}; - -export const getValue = (task: PipelineRunTaskDetails, path: string): string | null => { - const parts = path.split('.'); - if (!task.runDetails) { - return null; - } - - if (parts[0] === 'results' && parts[1]) { - const name = parts[1]; - return ( - task.runDetails.status?.taskResults?.find((result) => result.name === name)?.value ?? null - ); - } - - return null; -}; diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts index 157807e708..80b2f2bb1b 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.ts @@ -1,8 +1,135 @@ -import { KubeFlowTaskTopology } from '~/concepts/pipelines/content/types'; +import { PipelineRunKFv2, PipelineSpecVariable } from '~/concepts/pipelines/kfTypes'; +import { createNode } from '~/concepts/topology'; +import { PipelineNodeModelExpanded } from '~/concepts/topology/types'; +import { + composeArtifactType, + parseComponentsForArtifactRelationship, + parseInputOutput, + parseRuntimeInfo, + parseTasksForArtifactRelationship, + translateStatusForNode, +} from './parseUtils'; +import { KubeFlowTaskTopology } from './pipelineTaskTypes'; const EMPTY_STATE: KubeFlowTaskTopology = { taskMap: {}, nodes: [] }; export const usePipelineTaskTopology = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pipelineRun: unknown, -): KubeFlowTaskTopology => EMPTY_STATE; + spec?: PipelineSpecVariable, + run?: PipelineRunKFv2, +): KubeFlowTaskTopology => { + if (!spec) { + return EMPTY_STATE; + } + const pipelineSpec = spec.pipeline_spec ?? spec; + + const { + components, + deploymentSpec: { executors }, + root: { + dag: { tasks }, + }, + } = pipelineSpec; + const { run_details: runDetails } = run || {}; + + const componentArtifactMap = parseComponentsForArtifactRelationship(components); + const taskArtifactMap = parseTasksForArtifactRelationship(tasks); + + return Object.entries(tasks).reduce((acc, [taskId, taskValue]) => { + const taskName = taskValue.taskInfo.name; + + const componentRef = taskValue.componentRef.name; + const component = components[componentRef]; + const artifactsInComponent = componentArtifactMap[componentRef]; + const isGroupNode = !!component?.dag; + + const executorLabel = component?.executorLabel; + const executor = executorLabel ? executors[executorLabel] : undefined; + + const status = parseRuntimeInfo(taskId, runDetails); + + const newTaskMapEntries: KubeFlowTaskTopology['taskMap'] = {}; + const nodes: PipelineNodeModelExpanded[] = []; + const runAfter: string[] = taskValue.dependentTasks ?? []; + + if (artifactsInComponent) { + const artifactNodeData = taskArtifactMap[taskId]; + + Object.entries(artifactsInComponent).forEach(([artifactKey, data]) => { + const label = artifactKey; + const { artifactId } = + artifactNodeData?.find((a) => artifactKey === a.outputArtifactKey) ?? {}; + + // if no node needs it as an input, we don't really need a well known id + const id = artifactId ?? artifactKey; + + nodes.push( + createNode({ + id, + label, + runAfter: [taskId], + }), + ); + + newTaskMapEntries[id] = { + type: 'artifact', + name: label, + inputs: { + artifacts: [{ label: id, type: composeArtifactType(data) }], + }, + }; + }); + } + + // This task + newTaskMapEntries[taskId] = { + type: isGroupNode ? 'groupTask' : 'task', + name: taskName, + steps: executor ? [executor.container] : undefined, + inputs: parseInputOutput(component?.inputDefinitions), + outputs: parseInputOutput(component?.outputDefinitions), + status, + }; + if (taskValue.dependentTasks) { + // This task's runAfters may need artifact relationships -- find those artifactIds + runAfter.push( + ...taskValue.dependentTasks + .map((dependantTaskId) => { + const art = taskArtifactMap[dependantTaskId]; + return art ? art.map((v) => v.artifactId) : null; + }) + .filter((v): v is string[] => !!v) + .flat(), + ); + } + + // This task's rendering information + if (isGroupNode) { + // TODO: handle group nodes + nodes.push( + createNode({ + id: taskId, + label: taskName, + runAfter, + status: translateStatusForNode(status?.state), + }), + ); + } else { + nodes.push( + createNode({ + id: taskId, + label: taskName, + runAfter, + status: translateStatusForNode(status?.state), + }), + ); + } + + return { + taskMap: { + ...acc.taskMap, + ...newTaskMapEntries, + }, + nodes: [...acc.nodes, ...nodes], + }; + }, EMPTY_STATE); +}; diff --git a/frontend/src/concepts/pipelines/utils.ts b/frontend/src/concepts/pipelines/utils.ts index 790900e9c6..7f952717a6 100644 --- a/frontend/src/concepts/pipelines/utils.ts +++ b/frontend/src/concepts/pipelines/utils.ts @@ -5,6 +5,7 @@ import { } from '~/concepts/pipelines/content/configurePipelinesServer/const'; import { ELYRA_SECRET_NAME } from '~/concepts/pipelines/elyra/const'; import { allSettledPromises } from '~/utilities/allSettledPromises'; +import { PipelineSpec, PipelineSpecVariable } from '~/concepts/pipelines/kfTypes'; export const deleteServer = async (namespace: string, crName: string): Promise => { const dspa = await getPipelinesCR(namespace, crName); @@ -28,3 +29,6 @@ export const deleteServer = async (namespace: string, crName: string): Promise /^secret-[a-z0-9]{6}$/.test(name); + +export const getCorePipelineSpec = (spec?: PipelineSpecVariable): PipelineSpec | undefined => + spec?.pipeline_spec ?? spec; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 06fa4c46ca..58787b83e6 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -929,12 +929,14 @@ export type SelfSubjectAccessReviewKind = K8sResourceCommon & { }; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskSpecDigest = { name: string; outputs: unknown[]; // TODO: detail outputs version: string; }; +/** @deprecated - Tekton is no longer used */ type PipelineRunTaskSpecStep = { name: string; args?: string[]; @@ -942,17 +944,20 @@ type PipelineRunTaskSpecStep = { image: string; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskSpecResult = { name: string; type: string; description?: string; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskVolumeMount = { name: string; mountPath: string; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskSpec = { steps: PipelineRunTaskSpecStep[]; stepTemplate?: { @@ -970,17 +975,21 @@ export type PipelineRunTaskSpec = { }; }; }; + +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskParam = { name: string; value: string; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskWhen = { input: string; operator: string; values: string[]; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTask = { name: string; taskSpec: PipelineRunTaskSpec; @@ -990,26 +999,31 @@ export type PipelineRunTask = { runAfter?: string[]; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunPipelineSpec = { tasks: PipelineRunTask[]; }; +/** @deprecated - Tekton is no longer used */ export type SkippedTask = { name: string; reason: string; whenExpressions: PipelineRunTaskWhen; }; +/** @deprecated - Tekton is no longer used */ export type TaskRunResults = { name: string; type: string; value: string; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskStatusStep = { volumeMounts?: PipelineRunTaskVolumeMount[]; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskRunStatusProperties = { conditions?: K8sCondition[]; podName: string; @@ -1024,12 +1038,14 @@ export type PipelineRunTaskRunStatusProperties = { }; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunTaskRunStatus = { /** The task name; pipelineSpec.tasks[].name */ pipelineTaskName: string; status?: PipelineRunTaskRunStatusProperties; }; +/** @deprecated - Tekton is no longer used */ export type PipelineRunKind = K8sResourceCommon & { metadata: { name: string; diff --git a/frontend/src/pages/ApplicationsPage.tsx b/frontend/src/pages/ApplicationsPage.tsx index 0957931b73..40517ed8f3 100644 --- a/frontend/src/pages/ApplicationsPage.tsx +++ b/frontend/src/pages/ApplicationsPage.tsx @@ -31,7 +31,7 @@ type ApplicationsPageProps = { headerAction?: React.ReactNode; headerContent?: React.ReactNode; provideChildrenPadding?: boolean; - jobReferenceName?: React.ReactNode; + subtext?: React.ReactNode; loadingContent?: React.ReactNode; }; @@ -49,7 +49,7 @@ const ApplicationsPage: React.FC = ({ headerAction, headerContent, provideChildrenPadding, - jobReferenceName, + subtext, loadingContent, }) => { const renderHeader = () => ( @@ -64,8 +64,10 @@ const ApplicationsPage: React.FC = ({ {title} - {jobReferenceName} - {description && {description}} + + {subtext && {subtext}} + {description && {description}} + {headerAction} diff --git a/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx b/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx index c1e4a93fe9..bedb0d368a 100644 --- a/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx +++ b/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import PipelineServerActions from '~/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineServerActions'; +import PipelineServerActions from '~/concepts/pipelines/content/PipelineServerActions'; import PipelineCoreApplicationPage from '~/pages/pipelines/global/PipelineCoreApplicationPage'; import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; import PipelineAndVersionContextProvider from '~/concepts/pipelines/content/PipelineAndVersionContext'; diff --git a/frontend/src/pages/pipelines/global/pipelines/GlobalPipelines.tsx b/frontend/src/pages/pipelines/global/pipelines/GlobalPipelines.tsx index a1cd9f8ef5..ae1c849e23 100644 --- a/frontend/src/pages/pipelines/global/pipelines/GlobalPipelines.tsx +++ b/frontend/src/pages/pipelines/global/pipelines/GlobalPipelines.tsx @@ -4,7 +4,7 @@ import { pipelinesPageDescription, pipelinesPageTitle, } from '~/pages/pipelines/global/pipelines/const'; -import PipelineServerActions from '~/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineServerActions'; +import PipelineServerActions from '~/concepts/pipelines/content/PipelineServerActions'; import PipelineCoreApplicationPage from '~/pages/pipelines/global/PipelineCoreApplicationPage'; import PipelinesView from '~/pages/pipelines/global/pipelines/PipelinesView'; import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; diff --git a/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx b/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx index b6defeb8fe..98f25d587c 100644 --- a/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx +++ b/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx @@ -5,7 +5,7 @@ import DetailsSection from '~/pages/projects/screens/detail/DetailsSection'; import { PipelineServerTimedOut, usePipelinesAPI } from '~/concepts/pipelines/context'; import NoPipelineServer from '~/concepts/pipelines/NoPipelineServer'; import PipelinesList from '~/pages/projects/screens/detail/pipelines/PipelinesList'; -import PipelineServerActions from '~/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineServerActions'; +import PipelineServerActions from '~/concepts/pipelines/content/PipelineServerActions'; import PipelineAndVersionContextProvider from '~/concepts/pipelines/content/PipelineAndVersionContext'; import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; import ImportPipelineSplitButton from '~/concepts/pipelines/content/import/ImportPipelineSplitButton';