Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow history timeline #671

Merged
99 changes: 99 additions & 0 deletions src/views/workflow-history/__tests__/workflow-history.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Suspense } from 'react';

import { HttpResponse } from 'msw';

import { act, render, screen } from '@/test-utils/rtl';

import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types';

import { completedActivityTaskEvents } from '../__fixtures__/workflow-history-activity-events';
import WorkflowHistory from '../workflow-history';

jest.mock(
'../workflow-history-compact-event-card/workflow-history-compact-event-card',
() => jest.fn(() => <div>Compact group Card</div>)
);

jest.mock(
'../workflow-history-timeline-group/workflow-history-timeline-group',
() => jest.fn(() => <div>Timeline group card</div>)
);
jest.mock(
'../workflow-history-timeline-load-more/workflow-history-timeline-load-more',
() => jest.fn(() => <div>Load more</div>)
);

describe('WorkflowHistory', () => {
it('renders page correctly', async () => {
setup({});
expect(await screen.findByText('Workflow history')).toBeInTheDocument();
});

it('renders compact group cards', async () => {
setup({});
expect(await screen.findByText('Compact group Card')).toBeInTheDocument();
});

it('renders timeline group cards', async () => {
setup({});
expect(await screen.findByText('Timeline group card')).toBeInTheDocument();
});

it('renders load more section', async () => {
setup({});
expect(await screen.findByText('Load more')).toBeInTheDocument();
});

it('throws an error if the request fails', async () => {
try {
await act(() => setup({ error: true }));
} catch (error) {
expect((error as Error)?.message).toBe(
'Failed to fetch workflow history'
);
}
});
});

function setup({ error }: { error?: boolean }) {
render(
<Suspense>
<WorkflowHistory
params={{
domain: 'test-domain',
cluster: 'test-cluster',
runId: 'test-runid',
workflowId: 'test-workflowId',
workflowTab: 'history',
}}
/>
</Suspense>,
{
endpointsMocks: [
{
path: '/api/domains/:domain/:cluster/workflows/:workflowId/:runId/history',
httpMethod: 'GET',
...(error
? {
httpResolver: () => {
return HttpResponse.json(
{ message: 'Failed to fetch workflow history' },
{ status: 500 }
);
},
}
: {
jsonResponse: {
history: {
events: completedActivityTaskEvents,
},
archived: false,
nextPageToken: '',
rawHistory: [],
} satisfies GetWorkflowHistoryResponse,
}),
},
],
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const cssStylesObj = {
flexDirection: 'column',
alignItems: 'flex-start',
gap: $theme.sizing.scale200,
textAlign: 'start',
wordBreak: 'break-word',
}),
label: ($theme: Theme) => ({
...$theme.typography.LabelSmall,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export default function WorkflowHistoryCompactEventCard({
/>
</div>
)}
<div className={cls.secondaryLabel}>{secondaryLabel}</div>
<div suppressHydrationWarning className={cls.secondaryLabel}>
{secondaryLabel}
</div>
</div>
</Tile>
);
Expand Down
27 changes: 23 additions & 4 deletions src/views/workflow-history/workflow-history.styles.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { styled as createStyled, type Theme } from 'baseui';

import type {
StyletronCSSObject,
StyletronCSSObjectOf,
} from '@/hooks/use-styletron-classes';

export const styled = {
VerticalDivider: createStyled<'div', { $hidden?: boolean }>(
'div',
({ $theme, $hidden }: { $theme: Theme; $hidden?: boolean }) => ({
...$theme.borders.border200,
borderColor: $theme.colors.borderOpaque,
height: $hidden ? 0 : '100%',
marginLeft: $theme.sizing.scale500,
})
),
};

const cssStylesObj = {
pageContainer: {
display: 'flex',
Expand All @@ -11,17 +25,22 @@ const cssStylesObj = {
eventsContainer: (theme) => ({
display: 'flex',
marginTop: theme.sizing.scale500,
gap: theme.sizing.scale400,
overflow: 'hidden',
}),
compactSection: {
compactSection: (theme) => ({
display: 'flex',
flexDirection: 'column',
width: '370px',
},
timelineSection: {
gap: theme.sizing.scale400,
}),
timelineSection: (theme) => ({
display: 'flex',
flexDirection: 'column',
flex: 1,
},
paddingLeft: theme.sizing.scale900,
overflow: 'hidden',
}),
} satisfies StyletronCSSObject;

export const cssStyles: StyletronCSSObjectOf<typeof cssStylesObj> =
Expand Down
121 changes: 109 additions & 12 deletions src/views/workflow-history/workflow-history.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,123 @@
'use client';
import React from 'react';
import React, { useMemo } from 'react';

import {
useSuspenseInfiniteQuery,
type InfiniteData,
} from '@tanstack/react-query';
import { HeadingXSmall } from 'baseui/typography';
import queryString from 'query-string';

import PageSection from '@/components/page-section/page-section';
import useStyletronClasses from '@/hooks/use-styletron-classes';
import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types';
import request from '@/utils/request';
import { type RequestError } from '@/utils/request/request-error';
import sortBy from '@/utils/sort-by';
import type { WorkflowPageTabContentProps } from '@/views/workflow-page/workflow-page-tab-content/workflow-page-tab-content.types';

import { groupHistoryEvents } from './helpers/group-history-events';
import WorkflowHistoryCompactEventCard from './workflow-history-compact-event-card/workflow-history-compact-event-card';
import WorkflowHistoryTimelineGroup from './workflow-history-timeline-group/workflow-history-timeline-group';
import WorkflowHistoryTimelineLoadMore from './workflow-history-timeline-load-more/workflow-history-timeline-load-more';
import { cssStyles } from './workflow-history.styles';

export default function WorkflowHistory() {
export default function WorkflowHistory({
params,
}: WorkflowPageTabContentProps) {
const { cls } = useStyletronClasses(cssStyles);

const { workflowTab, ...historyQueryParams } = params;
const wfhistoryRequestArgs = {
Assem-Uber marked this conversation as resolved.
Show resolved Hide resolved
...historyQueryParams,
pageSize: 1,
waitForNewEvent: 'true',
};

const {
data: result,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
error,
} = useSuspenseInfiniteQuery<
GetWorkflowHistoryResponse,
RequestError,
InfiniteData<GetWorkflowHistoryResponse>,
[string, typeof wfhistoryRequestArgs],
string | undefined
>({
queryKey: ['workflow_history_paginated', wfhistoryRequestArgs] as const,
queryFn: ({ queryKey: [_, qp], pageParam }) =>
request(
`/api/domains/${qp.domain}/${qp.cluster}/workflows/${qp.workflowId}/${qp.runId}/history?${queryString.stringify({ pageSize: qp.pageSize, nextPage: pageParam, waitForNewEvent: qp.waitForNewEvent })}`
).then((res) => res.json()),
initialPageParam: undefined,
getNextPageParam: (lastPage) => {
if (!lastPage?.nextPageToken) return undefined;
return lastPage?.nextPageToken;
},
});

const workflowHistory = useMemo(() => {
return (result.pages || []).flat(1);
}, [result]);

const groupedHistoryEvents = useMemo(() => {
const events = workflowHistory
.map(({ history }) => history?.events || [])
.flat(1);
return groupHistoryEvents(events);
}, [workflowHistory]);
Assem-Uber marked this conversation as resolved.
Show resolved Hide resolved

const groupedHistoryEventsEntries = useMemo(() => {
return sortBy(
Object.entries(groupedHistoryEvents),
([_, { timeMs }]) => timeMs,
'ASC'
);
}, [groupedHistoryEvents]);

return (
<div className={cls.pageContainer}>
<HeadingXSmall>Workflow history</HeadingXSmall>
<div className={cls.eventsContainer}>
<section className={cls.compactSection}>
<p>compact</p>
</section>
<section className={cls.timelineSection}>
<p>timeline</p>
</section>
<PageSection>
<div className={cls.pageContainer}>
<HeadingXSmall>Workflow history</HeadingXSmall>
<div className={cls.eventsContainer}>
<section className={cls.compactSection}>
{groupedHistoryEventsEntries.map(
([groupId, { label, status, timeLabel }]) => (
<WorkflowHistoryCompactEventCard
key={groupId}
status={status}
label={label}
secondaryLabel={timeLabel}
showLabelPlaceholder={!label}
/>
)
)}
</section>
<section className={cls.timelineSection}>
{groupedHistoryEventsEntries.map(([groupId, group], index) => (
<WorkflowHistoryTimelineGroup
key={groupId}
status={group.status}
label={group.label}
timeLabel={group.timeLabel}
events={group.events}
eventsMetadata={group.eventsMetadata}
hasMissingEvents={group.hasMissingEvents}
isLastEvent={index === groupedHistoryEventsEntries.length - 1}
/>
))}
<WorkflowHistoryTimelineLoadMore
error={error}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
</section>
</div>
</div>
</div>
</PageSection>
);
}
Loading