@shipfox/client-workflows 22.0.1 → 22.0.3

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.
Files changed (40) hide show
  1. package/.storybook/preview.tsx +2 -2
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +31 -0
  4. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  5. package/dist/components/job-detail/agent-config-failure-callout.js +15 -0
  6. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  7. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  8. package/dist/components/job-detail/job-empty-states.js +8 -0
  9. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  10. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  11. package/dist/components/workflow-run-list/workflow-run-list-content.js +9 -15
  12. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  13. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  14. package/dist/components/workflow-run-list/workflow-run-list-states.js +36 -39
  15. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  16. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  17. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  18. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  19. package/dist/components/workflow-run-list/workflow-run-row.js +9 -13
  20. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  21. package/dist/core/entities/step.d.ts +2 -0
  22. package/dist/core/entities/step.d.ts.map +1 -1
  23. package/dist/core/entities/step.js.map +1 -1
  24. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  25. package/dist/hooks/api/workflow-run-mapper.js +6 -0
  26. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  27. package/dist/tsconfig.test.tsbuildinfo +1 -1
  28. package/package.json +8 -8
  29. package/src/components/job-detail/agent-config-failure-callout.stories.tsx +29 -0
  30. package/src/components/job-detail/agent-config-failure-callout.tsx +18 -0
  31. package/src/components/job-detail/job-empty-states.test.ts +21 -0
  32. package/src/components/job-detail/job-empty-states.tsx +10 -0
  33. package/src/components/workflow-run-list/workflow-run-list-content.tsx +4 -9
  34. package/src/components/workflow-run-list/workflow-run-list-states.tsx +36 -39
  35. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +5 -0
  36. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  37. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -10
  38. package/src/core/entities/step.ts +2 -0
  39. package/src/hooks/api/workflow-run-mapper.ts +4 -0
  40. 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": "22.0.1",
4
+ "version": "22.0.3",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -29,14 +29,14 @@
29
29
  "date-fns": "^4.1.0",
30
30
  "@shipfox/annotations-dto": "12.3.0",
31
31
  "@shipfox/api-definitions-dto": "12.3.0",
32
- "@shipfox/api-workflows-dto": "13.0.0",
32
+ "@shipfox/api-workflows-dto": "13.1.0",
33
33
  "@shipfox/client-api": "6.0.1",
34
- "@shipfox/client-logs": "22.0.0",
35
- "@shipfox/client-projects": "22.0.1",
36
- "@shipfox/client-shell": "22.0.1",
37
- "@shipfox/client-triggers": "22.0.1",
38
- "@shipfox/react-ui": "2.1.0",
39
- "@shipfox/client-ui": "22.0.0"
34
+ "@shipfox/client-logs": "22.0.3",
35
+ "@shipfox/client-projects": "22.0.3",
36
+ "@shipfox/client-shell": "22.0.3",
37
+ "@shipfox/client-triggers": "22.0.3",
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,
@@ -1,5 +1,4 @@
1
1
  import {QueryLoadError} from '@shipfox/client-ui';
2
- import {Panel} from '@shipfox/react-ui/panel';
3
2
  import type {WorkflowRunListItem} from '#core/workflow-run.js';
4
3
  import type {WorkflowRunListQuery} from './types.js';
5
4
  import {
@@ -53,18 +52,14 @@ export function WorkflowRunListContent({
53
52
  const showNoMatches = hasLoaded && runs.length === 0 && !showEmptyState;
54
53
 
55
54
  return (
56
- <div className="min-h-0 flex-1 overflow-y-auto">
55
+ <>
57
56
  {isPending ? <WorkflowRunListSkeleton /> : null}
58
57
  {!isPending ? (
59
- <Panel>
60
- <QueryLoadError query={query} subject="workflow runs" icon="pulseLine" variant="panel" />
61
- </Panel>
58
+ <QueryLoadError query={query} subject="workflow runs" icon="pulseLine" variant="panel" />
62
59
  ) : null}
63
60
  {!isPending && refreshFailed ? <WorkflowRunListStaleError query={query} /> : null}
64
61
  {showEmptyState ? (
65
- <Panel>
66
- <WorkflowRunListEmpty workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
67
- </Panel>
62
+ <WorkflowRunListEmpty workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
68
63
  ) : null}
69
64
  {showNoMatches ? (
70
65
  <WorkflowRunListNoMatches
@@ -86,6 +81,6 @@ export function WorkflowRunListContent({
86
81
  />
87
82
  </>
88
83
  ) : null}
89
- </div>
84
+ </>
90
85
  );
91
86
  }
@@ -2,7 +2,6 @@ import type {QueryLoadErrorQuery} from '@shipfox/client-ui';
2
2
  import {Button} from '@shipfox/react-ui/button';
3
3
  import {Callout} from '@shipfox/react-ui/callout';
4
4
  import {EmptyState} from '@shipfox/react-ui/empty-state';
5
- import {Panel} from '@shipfox/react-ui/panel';
6
5
  import {Skeleton} from '@shipfox/react-ui/skeleton';
7
6
  import {Text} from '@shipfox/react-ui/typography';
8
7
  import {Link} from '@tanstack/react-router';
@@ -16,7 +15,7 @@ const SKELETON_ROW_COUNT = 8;
16
15
  */
17
16
  export function WorkflowRunListSkeleton() {
18
17
  return (
19
- <Panel role="status" aria-label="Loading runs" className="@container divide-y">
18
+ <div role="status" aria-label="Loading runs" className="@container divide-y">
20
19
  {Array.from({length: SKELETON_ROW_COUNT}).map((_, index) => (
21
20
  <div
22
21
  // biome-ignore lint/suspicious/noArrayIndexKey: skeleton row, stable position
@@ -30,7 +29,7 @@ export function WorkflowRunListSkeleton() {
30
29
  <Skeleton className="h-12 w-48" />
31
30
  </div>
32
31
  ))}
33
- </Panel>
32
+ </div>
34
33
  );
35
34
  }
36
35
 
@@ -114,43 +113,41 @@ export function WorkflowRunListNoMatches({
114
113
  onLoadMore?: () => void;
115
114
  }) {
116
115
  return (
117
- <Panel>
118
- <div className="flex flex-col gap-inline">
119
- {isFetchNextPageError ? (
120
- <Callout role="alert" type="error">
121
- Could not load more workflow runs. Try again to continue searching older runs.
122
- </Callout>
123
- ) : null}
124
- <EmptyState
125
- icon="filterOffLine"
126
- title={hasNextPage ? 'No matches in loaded history' : 'No matching runs'}
127
- description={
128
- hasNextPage
129
- ? 'No loaded run matches these filters. Load more to search further back.'
130
- : 'No run matches these filters.'
131
- }
132
- action={
133
- <div className="flex flex-wrap justify-center gap-inline">
134
- {hasNextPage && onLoadMore ? (
135
- <Button
136
- type="button"
137
- size="sm"
138
- variant="primary"
139
- isLoading={isFetchingNextPage}
140
- onClick={onLoadMore}
141
- >
142
- Load more runs
143
- </Button>
144
- ) : null}
145
- <Button type="button" size="sm" variant="secondary" onClick={onClear}>
146
- Show all runs
116
+ <div className="flex flex-col gap-inline">
117
+ {isFetchNextPageError ? (
118
+ <Callout role="alert" type="error">
119
+ Could not load more workflow runs. Try again to continue searching older runs.
120
+ </Callout>
121
+ ) : null}
122
+ <EmptyState
123
+ icon="filterOffLine"
124
+ title={hasNextPage ? 'No matches in loaded history' : 'No matching runs'}
125
+ description={
126
+ hasNextPage
127
+ ? 'No loaded run matches these filters. Load more to search further back.'
128
+ : 'No run matches these filters.'
129
+ }
130
+ action={
131
+ <div className="flex flex-wrap justify-center gap-inline">
132
+ {hasNextPage && onLoadMore ? (
133
+ <Button
134
+ type="button"
135
+ size="sm"
136
+ variant="primary"
137
+ isLoading={isFetchingNextPage}
138
+ onClick={onLoadMore}
139
+ >
140
+ Load more runs
147
141
  </Button>
148
- </div>
149
- }
150
- variant="panel"
151
- />
152
- </div>
153
- </Panel>
142
+ ) : null}
143
+ <Button type="button" size="sm" variant="secondary" onClick={onClear}>
144
+ Show all runs
145
+ </Button>
146
+ </div>
147
+ }
148
+ variant="panel"
149
+ />
150
+ </div>
154
151
  );
155
152
  }
156
153
 
@@ -48,6 +48,7 @@ describe('WorkflowRunListView', () => {
48
48
  const body = panel?.querySelector<HTMLElement>('[data-slot="panel-body"]');
49
49
 
50
50
  expect(panel).not.toBeNull();
51
+ expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
51
52
  expect(header).not.toBeNull();
52
53
  expect(body).not.toBeNull();
53
54
  expect(within(header as HTMLElement).getByLabelText('Search runs')).toBeInTheDocument();
@@ -204,6 +205,7 @@ describe('WorkflowRunListView', () => {
204
205
  renderListView([], {query: loadedQuery({isPending: true, data: undefined})});
205
206
 
206
207
  expect(await screen.findByLabelText('Loading runs')).toBeInTheDocument();
208
+ expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
207
209
  expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
208
210
  });
209
211
 
@@ -217,6 +219,7 @@ describe('WorkflowRunListView', () => {
217
219
  renderListView([]);
218
220
 
219
221
  expect(await screen.findByText('No runs yet')).toBeInTheDocument();
222
+ expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
220
223
  expect(screen.getByRole('link', {name: 'View workflows'})).toBeInTheDocument();
221
224
  expect(screen.queryByRole('button', {name: 'Show all runs'})).not.toBeInTheDocument();
222
225
  });
@@ -228,6 +231,7 @@ describe('WorkflowRunListView', () => {
228
231
  await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
229
232
 
230
233
  expect(screen.getByText('No matching runs')).toBeInTheDocument();
234
+ expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
231
235
  expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
232
236
  expect(screen.getByRole('button', {name: 'Show all runs'})).toBeInTheDocument();
233
237
  });
@@ -248,6 +252,7 @@ describe('WorkflowRunListView', () => {
248
252
  });
249
253
 
250
254
  expect(await screen.findByLabelText('Search runs')).toBeInTheDocument();
255
+ expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
251
256
  expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
252
257
  expect(screen.queryByText('No matching runs')).not.toBeInTheDocument();
253
258
  });
@@ -77,7 +77,7 @@ export function WorkflowRunListView({
77
77
  hasActiveFilters={hasActiveFilters}
78
78
  />
79
79
  </PanelHeader>
80
- <PanelBody className="min-h-0 flex-1">
80
+ <PanelBody className="min-h-0 flex-1 overflow-y-auto">
81
81
  <WorkflowRunListContent
82
82
  query={query}
83
83
  totalRuns={runs.length}
@@ -1,6 +1,5 @@
1
1
  import {TriggerSourceIcon} from '@shipfox/client-triggers';
2
2
  import {Icon, type IconName} from '@shipfox/react-ui/icon';
3
- import {Panel} from '@shipfox/react-ui/panel';
4
3
  import {RelativeTime} from '@shipfox/react-ui/relative-time';
5
4
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
6
5
  import {Code, Text} from '@shipfox/react-ui/typography';
@@ -38,15 +37,13 @@ export function WorkflowRunRowList({
38
37
  // A container, not the viewport, drives the row's breakpoints. What a row can afford is its
39
38
  // own width; keying off the viewport would keep the job strip hidden on a wide screen or
40
39
  // crush it on a narrow one.
41
- <Panel className="@container">
42
- <ul className="divide-y divide-border-neutral-base">
43
- {runs.map((run) => (
44
- <li key={run.id}>
45
- <WorkflowRunRow run={run} workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
46
- </li>
47
- ))}
48
- </ul>
49
- </Panel>
40
+ <ul className="@container divide-y divide-border-neutral-base">
41
+ {runs.map((run) => (
42
+ <li key={run.id}>
43
+ <WorkflowRunRow run={run} workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
44
+ </li>
45
+ ))}
46
+ </ul>
50
47
  );
51
48
  }
52
49
 
@@ -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,