diff --git a/.storybook/preview.js b/.storybook/preview.js index 1b2809c1a..776dc9908 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,7 @@ import React from 'react'; import { StorybookContainer } from './StorybookContainer'; +//👇 Configures Storybook to log 'onXxx' actions (example: onArchiveTask and onPinTask ) in the UI export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChart.stories.tsx b/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChart.stories.tsx new file mode 100644 index 000000000..53574c927 --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChart.stories.tsx @@ -0,0 +1,25 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import * as React from 'react'; +import { NodeExecutionPhase } from 'models/Execution/enums'; +import { BarChart } from '.'; + +const barItems = [ + { phase: NodeExecutionPhase.FAILED, startOffsetSec: 0, durationSec: 5, isFromCache: false }, + { phase: NodeExecutionPhase.SUCCEEDED, startOffsetSec: 10, durationSec: 2, isFromCache: true }, + { phase: NodeExecutionPhase.SUCCEEDED, startOffsetSec: 0, durationSec: 1, isFromCache: true }, + { phase: NodeExecutionPhase.RUNNING, startOffsetSec: 0, durationSec: 10, isFromCache: false }, + { phase: NodeExecutionPhase.UNDEFINED, startOffsetSec: 15, durationSec: 25, isFromCache: false }, + { phase: NodeExecutionPhase.SUCCEEDED, startOffsetSec: 7, durationSec: 20, isFromCache: false }, +]; + +export default { + title: 'Workflow/Timeline', + component: BarChart, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; +export const BarSection = Template.bind({}); +BarSection.args = { + items: barItems, + chartTimeIntervalSec: 1, +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChartSingleItem.stories.tsx b/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChartSingleItem.stories.tsx new file mode 100644 index 000000000..481e2fb78 --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/BarChartSingleItem.stories.tsx @@ -0,0 +1,49 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import * as React from 'react'; +import { NodeExecutionPhase } from 'models/Execution/enums'; +import { BarItemData } from './utils'; +import { BarChart } from '.'; + +const phaseEnumTyping = { + options: Object.values(NodeExecutionPhase), + mapping: Object.values(NodeExecutionPhase), + control: { + type: 'select', + labels: Object.keys(NodeExecutionPhase), + }, +}; + +interface SingleItemProps extends BarItemData { + chartTimeIntervalSec: number; +} + +/** + * This is a fake storybook only component, to allow ease experimentation whith single bar item + */ +const SingleBarItem = (props: SingleItemProps) => { + const items = [props]; + return ; +}; + +export default { + title: 'Workflow/Timeline', + component: SingleBarItem, + // 👇 Creates specific argTypes + argTypes: { + phase: phaseEnumTyping, + }, +} as ComponentMeta; + +const TemplateSingleItem: ComponentStory = (args) => ( + +); + +export const BarChartSingleItem = TemplateSingleItem.bind({}); +// const phaseDataSingle = generateChartData([barItems[0]]); +BarChartSingleItem.args = { + phase: NodeExecutionPhase.ABORTED, + startOffsetSec: 15, + durationSec: 30, + isFromCache: false, + chartTimeIntervalSec: 5, +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/barOptions.ts b/src/components/Executions/ExecutionDetails/Timeline/BarChart/barOptions.ts new file mode 100644 index 000000000..99f50400a --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/barOptions.ts @@ -0,0 +1,60 @@ +import { Chart as ChartJS, registerables, Tooltip } from 'chart.js'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +ChartJS.register(...registerables, ChartDataLabels); + +// Create positioner to put tooltip at cursor position +Tooltip.positioners.cursor = function (_chartElements, coordinates) { + return coordinates; +}; + +export const getBarOptions = (chartTimeIntervalSec: number, tooltipLabels: string[][]) => { + return { + animation: false as const, + indexAxis: 'y' as const, + elements: { + bar: { + borderWidth: 2, + }, + }, + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + tooltip: { + // Setting up tooltip: https://www.chartjs.org/docs/latest/configuration/tooltip.html + position: 'cursor', + filter: function (tooltipItem) { + // no tooltip for offsets + return tooltipItem.datasetIndex === 1; + }, + callbacks: { + label: function (context) { + const index = context.dataIndex; + return tooltipLabels[index] ?? ''; + }, + }, + }, + }, + scales: { + x: { + format: Intl.DateTimeFormat, + position: 'top' as const, + ticks: { + display: false, + autoSkip: false, + stepSize: chartTimeIntervalSec, + }, + stacked: true, + }, + y: { + stacked: true, + }, + }, + }; +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/chartData.ts b/src/components/Executions/ExecutionDetails/Timeline/BarChart/chartData.ts new file mode 100644 index 000000000..adbf5e8bf --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/chartData.ts @@ -0,0 +1,75 @@ +import { timestampToDate } from 'common/utils'; +import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; +import { dNode } from 'models/Graph/types'; +import { BarItemData } from './utils'; + +const WEEK_DURATION_SEC = 7 * 24 * 3600; + +const EMPTY_BAR_ITEM: BarItemData = { + phase: NodeExecutionPhase.UNDEFINED, + startOffsetSec: 0, + durationSec: 0, + isFromCache: false, +}; + +export const getChartDurationData = ( + nodes: dNode[], + startedAt: Date, +): { items: BarItemData[]; totalDurationSec: number } => { + if (nodes.length === 0) return { items: [], totalDurationSec: 0 }; + + let totalDurationSec = 0; + const initialStartTime = startedAt.getTime(); + const result: BarItemData[] = nodes.map(({ execution }) => { + if (!execution) { + return EMPTY_BAR_ITEM; + } + + let phase = execution.closure.phase; + const isFromCache = + execution.closure.taskNodeMetadata?.cacheStatus === CatalogCacheStatus.CACHE_HIT; + + // Offset values + let startOffset = 0; + const startedAt = execution.closure.startedAt; + if (isFromCache) { + if (execution.closure.createdAt) { + startOffset = timestampToDate(execution.closure.createdAt).getTime() - initialStartTime; + } + } else if (startedAt) { + startOffset = timestampToDate(startedAt).getTime() - initialStartTime; + } + + // duration + let durationSec = 0; + if (isFromCache) { + const updatedAt = execution.closure.updatedAt?.seconds?.toNumber() ?? 0; + const createdAt = execution.closure.createdAt?.seconds?.toNumber() ?? 0; + durationSec = updatedAt - createdAt; + durationSec = durationSec === 0 ? 2 : durationSec; + } else if (phase === NodeExecutionPhase.RUNNING) { + if (startedAt) { + const duration = Date.now() - timestampToDate(startedAt).getTime(); + durationSec = duration / 1000; + if (durationSec > WEEK_DURATION_SEC) { + // TODO: https://github.com/flyteorg/flyteconsole/issues/332 + // In some cases tasks which were needed to be ABORTED are stuck in running state, + // In case if task is still running after a week - we assume it should have been aborted. + // The proper fix should be covered by isue: flyteconsole#332 + phase = NodeExecutionPhase.ABORTED; + const allegedDurationSec = Math.trunc(totalDurationSec - startOffset / 1000); + durationSec = allegedDurationSec > 0 ? allegedDurationSec : 10; + } + } + } else { + durationSec = execution.closure.duration?.seconds?.toNumber() ?? 0; + } + + const startOffsetSec = Math.trunc(startOffset / 1000); + totalDurationSec = Math.max(totalDurationSec, startOffsetSec + durationSec); + return { phase, startOffsetSec, durationSec, isFromCache }; + }); + + // Do we want to get initialStartTime from different place, to avoid recalculating it. + return { items: result, totalDurationSec }; +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/index.tsx b/src/components/Executions/ExecutionDetails/Timeline/BarChart/index.tsx new file mode 100644 index 000000000..903d96f9a --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/index.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { Bar } from 'react-chartjs-2'; +import { getBarOptions } from './barOptions'; +import { BarItemData, generateChartData, getChartData } from './utils'; + +interface BarChartProps { + items: BarItemData[]; + chartTimeIntervalSec: number; +} + +export const BarChart = (props: BarChartProps) => { + const phaseData = generateChartData(props.items); + + return ( + + ); +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/BarChart/utils.ts b/src/components/Executions/ExecutionDetails/Timeline/BarChart/utils.ts new file mode 100644 index 000000000..a8b1d9510 --- /dev/null +++ b/src/components/Executions/ExecutionDetails/Timeline/BarChart/utils.ts @@ -0,0 +1,139 @@ +import { getNodeExecutionPhaseConstants } from 'components/Executions/utils'; +import { primaryTextColor } from 'components/Theme/constants'; +import { NodeExecutionPhase } from 'models/Execution/enums'; + +export const CASHED_GREEN = 'rgba(74,227,174,0.25)'; // statusColors.SUCCESS (Mint20) with 25% opacity +export const TRANSPARENT = 'rgba(0, 0, 0, 0)'; + +export enum RelationToCache { + None = 'none', + ReadFromCaceh = 'Read from Cache', + WroteToCache = 'Wrote to cache', +} + +export interface BarItemData { + phase: NodeExecutionPhase; + startOffsetSec: number; + durationSec: number; + isFromCache: boolean; +} + +interface ChartDataInput { + elementsNumber: number; + durations: number[]; + startOffset: number[]; + offsetColor: string[]; + tooltipLabel: string[][]; + barLabel: string[]; + barColor: string[]; +} + +/** + * Depending on amounf of second provided shows data in + * XhXmXs or XmXs or Xs format + */ +export const formatSecondsToHmsFormat = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + seconds %= 3600; + const minutes = Math.floor(seconds / 60); + seconds = seconds % 60; + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } + return `${seconds}s`; +}; + +// narusina - check if exports are still needed +export const getOffsetColor = (isCachedValue: boolean[]) => { + const colors = isCachedValue.map((val) => (val === true ? CASHED_GREEN : TRANSPARENT)); + return colors; +}; + +/** + * Generates chart data maps per each BarItemData ("node") section + */ +export const generateChartData = (data: BarItemData[]): ChartDataInput => { + const durations: number[] = []; + const startOffset: number[] = []; + const offsetColor: string[] = []; + const tooltipLabel: string[][] = []; + const barLabel: string[] = []; + const barColor: string[] = []; + + data.forEach((element) => { + const phaseConstant = getNodeExecutionPhaseConstants( + element.phase ?? NodeExecutionPhase.UNDEFINED, + ); + + const durationString = formatSecondsToHmsFormat(element.durationSec); + const tooltipString = `${phaseConstant.text}: ${durationString}`; + // don't show Label if there is now duration yet. + const labelString = element.durationSec > 0 ? durationString : ''; + + durations.push(element.durationSec); + startOffset.push(element.startOffsetSec); + offsetColor.push(element.isFromCache ? CASHED_GREEN : TRANSPARENT); + tooltipLabel.push(element.isFromCache ? [tooltipString, 'Read from cache'] : [tooltipString]); + barLabel.push(element.isFromCache ? '\u229A From cache' : labelString); + barColor.push(phaseConstant.badgeColor); + }); + + return { + elementsNumber: data.length, + durations, + startOffset, + offsetColor, + tooltipLabel, + barLabel, + barColor, + }; +}; + +/** + * Generates chart data format suitable for Chart.js Bar. Each bar consists of two data items: + * |-----------|XXXXXXXXXXXXXXXX| + * |-|XXXXXX| + * |------|XXXXXXXXXXXXX| + * Where |---| is offset - usually transparent part to give user a feeling that timeline wasn't started from ZERO time position + * Where |XXX| is duration of the operation, colored per step Phase status. + */ +export const getChartData = (data: ChartDataInput) => { + const defaultStyle = { + barPercentage: 1, + borderWidth: 0, + }; + + return { + labels: Array(data.elementsNumber).fill(''), // clear up Chart Bar default labels + datasets: [ + // fill-in offsets + { + ...defaultStyle, + data: data.startOffset, + backgroundColor: data.offsetColor, + datalabels: { + labels: { + title: null, + }, + }, + }, + // fill in duration bars + { + ...defaultStyle, + data: data.durations, + backgroundColor: data.barColor, + datalabels: { + // Positioning info - https://chartjs-plugin-datalabels.netlify.app/guide/positioning.html + color: primaryTextColor, + align: 'end' as const, // related to text + anchor: 'start' as const, // related to bar + formatter: function (value, context) { + return data.barLabel[context.dataIndex] ?? ''; + }, + }, + }, + ], + }; +}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 3430e32b3..08dad1406 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -1,28 +1,23 @@ import * as React from 'react'; -import * as moment from 'moment-timezone'; -import { Bar } from 'react-chartjs-2'; -import { Chart as ChartJS, registerables } from 'chart.js'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; import { makeStyles, Typography } from '@material-ui/core'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { isEndNode, isStartNode, isExpanded } from 'components/WorkflowGraph/utils'; import { tableHeaderColor } from 'components/Theme/constants'; +import { timestampToDate } from 'common/utils'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; -import { TaskNames } from './taskNames'; -import { convertToPlainNodes, getBarOptions, TimeZone } from './helpers'; +import { getChartDurationData } from './BarChart/chartData'; +import { convertToPlainNodes } from './helpers'; import { ChartHeader } from './chartHeader'; -import { useChartDurationData } from './chartData'; import { useScaleContext } from './scaleContext'; - -// Register components to be usable by chart.js -ChartJS.register(...registerables, ChartDataLabels); +import { TaskNames } from './taskNames'; +import { BarChart } from './BarChart'; interface StyleProps { chartWidth: number; - durationLength: number; + itemsShown: number; } const useStyles = makeStyles((theme) => ({ @@ -30,7 +25,7 @@ const useStyles = makeStyles((theme) => ({ marginTop: -10, marginLeft: -15, width: `${props.chartWidth + 20}px`, - height: `${56 * props.durationLength + 20}px`, + height: `${56 * props.itemsShown + 20}px`, }), taskNames: { display: 'flex', @@ -85,8 +80,10 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime const [originalNodes, setOriginalNodes] = React.useState([]); const [showNodes, setShowNodes] = React.useState([]); + const [startedAt, setStartedAt] = React.useState(new Date()); const { compiledWorkflowClosure } = useNodeExecutionContext(); + const { chartInterval: chartTimeInterval } = useScaleContext(); React.useEffect(() => { const nodes: dNode[] = compiledWorkflowClosure @@ -99,39 +96,38 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime React.useEffect(() => { const initializeNodes = convertToPlainNodes(originalNodes); - setShowNodes( - initializeNodes.map((node) => { - const index = nodeExecutions.findIndex((exe) => exe.scopedId === node.scopedId); - return { - ...node, - execution: index >= 0 ? nodeExecutions[index] : undefined, - }; - }), - ); - }, [originalNodes, nodeExecutions]); + const updatedShownNodesMap = initializeNodes.map((node) => { + const index = nodeExecutions.findIndex((exe) => exe.scopedId === node.scopedId); + return { + ...node, + execution: index >= 0 ? nodeExecutions[index] : undefined, + }; + }); + setShowNodes(updatedShownNodesMap); - const { startedAt, totalDuration, durationLength, chartData } = useChartDurationData({ - nodes: showNodes, - }); - const { chartInterval: chartTimeInterval, setMaxValue } = useScaleContext(); - const styles = useStyles({ chartWidth: chartWidth, durationLength: durationLength }); + // set startTime for all timeline offset and duration calculations. + const firstStartedAt = updatedShownNodesMap[0]?.execution?.closure.startedAt; + if (firstStartedAt) { + setStartedAt(timestampToDate(firstStartedAt)); + } + }, [originalNodes, nodeExecutions]); - React.useEffect(() => { - setMaxValue(totalDuration); - }, [totalDuration, setMaxValue]); + const { items: barItemsData, totalDurationSec } = getChartDurationData(showNodes, startedAt); + const styles = useStyles({ chartWidth: chartWidth, itemsShown: showNodes.length }); React.useEffect(() => { - const calcWidth = Math.ceil(totalDuration / chartTimeInterval) * INTERVAL_LENGTH; + // Sync width of elements and intervals of ChartHeader (time labels) and BarChart + const calcWidth = Math.ceil(totalDurationSec / chartTimeInterval) * INTERVAL_LENGTH; if (durationsRef.current && calcWidth < durationsRef.current.clientWidth) { setLabelInterval( - durationsRef.current.clientWidth / Math.ceil(totalDuration / chartTimeInterval), + durationsRef.current.clientWidth / Math.ceil(totalDurationSec / chartTimeInterval), ); setChartWidth(durationsRef.current.clientWidth); } else { setChartWidth(calcWidth); setLabelInterval(INTERVAL_LENGTH); } - }, [totalDuration, chartTimeInterval, durationsRef]); + }, [totalDurationSec, chartTimeInterval, durationsRef]); const onGraphScroll = () => { // cover horizontal scroll only @@ -177,17 +173,6 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime setOriginalNodes([...originalNodes]); }; - const labels = React.useMemo(() => { - const len = Math.ceil(totalDuration / chartTimeInterval); - const lbs = len > 0 ? new Array(len).fill(0) : []; - return lbs.map((_, idx) => { - const time = moment.utc(new Date(startedAt.getTime() + idx * chartTimeInterval * 1000)); - return chartTimezone === TimeZone.UTC - ? time.format('hh:mm:ss A') - : time.local().format('hh:mm:ss A'); - }); - }, [chartTimezone, startedAt, chartTimeInterval, totalDuration]); - return ( <>
@@ -201,11 +186,17 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime
- +
- +
diff --git a/src/components/Executions/ExecutionDetails/Timeline/chartData.tsx b/src/components/Executions/ExecutionDetails/Timeline/chartData.tsx deleted file mode 100644 index dbef7d932..000000000 --- a/src/components/Executions/ExecutionDetails/Timeline/chartData.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { durationToMilliseconds, timestampToDate } from 'common/utils'; -import { getNodeExecutionPhaseConstants } from 'components/Executions/utils'; -import { NodeExecutionPhase } from 'models/Execution/enums'; -import { dNode } from 'models/Graph/types'; -import * as React from 'react'; - -interface DataProps { - nodes: dNode[]; -} - -export const useChartDurationData = (props: DataProps) => { - const colorData = React.useMemo(() => { - const definedExecutions = props.nodes.map( - ({ execution }) => - getNodeExecutionPhaseConstants(execution?.closure.phase ?? NodeExecutionPhase.UNDEFINED) - .badgeColor, - ); - return definedExecutions; - }, [props.nodes]); - - const startedAt = React.useMemo(() => { - if (props.nodes.length === 0 || !props.nodes[0].execution?.closure.startedAt) { - return new Date(); - } - return timestampToDate(props.nodes[0].execution?.closure.startedAt); - }, [props.nodes]); - - const stackedData = React.useMemo(() => { - let undefinedStart = 0; - for (const node of props.nodes) { - const exec = node.execution; - if (exec?.closure.startedAt) { - const startedTime = timestampToDate(exec?.closure.startedAt).getTime(); - const absoluteDuration = - startedTime - - startedAt.getTime() + - (exec?.closure.duration - ? durationToMilliseconds(exec?.closure.duration) - : Date.now() - startedTime); - if (absoluteDuration > undefinedStart) { - undefinedStart = absoluteDuration; - } - } - } - undefinedStart = undefinedStart / 1000; - - const definedExecutions = props.nodes.map(({ execution }) => - execution?.closure.startedAt - ? (timestampToDate(execution?.closure.startedAt).getTime() - startedAt.getTime()) / 1000 - : 0, - ); - - return definedExecutions; - }, [props.nodes, startedAt]); - - // Divide by 1000 to calculate all duration data be second based. - const durationData = React.useMemo(() => { - const definedExecutions = props.nodes.map((node) => { - const exec = node.execution; - if (!exec) return 0; - if (exec.closure.phase === NodeExecutionPhase.RUNNING) { - if (!exec.closure.startedAt) { - return 0; - } - return (Date.now() - timestampToDate(exec.closure.startedAt).getTime()) / 1000; - } - if (!exec.closure.duration) { - return 0; - } - return durationToMilliseconds(exec.closure.duration) / 1000; - }); - return definedExecutions; - }, [props.nodes]); - - const totalDuration = React.useMemo(() => { - const durations = durationData.map((duration, idx) => duration + stackedData[idx]); - return Math.max(...durations); - }, [durationData, stackedData]); - - const stackedColorData = React.useMemo(() => { - return durationData.map((duration) => { - return duration === 0 ? '#4AE3AE40' : 'rgba(0, 0, 0, 0)'; - }); - }, [durationData]); - - const chartData = React.useMemo(() => { - return { - labels: durationData.map(() => ''), - datasets: [ - { - data: stackedData, - backgroundColor: stackedColorData, - barPercentage: 1, - borderWidth: 0, - datalabels: { - labels: { - title: null, - }, - }, - }, - { - data: durationData.map((duration) => { - return duration === -1 ? 10 : duration === 0 ? 0.5 : duration; - }), - backgroundColor: colorData, - barPercentage: 1, - borderWidth: 0, - datalabels: { - color: '#292936' as const, - align: 'start' as const, - formatter: function (value, context) { - if (durationData[context.dataIndex] === -1) { - return ''; - } - return Math.round(value) + 's'; - }, - }, - }, - ], - }; - }, [durationData, stackedData, colorData, stackedColorData]); - - return { - startedAt, - totalDuration, - durationLength: durationData.length, - chartData, - }; -}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/chartHeader.tsx b/src/components/Executions/ExecutionDetails/Timeline/chartHeader.tsx index da56b6c63..1fafd08c8 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/chartHeader.tsx +++ b/src/components/Executions/ExecutionDetails/Timeline/chartHeader.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; +import * as moment from 'moment-timezone'; import makeStyles from '@material-ui/core/styles/makeStyles'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; +import { useScaleContext } from './scaleContext'; +import { TimeZone } from './helpers'; interface StyleProps { chartWidth: number; @@ -25,17 +28,37 @@ const useStyles = makeStyles((_theme) => ({ })); interface HeaderProps extends StyleProps { - labels: string[]; + chartTimezone: string; + totalDurationSec: number; + startedAt: Date; } export const ChartHeader = (props: HeaderProps) => { const styles = useStyles(props); + const { chartInterval: chartTimeInterval, setMaxValue } = useScaleContext(); + const { startedAt, chartTimezone, totalDurationSec } = props; + + React.useEffect(() => { + setMaxValue(props.totalDurationSec); + }, [props.totalDurationSec, setMaxValue]); + + const labels = React.useMemo(() => { + const len = Math.ceil(totalDurationSec / chartTimeInterval); + const lbs = len > 0 ? new Array(len).fill('') : []; + return lbs.map((_, idx) => { + const time = moment.utc(new Date(startedAt.getTime() + idx * chartTimeInterval * 1000)); + return chartTimezone === TimeZone.UTC + ? time.format('hh:mm:ss A') + : time.local().format('hh:mm:ss A'); + }); + }, [chartTimezone, startedAt, chartTimeInterval, totalDurationSec]); + return (
- {props.labels.map((label, idx) => { + {labels.map((label) => { return ( -
+
{label}
); diff --git a/src/components/Executions/ExecutionDetails/Timeline/helpers.ts b/src/components/Executions/ExecutionDetails/Timeline/helpers.ts index fad3a5569..ddab892a0 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/helpers.ts +++ b/src/components/Executions/ExecutionDetails/Timeline/helpers.ts @@ -28,45 +28,3 @@ export function convertToPlainNodes(nodes: dNode[], level = 0): dNode[] { }); return result; } - -export const getBarOptions = (chartTimeInterval: number) => { - return { - animation: false as const, - indexAxis: 'y' as const, - elements: { - bar: { - borderWidth: 2, - }, - }, - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - }, - title: { - display: false, - }, - tooltip: { - filter: function (tooltipItem) { - return tooltipItem.datasetIndex === 1; - }, - }, - }, - scales: { - x: { - format: Intl.DateTimeFormat, - position: 'top' as const, - ticks: { - display: false, - autoSkip: false, - stepSize: chartTimeInterval, - }, - stacked: true, - }, - y: { - stacked: true, - }, - }, - }; -}; diff --git a/src/components/Executions/ExecutionDetails/Timeline/index.tsx b/src/components/Executions/ExecutionDetails/Timeline/index.tsx index 70792e2ed..e0349ddab 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/index.tsx +++ b/src/components/Executions/ExecutionDetails/Timeline/index.tsx @@ -6,6 +6,7 @@ import { NodeExecutionsRequestConfigContext } from 'components/Executions/contex import { useAllTreeNodeExecutionGroupsQuery } from 'components/Executions/nodeExecutionQueries'; import { DataError } from 'components/Errors/DataError'; import { DetailsPanel } from 'components/common/DetailsPanel'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; import { NodeExecutionsTimelineContext } from './context'; import { ExecutionTimelineFooter } from './ExecutionTimelineFooter'; @@ -24,6 +25,9 @@ const useStyles = makeStyles(() => ({ flex: '1 1 0', overflowY: 'auto', }, + loading: { + margin: 'auto', + }, })); interface TimelineProps { @@ -53,12 +57,24 @@ export const ExecutionNodesTimeline = (props: TimelineProps) => { return ; }; + const TimelineLoading = () => { + return ( +
+ +
+ ); + }; + return (
- + {renderExecutionsTimeline} diff --git a/src/components/Executions/ExecutionDetails/Timeline/scaleContext.tsx b/src/components/Executions/ExecutionDetails/Timeline/scaleContext.tsx index 6977d23e4..343c82250 100644 --- a/src/components/Executions/ExecutionDetails/Timeline/scaleContext.tsx +++ b/src/components/Executions/ExecutionDetails/Timeline/scaleContext.tsx @@ -1,6 +1,7 @@ import { Mark } from '@material-ui/core/Slider'; import * as React from 'react'; import { createContext, useContext } from 'react'; +import { formatSecondsToHmsFormat } from './BarChart/utils'; const MIN_SCALE_VALUE = 60; // 1 min const MAX_SCALE_VALUE = 3600; // 1h @@ -36,16 +37,6 @@ export const ScaleContext = createContext({ /** Could be used to access the whole TimelineScaleState */ export const useScaleContext = (): TimelineScaleState => useContext(ScaleContext); -const formatSeconds = (time: number) => { - if (time < 60) { - return `${time}s`; - } - if (time % 60 === 0) { - return `${Math.floor(time / 60)}m`; - } - return `${Math.floor(time / 60)}m ${time % 60}s`; -}; - interface ScaleProviderProps { children?: React.ReactNode; } @@ -69,7 +60,7 @@ export const ScaleProvider = (props: ScaleProviderProps) => { for (let i = 0; i < percentage.length; ++i) { newMarks.push({ value: i, - label: formatSeconds(getIntervalValue(i)), + label: formatSecondsToHmsFormat(getIntervalValue(i)), }); } setMarks(newMarks); diff --git a/src/components/Executions/ExecutionDetails/test/BarChart.test.tsx b/src/components/Executions/ExecutionDetails/test/BarChart.test.tsx new file mode 100644 index 000000000..5d8270b4b --- /dev/null +++ b/src/components/Executions/ExecutionDetails/test/BarChart.test.tsx @@ -0,0 +1,68 @@ +import { getChartDurationData } from '../Timeline/BarChart/chartData'; +import { + CASHED_GREEN, + formatSecondsToHmsFormat, + generateChartData, + getOffsetColor, + TRANSPARENT, +} from '../Timeline/BarChart/utils'; +import { getMockExecutionsForBarChart, mockbarItems } from './__mocks__/NodeExecution.mock'; + +describe('ExecutionDetails > Timeline > BarChart', () => { + it('formatSecondsToHmsFormat works as expected', () => { + // more than hour + expect(formatSecondsToHmsFormat(4231)).toEqual('1h 10m 31s'); + expect(formatSecondsToHmsFormat(3601)).toEqual('1h 0m 1s'); + // 1 hour + expect(formatSecondsToHmsFormat(3600)).toEqual('1h 0m 0s'); + + // less than 1 hour and more than 1 minute + expect(formatSecondsToHmsFormat(3599)).toEqual('59m 59s'); + expect(formatSecondsToHmsFormat(600)).toEqual('10m 0s'); + expect(formatSecondsToHmsFormat(61)).toEqual('1m 1s'); + // 1 minute + expect(formatSecondsToHmsFormat(60)).toEqual('1m 0s'); + // less than minute + expect(formatSecondsToHmsFormat(59)).toEqual('59s'); + expect(formatSecondsToHmsFormat(23)).toEqual('23s'); + expect(formatSecondsToHmsFormat(0)).toEqual('0s'); + }); + + it('getOffsetColor returns colored background for cached items', () => { + const cachedArray = [false, true, false]; + const offsetColors = getOffsetColor(cachedArray); + + // If items is not cached - offset is transparent + expect(offsetColors[0]).toEqual(TRANSPARENT); + expect(offsetColors[2]).toEqual(TRANSPARENT); + // If cached - colored backfground + expect(offsetColors[1]).toEqual(CASHED_GREEN); + }); + + // Mock bars used below + // const mockbarItems = [ + // { phase: NodeExecutionPhase.FAILED, startOffsetSec: 0, durationSec: 15, isFromCache: false }, + // { phase: NodeExecutionPhase.SUCCEEDED, startOffsetSec: 5, durationSec: 11, isFromCache: true }, + // { phase: NodeExecutionPhase.RUNNING, startOffsetSec: 17, durationSec: 23, isFromCache: false }, + // { phase: NodeExecutionPhase.QUEUED, startOffsetSec: 39, durationSec: 0, isFromCache: false }, + // ]; + // it('getChartDurationData is properly generated from Node[] items', () => { + // /** */ + // const startTime = 1642627611; + // const mockData = getMockExecutionsForBarChart(startTime); + // const chartItems = getChartDurationData(mockData, new Date(1642627611 * 1000)); + + // expect(chartItems[0]).toEqual(mockbarItems[0]); + // }); + + it('generateChartData properly generates map of data for ChartBars', () => { + const chartData = generateChartData(mockbarItems); + expect(chartData.durations).toEqual([15, 11, 23, 0]); + expect(chartData.startOffset).toEqual([0, 5, 17, 39]); + expect(chartData.offsetColor).toEqual([TRANSPARENT, CASHED_GREEN, TRANSPARENT, TRANSPARENT]); + // labels looks as expected + expect(chartData.barLabel[0]).toEqual(formatSecondsToHmsFormat(mockbarItems[0].durationSec)); + expect(chartData.barLabel[1]).toEqual('\u229A From cache'); + expect(chartData.barLabel[3]).toEqual(''); + }); +}); diff --git a/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx b/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx index 3b11118f8..874dc7557 100644 --- a/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx @@ -19,6 +19,7 @@ jest.mock('../ExecutionWorkflowGraph.tsx', () => ({ jest.mock('chart.js', () => ({ Chart: { register: () => null }, + Tooltip: { positioners: { cursor: () => null } }, registerables: [], })); diff --git a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx index 74bf66482..02570b12a 100644 --- a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx @@ -1,11 +1,12 @@ import { render, waitFor } from '@testing-library/react'; import { cacheStatusMessages, viewSourceExecutionString } from 'components/Executions/constants'; import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { Core } from 'flyteidl'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { insertFixture } from 'mocks/data/insertFixture'; import { mockServer } from 'mocks/server'; +import { ResourceType } from 'models/Common/types'; +import { CatalogCacheStatus } from 'models/Execution/enums'; import { NodeExecution, TaskNodeMetadata } from 'models/Execution/types'; import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; import * as React from 'react'; @@ -53,10 +54,10 @@ describe('NodeExecutionDetails', () => { let taskNodeMetadata: TaskNodeMetadata; beforeEach(() => { taskNodeMetadata = { - cacheStatus: Core.CatalogCacheStatus.CACHE_MISS, + cacheStatus: CatalogCacheStatus.CACHE_MISS, catalogKey: { datasetId: makeIdentifier({ - resourceType: Core.ResourceType.DATASET, + resourceType: ResourceType.DATASET, }), sourceTaskExecution: { ...mockTaskExecution.id }, }, @@ -66,14 +67,14 @@ describe('NodeExecutionDetails', () => { }); [ - Core.CatalogCacheStatus.CACHE_DISABLED, - Core.CatalogCacheStatus.CACHE_HIT, - Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE, - Core.CatalogCacheStatus.CACHE_MISS, - Core.CatalogCacheStatus.CACHE_POPULATED, - Core.CatalogCacheStatus.CACHE_PUT_FAILURE, + CatalogCacheStatus.CACHE_DISABLED, + CatalogCacheStatus.CACHE_HIT, + CatalogCacheStatus.CACHE_LOOKUP_FAILURE, + CatalogCacheStatus.CACHE_MISS, + CatalogCacheStatus.CACHE_POPULATED, + CatalogCacheStatus.CACHE_PUT_FAILURE, ].forEach((cacheStatusValue) => - it(`renders correct status for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { + it(`renders correct status for ${CatalogCacheStatus[cacheStatusValue]}`, async () => { taskNodeMetadata.cacheStatus = cacheStatusValue; mockServer.insertNodeExecution(execution); const { getByText } = renderComponent(); @@ -82,7 +83,7 @@ describe('NodeExecutionDetails', () => { ); it('renders source execution link for cache hits', async () => { - taskNodeMetadata.cacheStatus = Core.CatalogCacheStatus.CACHE_HIT; + taskNodeMetadata.cacheStatus = CatalogCacheStatus.CACHE_HIT; const sourceWorkflowExecutionId = taskNodeMetadata.catalogKey!.sourceTaskExecution.nodeExecutionId.executionId; const { getByText } = renderComponent(); diff --git a/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx b/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx index 31949aa4d..d1e68dba2 100644 --- a/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx @@ -18,6 +18,7 @@ jest.mock('../ExecutionWorkflowGraph.tsx', () => ({ jest.mock('chart.js', () => ({ Chart: { register: () => null }, + Tooltip: { positioners: { cursor: () => null } }, registerables: [], })); diff --git a/src/components/Executions/ExecutionDetails/test/__mocks__/NodeExecution.mock.ts b/src/components/Executions/ExecutionDetails/test/__mocks__/NodeExecution.mock.ts new file mode 100644 index 000000000..d5a0a33f5 --- /dev/null +++ b/src/components/Executions/ExecutionDetails/test/__mocks__/NodeExecution.mock.ts @@ -0,0 +1,77 @@ +import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; + +const dNodeBasicExecution = { + id: 'other-root-n0', + scopedId: 'other-root-n0', + execution: { + id: { + nodeId: 'other-root-n0', + executionId: { project: 'flytesnacks', domain: 'development', name: 'rnktdb3skr' }, + }, + closure: { + phase: 3, + startedAt: { seconds: { low: 1642627611, high: 0, unsigned: false }, nanos: 0 }, + duration: { seconds: { low: 55, high: 0, unsigned: false }, nanos: 0 }, + createdAt: { seconds: { low: 1642627611, high: 0, unsigned: false }, nanos: 0 }, + updatedAt: { seconds: { low: 1642627666, high: 0, unsigned: false }, nanos: 0 }, + outputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-rnktdb3skr/other-root-n0/data/0/outputs.pb', + }, + metadata: { isParentNode: true, specNodeId: 'other-root-n0' }, + scopedId: 'other-root-n0', + }, +}; + +const getMockNodeExecution = ( + initialStartSec: number, + phase: NodeExecutionPhase, + startOffsetSec: number, + durationSec: number, + cacheStatus?: CatalogCacheStatus, +) => { + const node = { ...dNodeBasicExecution } as any; + node.execution.closure.phase = phase; + if (cacheStatus) { + node.execution.closure = { + ...node.execution.closure, + taskNodeMetadata: { + cacheStatus: cacheStatus, + }, + }; + if (cacheStatus === CatalogCacheStatus.CACHE_HIT) { + node.execution.closure.createdAt.seconds.low = initialStartSec + startOffsetSec; + node.execution.closure.updatedAt.seconds.low = initialStartSec + startOffsetSec + durationSec; + return { + ...node, + execution: { + ...node.execution, + closure: { + ...node.execution.closure, + startedAt: undefined, + duration: undefined, + }, + }, + }; + } + } + node.execution.closure.startedAt.seconds.low = initialStartSec + startOffsetSec; + node.execution.closure.duration.seconds.low = initialStartSec + startOffsetSec + durationSec; + return node; +}; + +export const mockbarItems = [ + { phase: NodeExecutionPhase.FAILED, startOffsetSec: 0, durationSec: 15, isFromCache: false }, + { phase: NodeExecutionPhase.SUCCEEDED, startOffsetSec: 5, durationSec: 11, isFromCache: true }, + { phase: NodeExecutionPhase.RUNNING, startOffsetSec: 17, durationSec: 23, isFromCache: false }, + { phase: NodeExecutionPhase.QUEUED, startOffsetSec: 39, durationSec: 0, isFromCache: false }, +]; + +export const getMockExecutionsForBarChart = (startTimeSec: number) => { + const start = startTimeSec; + return [ + getMockNodeExecution(start, NodeExecutionPhase.FAILED, 0, 15), + getMockNodeExecution(start, NodeExecutionPhase.SUCCEEDED, 5, 11, CatalogCacheStatus.CACHE_HIT), + getMockNodeExecution(start, NodeExecutionPhase.RUNNING, 17, 23, CatalogCacheStatus.CACHE_MISS), + getMockNodeExecution(start, NodeExecutionPhase.QUEUED, 39, 0), + ]; +}; diff --git a/src/components/Executions/NodeExecutionCacheStatus.tsx b/src/components/Executions/NodeExecutionCacheStatus.tsx index 6d3a10558..72b1fbf14 100644 --- a/src/components/Executions/NodeExecutionCacheStatus.tsx +++ b/src/components/Executions/NodeExecutionCacheStatus.tsx @@ -7,7 +7,7 @@ import classnames from 'classnames'; import { assertNever } from 'common/utils'; import { PublishedWithChangesOutlined } from 'components/common/PublishedWithChanges'; import { useCommonStyles } from 'components/common/styles'; -import { Core } from 'flyteidl'; +import { CatalogCacheStatus } from 'models/Execution/enums'; import { TaskNodeMetadata } from 'models/Execution/types'; import * as React from 'react'; import { Link as RouterLink } from 'react-router-dom'; @@ -29,25 +29,25 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -/** Renders the appropriate icon for a given `Core.CatalogCacheStatus` */ +/** Renders the appropriate icon for a given CatalogCacheStatus */ export const NodeExecutionCacheStatusIcon: React.FC< SvgIconProps & { - status: Core.CatalogCacheStatus; + status: CatalogCacheStatus; } > = React.forwardRef(({ status, ...props }, ref) => { switch (status) { - case Core.CatalogCacheStatus.CACHE_DISABLED: - case Core.CatalogCacheStatus.CACHE_MISS: { + case CatalogCacheStatus.CACHE_DISABLED: + case CatalogCacheStatus.CACHE_MISS: { return ; } - case Core.CatalogCacheStatus.CACHE_HIT: { + case CatalogCacheStatus.CACHE_HIT: { return ; } - case Core.CatalogCacheStatus.CACHE_POPULATED: { + case CatalogCacheStatus.CACHE_POPULATED: { return ; } - case Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE: - case Core.CatalogCacheStatus.CACHE_PUT_FAILURE: { + case CatalogCacheStatus.CACHE_LOOKUP_FAILURE: + case CatalogCacheStatus.CACHE_PUT_FAILURE: { return ; } default: { diff --git a/src/components/Executions/Tables/nodeExecutionColumns.tsx b/src/components/Executions/Tables/nodeExecutionColumns.tsx index adac6b93f..e03d00d47 100644 --- a/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -2,9 +2,8 @@ import { Tooltip, Typography } from '@material-ui/core'; import { formatDateLocalTimezone, formatDateUTC, millisecondsToHMS } from 'common/formatters'; import { timestampToDate } from 'common/utils'; import { useCommonStyles } from 'components/common/styles'; -import { Core } from 'flyteidl'; import { isEqual } from 'lodash'; -import { NodeExecutionPhase } from 'models/Execution/enums'; +import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; import { TaskNodeMetadata } from 'models/Execution/types'; import * as React from 'react'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; @@ -107,10 +106,7 @@ const DisplayType: React.FC = ({ execution }) => return {type}; }; -const hiddenCacheStatuses = [ - Core.CatalogCacheStatus.CACHE_MISS, - Core.CatalogCacheStatus.CACHE_DISABLED, -]; +const hiddenCacheStatuses = [CatalogCacheStatus.CACHE_MISS, CatalogCacheStatus.CACHE_DISABLED]; function hasCacheStatus(taskNodeMetadata?: TaskNodeMetadata): taskNodeMetadata is TaskNodeMetadata { if (!taskNodeMetadata) { return false; diff --git a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx index ac3422205..c6e679f37 100644 --- a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -20,7 +20,6 @@ import { makeNodeExecutionListQuery } from 'components/Executions/nodeExecutionQ import { NodeExecutionDisplayType } from 'components/Executions/types'; import { nodeExecutionIsTerminal } from 'components/Executions/utils'; import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; -import { Core } from 'flyteidl'; import { cloneDeep } from 'lodash'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { dynamicExternalSubWorkflow } from 'mocks/data/fixtures/dynamicExternalSubworkflow'; @@ -35,8 +34,9 @@ import { notFoundError } from 'mocks/errors'; import { mockServer } from 'mocks/server'; import { FilterOperationName, RequestConfig } from 'models/AdminEntity/types'; import { nodeExecutionQueryParams } from 'models/Execution/constants'; -import { NodeExecutionPhase } from 'models/Execution/enums'; +import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; import { Execution, NodeExecution, TaskNodeMetadata } from 'models/Execution/types'; +import { ResourceType } from 'models/Common/types'; import * as React from 'react'; import { QueryClient, QueryClientProvider, useQueryClient } from 'react-query'; import { makeIdentifier } from 'test/modelUtils'; @@ -225,10 +225,10 @@ describe('NodeExecutionsTable', () => { const { taskExecutions } = nodeExecutions.pythonNode; cachedNodeExecution = nodeExecutions.pythonNode.data; taskNodeMetadata = { - cacheStatus: Core.CatalogCacheStatus.CACHE_MISS, + cacheStatus: CatalogCacheStatus.CACHE_MISS, catalogKey: { datasetId: makeIdentifier({ - resourceType: Core.ResourceType.DATASET, + resourceType: ResourceType.DATASET, }), sourceTaskExecution: { ...taskExecutions.firstAttempt.data.id, @@ -239,12 +239,12 @@ describe('NodeExecutionsTable', () => { }); [ - Core.CatalogCacheStatus.CACHE_HIT, - Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE, - Core.CatalogCacheStatus.CACHE_POPULATED, - Core.CatalogCacheStatus.CACHE_PUT_FAILURE, + CatalogCacheStatus.CACHE_HIT, + CatalogCacheStatus.CACHE_LOOKUP_FAILURE, + CatalogCacheStatus.CACHE_POPULATED, + CatalogCacheStatus.CACHE_PUT_FAILURE, ].forEach((cacheStatusValue) => - it(`renders correct icon for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { + it(`renders correct icon for ${CatalogCacheStatus[cacheStatusValue]}`, async () => { taskNodeMetadata.cacheStatus = cacheStatusValue; updateNodeExecutions([cachedNodeExecution]); const { getByTitle } = renderTable(); @@ -255,9 +255,9 @@ describe('NodeExecutionsTable', () => { }), ); - [Core.CatalogCacheStatus.CACHE_DISABLED, Core.CatalogCacheStatus.CACHE_MISS].forEach( + [CatalogCacheStatus.CACHE_DISABLED, CatalogCacheStatus.CACHE_MISS].forEach( (cacheStatusValue) => - it(`renders no icon for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { + it(`renders no icon for ${CatalogCacheStatus[cacheStatusValue]}`, async () => { taskNodeMetadata.cacheStatus = cacheStatusValue; updateNodeExecutions([cachedNodeExecution]); const { getByText, queryByTitle } = renderTable(); diff --git a/src/components/Executions/constants.ts b/src/components/Executions/constants.ts index 4af6a7c00..ca70252f1 100644 --- a/src/components/Executions/constants.ts +++ b/src/components/Executions/constants.ts @@ -4,8 +4,8 @@ import { secondaryTextColor, statusColors, } from 'components/Theme/constants'; -import { Core } from 'flyteidl'; import { + CatalogCacheStatus, NodeExecutionPhase, TaskExecutionPhase, WorkflowExecutionPhase, @@ -196,14 +196,13 @@ export const taskTypeToNodeExecutionDisplayType: { [TaskType.ARRAY_K8S]: NodeExecutionDisplayType.ARRAY_K8S, }; -export const cacheStatusMessages: { [k in Core.CatalogCacheStatus]: string } = { - [Core.CatalogCacheStatus.CACHE_DISABLED]: 'Caching was disabled for this execution.', - [Core.CatalogCacheStatus.CACHE_HIT]: 'Output for this execution was read from cache.', - [Core.CatalogCacheStatus.CACHE_LOOKUP_FAILURE]: 'Failed to lookup cache information.', - [Core.CatalogCacheStatus.CACHE_MISS]: 'No cached output was found for this execution.', - [Core.CatalogCacheStatus.CACHE_POPULATED]: 'The result of this execution was written to cache.', - [Core.CatalogCacheStatus.CACHE_PUT_FAILURE]: - 'Failed to write output for this execution to cache.', +export const cacheStatusMessages: { [k in CatalogCacheStatus]: string } = { + [CatalogCacheStatus.CACHE_DISABLED]: 'Caching was disabled for this execution.', + [CatalogCacheStatus.CACHE_HIT]: 'Output for this execution was read from cache.', + [CatalogCacheStatus.CACHE_LOOKUP_FAILURE]: 'Failed to lookup cache information.', + [CatalogCacheStatus.CACHE_MISS]: 'No cached output was found for this execution.', + [CatalogCacheStatus.CACHE_POPULATED]: 'The result of this execution was written to cache.', + [CatalogCacheStatus.CACHE_PUT_FAILURE]: 'Failed to write output for this execution to cache.', }; export const unknownCacheStatusString = 'Cache status is unknown'; export const viewSourceExecutionString = 'View source execution'; diff --git a/src/components/Executions/nodeExecutionQueries.ts b/src/components/Executions/nodeExecutionQueries.ts index c01981475..7d5e3b18b 100644 --- a/src/components/Executions/nodeExecutionQueries.ts +++ b/src/components/Executions/nodeExecutionQueries.ts @@ -243,7 +243,7 @@ async function fetchGroupsForParentNodeExecution( /** GraphUX uses workflowClosure which uses scopedId. This builds a scopedId via parent * nodeExecution to enable mapping between graph and other components */ let scopedId = parentScopeId; - if (scopedId != undefined) { + if (scopedId !== undefined) { scopedId += `-0-${child.metadata?.specNodeId}`; child['scopedId'] = scopedId; } else { diff --git a/src/models/Execution/enums.ts b/src/models/Execution/enums.ts index ea8935845..3209223ca 100644 --- a/src/models/Execution/enums.ts +++ b/src/models/Execution/enums.ts @@ -17,4 +17,6 @@ export type NodeExecutionPhase = Core.NodeExecution.Phase; export const NodeExecutionPhase = Core.NodeExecution.Phase; export type TaskExecutionPhase = Core.TaskExecution.Phase; export const TaskExecutionPhase = Core.TaskExecution.Phase; +export type CatalogCacheStatus = Core.CatalogCacheStatus; +export const CatalogCacheStatus = Core.CatalogCacheStatus; /* eslint-enable @typescript-eslint/no-redeclare */