diff --git a/packages/composites/ui-atoms/src/Icons/MuiLaunchPlanIcon/index.tsx b/packages/composites/ui-atoms/src/Icons/MuiLaunchPlanIcon/index.tsx new file mode 100644 index 000000000..cd9f459fc --- /dev/null +++ b/packages/composites/ui-atoms/src/Icons/MuiLaunchPlanIcon/index.tsx @@ -0,0 +1,39 @@ +import { makeStyles } from '@material-ui/core/styles'; +import { SvgIconProps, Theme } from '@material-ui/core'; +import classnames from 'classnames'; +import * as React from 'react'; + +const useStyles = makeStyles((theme: Theme) => ({ + svg: { + marginTop: 0, + marginRight: theme.spacing(2), + display: 'inline-block', + fontSize: '1.5rem', + transition: 'fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + flexShrink: 0, + userSelect: 'none', + color: '#666666', + }, +})); + +export const MuiLaunchPlanIcon = (props: SvgIconProps): JSX.Element => { + const { fill, className, width = '1em', height = '1em', fontSize } = props; + const styles = useStyles(); + return ( + + + + ); +}; diff --git a/packages/composites/ui-atoms/src/Icons/index.tsx b/packages/composites/ui-atoms/src/Icons/index.tsx index 0cbe71ead..2f6fb0418 100644 --- a/packages/composites/ui-atoms/src/Icons/index.tsx +++ b/packages/composites/ui-atoms/src/Icons/index.tsx @@ -1,3 +1,4 @@ export { FlyteLogo } from './FlyteLogo'; export { InfoIcon } from './InfoIcon'; export { RerunIcon } from './RerunIcon'; +export { MuiLaunchPlanIcon } from './MuiLaunchPlanIcon'; diff --git a/packages/zapp/console/src/common/utils.ts b/packages/zapp/console/src/common/utils.ts index 0f5973571..138d9f17e 100644 --- a/packages/zapp/console/src/common/utils.ts +++ b/packages/zapp/console/src/common/utils.ts @@ -1,5 +1,8 @@ import { Protobuf } from 'flyteidl'; import * as Long from 'long'; +import { WorkflowExecutionPhase } from 'models/Execution/enums'; +import { WorkflowExecutionIdentifier } from 'models/Execution/types'; +import { Routes } from 'routes/routes'; /** Determines if a given date string or object is a valid, usable date. This will detect * JS Date objects which have been initialized with invalid values as well as strings which @@ -101,3 +104,22 @@ export function toBoolean(value?: string): boolean { export function stringifyValue(value: unknown): string { return JSON.stringify(value, null, 2); } + +export const padExecutions = (items: WorkflowExecutionPhase[]) => { + if (items.length >= 10) { + return items.slice(0, 10).reverse(); + } + const emptyExecutions = new Array(10 - items.length).fill(WorkflowExecutionPhase.QUEUED); + return [...items, ...emptyExecutions].reverse(); +}; + +export const padExecutionPaths = (items: WorkflowExecutionIdentifier[]) => { + if (items.length >= 10) { + return items + .slice(0, 10) + .map((id) => Routes.ExecutionDetails.makeUrl(id)) + .reverse(); + } + const emptyExecutions = new Array(10 - items.length).fill(null); + return [...items.map((id) => Routes.ExecutionDetails.makeUrl(id)), ...emptyExecutions].reverse(); +}; diff --git a/packages/zapp/console/src/components/Entities/EntityDetails.tsx b/packages/zapp/console/src/components/Entities/EntityDetails.tsx index 16a9c1dfd..62d15fcaf 100644 --- a/packages/zapp/console/src/components/Entities/EntityDetails.tsx +++ b/packages/zapp/console/src/components/Entities/EntityDetails.tsx @@ -8,6 +8,7 @@ import { ResourceIdentifier } from 'models/Common/types'; import * as React from 'react'; import { entitySections } from './constants'; import { EntityDetailsHeader } from './EntityDetailsHeader'; +import { EntityInputs } from './EntityInputs'; import { EntityExecutions } from './EntityExecutions'; import { EntitySchedules } from './EntitySchedules'; import { EntityVersions } from './EntityVersions'; @@ -16,7 +17,7 @@ import { EntityExecutionsBarChart } from './EntityExecutionsBarChart'; const useStyles = makeStyles((theme: Theme) => ({ metadataContainer: { display: 'flex', - marginBottom: theme.spacing(5), + marginBottom: theme.spacing(2), marginTop: theme.spacing(2), width: '100%', }, @@ -39,6 +40,10 @@ const useStyles = makeStyles((theme: Theme) => ({ flex: '1 2 auto', marginRight: theme.spacing(30), }, + inputsContainer: { + display: 'flex', + flexDirection: 'column', + }, })); interface EntityDetailsProps { @@ -67,13 +72,19 @@ export const EntityDetails: React.FC = ({ id }) => { ) : null} - {sections.schedules ? ( + {!sections.inputs && sections.schedules ? (
) : null} + {sections.inputs ? ( +
+ +
+ ) : null} + {sections.versions ? (
diff --git a/packages/zapp/console/src/components/Entities/EntityDetailsHeader.tsx b/packages/zapp/console/src/components/Entities/EntityDetailsHeader.tsx index c22b16291..3dd1c7824 100644 --- a/packages/zapp/console/src/components/Entities/EntityDetailsHeader.tsx +++ b/packages/zapp/console/src/components/Entities/EntityDetailsHeader.tsx @@ -68,6 +68,7 @@ export const EntityDetailsHeader: React.FC = ({ const domain = getProjectDomain(project, id.domain); const headerText = `${domain.name} / ${id.name}`; + return ( <>
diff --git a/packages/zapp/console/src/components/Entities/EntityExecutions.tsx b/packages/zapp/console/src/components/Entities/EntityExecutions.tsx index da2e701b8..b59bfce88 100644 --- a/packages/zapp/console/src/components/Entities/EntityExecutions.tsx +++ b/packages/zapp/console/src/components/Entities/EntityExecutions.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { contentMarginGridUnits } from 'common/layout'; import { WaitForData } from 'components/common/WaitForData'; @@ -15,8 +14,6 @@ import { executionSortFields } from 'models/Execution/constants'; import { compact } from 'lodash'; import { useOnlyMyExecutionsFilterState } from 'components/Executions/filters/useOnlyMyExecutionsFilterState'; import { executionFilterGenerator } from './generators'; -import { entityStrings } from './constants'; -import t, { patternKey } from './strings'; const useStyles = makeStyles((theme: Theme) => ({ filtersContainer: { @@ -78,9 +75,6 @@ export const EntityExecutions: React.FC = ({ return ( <> - - {t(patternKey('allExecutionsChartTitle', entityStrings[id.resourceType]))} -
({ + header: { + marginBottom: theme.spacing(1), + }, + divider: { + borderBottom: `1px solid ${theme.palette.divider}`, + marginBottom: theme.spacing(1), + }, + rowContainer: { + display: 'flex', + marginTop: theme.spacing(3), + }, + firstColumnContainer: { + width: '60%', + marginRight: theme.spacing(3), + }, + secondColumnContainer: { + width: '40%', + }, + configs: { + listStyleType: 'none', + paddingInlineStart: 0, + }, + config: { + display: 'flex', + }, + configName: { + color: theme.palette.grey[400], + marginRight: theme.spacing(2), + minWidth: '95px', + }, + configValue: { + color: '#333', + fontSize: '14px', + }, + headCell: { + color: theme.palette.grey[400], + }, + noInputs: { + color: theme.palette.grey[400], + }, +})); + +interface Input { + name: string; + type?: string; + required?: boolean; + defaultValue?: string; +} + +/** Fetches and renders the expected & fixed inputs for a given Entity (LaunchPlan) ID */ +export const EntityInputs: React.FC<{ + id: ResourceIdentifier; +}> = ({ id }) => { + const styles = useStyles(); + + const launchPlanState = useLaunchPlans( + { project: id.project, domain: id.domain }, + { + limit: 1, + filter: [ + { + key: 'launch_plan.name', + operation: FilterOperationName.EQ, + value: id.name, + }, + ], + }, + ); + + const closure = launchPlanState?.value?.length + ? launchPlanState.value[0].closure + : ({} as LaunchPlanClosure); + + const spec = launchPlanState?.value?.length + ? launchPlanState.value[0].spec + : ({} as LaunchPlanSpec); + + const expectedInputs = React.useMemo(() => { + const results = [] as Input[]; + Object.keys(closure?.expectedInputs?.parameters ?? {}).forEach((name) => { + const parameter = closure?.expectedInputs.parameters[name]; + if (parameter?.var?.type) { + const typeDefinition = getInputDefintionForLiteralType(parameter.var.type); + results.push({ + name, + type: formatType(typeDefinition), + required: !!parameter.required, + defaultValue: parameter.default?.value, + }); + } + }); + return results; + }, [closure]); + + const fixedInputs = React.useMemo(() => { + const inputsMap = transformLiterals(spec?.fixedInputs?.literals ?? {}); + return Object.keys(inputsMap).map((name) => ({ name, defaultValue: inputsMap[name] })); + }, [spec]); + + const configs = React.useMemo( + () => [ + { name: t('configType'), value: 'single (csv)' }, + { + name: t('configUrl'), + value: + 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv', + }, + { name: t('configSeed'), value: '7' }, + { name: t('configTestSplitRatio'), value: '0.33' }, + ], + [], + ); + + return ( + <> + + {t('launchPlanLatest')} + +
+
+
+ + {t('expectedInputs')} + + {expectedInputs.length ? ( + + + + + + + {t('inputsName')} + + + + + {t('inputsType')} + + + + + {t('inputsRequired')} + + + + + {t('inputsDefault')} + + + + + + {expectedInputs.map(({ name, type, required, defaultValue }) => ( + + {name} + {type} + + {required ? : ''} + + {defaultValue || '-'} + + ))} + +
+
+ ) : ( +

{t('noExpectedInputs')}

+ )} +
+
+ + {t('fixedInputs')} + + {fixedInputs.length ? ( + + + + + + + {t('inputsName')} + + + + + {t('inputsDefault')} + + + + + + {fixedInputs.map(({ name, defaultValue }) => ( + + {name} + {defaultValue || '-'} + + ))} + +
+
+ ) : ( +

{t('noFixedInputs')}

+ )} +
+
+ {/*
+
+ + {t('configuration')} + +
    + {configs.map(({ name, value }) => ( +
  • + {name}: + {value} +
  • + ))} +
+
+
+
*/} + + ); +}; diff --git a/packages/zapp/console/src/components/Entities/EntityVersions.tsx b/packages/zapp/console/src/components/Entities/EntityVersions.tsx index 4509f3090..a9c1339cf 100644 --- a/packages/zapp/console/src/components/Entities/EntityVersions.tsx +++ b/packages/zapp/console/src/components/Entities/EntityVersions.tsx @@ -11,7 +11,7 @@ import { isLoadingState } from 'components/hooks/fetchMachine'; import { useEntityVersions } from 'components/hooks/Entity/useEntityVersions'; import { interactiveTextColor } from 'components/Theme/constants'; import { SortDirection } from 'models/AdminEntity/types'; -import { Identifier, ResourceIdentifier } from 'models/Common/types'; +import { Identifier, ResourceIdentifier, ResourceType } from 'models/Common/types'; import { executionSortFields } from 'models/Execution/constants'; import { executionFilterGenerator, versionDetailsUrlGenerator } from './generators'; import { WorkflowVersionsTablePageSize, entityStrings } from './constants'; @@ -20,6 +20,7 @@ import t, { patternKey } from './strings'; const useStyles = makeStyles((theme: Theme) => ({ headerContainer: { display: 'flex', + marginTop: theme.spacing(3), }, collapseButton: { marginTop: theme.spacing(-0.5), @@ -114,7 +115,7 @@ export const EntityVersions: React.FC = ({ id, showAll = fa ) : ( diff --git a/packages/zapp/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx b/packages/zapp/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx index 931edeb09..91a274130 100644 --- a/packages/zapp/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx +++ b/packages/zapp/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { withRouteParams } from 'components/common/withRouteParams'; -import { ResourceIdentifier } from 'models/Common/types'; +import { ResourceIdentifier, ResourceType } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { WaitForData } from 'components/common/WaitForData'; import { useProject } from 'components/hooks/useProjects'; @@ -13,7 +13,11 @@ import { typeNameToEntityResource } from '../constants'; import { versionsDetailsSections } from './constants'; import { EntityVersionDetails } from './EntityVersionDetails'; -const useStyles = makeStyles((theme: Theme) => ({ +interface StyleProps { + resourceType: ResourceType; +} + +const useStyles = makeStyles((theme: Theme) => ({ verionDetailsContainer: { marginTop: theme.spacing(2), display: 'flex', @@ -39,9 +43,9 @@ const useStyles = makeStyles((theme: Theme) => ({ versionsContainer: { display: 'flex', flex: '0 1 auto', - height: '40%', + height: ({ resourceType }) => (resourceType === ResourceType.LAUNCH_PLAN ? '100%' : '40%'), flexDirection: 'column', - overflowY: 'scroll', + overflowY: 'auto', }, })); @@ -81,7 +85,7 @@ const EntityVersionsDetailsContainerImpl: React.FC diff --git a/packages/zapp/console/src/components/Entities/constants.ts b/packages/zapp/console/src/components/Entities/constants.ts index efef36a63..6729064c3 100644 --- a/packages/zapp/console/src/components/Entities/constants.ts +++ b/packages/zapp/console/src/components/Entities/constants.ts @@ -4,7 +4,7 @@ type EntityStringMap = { [k in ResourceType]: string }; export const entityStrings: EntityStringMap = { [ResourceType.DATASET]: 'dataset', - [ResourceType.LAUNCH_PLAN]: 'launch plan', + [ResourceType.LAUNCH_PLAN]: 'launch_plan', [ResourceType.TASK]: 'task', [ResourceType.UNSPECIFIED]: 'item', [ResourceType.WORKFLOW]: 'workflow', @@ -14,7 +14,7 @@ type TypeNameToEntityResourceType = { [key: string]: ResourceType }; export const typeNameToEntityResource: TypeNameToEntityResourceType = { ['dataset']: ResourceType.DATASET, - ['launch plan']: ResourceType.LAUNCH_PLAN, + ['launch_plan']: ResourceType.LAUNCH_PLAN, ['task']: ResourceType.TASK, ['item']: ResourceType.UNSPECIFIED, ['workflow']: ResourceType.WORKFLOW, @@ -27,15 +27,17 @@ interface EntitySectionsFlags { schedules?: boolean; versions?: boolean; descriptionInputsAndOutputs?: boolean; + inputs?: boolean; } export const entitySections: { [k in ResourceType]: EntitySectionsFlags } = { [ResourceType.DATASET]: { description: true }, [ResourceType.LAUNCH_PLAN]: { - description: true, executions: true, - launch: true, + launch: false, + inputs: true, schedules: true, + versions: true, }, [ResourceType.TASK]: { description: true, diff --git a/packages/zapp/console/src/components/Entities/generators.ts b/packages/zapp/console/src/components/Entities/generators.ts index f10c41fee..c84aefc8c 100644 --- a/packages/zapp/console/src/components/Entities/generators.ts +++ b/packages/zapp/console/src/components/Entities/generators.ts @@ -6,10 +6,30 @@ import { entityStrings } from './constants'; const noFilters = () => []; export const executionFilterGenerator: { - [k in ResourceType]: (id: ResourceIdentifier) => FilterOperation[]; + [k in ResourceType]: (id: ResourceIdentifier, version?: string) => FilterOperation[]; } = { [ResourceType.DATASET]: noFilters, - [ResourceType.LAUNCH_PLAN]: noFilters, + [ResourceType.LAUNCH_PLAN]: ({ name }, version) => + version + ? [ + { + key: 'launch_plan.name', + operation: FilterOperationName.EQ, + value: name, + }, + { + key: 'launch_plan.version', + operation: FilterOperationName.EQ, + value: version, + }, + ] + : [ + { + key: 'launch_plan.name', + operation: FilterOperationName.EQ, + value: name, + }, + ], [ResourceType.TASK]: ({ name }) => [ { key: 'task.name', @@ -29,6 +49,8 @@ export const executionFilterGenerator: { const workflowListGenerator = ({ project, domain }: ResourceIdentifier) => Routes.ProjectDetails.sections.workflows.makeUrl(project, domain); +const launchPlanListGenerator = ({ project, domain }: ResourceIdentifier) => + Routes.ProjectDetails.sections.launchPlans.makeUrl(project, domain); const taskListGenerator = ({ project, domain }: ResourceIdentifier) => Routes.ProjectDetails.sections.tasks.makeUrl(project, domain); const unspecifiedGenerator = ({ project, domain }: ResourceIdentifier | Identifier) => { @@ -42,7 +64,7 @@ export const backUrlGenerator: { [k in ResourceType]: (id: ResourceIdentifier) => string; } = { [ResourceType.DATASET]: unimplementedGenerator, - [ResourceType.LAUNCH_PLAN]: unimplementedGenerator, + [ResourceType.LAUNCH_PLAN]: launchPlanListGenerator, [ResourceType.TASK]: taskListGenerator, [ResourceType.UNSPECIFIED]: unspecifiedGenerator, [ResourceType.WORKFLOW]: workflowListGenerator, @@ -50,6 +72,8 @@ export const backUrlGenerator: { const workflowDetailGenerator = ({ project, domain, name }: ResourceIdentifier) => Routes.WorkflowDetails.makeUrl(project, domain, name); +const launchPlanDetailGenerator = ({ project, domain, name }: ResourceIdentifier) => + Routes.LaunchPlanDetails.makeUrl(project, domain, name); const taskDetailGenerator = ({ project, domain, name }: ResourceIdentifier) => Routes.TaskDetails.makeUrl(project, domain, name); @@ -57,7 +81,7 @@ export const backToDetailUrlGenerator: { [k in ResourceType]: (id: ResourceIdentifier) => string; } = { [ResourceType.DATASET]: unimplementedGenerator, - [ResourceType.LAUNCH_PLAN]: unimplementedGenerator, + [ResourceType.LAUNCH_PLAN]: launchPlanDetailGenerator, [ResourceType.TASK]: taskDetailGenerator, [ResourceType.UNSPECIFIED]: unspecifiedGenerator, [ResourceType.WORKFLOW]: workflowDetailGenerator, @@ -79,12 +103,20 @@ const taskVersionDetailsGenerator = ({ project, domain, name, version }: Identif entityStrings[ResourceType.TASK], version, ); +const launchPlanVersionDetailsGenerator = ({ project, domain, name, version }: Identifier) => + Routes.EntityVersionDetails.makeUrl( + project, + domain, + name, + entityStrings[ResourceType.LAUNCH_PLAN], + version, + ); const entityMapVersionDetailsUrl: { [k in ResourceType]: (id: Identifier) => string; } = { [ResourceType.DATASET]: unimplementedGenerator, - [ResourceType.LAUNCH_PLAN]: unimplementedGenerator, + [ResourceType.LAUNCH_PLAN]: launchPlanVersionDetailsGenerator, [ResourceType.TASK]: taskVersionDetailsGenerator, [ResourceType.UNSPECIFIED]: unspecifiedGenerator, [ResourceType.WORKFLOW]: workflowVersopmDetailsGenerator, diff --git a/packages/zapp/console/src/components/Entities/strings.ts b/packages/zapp/console/src/components/Entities/strings.ts index e12b28fd3..fd0bb7840 100644 --- a/packages/zapp/console/src/components/Entities/strings.ts +++ b/packages/zapp/console/src/components/Entities/strings.ts @@ -11,9 +11,14 @@ const str = { noSchedules_workflow: 'This workflow has no schedules.', noSchedules_task: 'This task has no schedules.', allExecutionsChartTitle_workflow: 'All Executions in the Workflow', - allExecutionsChartTitle_task: 'All Execuations in the Task', + allExecutionsChartTitle_task: 'All Executions in the Task', + allExecutionsChartTitle_launch_plan: 'All Executions Using Launch Plan', versionsTitle_workflow: 'Recent Workflow Versions', versionsTitle_task: 'Recent Task Versions', + versionsTitle_launch_plan: 'Launch Plan Versions', + searchName_launch_plan: 'Search Launch Plan Name', + searchName_task: 'Search Task Name', + searchName_workflow: 'Search Workflow Name', details_task: 'Task Details', inputsFieldName: 'Inputs', outputsFieldName: 'Outputs', @@ -25,6 +30,20 @@ const str = { value: 'Value', basicInformation: 'Basic Information', description: 'Description', + launchPlanLatest: 'Launch Plan Detail (Latest Version)', + expectedInputs: 'Expected Inputs', + fixedInputs: 'Fixed Inputs', + inputsName: 'Name', + inputsType: 'Type', + inputsRequired: 'Required', + inputsDefault: 'Default Value', + configuration: 'Configuration', + configType: 'type', + configUrl: 'url', + configSeed: 'seed', + configTestSplitRatio: 'test_split_ratio', + noExpectedInputs: 'This launch plan has no expected inputs.', + noFixedInputs: 'This launch plan has no fixed inputs.', }; export { patternKey } from '@flyteconsole/locale'; diff --git a/packages/zapp/console/src/components/Executions/Tables/WorkflowVersionRow.tsx b/packages/zapp/console/src/components/Executions/Tables/WorkflowVersionRow.tsx index 570ea96ba..8914b1475 100644 --- a/packages/zapp/console/src/components/Executions/Tables/WorkflowVersionRow.tsx +++ b/packages/zapp/console/src/components/Executions/Tables/WorkflowVersionRow.tsx @@ -5,6 +5,11 @@ import * as React from 'react'; import { ListRowProps } from 'react-virtualized'; import { Workflow } from 'models/Workflow/types'; import TableRow from '@material-ui/core/TableRow'; +import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; +import { executionSortFields } from 'models/Execution/constants'; +import { SortDirection } from 'models/AdminEntity/types'; +import { executionFilterGenerator } from 'components/Entities/generators'; +import { ResourceIdentifier } from 'models/Common/types'; import { useWorkflowVersionsColumnStyles } from './styles'; import { WorkflowExecutionsTableState, WorkflowVersionColumnDefinition } from './types'; @@ -51,6 +56,31 @@ export const WorkflowVersionRow: React.FC = ({ const versionTableStyles = useWorkflowVersionsColumnStyles(); const styles = useStyles(); + const sort = { + key: executionSortFields.createdAt, + direction: SortDirection.DESCENDING, + }; + + const baseFilters = React.useMemo( + () => + workflow.id.resourceType + ? executionFilterGenerator[workflow.id.resourceType]( + workflow.id as ResourceIdentifier, + workflow.id.version, + ) + : [], + [workflow.id], + ); + + const executions = useWorkflowExecutions( + { domain: workflow.id.domain, project: workflow.id.project, version: workflow.id.version }, + { + sort, + filter: baseFilters, + limit: 10, + }, + ); + return ( {versionView && ( @@ -74,6 +104,7 @@ export const WorkflowVersionRow: React.FC = ({ {cellRenderer({ workflow, state, + executions, })} ))} diff --git a/packages/zapp/console/src/components/Executions/Tables/constants.ts b/packages/zapp/console/src/components/Executions/Tables/constants.ts index 9dbcc1145..8e7238444 100644 --- a/packages/zapp/console/src/components/Executions/Tables/constants.ts +++ b/packages/zapp/console/src/components/Executions/Tables/constants.ts @@ -29,4 +29,5 @@ export const workflowVersionsTableColumnWidths = { release: 150, lastRun: 175, createdAt: 260, + recentRun: 160, }; diff --git a/packages/zapp/console/src/components/Executions/Tables/styles.ts b/packages/zapp/console/src/components/Executions/Tables/styles.ts index 4e5709100..d1469db13 100644 --- a/packages/zapp/console/src/components/Executions/Tables/styles.ts +++ b/packages/zapp/console/src/components/Executions/Tables/styles.ts @@ -178,4 +178,10 @@ export const useWorkflowVersionsColumnStyles = makeStyles(() => ({ columnCreatedAt: { flexBasis: workflowVersionsTableColumnWidths.createdAt, }, + columnLastRun: { + flexBasis: workflowVersionsTableColumnWidths.lastRun, + }, + columnRecentRun: { + flexBasis: workflowVersionsTableColumnWidths.recentRun, + }, })); diff --git a/packages/zapp/console/src/components/Executions/Tables/types.ts b/packages/zapp/console/src/components/Executions/Tables/types.ts index 59ab75b23..49bf992a0 100644 --- a/packages/zapp/console/src/components/Executions/Tables/types.ts +++ b/packages/zapp/console/src/components/Executions/Tables/types.ts @@ -1,3 +1,4 @@ +import { PaginatedFetchableData } from 'components/hooks/types'; import { Execution, NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; import { Workflow } from 'models/Workflow/types'; @@ -32,6 +33,7 @@ export type WorkflowExecutionColumnDefinition = ColumnDefinition; } export type WorkflowVersionColumnDefinition = ColumnDefinition; diff --git a/packages/zapp/console/src/components/Executions/Tables/useWorkflowVersionsTableColumns.tsx b/packages/zapp/console/src/components/Executions/Tables/useWorkflowVersionsTableColumns.tsx index aefdfbbb5..f2a750105 100644 --- a/packages/zapp/console/src/components/Executions/Tables/useWorkflowVersionsTableColumns.tsx +++ b/packages/zapp/console/src/components/Executions/Tables/useWorkflowVersionsTableColumns.tsx @@ -1,6 +1,9 @@ import { Typography } from '@material-ui/core'; import { formatDateUTC } from 'common/formatters'; -import { timestampToDate } from 'common/utils'; +import { padExecutionPaths, padExecutions, timestampToDate } from 'common/utils'; +import { WaitForData } from 'components/common/WaitForData'; +import ProjectStatusBar from 'components/Project/ProjectStatusBar'; +import * as moment from 'moment'; import * as React from 'react'; import { useWorkflowVersionsColumnStyles } from './styles'; import { WorkflowVersionColumnDefinition } from './types'; @@ -36,6 +39,39 @@ export function useWorkflowVersionsTableColumns(): WorkflowVersionColumnDefiniti key: 'createdAt', label: 'time created', }, + { + cellRenderer: ({ executions }) => { + return ( + + + {executions.value.length + ? moment(timestampToDate(executions.value[0].closure.createdAt)).fromNow() + : ''} + + + ); + }, + className: styles.columnLastRun, + key: 'lastExecution', + label: 'last execution', + }, + { + cellRenderer: ({ executions }) => { + return ( + + execution.closure.phase) || [], + )} + paths={padExecutionPaths(executions.value.map((execution) => execution.id) || [])} + /> + + ); + }, + className: styles.columnRecentRun, + key: 'recentRun', + label: 'recent run', + }, ], [styles], ); diff --git a/packages/zapp/console/src/components/LaunchPlan/LaunchPlanDetails.tsx b/packages/zapp/console/src/components/LaunchPlan/LaunchPlanDetails.tsx new file mode 100644 index 000000000..c8f87d2c4 --- /dev/null +++ b/packages/zapp/console/src/components/LaunchPlan/LaunchPlanDetails.tsx @@ -0,0 +1,33 @@ +import { withRouteParams } from 'components/common/withRouteParams'; +import { EntityDetails } from 'components/Entities/EntityDetails'; +import { ResourceIdentifier, ResourceType } from 'models/Common/types'; +import * as React from 'react'; + +export interface LaunchPlanDetailsRouteParams { + projectId: string; + domainId: string; + launchPlanName: string; +} +export type LaunchPlanDetailsProps = LaunchPlanDetailsRouteParams; + +/** The view component for the LaunchPlan landing page */ +export const LaunchPlanDetailsContainer: React.FC = ({ + projectId, + domainId, + launchPlanName, +}) => { + const id = React.useMemo( + () => ({ + resourceType: ResourceType.LAUNCH_PLAN, + project: projectId, + domain: domainId, + name: launchPlanName, + }), + [projectId, domainId, launchPlanName], + ); + return ; +}; + +export const LaunchPlanDetails = withRouteParams( + LaunchPlanDetailsContainer, +); diff --git a/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx b/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx new file mode 100644 index 000000000..e8b7137a5 --- /dev/null +++ b/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx @@ -0,0 +1,159 @@ +import { makeStyles, Theme } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { useNamedEntityListStyles } from 'components/common/SearchableNamedEntityList'; +import { useCommonStyles } from 'components/common/styles'; +import { separatorColor, primaryTextColor, launchPlanLabelColor } from 'components/Theme/constants'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Routes } from 'routes/routes'; +import { debounce } from 'lodash'; +import { Typography, FormGroup } from '@material-ui/core'; +import { ResourceType } from 'models/Common/types'; +import { MuiLaunchPlanIcon } from '@flyteconsole/ui-atoms'; +import { LaunchPlanListStructureItem } from './types'; +import { SearchableInput } from '../common/SearchableList'; +import { useSearchableListState } from '../common/useSearchableListState'; +import t, { patternKey } from '../Entities/strings'; +import { entityStrings } from '../Entities/constants'; + +interface SearchableLaunchPlanNameItemProps { + item: LaunchPlanListStructureItem; +} + +interface SearchableLaunchPlanNameListProps { + launchPlans: LaunchPlanListStructureItem[]; +} + +export const showOnHoverClass = 'showOnHover'; + +const useStyles = makeStyles((theme: Theme) => ({ + container: { + padding: theme.spacing(2), + paddingRight: theme.spacing(5), + }, + filterGroup: { + display: 'flex', + flexWrap: 'nowrap', + flexDirection: 'row', + margin: theme.spacing(4, 5, 0, 2), + }, + itemContainer: { + padding: theme.spacing(3, 3), + border: 'none', + borderTop: `1px solid ${separatorColor}`, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + position: 'relative', + // All children using the showOnHover class will be hidden until + // the mouse enters the container + [`& .${showOnHoverClass}`]: { + opacity: 0, + }, + [`&:hover .${showOnHoverClass}`]: { + opacity: 1, + }, + }, + itemName: { + display: 'flex', + fontWeight: 600, + color: primaryTextColor, + alignItems: 'center', + }, + itemIcon: { + marginRight: theme.spacing(2), + color: '#636379', + }, + itemRow: { + display: 'flex', + marginBottom: theme.spacing(1), + '&:last-child': { + marginBottom: 0, + }, + alignItems: 'center', + width: '100%', + }, + itemLabel: { + width: 140, + fontSize: 14, + color: launchPlanLabelColor, + }, + searchInputContainer: { + padding: 0, + }, +})); + +/** + * Renders individual searchable launchPlan item + * @param item + * @returns + */ +const SearchableLaunchPlanNameItem: React.FC = React.memo( + ({ item }) => { + const commonStyles = useCommonStyles(); + const listStyles = useNamedEntityListStyles(); + const styles = useStyles(); + const { id } = item; + + return ( + +
+
+ +
{id.name}
+
+
+ + ); + }, +); + +/** + * Renders a searchable list of LaunchPlan names, with associated descriptions + * @param launchPlans + * @constructor + */ +export const SearchableLaunchPlanNameList: React.FC = ({ + launchPlans, +}) => { + const styles = useStyles(); + const [search, setSearch] = React.useState(''); + const { results, setSearchString } = useSearchableListState({ + items: launchPlans, + propertyGetter: ({ id }) => id.name, + }); + + const onSearchChange = (event: React.ChangeEvent) => { + const searchString = event.target.value; + setSearch(searchString); + const debouncedSearch = debounce(() => setSearchString(searchString), 1000); + debouncedSearch(); + }; + const onClear = () => setSearch(''); + + return ( + <> + + + +
+ {results.map(({ value }) => ( + + ))} +
+ + ); +}; diff --git a/packages/zapp/console/src/components/LaunchPlan/launchPlanQueries.ts b/packages/zapp/console/src/components/LaunchPlan/launchPlanQueries.ts new file mode 100644 index 000000000..3f9da6cca --- /dev/null +++ b/packages/zapp/console/src/components/LaunchPlan/launchPlanQueries.ts @@ -0,0 +1,25 @@ +import { QueryInput, QueryType } from 'components/data/types'; +import { getLaunchPlan } from 'models/Launch/api'; +import { LaunchPlan, LaunchPlanId } from 'models/Launch/types'; +import { QueryClient } from 'react-query'; + +export function makeLaunchPlanQuery( + queryClient: QueryClient, + id: LaunchPlanId, +): QueryInput { + return { + queryKey: [QueryType.LaunchPlan, id], + queryFn: async () => { + const launchPlan = await getLaunchPlan(id); + + return launchPlan; + }, + // `LaunchPlan` objects (individual versions) are immutable and safe to + // cache indefinitely once retrieved in full + staleTime: Infinity, + }; +} + +export async function fetchLaunchPlan(queryClient: QueryClient, id: LaunchPlanId) { + return queryClient.fetchQuery(makeLaunchPlanQuery(queryClient, id)); +} diff --git a/packages/zapp/console/src/components/LaunchPlan/types.ts b/packages/zapp/console/src/components/LaunchPlan/types.ts new file mode 100644 index 000000000..a18b54f4b --- /dev/null +++ b/packages/zapp/console/src/components/LaunchPlan/types.ts @@ -0,0 +1,8 @@ +import { NamedEntityIdentifier } from 'models/Common/types'; +import { NamedEntityState } from 'models/enums'; + +export type LaunchPlanListStructureItem = { + id: NamedEntityIdentifier; + description: string; + state: NamedEntityState; +}; diff --git a/packages/zapp/console/src/components/LaunchPlan/useLaunchPlanInfoList.ts b/packages/zapp/console/src/components/LaunchPlan/useLaunchPlanInfoList.ts new file mode 100644 index 000000000..66ddb52e7 --- /dev/null +++ b/packages/zapp/console/src/components/LaunchPlan/useLaunchPlanInfoList.ts @@ -0,0 +1,28 @@ +import { DomainIdentifierScope, ResourceType } from 'models/Common/types'; +import { RequestConfig } from 'models/AdminEntity/types'; +import { usePagination } from 'components/hooks/usePagination'; +import { useAPIContext } from 'components/data/apiContext'; +import { LaunchPlanListStructureItem } from './types'; + +export const useLaunchPlanInfoList = (scope: DomainIdentifierScope, config?: RequestConfig) => { + const { listNamedEntities } = useAPIContext(); + + return usePagination( + { ...config, fetchArg: scope }, + async (scope, requestConfig) => { + const { entities, ...rest } = await listNamedEntities( + { ...scope, resourceType: ResourceType.LAUNCH_PLAN }, + requestConfig, + ); + + return { + entities: entities.map(({ id, metadata: { description, state } }) => ({ + id, + description, + state, + })), + ...rest, + }; + }, + ); +}; diff --git a/packages/zapp/console/src/components/Navigation/ProjectNavigation.tsx b/packages/zapp/console/src/components/Navigation/ProjectNavigation.tsx index b4b75f645..590d631f1 100644 --- a/packages/zapp/console/src/components/Navigation/ProjectNavigation.tsx +++ b/packages/zapp/console/src/components/Navigation/ProjectNavigation.tsx @@ -13,6 +13,7 @@ import * as React from 'react'; import { matchPath, NavLink, NavLinkProps } from 'react-router-dom'; import { history } from 'routes/history'; import { Routes } from 'routes/routes'; +import { MuiLaunchPlanIcon } from '@flyteconsole/ui-atoms'; import { ProjectSelector } from './ProjectSelector'; interface ProjectNavigationRouteParams { @@ -113,6 +114,20 @@ const ProjectNavigationImpl: React.FC = ({ path: Routes.ProjectDetails.sections.tasks.makeUrl(project.value.id, domainId), text: 'Tasks', }, + { + icon: MuiLaunchPlanIcon, + isActive: (match, location) => { + const finalMatch = match + ? match + : matchPath(location.pathname, { + path: Routes.LaunchPlanDetails.path, + exact: false, + }); + return !!finalMatch; + }, + path: Routes.ProjectDetails.sections.launchPlans.makeUrl(project.value.id, domainId), + text: 'Launch Plans', + }, ]; return ( diff --git a/packages/zapp/console/src/components/Project/ProjectDetails.tsx b/packages/zapp/console/src/components/Project/ProjectDetails.tsx index 96f346856..f7cb7277b 100644 --- a/packages/zapp/console/src/components/Project/ProjectDetails.tsx +++ b/packages/zapp/console/src/components/Project/ProjectDetails.tsx @@ -11,6 +11,7 @@ import { Routes } from 'routes/routes'; import { ProjectDashboard } from './ProjectDashboard'; import { ProjectTasks } from './ProjectTasks'; import { ProjectWorkflows } from './ProjectWorkflows'; +import { ProjectLaunchPlans } from './ProjectLaunchPlans'; const useStyles = makeStyles((theme: Theme) => ({ tab: { @@ -30,11 +31,12 @@ const entityTypeToComponent = { executions: ProjectDashboard, tasks: ProjectTasks, workflows: ProjectWorkflows, + launchPlans: ProjectLaunchPlans, }; const ProjectEntitiesByDomain: React.FC<{ project: Project; - entityType: 'executions' | 'tasks' | 'workflows'; + entityType: 'executions' | 'tasks' | 'workflows' | 'launchPlans'; }> = ({ entityType, project }) => { const styles = useStyles(); const { params, setQueryState } = useQueryState<{ domain: string }>(); @@ -71,6 +73,10 @@ const ProjectTasksByDomain: React.FC<{ project: Project }> = ({ project }) => ( ); +const ProjectLaunchPlansByDomain: React.FC<{ project: Project }> = ({ project }) => ( + +); + /** The view component for the Project landing page */ export const ProjectDetailsContainer: React.FC = ({ projectId }) => { const project = useProject(projectId); @@ -88,6 +94,9 @@ export const ProjectDetailsContainer: React.FC = ({ p + + + ); diff --git a/packages/zapp/console/src/components/Project/ProjectLaunchPlans.tsx b/packages/zapp/console/src/components/Project/ProjectLaunchPlans.tsx new file mode 100644 index 000000000..7fe696802 --- /dev/null +++ b/packages/zapp/console/src/components/Project/ProjectLaunchPlans.tsx @@ -0,0 +1,38 @@ +import { WaitForData } from 'components/common/WaitForData'; +import { SearchableLaunchPlanNameList } from 'components/LaunchPlan/SearchableLaunchPlanNameList'; +import { limits } from 'models/AdminEntity/constants'; +import { SortDirection } from 'models/AdminEntity/types'; +import { launchSortFields } from 'models/Launch/constants'; +import * as React from 'react'; +import { useLaunchPlanInfoList } from '../LaunchPlan/useLaunchPlanInfoList'; + +export interface ProjectLaunchPlansProps { + projectId: string; + domainId: string; +} + +const DEFAULT_SORT = { + direction: SortDirection.ASCENDING, + key: launchSortFields.name, +}; + +/** A listing of the LaunchPlans registered for a project */ +export const ProjectLaunchPlans: React.FC = ({ + domainId: domain, + projectId: project, +}) => { + const launchPlans = useLaunchPlanInfoList( + { domain, project }, + { + limit: limits.NONE, + sort: DEFAULT_SORT, + filter: [], + }, + ); + + return ( + + + + ); +}; diff --git a/packages/zapp/console/src/components/Theme/constants.ts b/packages/zapp/console/src/components/Theme/constants.ts index 1e3703329..39d879978 100644 --- a/packages/zapp/console/src/components/Theme/constants.ts +++ b/packages/zapp/console/src/components/Theme/constants.ts @@ -45,6 +45,7 @@ export const mutedButtonHoverColor = COLOR_SPECTRUM.gray60.color; export const errorBackgroundColor = '#FBFBFC'; export const workflowLabelColor = COLOR_SPECTRUM.gray25.color; +export const launchPlanLabelColor = COLOR_SPECTRUM.gray25.color; export const statusColors = { FAILURE: COLOR_SPECTRUM.red20.color, diff --git a/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx b/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx index 2719a501a..5d4da6a19 100644 --- a/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -7,9 +7,7 @@ import { separatorColor, primaryTextColor, workflowLabelColor } from 'components import * as React from 'react'; import { Link } from 'react-router-dom'; import { Routes } from 'routes/routes'; -import { WorkflowExecutionPhase } from 'models/Execution/enums'; import { Shimmer } from 'components/common/Shimmer'; -import { WorkflowExecutionIdentifier } from 'models/Execution/types'; import { debounce } from 'lodash'; import { IconButton, @@ -27,6 +25,7 @@ import { NamedEntityState } from 'models/enums'; import { updateWorkflowState } from 'models/Workflow/api'; import { useState } from 'react'; import { useSnackbar } from 'notistack'; +import { padExecutionPaths, padExecutions } from 'common/utils'; import { WorkflowListStructureItem } from './types'; import ProjectStatusBar from '../Project/ProjectStatusBar'; import { workflowNoInputsString } from '../Launch/LaunchForm/constants'; @@ -142,25 +141,6 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -const padExecutions = (items: WorkflowExecutionPhase[]) => { - if (items.length >= 10) { - return items.slice(0, 10).reverse(); - } - const emptyExecutions = new Array(10 - items.length).fill(WorkflowExecutionPhase.QUEUED); - return [...items, ...emptyExecutions].reverse(); -}; - -const padExecutionPaths = (items: WorkflowExecutionIdentifier[]) => { - if (items.length >= 10) { - return items - .slice(0, 10) - .map((id) => Routes.ExecutionDetails.makeUrl(id)) - .reverse(); - } - const emptyExecutions = new Array(10 - items.length).fill(null); - return [...items.map((id) => Routes.ExecutionDetails.makeUrl(id)), ...emptyExecutions].reverse(); -}; - const getArchiveIcon = (isArchived: boolean) => isArchived ? : ; diff --git a/packages/zapp/console/src/components/data/types.ts b/packages/zapp/console/src/components/data/types.ts index 10db6648d..7b7fbd02d 100644 --- a/packages/zapp/console/src/components/data/types.ts +++ b/packages/zapp/console/src/components/data/types.ts @@ -14,6 +14,7 @@ export enum QueryType { Workflow = 'workflow', WorkflowExecution = 'workflowExecution', WorkflowExecutionList = 'workflowExecutionList', + LaunchPlan = 'launchPlan', } type QueryKeyArray = [QueryType, ...unknown[]]; diff --git a/packages/zapp/console/src/components/hooks/Entity/useEntityVersions.ts b/packages/zapp/console/src/components/hooks/Entity/useEntityVersions.ts index df2eb9ee3..ecec1862f 100644 --- a/packages/zapp/console/src/components/hooks/Entity/useEntityVersions.ts +++ b/packages/zapp/console/src/components/hooks/Entity/useEntityVersions.ts @@ -1,6 +1,5 @@ -import { IdentifierScope, Identifier, ResourceIdentifier } from 'models/Common/types'; +import { IdentifierScope, ResourceIdentifier } from 'models/Common/types'; import { RequestConfig } from 'models/AdminEntity/types'; -import { entityStrings } from 'components/Entities/constants'; import { usePagination } from '../usePagination'; import { EntityType, entityFunctions } from './constants'; diff --git a/packages/zapp/console/src/models/Launch/constants.ts b/packages/zapp/console/src/models/Launch/constants.ts index f38ef3581..beec28925 100644 --- a/packages/zapp/console/src/models/Launch/constants.ts +++ b/packages/zapp/console/src/models/Launch/constants.ts @@ -1,3 +1,3 @@ export const launchSortFields = { - createdAt: 'created_at', + name: 'name', }; diff --git a/packages/zapp/console/src/routes/ApplicationRouter.tsx b/packages/zapp/console/src/routes/ApplicationRouter.tsx index 0336daf18..3b84cb7e1 100644 --- a/packages/zapp/console/src/routes/ApplicationRouter.tsx +++ b/packages/zapp/console/src/routes/ApplicationRouter.tsx @@ -30,6 +30,11 @@ export const ApplicationRouter: React.FC = () => ( path={Routes.TaskDetails.path} component={withSideNavigation(components.taskDetails)} /> + + makeProjectBoundPath(project, `/launchPlans${domain ? `?domain=${domain}` : ''}`), + path: `${projectBasePath}/launchPlans`, + }, }, }; @@ -62,6 +67,13 @@ export class Routes { path: `${projectDomainBasePath}/workflows/:workflowName`, }; + // LaunchPlans + static LaunchPlanDetails = { + makeUrl: (project: string, domain: string, launchPlanName: string) => + makeProjectDomainBoundPath(project, domain, `/launchPlans/${launchPlanName}`), + path: `${projectDomainBasePath}/launchPlans/:launchPlanName`, + }; + // Entity Version Details static EntityVersionDetails = { makeUrl: (