Skip to content

Commit c3b7529

Browse files
Create UI for individual diagnostics issue (#966)
* Create UI for individual Workflow Diagnostics Issues * Use useExpansionToggle hook in WorkflowDiagnosticsContent to control expansion state of individual issues * Modify WorkflowDiagnosticsList to take list of Issue Groups instead of diagnostics response * Add Runbook link to WorkflowDiagnosticsList component (which lists issue groups) * Display issue fields in table-like format * Create placeholder metadata table for Issue Metadata and Root Cause Metadata
1 parent 1ed2ad7 commit c3b7529

14 files changed

+518
-67
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { mockWorkflowDiagnosticsResult } from '@/route-handlers/diagnose-workflow/__fixtures__/mock-workflow-diagnostics-result';
2+
3+
import { type DiagnosticsIssuesGroup } from '../workflow-diagnostics.types';
4+
5+
export const mockWorkflowDiagnosticsIssueGroups = [
6+
['Timeouts', mockWorkflowDiagnosticsResult.result.Timeouts],
7+
['Failures', mockWorkflowDiagnosticsResult.result.Failures],
8+
['Retries', mockWorkflowDiagnosticsResult.result.Retries],
9+
] as const satisfies Array<[string, DiagnosticsIssuesGroup]>;

src/views/workflow-diagnostics/workflow-diagnostics-content/workflow-diagnostics-content.tsx

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,37 @@
22

33
import React, { useMemo, useState } from 'react';
44

5+
import { Button } from 'baseui/button';
56
import Image from 'next/image';
7+
import { MdUnfoldLess, MdUnfoldMore } from 'react-icons/md';
68

79
import circleCheck from '@/assets/circle-check.svg';
810
import PanelSection from '@/components/panel-section/panel-section';
11+
import useExpansionToggle from '@/hooks/use-expansion-toggle/use-expansion-toggle';
912

1013
import WorkflowDiagnosticsJson from '../workflow-diagnostics-json/workflow-diagnostics-json';
1114
import WorkflowDiagnosticsList from '../workflow-diagnostics-list/workflow-diagnostics-list';
1215
import WorkflowDiagnosticsViewToggle from '../workflow-diagnostics-view-toggle/workflow-diagnostics-view-toggle';
1316
import { type DiagnosticsViewMode } from '../workflow-diagnostics-view-toggle/workflow-diagnostics-view-toggle.types';
1417

1518
import { styled } from './workflow-diagnostics-content.styles';
16-
import { type Props } from './workflow-diagnostics-content.types';
19+
import {
20+
type IssueExpansionID,
21+
type Props,
22+
} from './workflow-diagnostics-content.types';
1723

1824
export default function WorkflowDiagnosticsContent({
19-
domain,
20-
cluster,
21-
workflowId,
22-
runId,
2325
diagnosticsResult,
26+
...workflowPageParams
2427
}: Props) {
2528
const [activeView, setActiveView] = useState<DiagnosticsViewMode>('list');
2629

27-
const nonEmptyIssueGroups: Array<string> = useMemo(
30+
const issuesGroups = useMemo(
31+
() => Object.entries(diagnosticsResult.result),
32+
[diagnosticsResult.result]
33+
);
34+
35+
const nonEmptyIssuesGroups: Array<string> = useMemo(
2836
() =>
2937
Object.entries(diagnosticsResult.result)
3038
.map(([name, issuesGroup]) => {
@@ -36,7 +44,30 @@ export default function WorkflowDiagnosticsContent({
3644
[diagnosticsResult.result]
3745
);
3846

39-
if (nonEmptyIssueGroups.length === 0) {
47+
const allIssueExpansionIds = useMemo(
48+
() =>
49+
issuesGroups
50+
.map(
51+
([groupName, issuesGroup]) =>
52+
issuesGroup?.issues.map(
53+
({ issueId }): IssueExpansionID => `${groupName}.${issueId}`
54+
) ?? []
55+
)
56+
.flat(1),
57+
[issuesGroups]
58+
);
59+
60+
const {
61+
areAllItemsExpanded,
62+
toggleAreAllItemsExpanded,
63+
getIsItemExpanded,
64+
toggleIsItemExpanded,
65+
} = useExpansionToggle<IssueExpansionID>({
66+
items: allIssueExpansionIds,
67+
initialState: {},
68+
});
69+
70+
if (nonEmptyIssuesGroups.length === 0) {
4071
return (
4172
<PanelSection>
4273
<styled.NoIssuesContainer>
@@ -57,20 +88,32 @@ export default function WorkflowDiagnosticsContent({
5788
activeView={activeView}
5889
setActiveView={setActiveView}
5990
/>
60-
{/* TODO: Add a button here to expand all diagnostics issues, hide in JSON mode ofc */}
91+
<Button
92+
size="compact"
93+
kind="secondary"
94+
onClick={() => toggleAreAllItemsExpanded()}
95+
endEnhancer={
96+
areAllItemsExpanded ? (
97+
<MdUnfoldLess size={16} />
98+
) : (
99+
<MdUnfoldMore size={16} />
100+
)
101+
}
102+
>
103+
{areAllItemsExpanded ? 'Collapse all' : 'Expand all'}
104+
</Button>
61105
</styled.ButtonsContainer>
62106
{activeView === 'list' ? (
63107
<WorkflowDiagnosticsList
64-
domain={domain}
65-
cluster={cluster}
66-
workflowId={workflowId}
67-
runId={runId}
68-
diagnosticsResult={diagnosticsResult}
108+
{...workflowPageParams}
109+
diagnosticsIssuesGroups={issuesGroups}
110+
getIsIssueExpanded={getIsItemExpanded}
111+
toggleIsIssueExpanded={toggleIsItemExpanded}
69112
/>
70113
) : (
71114
<WorkflowDiagnosticsJson
72-
workflowId={workflowId}
73-
runId={runId}
115+
workflowId={workflowPageParams.workflowId}
116+
runId={workflowPageParams.runId}
74117
diagnosticsResult={diagnosticsResult}
75118
/>
76119
)}
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { type WorkflowDiagnosticsResult } from '@/route-handlers/diagnose-workflow/diagnose-workflow.types';
2+
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
23

34
export type Props = {
4-
domain: string;
5-
cluster: string;
6-
workflowId: string;
7-
runId: string;
85
diagnosticsResult: WorkflowDiagnosticsResult;
9-
};
6+
} & WorkflowPageParams;
7+
8+
export type IssueExpansionID = `${string}.${number}`;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React from 'react';
2+
3+
import { render, screen, userEvent } from '@/test-utils/rtl';
4+
5+
import { mockWorkflowDiagnosticsIssueGroups } from '../../__fixtures__/mock-workflow-diagnostics-issue-groups';
6+
import WorkflowDiagnosticsIssue from '../workflow-diagnostics-issue';
7+
import { type Props } from '../workflow-diagnostics-issue.types';
8+
9+
jest.mock(
10+
'../../workflow-diagnostics-metadata-table/workflow-diagnostics-metadata-table',
11+
() => {
12+
return function MockWorkflowDiagnosticsMetadataTable({ metadata }: any) {
13+
return (
14+
<div data-testid="workflow-diagnostics-metadata-table">
15+
{JSON.stringify(metadata)}
16+
</div>
17+
);
18+
};
19+
}
20+
);
21+
22+
const mockIssue = mockWorkflowDiagnosticsIssueGroups[1][1].issues[0];
23+
const mockRootCauses =
24+
mockWorkflowDiagnosticsIssueGroups[1][1].rootCauses.slice(0, 1);
25+
26+
describe(WorkflowDiagnosticsIssue.name, () => {
27+
afterEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('renders issue header with correct content', () => {
32+
setup({});
33+
34+
expect(screen.getByText('Activity Failed')).toBeInTheDocument();
35+
expect(screen.getByText('Details')).toBeInTheDocument();
36+
});
37+
38+
it('shows content when expanded', () => {
39+
setup({ isExpanded: true });
40+
41+
expect(
42+
screen.getByText(
43+
'The failure is because of an error returned from the service code'
44+
)
45+
).toBeInTheDocument();
46+
});
47+
48+
it('renders root causes when they exist', () => {
49+
setup({ isExpanded: true });
50+
51+
expect(screen.getByText('Root Cause')).toBeInTheDocument();
52+
expect(
53+
screen.getByText(
54+
'There is an issue in the worker service that is causing a failure. Check identity for service logs'
55+
)
56+
).toBeInTheDocument();
57+
});
58+
59+
it('renders multiple root causes with correct label', () => {
60+
const multipleRootCauses = [
61+
mockRootCauses[0],
62+
{ ...mockRootCauses[0], rootCauseType: 'Another root cause' },
63+
];
64+
65+
setup({ rootCauses: multipleRootCauses, isExpanded: true });
66+
67+
expect(screen.getByText('Root Causes')).toBeInTheDocument();
68+
expect(
69+
screen.getByText(
70+
'There is an issue in the worker service that is causing a failure. Check identity for service logs'
71+
)
72+
).toBeInTheDocument();
73+
expect(screen.getByText('Another root cause')).toBeInTheDocument();
74+
});
75+
76+
it('does not render root causes section when no root causes exist', () => {
77+
setup({ rootCauses: [] });
78+
79+
expect(screen.queryByText('Root Cause')).not.toBeInTheDocument();
80+
expect(screen.queryByText('Root Causes')).not.toBeInTheDocument();
81+
});
82+
83+
it('renders issue metadata when it exists', () => {
84+
setup({ isExpanded: true });
85+
86+
expect(screen.getByText('Metadata')).toBeInTheDocument();
87+
expect(
88+
screen.getByTestId('workflow-diagnostics-metadata-table')
89+
).toBeInTheDocument();
90+
});
91+
92+
it('does not render metadata section when issue has no metadata', () => {
93+
const issueWithoutMetadata = { ...mockIssue, metadata: null };
94+
setup({ issue: issueWithoutMetadata, isExpanded: true });
95+
96+
expect(screen.queryByText('Metadata')).not.toBeInTheDocument();
97+
expect(
98+
screen.queryByTestId('workflow-diagnostics-metadata-table')
99+
).not.toBeInTheDocument();
100+
});
101+
102+
it('renders root cause metadata when it exists', () => {
103+
const rootCauseWithMetadata = {
104+
...mockRootCauses[0],
105+
metadata: { testKey: 'testValue' },
106+
};
107+
108+
setup({ rootCauses: [rootCauseWithMetadata], isExpanded: true });
109+
110+
const metadataTables = screen.getAllByTestId(
111+
'workflow-diagnostics-metadata-table'
112+
);
113+
expect(metadataTables).toHaveLength(2); // One for issue metadata, one for root cause metadata
114+
});
115+
116+
it('renders details button with correct text and calls onChangePanel when clicked', async () => {
117+
const { user, mockOnChangePanel } = setup({});
118+
119+
const detailsButton = screen.getByText('Details');
120+
expect(detailsButton).toBeInTheDocument();
121+
122+
await user.click(detailsButton);
123+
124+
expect(mockOnChangePanel).toHaveBeenCalledTimes(1);
125+
});
126+
});
127+
128+
function setup(overrides: Partial<Props>) {
129+
const user = userEvent.setup();
130+
const mockOnChangePanel = jest.fn();
131+
132+
const props = {
133+
issue: mockIssue,
134+
rootCauses: mockRootCauses,
135+
isExpanded: false,
136+
onChangePanel: mockOnChangePanel,
137+
domain: 'test-domain',
138+
cluster: 'test-cluster',
139+
workflowId: 'test-workflow-id',
140+
runId: 'test-run-id',
141+
...overrides,
142+
};
143+
144+
const renderResult = render(<WorkflowDiagnosticsIssue {...props} />);
145+
146+
return { ...renderResult, mockOnChangePanel, user };
147+
}

0 commit comments

Comments
 (0)