@shipfox/client-workflows 22.0.2 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview.tsx +2 -2
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +43 -0
- package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
- package/dist/components/job-detail/agent-config-failure-callout.js +15 -0
- package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
- package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
- package/dist/components/job-detail/job-empty-states.js +8 -0
- package/dist/components/job-detail/job-empty-states.js.map +1 -1
- package/dist/core/entities/job.d.ts +2 -2
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/step.d.ts +2 -0
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +6 -0
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +74 -24
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/job-detail/agent-config-failure-callout.stories.tsx +29 -0
- package/src/components/job-detail/agent-config-failure-callout.tsx +18 -0
- package/src/components/job-detail/job-empty-states.test.ts +21 -0
- package/src/components/job-detail/job-empty-states.tsx +10 -0
- package/src/core/entities/job.ts +2 -2
- package/src/core/entities/step.ts +2 -0
- package/src/hooks/api/workflow-run-mapper.ts +4 -0
- package/src/pages/project-workflows-page.test.tsx +85 -5
- package/src/pages/project-workflows-page.tsx +71 -17
- package/test/fixtures/workflow-run.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "23.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"date-fns": "^4.1.0",
|
|
30
30
|
"@shipfox/annotations-dto": "12.3.0",
|
|
31
|
-
"@shipfox/api-definitions-dto": "
|
|
32
|
-
"@shipfox/api-workflows-dto": "
|
|
31
|
+
"@shipfox/api-definitions-dto": "14.0.0",
|
|
32
|
+
"@shipfox/api-workflows-dto": "14.0.0",
|
|
33
33
|
"@shipfox/client-api": "6.0.1",
|
|
34
|
-
"@shipfox/client-logs": "22.0.
|
|
35
|
-
"@shipfox/client-projects": "
|
|
36
|
-
"@shipfox/client-shell": "
|
|
37
|
-
"@shipfox/client-triggers": "
|
|
38
|
-
"@shipfox/client-ui": "22.0.
|
|
39
|
-
"@shipfox/react-ui": "2.
|
|
34
|
+
"@shipfox/client-logs": "22.0.3",
|
|
35
|
+
"@shipfox/client-projects": "23.0.0",
|
|
36
|
+
"@shipfox/client-shell": "23.0.0",
|
|
37
|
+
"@shipfox/client-triggers": "23.0.0",
|
|
38
|
+
"@shipfox/client-ui": "22.0.3",
|
|
39
|
+
"@shipfox/react-ui": "2.2.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -83,6 +83,19 @@ const errorCases: Array<{
|
|
|
83
83
|
category: 'user',
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
label: 'Managed-only policy',
|
|
88
|
+
error: {
|
|
89
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
90
|
+
code: 'workspace-providers-disabled',
|
|
91
|
+
managedProviderId: 'shipfox',
|
|
92
|
+
exitCode: null,
|
|
93
|
+
signal: undefined,
|
|
94
|
+
reason: 'agent_config_invalid',
|
|
95
|
+
agentConfigIssue: 'provider_unsupported',
|
|
96
|
+
category: 'user',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
86
99
|
];
|
|
87
100
|
|
|
88
101
|
export const Playground: Story = {};
|
|
@@ -113,6 +126,22 @@ export const TestProviderUnsupported: Story = {
|
|
|
113
126
|
play: assertCallout('Choose a supported model provider', false),
|
|
114
127
|
};
|
|
115
128
|
|
|
129
|
+
export const TestManagedOnlyPolicy: Story = {
|
|
130
|
+
args: {
|
|
131
|
+
error: {
|
|
132
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
133
|
+
code: 'workspace-providers-disabled',
|
|
134
|
+
managedProviderId: 'shipfox',
|
|
135
|
+
exitCode: null,
|
|
136
|
+
signal: undefined,
|
|
137
|
+
reason: 'agent_config_invalid',
|
|
138
|
+
agentConfigIssue: 'provider_unsupported',
|
|
139
|
+
category: 'user',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
play: assertCallout('Use shipfox for this instance', false),
|
|
143
|
+
};
|
|
144
|
+
|
|
116
145
|
function makeError(agentConfigIssue: AgentConfigIssueValue): StepError {
|
|
117
146
|
return {
|
|
118
147
|
message: 'Agent configuration is invalid',
|
|
@@ -9,6 +9,8 @@ import {Button} from '@shipfox/react-ui/button';
|
|
|
9
9
|
import {Link} from '@tanstack/react-router';
|
|
10
10
|
import type {AgentStepConfig, StepError} from '#core/workflow-run.js';
|
|
11
11
|
|
|
12
|
+
const MANAGED_ONLY_PROVIDER_CODE = 'workspace-providers-disabled';
|
|
13
|
+
|
|
12
14
|
export function AgentConfigFailureCallout({
|
|
13
15
|
workspaceSlug,
|
|
14
16
|
config,
|
|
@@ -47,6 +49,15 @@ function agentConfigFailureCopy(
|
|
|
47
49
|
config: AgentStepConfig | null,
|
|
48
50
|
error: StepError | null,
|
|
49
51
|
): {title: string; description: string; showProviderCta: boolean} {
|
|
52
|
+
const managedOnlyProvider = managedOnlyProviderFromError(error);
|
|
53
|
+
if (managedOnlyProvider) {
|
|
54
|
+
return {
|
|
55
|
+
title: `Use ${managedOnlyProvider} for this instance`,
|
|
56
|
+
description: `This instance only supports provider \`${managedOnlyProvider}\`. Update this step to use \`${managedOnlyProvider}\`, or remove its provider field to use the managed default.`,
|
|
57
|
+
showProviderCta: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
const provider = configValue(config?.provider, 'the selected provider');
|
|
51
62
|
const model = configValue(config?.model, 'the selected model');
|
|
52
63
|
|
|
@@ -92,6 +103,13 @@ function agentConfigFailureCopy(
|
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
106
|
+
function managedOnlyProviderFromError(error: StepError | null): string | undefined {
|
|
107
|
+
if (error?.code !== MANAGED_ONLY_PROVIDER_CODE && error?.managedProviderId === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return error.managedProviderId ?? 'the managed provider';
|
|
111
|
+
}
|
|
112
|
+
|
|
95
113
|
function configValue(value: string | null | undefined, fallback: string): string {
|
|
96
114
|
return value ?? fallback;
|
|
97
115
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {Step} from '#core/workflow-run.js';
|
|
1
2
|
import {
|
|
2
3
|
workflowJob,
|
|
3
4
|
workflowJobExecutionDto,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
jobSucceededSummary,
|
|
10
11
|
outputFailureDescriptionForExecution,
|
|
11
12
|
skippedJobDescription,
|
|
13
|
+
toSelectedAttemptError,
|
|
12
14
|
} from './job-empty-states.js';
|
|
13
15
|
|
|
14
16
|
describe('jobSucceededSummary', () => {
|
|
@@ -32,6 +34,25 @@ describe('jobSucceededSummary', () => {
|
|
|
32
34
|
});
|
|
33
35
|
});
|
|
34
36
|
|
|
37
|
+
describe('toSelectedAttemptError', () => {
|
|
38
|
+
test('preserves managed-provider metadata from a historical attempt', () => {
|
|
39
|
+
const error = toSelectedAttemptError({type: 'agent'} as Step, {
|
|
40
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
41
|
+
code: 'workspace-providers-disabled',
|
|
42
|
+
managedProviderId: 'shipfox',
|
|
43
|
+
reason: 'agent_config_invalid',
|
|
44
|
+
agentConfigIssue: 'provider_unsupported',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(error).toMatchObject({
|
|
48
|
+
code: 'workspace-providers-disabled',
|
|
49
|
+
managedProviderId: 'shipfox',
|
|
50
|
+
reason: 'agent_config_invalid',
|
|
51
|
+
agentConfigIssue: 'provider_unsupported',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
35
56
|
describe('skippedJobDescription', () => {
|
|
36
57
|
test('explains when materialized output exceeds the configured size limit', () => {
|
|
37
58
|
expect(skippedJobDescription('output_too_large')).toBe(
|
|
@@ -292,8 +292,18 @@ export function toSelectedAttemptError(
|
|
|
292
292
|
|
|
293
293
|
if (resolvedReason === undefined) return null;
|
|
294
294
|
|
|
295
|
+
const code = typeof error.code === 'string' ? error.code : undefined;
|
|
296
|
+
const managedProviderId =
|
|
297
|
+
typeof error.managedProviderId === 'string'
|
|
298
|
+
? error.managedProviderId
|
|
299
|
+
: typeof error.managed_provider_id === 'string'
|
|
300
|
+
? error.managed_provider_id
|
|
301
|
+
: undefined;
|
|
302
|
+
|
|
295
303
|
return {
|
|
296
304
|
message: typeof error.message === 'string' ? error.message : '',
|
|
305
|
+
...(code === undefined ? {} : {code}),
|
|
306
|
+
...(managedProviderId === undefined ? {} : {managedProviderId}),
|
|
297
307
|
exitCode: exitCode === null || typeof exitCode === 'number' ? exitCode : null,
|
|
298
308
|
signal: typeof error.signal === 'string' ? error.signal : undefined,
|
|
299
309
|
reason: resolvedReason,
|
package/src/core/entities/job.ts
CHANGED
|
@@ -28,13 +28,13 @@ export type JobStatusReason =
|
|
|
28
28
|
export interface JobListening {
|
|
29
29
|
on: Array<{
|
|
30
30
|
source: string;
|
|
31
|
-
event
|
|
31
|
+
event?: string | undefined;
|
|
32
32
|
inputs?: Record<string, unknown> | undefined;
|
|
33
33
|
filter?: string | undefined;
|
|
34
34
|
}>;
|
|
35
35
|
until: Array<{
|
|
36
36
|
source: string;
|
|
37
|
-
event
|
|
37
|
+
event?: string | undefined;
|
|
38
38
|
inputs?: Record<string, unknown> | undefined;
|
|
39
39
|
filter?: string | undefined;
|
|
40
40
|
}> | null;
|
|
@@ -51,6 +51,8 @@ export interface StepSourceLocation {
|
|
|
51
51
|
|
|
52
52
|
export interface StepError {
|
|
53
53
|
message: string;
|
|
54
|
+
code?: string | undefined;
|
|
55
|
+
managedProviderId?: string | undefined;
|
|
54
56
|
exitCode: number | null;
|
|
55
57
|
signal: string | undefined;
|
|
56
58
|
reason: StepErrorReason | undefined;
|
|
@@ -217,6 +217,10 @@ export function toStep(dto: WorkflowRunStepDetailDto): Step {
|
|
|
217
217
|
error: dto.error
|
|
218
218
|
? {
|
|
219
219
|
message: dto.error.message,
|
|
220
|
+
...(dto.error.code === undefined ? {} : {code: dto.error.code}),
|
|
221
|
+
...(dto.error.managed_provider_id === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: {managedProviderId: dto.error.managed_provider_id}),
|
|
220
224
|
exitCode: dto.error.exit_code ?? null,
|
|
221
225
|
signal: dto.error.signal,
|
|
222
226
|
reason: dto.error.reason,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
2
|
+
import {fireEvent, screen, waitFor, within} from '@testing-library/react';
|
|
3
3
|
import {
|
|
4
4
|
jsonResponse,
|
|
5
5
|
PROJECT_TEST_WID,
|
|
@@ -69,11 +69,12 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
69
69
|
finished_at: null,
|
|
70
70
|
last_error_code: 'no-workflow-files',
|
|
71
71
|
last_error_message: 'No workflow files found',
|
|
72
|
-
|
|
72
|
+
diagnostics: [
|
|
73
73
|
{
|
|
74
74
|
code: 're-evaluating-command',
|
|
75
75
|
message: 'Workflow data is re-executed as shell code.',
|
|
76
76
|
path: 'jobs.build.steps.0.run',
|
|
77
|
+
severity: 'warning',
|
|
77
78
|
},
|
|
78
79
|
],
|
|
79
80
|
},
|
|
@@ -104,16 +105,20 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
104
105
|
finished_at: '2026-05-07T01:00:00.000Z',
|
|
105
106
|
last_error_code: null,
|
|
106
107
|
last_error_message: null,
|
|
107
|
-
|
|
108
|
+
diagnostics: [
|
|
108
109
|
{
|
|
109
110
|
code: 're-evaluating-command',
|
|
110
111
|
message: 'Workflow data is re-executed as shell code.',
|
|
111
112
|
path: 'jobs.build.steps.0.run',
|
|
113
|
+
file_path: '.shipfox/workflows/warning.yml',
|
|
114
|
+
severity: 'warning',
|
|
112
115
|
},
|
|
113
116
|
{
|
|
114
117
|
code: 're-evaluating-command',
|
|
115
118
|
message: 'Workflow data is re-executed as shell code.',
|
|
116
119
|
path: 'jobs.build.steps.0.run',
|
|
120
|
+
file_path: '.shipfox/workflows/warning.yml',
|
|
121
|
+
severity: 'warning',
|
|
117
122
|
},
|
|
118
123
|
],
|
|
119
124
|
},
|
|
@@ -125,12 +130,87 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
125
130
|
renderWorkflowsPage();
|
|
126
131
|
|
|
127
132
|
expect(await screen.findByText('Workflow definition warnings')).toBeInTheDocument();
|
|
133
|
+
// Both warnings group under the shared workflow file; the file label renders once.
|
|
128
134
|
expect(screen.getAllByText('Workflow data is re-executed as shell code.')).toHaveLength(2);
|
|
129
|
-
expect(screen.
|
|
135
|
+
expect(screen.getAllByText('.shipfox/workflows/warning.yml')).toHaveLength(1);
|
|
130
136
|
expect(screen.getAllByText('jobs.build.steps.0.run')).toHaveLength(2);
|
|
137
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
131
138
|
expect(screen.queryByText('Workflow sync failed')).not.toBeInTheDocument();
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
test('shows errors and warnings together with severity labels, grouped by file path', async () => {
|
|
142
|
+
configureApiClient({
|
|
143
|
+
fetchImpl: createProjectDetailFetch({
|
|
144
|
+
definitions: jsonResponse(
|
|
145
|
+
definitionsDto({
|
|
146
|
+
sync: {
|
|
147
|
+
ref: 'main',
|
|
148
|
+
status: 'succeeded',
|
|
149
|
+
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
150
|
+
started_at: '2026-05-07T00:59:55.000Z',
|
|
151
|
+
finished_at: '2026-05-07T01:00:00.000Z',
|
|
152
|
+
last_error_code: null,
|
|
153
|
+
last_error_message: null,
|
|
154
|
+
diagnostics: [
|
|
155
|
+
{
|
|
156
|
+
code: 'invalid-trigger-event',
|
|
157
|
+
message: 'Trigger event is never delivered by this source.',
|
|
158
|
+
path: 'triggers.on_deploy',
|
|
159
|
+
file_path: '.shipfox/workflows/deploy.yml',
|
|
160
|
+
severity: 'error',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
code: 'unknown-trigger-source',
|
|
164
|
+
message: 'No connection matches this source slug.',
|
|
165
|
+
path: 'triggers.on_deploy',
|
|
166
|
+
file_path: '.shipfox/workflows/deploy.yml',
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
code: 're-evaluating-command',
|
|
171
|
+
message: 'Workflow data is re-executed as shell code.',
|
|
172
|
+
path: 'jobs.build.steps.0.run',
|
|
173
|
+
file_path: '.shipfox/workflows/build.yml',
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
renderWorkflowsPage();
|
|
184
|
+
|
|
185
|
+
expect(await screen.findByText('Workflow definition diagnostics')).toBeInTheDocument();
|
|
186
|
+
const diagnosticsCallout = screen.getByRole('status');
|
|
187
|
+
expect(diagnosticsCallout).toBeInTheDocument();
|
|
188
|
+
// Two groups: the deploy workflow file and the build workflow file.
|
|
189
|
+
expect(within(diagnosticsCallout).getAllByText('.shipfox/workflows/deploy.yml')).toHaveLength(
|
|
190
|
+
1,
|
|
191
|
+
);
|
|
192
|
+
expect(within(diagnosticsCallout).getAllByText('.shipfox/workflows/build.yml')).toHaveLength(1);
|
|
193
|
+
expect(within(diagnosticsCallout).getAllByText('triggers.on_deploy')).toHaveLength(2);
|
|
194
|
+
expect(within(diagnosticsCallout).getAllByText('jobs.build.steps.0.run')).toHaveLength(1);
|
|
195
|
+
const errorRow = within(diagnosticsCallout).getByText(
|
|
196
|
+
'Trigger event is never delivered by this source.',
|
|
197
|
+
);
|
|
198
|
+
const warningRow = within(diagnosticsCallout).getByText(
|
|
199
|
+
'No connection matches this source slug.',
|
|
200
|
+
);
|
|
201
|
+
expect(errorRow).toHaveClass('text-tag-error-text');
|
|
202
|
+
expect(warningRow).not.toHaveClass('text-tag-error-text');
|
|
203
|
+
expect(
|
|
204
|
+
within(diagnosticsCallout).getByText('Error:', {selector: 'span.font-medium'}),
|
|
205
|
+
).toBeInTheDocument();
|
|
206
|
+
expect(
|
|
207
|
+
within(diagnosticsCallout).getAllByText('Warning:', {selector: 'span.font-medium'}),
|
|
208
|
+
).toHaveLength(2);
|
|
209
|
+
expect(
|
|
210
|
+
within(diagnosticsCallout).getByText('Workflow data is re-executed as shell code.'),
|
|
211
|
+
).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
134
214
|
test('opens and closes the definition drawer by clicking the row', async () => {
|
|
135
215
|
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
136
216
|
|
|
@@ -301,7 +381,7 @@ function baseDefinitionsDto() {
|
|
|
301
381
|
finished_at: '2026-05-07T01:00:00.000Z',
|
|
302
382
|
last_error_code: null,
|
|
303
383
|
last_error_message: null,
|
|
304
|
-
|
|
384
|
+
diagnostics: [],
|
|
305
385
|
},
|
|
306
386
|
};
|
|
307
387
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {ApiError} from '@shipfox/client-api';
|
|
2
2
|
import {
|
|
3
3
|
type Definition,
|
|
4
|
+
type DefinitionSyncDiagnostic,
|
|
4
5
|
type DefinitionSyncSummary,
|
|
5
6
|
SourceStrip,
|
|
6
7
|
useDefinitionsInfiniteQuery,
|
|
@@ -101,7 +102,7 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
101
102
|
/>
|
|
102
103
|
|
|
103
104
|
<WorkflowSyncAlert sync={sync} />
|
|
104
|
-
<
|
|
105
|
+
<WorkflowSyncDiagnostics sync={sync} />
|
|
105
106
|
|
|
106
107
|
<WorkflowDefinitionsList
|
|
107
108
|
definitions={definitions}
|
|
@@ -382,30 +383,54 @@ function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefin
|
|
|
382
383
|
);
|
|
383
384
|
}
|
|
384
385
|
|
|
385
|
-
function
|
|
386
|
-
if (sync?.status !== 'succeeded' || sync.
|
|
386
|
+
function WorkflowSyncDiagnostics({sync}: {sync: DefinitionSyncSummary | null | undefined}) {
|
|
387
|
+
if (sync?.status !== 'succeeded' || sync.diagnostics.length === 0) return null;
|
|
387
388
|
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
const hasErrors = sync.diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
390
|
+
const hasWarnings = sync.diagnostics.some((diagnostic) => diagnostic.severity === 'warning');
|
|
391
|
+
const groups = groupDiagnosticsByFilePath(sync.diagnostics);
|
|
392
|
+
const title =
|
|
393
|
+
hasErrors && hasWarnings
|
|
394
|
+
? 'Workflow definition diagnostics'
|
|
395
|
+
: hasErrors
|
|
396
|
+
? 'Workflow definition errors'
|
|
397
|
+
: 'Workflow definition warnings';
|
|
395
398
|
|
|
396
399
|
return (
|
|
397
|
-
<Callout role="status" type=
|
|
400
|
+
<Callout role="status" type={hasErrors ? 'error' : 'warning'}>
|
|
398
401
|
<div className="flex flex-col gap-inline">
|
|
399
402
|
<Text size="sm" bold>
|
|
400
|
-
|
|
403
|
+
{title}
|
|
401
404
|
</Text>
|
|
402
405
|
<ul className="flex flex-col gap-tight">
|
|
403
|
-
{
|
|
404
|
-
<li key={key}>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<Code className="text-foreground-neutral-muted">{warning.path}</Code>
|
|
406
|
+
{groups.map((group) => (
|
|
407
|
+
<li key={group.key} className="flex flex-col gap-tight">
|
|
408
|
+
{group.filePath ? (
|
|
409
|
+
<Code className="text-foreground-neutral-muted">{group.filePath}</Code>
|
|
408
410
|
) : null}
|
|
411
|
+
<ul className="flex flex-col gap-tight">
|
|
412
|
+
{group.items.map(({key, diagnostic}) => {
|
|
413
|
+
const severityLabel = diagnostic.severity === 'error' ? 'Error' : 'Warning';
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<li key={key}>
|
|
417
|
+
{diagnostic.path ? (
|
|
418
|
+
<Code className="text-foreground-neutral-muted">{diagnostic.path}</Code>
|
|
419
|
+
) : null}
|
|
420
|
+
<Text size="sm">
|
|
421
|
+
<span className="font-medium">{severityLabel}:</span>{' '}
|
|
422
|
+
<span
|
|
423
|
+
className={
|
|
424
|
+
diagnostic.severity === 'error' ? 'text-tag-error-text' : undefined
|
|
425
|
+
}
|
|
426
|
+
>
|
|
427
|
+
{diagnostic.message}
|
|
428
|
+
</span>
|
|
429
|
+
</Text>
|
|
430
|
+
</li>
|
|
431
|
+
);
|
|
432
|
+
})}
|
|
433
|
+
</ul>
|
|
409
434
|
</li>
|
|
410
435
|
))}
|
|
411
436
|
</ul>
|
|
@@ -414,6 +439,35 @@ function WorkflowSyncWarnings({sync}: {sync: DefinitionSyncSummary | null | unde
|
|
|
414
439
|
);
|
|
415
440
|
}
|
|
416
441
|
|
|
442
|
+
interface DiagnosticGroup {
|
|
443
|
+
key: string;
|
|
444
|
+
filePath: string | undefined;
|
|
445
|
+
items: {key: string; diagnostic: DefinitionSyncDiagnostic}[];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function groupDiagnosticsByFilePath(
|
|
449
|
+
diagnostics: readonly DefinitionSyncDiagnostic[],
|
|
450
|
+
): DiagnosticGroup[] {
|
|
451
|
+
const groups: DiagnosticGroup[] = [];
|
|
452
|
+
const indexByFilePath = new Map<string, number>();
|
|
453
|
+
for (const diagnostic of diagnostics) {
|
|
454
|
+
const filePathKey = diagnostic.filePath ?? '';
|
|
455
|
+
const groupIndex = indexByFilePath.get(filePathKey);
|
|
456
|
+
if (groupIndex === undefined) {
|
|
457
|
+
indexByFilePath.set(filePathKey, groups.length);
|
|
458
|
+
groups.push({
|
|
459
|
+
key: `${filePathKey}-${groups.length}`,
|
|
460
|
+
filePath: diagnostic.filePath,
|
|
461
|
+
items: [{key: `${filePathKey}-0`, diagnostic}],
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
const group = groups[groupIndex];
|
|
465
|
+
if (group) group.items.push({key: `${filePathKey}-${group.items.length}`, diagnostic});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return groups;
|
|
469
|
+
}
|
|
470
|
+
|
|
417
471
|
function DefinitionSheet({
|
|
418
472
|
definition,
|
|
419
473
|
onOpenChange,
|