Skip to content

Commit ae04b27

Browse files
Create barebones Issues List UI for Workflow Diagnostics (#958)
* Split Diagnostics result fields into separate schemas to easier retrieve types * Make Root Cause field optional for an issues group (this is the case with some issues that are self-explanatory) * Implement WorkflowDiagnosticsList with sections for issue groups * Create placeholder WorkflowDiagnosticsIssue component for diagnostics issue with root cause * Add placeholder fallback for Workflow Diagnostics content when there are no issues
1 parent ecfabc9 commit ae04b27

12 files changed

+317
-21
lines changed

src/route-handlers/diagnose-workflow/diagnose-workflow.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { type ZodError, type z } from 'zod';
22

33
import { type DefaultMiddlewaresContext } from '@/utils/route-handlers-middleware';
44

5+
import type workflowDiagnosticsIssueSchema from './schemas/workflow-diagnostics-issue-schema';
56
import type workflowDiagnosticsResultSchema from './schemas/workflow-diagnostics-result-schema';
7+
import type workflowDiagnosticsRootCauseSchema from './schemas/workflow-diagnostics-root-cause-schema';
68

79
export type RouteParams = {
810
domain: string;
@@ -19,6 +21,14 @@ export type WorkflowDiagnosticsResult = z.infer<
1921
typeof workflowDiagnosticsResultSchema
2022
>;
2123

24+
export type WorkflowDiagnosticsIssue = z.infer<
25+
typeof workflowDiagnosticsIssueSchema
26+
>;
27+
28+
export type WorkflowDiagnosticsRootCause = z.infer<
29+
typeof workflowDiagnosticsRootCauseSchema
30+
>;
31+
2232
export type DiagnoseWorkflowResponse =
2333
| {
2434
result: WorkflowDiagnosticsResult;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod';
2+
3+
const workflowDiagnosticsIssueSchema = z.object({
4+
IssueID: z.number(),
5+
InvariantType: z.string(),
6+
Reason: z.string(),
7+
Metadata: z.any(),
8+
});
9+
export default workflowDiagnosticsIssueSchema;

src/route-handlers/diagnose-workflow/schemas/workflow-diagnostics-result-schema.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import { z } from 'zod';
22

3+
import workflowDiagnosticsIssueSchema from './workflow-diagnostics-issue-schema';
4+
import workflowDiagnosticsRootCauseSchema from './workflow-diagnostics-root-cause-schema';
5+
36
const workflowDiagnosticsResultSchema = z.object({
47
DiagnosticsResult: z.record(
58
z.string(),
69
z
710
.object({
8-
Issues: z.array(
9-
z.object({
10-
IssueID: z.number(),
11-
InvariantType: z.string(),
12-
Reason: z.string(),
13-
Metadata: z.any(),
14-
})
15-
),
16-
RootCause: z.array(
17-
z.object({
18-
IssueID: z.number(),
19-
RootCauseType: z.string(),
20-
Metadata: z.any(),
21-
})
22-
),
23-
Runbooks: z.array(z.string()),
11+
Issues: z.array(workflowDiagnosticsIssueSchema),
12+
RootCause: z.array(workflowDiagnosticsRootCauseSchema).optional(),
13+
Runbooks: z.array(z.string()).optional().or(z.null()),
2414
})
2515
.or(z.null())
2616
),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod';
2+
3+
const workflowDiagnosticsRootCauseSchema = z.object({
4+
IssueID: z.number(),
5+
RootCauseType: z.string(),
6+
Metadata: z.any(),
7+
});
8+
9+
export default workflowDiagnosticsRootCauseSchema;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,27 @@ describe(WorkflowDiagnosticsContent.name, () => {
8282
expect(screen.getByText('Is list view enabled: true')).toBeInTheDocument();
8383
expect(screen.getByText('Active View: list')).toBeInTheDocument();
8484
});
85+
86+
it('should handle empty diagnostics result', async () => {
87+
setup({
88+
diagnosticsResult: {
89+
DiagnosticsResult: {
90+
Timeouts: null,
91+
Failures: {
92+
Issues: [],
93+
RootCause: [],
94+
Runbooks: [],
95+
},
96+
Retries: null,
97+
},
98+
DiagnosticsCompleted: true,
99+
},
100+
});
101+
102+
expect(
103+
await screen.findByText('No issues found with this workflow')
104+
).toBeInTheDocument();
105+
});
85106
});
86107

87108
function setup({

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useState } from 'react';
3+
import React, { useMemo, useState } from 'react';
44

55
import WorkflowDiagnosticsJson from '../workflow-diagnostics-json/workflow-diagnostics-json';
66
import WorkflowDiagnosticsList from '../workflow-diagnostics-list/workflow-diagnostics-list';
@@ -19,6 +19,23 @@ export default function WorkflowDiagnosticsContent({
1919
}: Props) {
2020
const [activeView, setActiveView] = useState<DiagnosticsViewMode>('list');
2121

22+
const issuesGroupsWithEntries: Array<string> = useMemo(
23+
() =>
24+
Object.entries(diagnosticsResult.DiagnosticsResult)
25+
.map(([name, issuesGroup]) => {
26+
if (!issuesGroup) return null;
27+
if (issuesGroup.Issues.length === 0) return null;
28+
return name;
29+
})
30+
.filter((name) => name !== null) as Array<string>,
31+
[diagnosticsResult.DiagnosticsResult]
32+
);
33+
34+
if (issuesGroupsWithEntries.length === 0) {
35+
// TODO: Add a No Issues Found panel
36+
return <div>No issues found with this workflow</div>;
37+
}
38+
2239
return (
2340
<>
2441
<styled.ButtonsContainer>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type Props } from './workflow-diagnostics-issue.types';
2+
3+
export default function WorkflowDiagnosticsIssue({ issue, rootCauses }: Props) {
4+
return (
5+
<div>
6+
<div></div>
7+
<div>Issue</div>
8+
<div>{JSON.stringify(issue)}</div>
9+
<div>Root causes</div>
10+
<div>{JSON.stringify(rootCauses)}</div>
11+
</div>
12+
);
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {
2+
type DiagnosticsIssue,
3+
type DiagnosticsRootCause,
4+
} from '../workflow-diagnostics.types';
5+
6+
export type Props = {
7+
issue: DiagnosticsIssue;
8+
rootCauses: Array<DiagnosticsRootCause>;
9+
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React from 'react';
2+
3+
import { render, screen, within } from '@/test-utils/rtl';
4+
5+
import { mockWorkflowDiagnosticsResult } from '@/route-handlers/diagnose-workflow/__fixtures__/mock-workflow-diagnostics-result';
6+
7+
import WorkflowDiagnosticsList from '../workflow-diagnostics-list';
8+
import { type Props } from '../workflow-diagnostics-list.types';
9+
10+
jest.mock('../../workflow-diagnostics-issue/workflow-diagnostics-issue', () => {
11+
return function MockWorkflowDiagnosticsIssue({ issue, rootCauses }: any) {
12+
return (
13+
<div data-testid="workflow-diagnostics-issue">
14+
<div data-testid="issue-id">{issue.IssueID}</div>
15+
<div data-testid="issue-type">{issue.InvariantType}</div>
16+
<div data-testid="issue-reason">{issue.Reason}</div>
17+
<div data-testid="root-causes-count">{rootCauses.length}</div>
18+
</div>
19+
);
20+
};
21+
});
22+
23+
describe(WorkflowDiagnosticsList.name, () => {
24+
afterEach(() => {
25+
jest.clearAllMocks();
26+
});
27+
28+
it('renders all issue groups with correct titles', () => {
29+
setup({});
30+
31+
expect(screen.getByText('Failures')).toBeInTheDocument();
32+
// Timeouts and Retries are null, so they shouldn't be rendered
33+
expect(screen.queryByText('Timeouts')).not.toBeInTheDocument();
34+
expect(screen.queryByText('Retries')).not.toBeInTheDocument();
35+
});
36+
37+
it('renders all issues within each group', () => {
38+
setup({});
39+
40+
const diagnosticIssues = screen.getAllByTestId(
41+
'workflow-diagnostics-issue'
42+
);
43+
expect(diagnosticIssues).toHaveLength(5); // 5 issues from Failures group
44+
});
45+
46+
it('passes correct props to WorkflowDiagnosticsIssue components', () => {
47+
setup({});
48+
49+
const diagnosticIssues = screen.getAllByTestId(
50+
'workflow-diagnostics-issue'
51+
);
52+
53+
// Check first issue (Activity Failed)
54+
const firstIssue = diagnosticIssues[0];
55+
expect(within(firstIssue).getByTestId('issue-id')).toHaveTextContent('0');
56+
expect(within(firstIssue).getByTestId('issue-type')).toHaveTextContent(
57+
'Activity Failed'
58+
);
59+
expect(within(firstIssue).getByTestId('issue-reason')).toHaveTextContent(
60+
'The failure is because of an error returned from the service code'
61+
);
62+
expect(
63+
within(firstIssue).getByTestId('root-causes-count')
64+
).toHaveTextContent('1');
65+
66+
// Check second issue (Activity Failed)
67+
const secondIssue = diagnosticIssues[1];
68+
expect(within(secondIssue).getByTestId('issue-id')).toHaveTextContent('1');
69+
expect(within(secondIssue).getByTestId('issue-type')).toHaveTextContent(
70+
'Activity Failed'
71+
);
72+
expect(within(secondIssue).getByTestId('issue-reason')).toHaveTextContent(
73+
'The failure is because of an error returned from the service code'
74+
);
75+
expect(
76+
within(secondIssue).getByTestId('root-causes-count')
77+
).toHaveTextContent('1');
78+
79+
// Check third issue (Activity Failed)
80+
const thirdIssue = diagnosticIssues[2];
81+
expect(within(thirdIssue).getByTestId('issue-id')).toHaveTextContent('2');
82+
expect(within(thirdIssue).getByTestId('issue-type')).toHaveTextContent(
83+
'Activity Failed'
84+
);
85+
expect(within(thirdIssue).getByTestId('issue-reason')).toHaveTextContent(
86+
'The failure is because of an error returned from the service code'
87+
);
88+
expect(
89+
within(thirdIssue).getByTestId('root-causes-count')
90+
).toHaveTextContent('1');
91+
});
92+
93+
it('filters root causes correctly for each issue', () => {
94+
setup({});
95+
96+
const diagnosticIssues = screen.getAllByTestId(
97+
'workflow-diagnostics-issue'
98+
);
99+
100+
// Each issue should have exactly one root cause
101+
diagnosticIssues.forEach((issue) => {
102+
const rootCausesCount = within(issue).getByTestId('root-causes-count');
103+
expect(rootCausesCount).toHaveTextContent('1');
104+
});
105+
});
106+
107+
it('handles empty diagnostics result', () => {
108+
setup({
109+
diagnosticsResult: {
110+
DiagnosticsResult: {},
111+
DiagnosticsCompleted: true,
112+
},
113+
});
114+
115+
// Should render the container but no issue groups
116+
expect(
117+
screen.queryByTestId('workflow-diagnostics-issue')
118+
).not.toBeInTheDocument();
119+
});
120+
121+
it('hides issues groups with no issues', () => {
122+
setup({
123+
diagnosticsResult: {
124+
DiagnosticsResult: {
125+
...mockWorkflowDiagnosticsResult.DiagnosticsResult,
126+
'Empty Issues Group': {
127+
Issues: [],
128+
RootCause: [],
129+
Runbooks: [],
130+
},
131+
},
132+
DiagnosticsCompleted: true,
133+
},
134+
});
135+
136+
expect(screen.getByText('Failures')).toBeInTheDocument();
137+
expect(screen.queryByText('Empty Issues Group')).not.toBeInTheDocument();
138+
});
139+
});
140+
141+
function setup(overrides: Partial<Props>) {
142+
const mockProps: Props = {
143+
domain: 'test-domain',
144+
cluster: 'test-cluster',
145+
workflowId: 'test-workflow-id',
146+
runId: 'test-run-id',
147+
diagnosticsResult: mockWorkflowDiagnosticsResult,
148+
...overrides,
149+
};
150+
151+
render(<WorkflowDiagnosticsList {...mockProps} />);
152+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
import { type StyleObject } from 'styletron-react';
3+
4+
export const styled = {
5+
IssuesGroupsContainer: createStyled('div', {
6+
display: 'flex',
7+
flexDirection: 'column',
8+
alignItems: 'stretch',
9+
}),
10+
IssuesTitle: createStyled(
11+
'div',
12+
({ $theme }: { $theme: Theme }): StyleObject => ({
13+
...$theme.typography.LabelLarge,
14+
})
15+
),
16+
IssuesGroup: createStyled(
17+
'div',
18+
({ $theme }: { $theme: Theme }): StyleObject => ({
19+
display: 'flex',
20+
flexDirection: 'column',
21+
gap: $theme.sizing.scale500,
22+
':not(:first-child)': {
23+
paddingTop: $theme.sizing.scale600,
24+
},
25+
':not(:last-child)': {
26+
borderBottom: `1px solid ${$theme.colors.borderOpaque}`,
27+
paddingBottom: $theme.sizing.scale600,
28+
},
29+
})
30+
),
31+
};

0 commit comments

Comments
 (0)