Skip to content

Commit

Permalink
chore: add loading state + simplify storybook
Browse files Browse the repository at this point in the history
Signed-off-by: Nastya Rusina <nastya@union.ai>
  • Loading branch information
anrusina committed Mar 28, 2022
1 parent 87846da commit 9ecf4d0
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,46 +1,25 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import * as React from 'react';
import { Bar } from 'react-chartjs-2';
import { NodeExecutionPhase } from 'models/Execution/enums';
import { generateChartData, getChartData } from './utils';
import { getBarOptions } from './barOptions';

const isFromCache = [false, true, true, false, false, false];
const phaseStatus = [
NodeExecutionPhase.FAILED,
NodeExecutionPhase.SUCCEEDED,
NodeExecutionPhase.SUCCEEDED,
NodeExecutionPhase.RUNNING,
NodeExecutionPhase.UNDEFINED,
NodeExecutionPhase.SUCCEEDED,
];
import { BarChart } from '.';

const barItems = [
{ phase: phaseStatus[0], startOffsetSec: 0, durationSec: 5, isFromCache: isFromCache[0] },
{ phase: phaseStatus[1], startOffsetSec: 10, durationSec: 2, isFromCache: isFromCache[1] },
{ phase: phaseStatus[2], startOffsetSec: 0, durationSec: 1, isFromCache: isFromCache[2] },
{ phase: phaseStatus[3], startOffsetSec: 0, durationSec: 10, isFromCache: isFromCache[3] },
{ phase: phaseStatus[4], startOffsetSec: 15, durationSec: 25, isFromCache: isFromCache[4] },
{ phase: phaseStatus[5], startOffsetSec: 7, durationSec: 20, isFromCache: isFromCache[5] },
{ 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: Bar,
} as ComponentMeta<typeof Bar>;

const Template: ComponentStory<typeof Bar> = (args) => <Bar {...args} />;
export const BarSingle = Template.bind({});
const phaseDataSingle = generateChartData([barItems[0]]);
BarSingle.args = {
options: getBarOptions(1, phaseDataSingle.tooltipLabel) as any,
data: getChartData(phaseDataSingle),
};
component: BarChart,
} as ComponentMeta<typeof BarChart>;

export const BarSection = () => {
const phaseData = generateChartData(barItems);
// Chart interval - 1s
return (
<Bar options={getBarOptions(1, phaseData.tooltipLabel) as any} data={getChartData(phaseData)} />
);
const Template: ComponentStory<typeof BarChart> = (args) => <BarChart {...args} />;
export const BarSection = Template.bind({});
BarSection.args = {
items: barItems,
chartTimeIntervalSec: 1,
};
Original file line number Diff line number Diff line change
@@ -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 <BarChart items={items} chartTimeIntervalSec={props.chartTimeIntervalSec} />;
};

export default {
title: 'Workflow/Timeline',
component: SingleBarItem,
// 👇 Creates specific argTypes
argTypes: {
phase: phaseEnumTyping,
},
} as ComponentMeta<typeof SingleBarItem>;

const TemplateSingleItem: ComponentStory<typeof SingleBarItem> = (args) => (
<SingleBarItem {...args} />
);

export const BarChartSingleItem = TemplateSingleItem.bind({});
// const phaseDataSingle = generateChartData([barItems[0]]);
BarChartSingleItem.args = {
phase: NodeExecutionPhase.ABORTED,
startOffsetSec: 15,
durationSec: 30,
isFromCache: false,
chartTimeIntervalSec: 5,
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Tooltip.positioners.cursor = function (_chartElements, coordinates) {
return coordinates;
};

export const getBarOptions = (chartTimeInterval: number, tooltipLabels: string[][]) => {
export const getBarOptions = (chartTimeIntervalSec: number, tooltipLabels: string[][]) => {
return {
animation: false as const,
indexAxis: 'y' as const,
Expand Down Expand Up @@ -48,7 +48,7 @@ export const getBarOptions = (chartTimeInterval: number, tooltipLabels: string[]
ticks: {
display: false,
autoSkip: false,
stepSize: chartTimeInterval,
stepSize: chartTimeIntervalSec,
},
stacked: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ const EMPTY_BAR_ITEM: BarItemData = {
isFromCache: false,
};

export const getChartDurationData = (nodes: dNode[], startedAt: Date): BarItemData[] => {
if (nodes.length === 0) return [];
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) {
Expand Down Expand Up @@ -51,9 +55,11 @@ export const getChartDurationData = (nodes: dNode[], startedAt: Date): BarItemDa
durationSec = execution.closure.duration?.seconds?.toNumber() ?? 0;
}

return { phase, startOffsetSec: startOffset / 1000, durationSec, isFromCache };
const startOffsetSec = 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 result;
return { items: result, totalDurationSec };
};
Original file line number Diff line number Diff line change
@@ -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 (
<Bar
options={getBarOptions(props.chartTimeIntervalSec, phaseData.tooltipLabel) as any}
data={getChartData(phaseData)}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export interface BarItemData {

interface ChartDataInput {
elementsNumber: number;
totalDurationSec: number;
durations: number[];
startOffset: number[];
offsetColor: string[];
Expand Down Expand Up @@ -63,7 +62,6 @@ export const generateChartData = (data: BarItemData[]): ChartDataInput => {
const barLabel: string[] = [];
const barColor: string[] = [];

let totalDurationSec = 0;
data.forEach((element) => {
const phaseConstant = getNodeExecutionPhaseConstants(
element.phase ?? NodeExecutionPhase.UNDEFINED,
Expand All @@ -80,13 +78,10 @@ export const generateChartData = (data: BarItemData[]): ChartDataInput => {
tooltipLabel.push(element.isFromCache ? [tooltipString, 'Read from cache'] : [tooltipString]);
barLabel.push(element.isFromCache ? '\u229A From cache' : labelString);
barColor.push(phaseConstant.badgeColor);

totalDurationSec = Math.max(totalDurationSec, element.startOffsetSec + element.durationSec);
});

return {
elementsNumber: data.length,
totalDurationSec,
durations,
startOffset,
offsetColor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as React from 'react';
import * as moment from 'moment-timezone';
import { Bar } from 'react-chartjs-2';
import { makeStyles, Typography } from '@material-ui/core';

import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails';
Expand All @@ -10,13 +8,12 @@ import { tableHeaderColor } from 'components/Theme/constants';
import { timestampToDate } from 'common/utils';
import { NodeExecution } from 'models/Execution/types';
import { dNode } from 'models/Graph/types';
import { generateChartData, getChartData } from './BarChart/utils';
import { getChartDurationData } from './BarChart/chartData';
import { convertToPlainNodes, TimeZone } from './helpers';
import { convertToPlainNodes } from './helpers';
import { ChartHeader } from './chartHeader';
import { useScaleContext } from './scaleContext';
import { TaskNames } from './taskNames';
import { getBarOptions } from './BarChart/barOptions';
import { BarChart } from './BarChart';

interface StyleProps {
chartWidth: number;
Expand Down Expand Up @@ -86,6 +83,7 @@ export const ExecutionTimeline: React.FC<ExProps> = ({ nodeExecutions, chartTime
const [startedAt, setStartedAt] = React.useState<Date>(new Date());

const { compiledWorkflowClosure } = useNodeExecutionContext();
const { chartInterval: chartTimeInterval } = useScaleContext();

React.useEffect(() => {
const nodes: dNode[] = compiledWorkflowClosure
Expand Down Expand Up @@ -114,29 +112,22 @@ export const ExecutionTimeline: React.FC<ExProps> = ({ nodeExecutions, chartTime
}
}, [originalNodes, nodeExecutions]);

const barItemsData = getChartDurationData(showNodes, startedAt);
const chartDataInput = generateChartData(barItemsData);
const { chartInterval: chartTimeInterval, setMaxValue } = useScaleContext();
const styles = useStyles({ chartWidth: chartWidth, itemsShown: chartDataInput.elementsNumber });
const { items: barItemsData, totalDurationSec } = getChartDurationData(showNodes, startedAt);
const styles = useStyles({ chartWidth: chartWidth, itemsShown: showNodes.length });

React.useEffect(() => {
setMaxValue(chartDataInput.totalDurationSec);
}, [chartDataInput.totalDurationSec, setMaxValue]);

React.useEffect(() => {
const calcWidth =
Math.ceil(chartDataInput.totalDurationSec / 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(chartDataInput.totalDurationSec / chartTimeInterval),
durationsRef.current.clientWidth / Math.ceil(totalDurationSec / chartTimeInterval),
);
setChartWidth(durationsRef.current.clientWidth);
} else {
setChartWidth(calcWidth);
setLabelInterval(INTERVAL_LENGTH);
}
}, [chartDataInput.totalDurationSec, chartTimeInterval, durationsRef]);
}, [totalDurationSec, chartTimeInterval, durationsRef]);

const onGraphScroll = () => {
// cover horizontal scroll only
Expand Down Expand Up @@ -182,17 +173,6 @@ export const ExecutionTimeline: React.FC<ExProps> = ({ nodeExecutions, chartTime
setOriginalNodes([...originalNodes]);
};

const labels = React.useMemo(() => {
const len = Math.ceil(chartDataInput.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, chartDataInput.totalDurationSec]);

return (
<>
<div className={styles.taskNames}>
Expand All @@ -206,14 +186,17 @@ export const ExecutionTimeline: React.FC<ExProps> = ({ nodeExecutions, chartTime
</div>
<div className={styles.taskDurations}>
<div className={styles.taskDurationsLabelsView} ref={durationsLabelsRef}>
<ChartHeader labels={labels} chartWidth={chartWidth} labelInterval={labelInterval} />
<ChartHeader
startedAt={startedAt}
chartWidth={chartWidth}
labelInterval={labelInterval}
chartTimezone={chartTimezone}
totalDurationSec={totalDurationSec}
/>
</div>
<div className={styles.taskDurationsView} ref={durationsRef} onScroll={onGraphScroll}>
<div className={styles.chartHeader}>
<Bar
options={getBarOptions(chartTimeInterval, chartDataInput.tooltipLabel) as any}
data={getChartData(chartDataInput)}
/>
<BarChart items={barItemsData} chartTimeIntervalSec={chartTimeInterval} />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<div className={styles.chartHeader}>
{props.labels.map((label, idx) => {
{labels.map((label) => {
return (
<div className={styles.taskDurationsLabelItem} key={`duration-tick-${label}-${idx}`}>
<div className={styles.taskDurationsLabelItem} key={label}>
{label}
</div>
);
Expand Down
Loading

0 comments on commit 9ecf4d0

Please sign in to comment.