diff --git a/src/ui/common/src/components/tables/CheckTableItem.tsx b/src/ui/common/src/components/tables/CheckTableItem.tsx index dd25a278d..d6f2e1c06 100644 --- a/src/ui/common/src/components/tables/CheckTableItem.tsx +++ b/src/ui/common/src/components/tables/CheckTableItem.tsx @@ -9,42 +9,57 @@ import Box from '@mui/material/Box'; import React from 'react'; import { theme } from '../../styles/theme/theme'; +import { stringToExecutionStatus } from '../../utils/shared'; +import { StatusIndicator } from '../workflows/workflowStatus'; interface CheckTableItemProps { checkValue: string; + status?: string; } export const CheckTableItem: React.FC = ({ checkValue, + status, }) => { let iconColor = theme.palette.black; let checkIcon = faMinus; - switch (checkValue.toLowerCase()) { - case 'true': { - checkIcon = faCircleCheck; - iconColor = theme.palette.Success; - break; - } - case 'false': { - checkIcon = faCircleExclamation; - iconColor = theme.palette.Error; - break; - } - case 'warning': { - checkIcon = faTriangleExclamation; - iconColor = theme.palette.Warning; - break; - } - case 'none': { - checkIcon = faMinus; - iconColor = theme.palette.black; - break; - } - default: { - // None of the icon cases met, just fall through and render table value. - return <>{checkValue}; + if (checkValue) { + switch (checkValue.toLowerCase()) { + case 'true': { + checkIcon = faCircleCheck; + iconColor = theme.palette.Success; + break; + } + case 'false': { + checkIcon = faCircleExclamation; + iconColor = theme.palette.Error; + break; + } + case 'warning': { + checkIcon = faTriangleExclamation; + iconColor = theme.palette.Warning; + break; + } + case 'none': { + checkIcon = faMinus; + iconColor = theme.palette.black; + break; + } + default: { + // None of the icon cases met, just fall through and render table value. + return <>{checkValue}; + } } + } else { + // Check value not found, render the status indicator for this check. + return ( + + ); } return ( diff --git a/src/ui/common/src/components/tables/MetricTableItem.tsx b/src/ui/common/src/components/tables/MetricTableItem.tsx new file mode 100644 index 000000000..c6348ff81 --- /dev/null +++ b/src/ui/common/src/components/tables/MetricTableItem.tsx @@ -0,0 +1,38 @@ +import { Typography } from '@mui/material'; +import React from 'react'; + +import { stringToExecutionStatus } from '../../utils/shared'; +import { StatusIndicator } from '../workflows/workflowStatus'; + +interface MetricTableItemProps { + metricValue?: string; + // ExecutionStatus serialized as a string. + status?: string; +} + +export const MetricTableItem: React.FC = ({ + metricValue, + status, +}) => { + if (!metricValue) { + return ( + + ); + } + + return ( + + {metricValue.toString()} + + ); +}; + +export default MetricTableItem; diff --git a/src/ui/common/src/components/tables/OperatorExecStateTable.tsx b/src/ui/common/src/components/tables/OperatorExecStateTable.tsx index d65086436..bbb2e38bb 100644 --- a/src/ui/common/src/components/tables/OperatorExecStateTable.tsx +++ b/src/ui/common/src/components/tables/OperatorExecStateTable.tsx @@ -9,6 +9,7 @@ import * as React from 'react'; import { Data, DataSchema } from '../../utils/data'; import { CheckTableItem } from './CheckTableItem'; +import MetricTableItem from './MetricTableItem'; export enum OperatorExecStateTableType { Metric = 'metric', @@ -44,6 +45,37 @@ export const OperatorExecStateTable: React.FC = ({ tableAlign = 'left', tableType, }) => { + const getTableItem = (tableType, columnName, value, status) => { + // return title text in bold. + if (columnName === 'title') { + return ( + + {value.toString()} + + ); + } else if (tableType === OperatorExecStateTableType.Metric) { + // Send off to the MetricTableItem component. + return ; + } else if (tableType === OperatorExecStateTableType.Check) { + return ; + } + + // Default case, code here shouldn't get hit assuming this table is just used to render metrics and cheecks. + return ( + + {value.toString()} + + ); + }; + return ( = ({ const columnName = column.name.toLowerCase(); const value = row[columnName]; - // For title columns we should just render the text. - // For a check's value column, we should render the appropriate icon. return ( - {tableType === OperatorExecStateTableType.Metric || - columnName === 'title' ? ( - - {value.toString()} - - ) : ( - - )} + {getTableItem(tableType, columnName, value, row.status)} ); })} diff --git a/src/ui/common/src/components/workflows/artifact/check/history.tsx b/src/ui/common/src/components/workflows/artifact/check/history.tsx index df392ebe5..da629691c 100644 --- a/src/ui/common/src/components/workflows/artifact/check/history.tsx +++ b/src/ui/common/src/components/workflows/artifact/check/history.tsx @@ -5,7 +5,9 @@ import React from 'react'; import { ArtifactResultsWithLoadingStatus } from '../../../../reducers/artifactResults'; import { theme } from '../../../../styles/theme/theme'; import { Data, DataSchema } from '../../../../utils/data'; -import ExecutionStatus from '../../../../utils/shared'; +import ExecutionStatus, { + stringToExecutionStatus, +} from '../../../../utils/shared'; import { isFailed, isInitial, isLoading } from '../../../../utils/shared'; import { StatusIndicator } from '../../workflowStatus'; @@ -49,13 +51,20 @@ const CheckHistory: React.FC = ({ schema: checkHistorySchema, data: (historyWithLoadingStatus.results?.results ?? []).map( (artifactStatusResult) => { + let timestamp = new Date( + artifactStatusResult.exec_state?.timestamps?.finished_at + ).toLocaleString(); + + // Checks that are canceled / fail to execute have no exec_state or finished_at time. + if (timestamp === 'Invalid Date') { + timestamp = 'Unknown'; + } + return { status: artifactStatusResult.exec_state?.status ?? 'Unknown', level: checkLevel ? checkLevel : 'undefined', value: artifactStatusResult.content_serialized, - timestamp: new Date( - artifactStatusResult.exec_state?.timestamps?.finished_at - ).toLocaleString(), + timestamp, }; } ), @@ -103,7 +112,11 @@ const CheckHistory: React.FC = ({ width="auto" > - + {entry.timestamp.toLocaleString()} diff --git a/src/ui/common/src/components/workflows/artifact/metric/history.tsx b/src/ui/common/src/components/workflows/artifact/metric/history.tsx index 0883569db..5bb2e8b39 100644 --- a/src/ui/common/src/components/workflows/artifact/metric/history.tsx +++ b/src/ui/common/src/components/workflows/artifact/metric/history.tsx @@ -49,16 +49,24 @@ const MetricsHistory: React.FC = ({ historyWithLoadingStatus }) => { schema: metricHistorySchema, data: (historyWithLoadingStatus.results?.results ?? []).map( (artifactStatusResult) => { + let timestamp = new Date( + artifactStatusResult.exec_state?.timestamps?.finished_at + ).toLocaleString(); + + // Metrics that are canceled / fail to execute have no exec_state, and thus no date. + if (timestamp === 'Invalid Date') { + timestamp = 'Unknown'; + } + return { status: artifactStatusResult.exec_state?.status ?? 'Unknown', - timestamp: new Date( - artifactStatusResult.exec_state?.timestamps?.finished_at - ).toLocaleString(), + timestamp, value: artifactStatusResult.content_serialized, }; } ), }; + const dataSortedByLatest = historicalData.data.sort( (x, y) => Date.parse(y['timestamp'] as string) - @@ -144,10 +152,12 @@ const MetricsHistory: React.FC = ({ historyWithLoadingStatus }) => { - {entry.timestamp.toLocaleString()} + {entry.timestamp} - {entry.value.toString()} + + {entry.value ? entry.value.toString() : '-'} + ); })} diff --git a/src/ui/common/src/components/workflows/artifact/metricsAndChecksOverview.tsx b/src/ui/common/src/components/workflows/artifact/metricsAndChecksOverview.tsx index 1746df71e..157cff46d 100644 --- a/src/ui/common/src/components/workflows/artifact/metricsAndChecksOverview.tsx +++ b/src/ui/common/src/components/workflows/artifact/metricsAndChecksOverview.tsx @@ -28,18 +28,21 @@ export const MetricsOverview: React.FC = ({ }) => { const metricTableEntries = { schema: schema, - data: metrics - .map((metricArtf) => { - let name = metricArtf.name; - if (name.endsWith('artifact') || name.endsWith('Aritfact')) { - name = name.slice(0, 0 - 'artifact'.length); - } - return { - title: name, - value: metricArtf.result?.content_serialized, - }; - }) - .filter((x) => !!x.value), + data: metrics.map((metricArtf) => { + let title = metricArtf.name; + if (title.endsWith('artifact') || title.endsWith('Aritfact')) { + title = title.slice(0, 0 - 'artifact'.length); + } + + const value = metricArtf.result?.content_serialized; + const status = metricArtf.result?.exec_state?.status; + + return { + title, + value, + status, + }; + }), }; return ( @@ -74,18 +77,16 @@ export type ChecksOverviewProps = { export const ChecksOverview: React.FC = ({ checks }) => { const checkTableEntries = { schema: schema, - data: checks - .map((checkArtf) => { - let name = checkArtf.name; - if (name.endsWith('artifact') || name.endsWith('Aritfact')) { - name = name.slice(0, 0 - 'artifact'.length); - } - return { - title: name, - value: checkArtf.result?.content_serialized, - }; - }) - .filter((x) => !!x.value), + data: checks.map((checkArtf) => { + let name = checkArtf.name; + if (name.endsWith('artifact') || name.endsWith('Aritfact')) { + name = name.slice(0, 0 - 'artifact'.length); + } + return { + title: name, + value: checkArtf.result?.content_serialized, + }; + }), }; return ( diff --git a/src/ui/common/src/handlers/responses/dag.ts b/src/ui/common/src/handlers/responses/dag.ts index 06648ae6a..241e02750 100644 --- a/src/ui/common/src/handlers/responses/dag.ts +++ b/src/ui/common/src/handlers/responses/dag.ts @@ -57,20 +57,26 @@ export function getMetricsAndChecksOnArtifact( opResult.spec?.type === OperatorType.Check ); - const metricsArtfIds = metricsOp.flatMap((opResult) => - opResult !== undefined ? opResult.outputs : [] - ); + const metricsArtfIds = metricsOp.flatMap((opResult) => { + return opResult !== undefined ? opResult.outputs : []; + }); + const metricsArtf = metricsArtfIds.map((id) => dagResult.artifacts[id]); const metricsDownstreamIds = metricsArtf.flatMap( (artfResult) => artfResult.to ); + const metricsDownstreamOps = metricsDownstreamIds.map( (id) => dagResult.operators[id] ); + checksOp.push(...metricsDownstreamOps); + const checksArtfIds = checksOp.flatMap((opResult) => opResult !== undefined ? opResult.outputs : [] ); + const checksArtf = checksArtfIds.map((id) => dagResult.artifacts[id]); + return { checks: checksArtf, metrics: metricsArtf }; } diff --git a/src/ui/common/src/utils/data.ts b/src/ui/common/src/utils/data.ts index 79a1e58d7..76e2cc3b1 100644 --- a/src/ui/common/src/utils/data.ts +++ b/src/ui/common/src/utils/data.ts @@ -42,6 +42,7 @@ export type Data = { // each key of the row object corresponds to a column. // column names must be unique (obviously ;) ) data: TableRow[]; + status?: string; }; export type DataPreviewLoadSpec = { diff --git a/src/ui/common/src/utils/shared.ts b/src/ui/common/src/utils/shared.ts index 75d407f1a..dfed53b60 100644 --- a/src/ui/common/src/utils/shared.ts +++ b/src/ui/common/src/utils/shared.ts @@ -36,6 +36,38 @@ export enum ExecutionStatus { Running = 'running', } +export const stringToExecutionStatus = (status: string): ExecutionStatus => { + let executionStatus = ExecutionStatus.Unknown; + switch (status) { + case 'unknown': + executionStatus = ExecutionStatus.Unknown; + break; + case 'succeeded': + executionStatus = ExecutionStatus.Succeeded; + break; + case 'failed': + executionStatus = ExecutionStatus.Failed; + break; + case 'pending': + executionStatus = ExecutionStatus.Pending; + break; + case 'canceled': + executionStatus = ExecutionStatus.Canceled; + break; + case 'registered': + executionStatus = ExecutionStatus.Registered; + break; + case 'running': + executionStatus = ExecutionStatus.Running; + break; + default: + executionStatus = ExecutionStatus.Unknown; + break; + } + + return executionStatus; +}; + export type ExecutionTimestamps = { registered_at?: string; pending_at?: string;