Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
MdHourglassBottom,
MdOutlineMonitorHeart,
MdReplay,
} from 'react-icons/md';

import { type DetailsRowItemParser } from '../workflow-history-details-row/workflow-history-details-row.types';
import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json/workflow-history-details-row-json';
import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json/workflow-history-details-row-tooltip-json';

const workflowHistoryDetailsRowParsersConfig: Array<DetailsRowItemParser> = [
{
name: 'Heartbeat time',
matcher: (name) => name === 'lastHeartbeatTime',
icon: MdOutlineMonitorHeart,
},
{
name: 'Json as PrettyJson',
matcher: (name, value) =>
value !== null &&
new RegExp(
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$'
).test(name),
icon: null,
customRenderValue: WorkflowHistoryDetailsRowJson,
customTooltipContent: WorkflowHistoryDetailsRowTooltipJson,
invertTooltipColors: true,
},
{
name: 'Timeouts with timer icon',
matcher: (name) =>
new RegExp('(TimeoutSeconds|BackoffSeconds|InSeconds)$').test(name),
icon: MdHourglassBottom,
},
{
name: '"attempt" greater than 1, as "retries"',
matcher: (name) => name === 'attempt',
hide: (_, value) => typeof value === 'number' && value <= 0,
icon: MdReplay,
customTooltipContent: () => 'retries',
},
];

export default workflowHistoryDetailsRowParsersConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from '@/test-utils/rtl';

import WorkflowHistoryDetailsRowJson from '../workflow-history-details-row-json';

describe(WorkflowHistoryDetailsRowJson.name, () => {
it('renders the stringified JSON value', () => {
render(
<WorkflowHistoryDetailsRowJson
value={{ key: 'value', nested: { number: 123 } }}
isNegative={false}
label="test-label"
domain="test-domain"
cluster="test-cluster"
workflowId="test-workflow-id"
runId="test-run-id"
/>
);

expect(
screen.getByText('{"key":"value","nested":{"number":123}}')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { styled as createStyled } from 'baseui';

export const styled = {
JsonViewContainer: createStyled<'div', { $isNegative: boolean }>(
'div',
({ $theme, $isNegative }) => ({
color: $isNegative ? $theme.colors.contentNegative : '#A964F7',
maxWidth: '360px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
...$theme.typography.MonoParagraphXSmall,
})
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import losslessJsonStringify from '@/utils/lossless-json-stringify';

import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';

import { styled } from './workflow-history-details-row-json.styles';

export default function WorkflowHistoryDetailsRowJson({
value,
isNegative,
}: DetailsRowValueComponentProps) {
return (
<styled.JsonViewContainer $isNegative={isNegative ?? false}>
{losslessJsonStringify(value)}
</styled.JsonViewContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';

export type Props = DetailsRowValueComponentProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { render, screen } from '@/test-utils/rtl';

import WorkflowHistoryDetailsRowTooltipJson from '../workflow-history-details-row-tooltip-json';

jest.mock(
'@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json',
() =>
jest.fn(({ entryValue, isNegative }) => (
<div data-testid="event-details-json">
Event Details Json: {JSON.stringify(entryValue)}
{isNegative && ' (negative)'}
</div>
))
);

describe(WorkflowHistoryDetailsRowTooltipJson.name, () => {
it('renders the label and passes value to WorkflowHistoryEventDetailsJson', () => {
render(
<WorkflowHistoryDetailsRowTooltipJson
value={{ key: 'value', nested: { number: 123 } }}
label="test-label"
isNegative={false}
domain="test-domain"
cluster="test-cluster"
workflowId="test-workflow-id"
runId="test-run-id"
/>
);

expect(screen.getByText('test-label')).toBeInTheDocument();
expect(screen.getByTestId('event-details-json')).toBeInTheDocument();
expect(
screen.getByText(
/Event Details Json: \{"key":"value","nested":\{"number":123\}\}/
)
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { styled as createStyled } from 'baseui';

export const styled = {
JsonPreviewContainer: createStyled('div', ({ $theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: $theme.sizing.scale200,
})),
JsonPreviewLabel: createStyled('div', ({ $theme }) => ({
...$theme.typography.LabelXSmall,
})),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import WorkflowHistoryEventDetailsJson from '@/views/workflow-history/workflow-history-event-details-json/workflow-history-event-details-json';

import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';

import { styled } from './workflow-history-details-row-tooltip-json.styles';

export default function WorkflowHistoryDetailsRowTooltipJson({
value,
label,
isNegative,
}: DetailsRowValueComponentProps) {
return (
<styled.JsonPreviewContainer>
<styled.JsonPreviewLabel>{label}</styled.JsonPreviewLabel>
<WorkflowHistoryEventDetailsJson
entryValue={value}
isNegative={isNegative}
/>
</styled.JsonPreviewContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type DetailsRowValueComponentProps } from '../workflow-history-details-row/workflow-history-details-row.types';

export type Props = DetailsRowValueComponentProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { render, screen, userEvent } from '@/test-utils/rtl';

import type { WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';

import { type EventDetailsEntries } from '../../workflow-history-event-details/workflow-history-event-details.types';
import WorkflowHistoryDetailsRow from '../workflow-history-details-row';
import { type DetailsRowItem } from '../workflow-history-details-row.types';

jest.mock('../helpers/get-parsed-details-row-items', () =>
jest.fn((detailsEntries: EventDetailsEntries) =>
detailsEntries.reduce<Array<DetailsRowItem>>((acc, entry) => {
if (!entry.isGroup) {
acc.push({
path: entry.path,
label: entry.path,
value: entry.value,
icon: ({ size }: any) => (
<span data-testid={`icon-${entry.path}`} data-size={size} />
),
renderValue: ({ value, isNegative }: any) => (
<span
data-testid={`field-${entry.path}`}
data-negative={isNegative}
>
{value}
</span>
),
renderTooltip: ({ label }: any) => (
<span data-testid={`tooltip-${entry.path}`}>{label}</span>
),
invertTooltipColors: acc.length === 1, // Second item has inverted tooltip
omitWrapping: acc.length === 2, // Third item omits wrapping
});
}
return acc;
}, [])
)
);

const mockWorkflowPageParams: WorkflowPageParams = {
cluster: 'test-cluster',
domain: 'test-domain',
workflowId: 'test-workflow',
runId: 'test-run',
};

const mockDetailsEntries: EventDetailsEntries = [
{
key: 'field1',
path: 'field1',
isGroup: false,
value: 'value1',
isNegative: false,
renderConfig: null,
},
{
key: 'field2',
path: 'field2',
isGroup: false,
value: 'value2',
isNegative: true,
renderConfig: null,
},
{
key: 'field3',
path: 'field3',
isGroup: false,
value: 'value3',
isNegative: false,
renderConfig: null,
},
];

describe(WorkflowHistoryDetailsRow.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render details row items when detailsEntries has items', () => {
setup();

expect(screen.getByTestId('field-field1')).toBeInTheDocument();
expect(screen.getByTestId('field-field2')).toBeInTheDocument();
expect(screen.getByTestId('field-field3')).toBeInTheDocument();
expect(screen.getByText('value1')).toBeInTheDocument();
expect(screen.getByText('value2')).toBeInTheDocument();
expect(screen.getByText('value3')).toBeInTheDocument();
});

it('should mark negative fields correctly', () => {
setup();

const negativeField = screen.getByTestId('field-field2');
expect(negativeField).toHaveAttribute('data-negative', 'true');

const positiveField = screen.getByTestId('field-field1');
expect(positiveField).toHaveAttribute('data-negative', 'false');
});

it('should render icons when provided in item config', () => {
setup();

expect(screen.getByTestId('icon-field1')).toBeInTheDocument();
expect(screen.getByTestId('icon-field2')).toBeInTheDocument();
expect(screen.getByTestId('icon-field3')).toBeInTheDocument();
});

it('should render tooltip content on hover', async () => {
const { user } = setup();

const field1 = screen.getByTestId('field-field1');
await user.hover(field1);

expect(await screen.findByTestId('tooltip-field1')).toBeInTheDocument();
expect(screen.getByText('field1')).toBeInTheDocument();
});
});

function setup({
detailsEntries = mockDetailsEntries,
workflowPageParams = mockWorkflowPageParams,
}: {
detailsEntries?: EventDetailsEntries;
workflowPageParams?: WorkflowPageParams;
} = {}) {
const user = userEvent.setup();

const renderResult = render(
<WorkflowHistoryDetailsRow
detailsEntries={detailsEntries}
{...workflowPageParams}
/>
);

return { user, ...renderResult };
}
Loading