@shipfox/client-workflows 22.0.0 → 22.0.1
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -1
- package/dist/components/job-detail/job-execution-time-text.js +2 -15
- package/dist/components/job-detail/job-execution-time-text.js.map +1 -1
- package/dist/components/job-graph/job-duration-format.js +1 -1
- package/dist/components/job-graph/job-duration-format.js.map +1 -1
- package/dist/pages/project-workflows-page.js +11 -14
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/components/job-detail/job-execution-time-text.test.ts +46 -0
- package/src/components/job-detail/job-execution-time-text.tsx +2 -23
- package/src/components/job-graph/job-duration-format.ts +1 -1
- package/src/pages/project-workflows-page.test.tsx +14 -4
- package/src/pages/project-workflows-page.tsx +7 -10
- 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.
|
|
4
|
+
"version": "22.0.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"@shipfox/annotations-dto": "12.3.0",
|
|
31
31
|
"@shipfox/api-definitions-dto": "12.3.0",
|
|
32
32
|
"@shipfox/api-workflows-dto": "13.0.0",
|
|
33
|
-
"@shipfox/client-logs": "22.0.0",
|
|
34
33
|
"@shipfox/client-api": "6.0.1",
|
|
35
|
-
"@shipfox/client-
|
|
36
|
-
"@shipfox/client-
|
|
37
|
-
"@shipfox/client-
|
|
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
38
|
"@shipfox/react-ui": "2.1.0",
|
|
39
39
|
"@shipfox/client-ui": "22.0.0"
|
|
40
40
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {formatJobExecutionTime} from './job-execution-time-text.js';
|
|
2
|
+
|
|
3
|
+
const FROM_ISO = '2026-08-15T08:51:00.000Z';
|
|
4
|
+
const FROM_MS = Date.parse(FROM_ISO);
|
|
5
|
+
|
|
6
|
+
describe('formatJobExecutionTime', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.useRealTimers();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test.each([
|
|
12
|
+
[999, '0s'],
|
|
13
|
+
[37_999, '37s'],
|
|
14
|
+
[60_000, '1m 00s'],
|
|
15
|
+
[124_999, '2m 04s'],
|
|
16
|
+
[3_780_999, '1h 03m'],
|
|
17
|
+
])('formats a live %i ms timer as %s', (elapsedMs, expected) => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
vi.setSystemTime(FROM_MS + elapsedMs);
|
|
20
|
+
|
|
21
|
+
const result = formatJobExecutionTime({
|
|
22
|
+
state: 'live',
|
|
23
|
+
fromIso: FROM_ISO,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).toBe(expected);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('falls back to zero seconds for an invalid live timestamp', () => {
|
|
30
|
+
const result = formatJobExecutionTime({state: 'live', fromIso: 'not-a-date'});
|
|
31
|
+
|
|
32
|
+
expect(result).toBe('0s');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test.each([
|
|
36
|
+
[{seconds: 37}, '37s'],
|
|
37
|
+
[{minutes: 1}, '1m 00s'],
|
|
38
|
+
[{minutes: 2, seconds: 4}, '2m 04s'],
|
|
39
|
+
[{hours: 1}, '1h 00m'],
|
|
40
|
+
[{hours: 1, minutes: 3}, '1h 03m'],
|
|
41
|
+
])('formats a fixed timer consistently as %s', (elapsed, expected) => {
|
|
42
|
+
const result = formatJobExecutionTime({state: 'fixed', elapsed});
|
|
43
|
+
|
|
44
|
+
expect(result).toBe(expected);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {useTimeTick} from '@shipfox/react-ui/time-ticker';
|
|
2
|
-
import {formatDuration} from '@shipfox/react-ui/utils';
|
|
3
2
|
import type {JobExecutionTime} from '#core/workflow-run.js';
|
|
3
|
+
import {formatJobExecutionTimeLabel} from '../job-graph/job-duration-format.js';
|
|
4
4
|
|
|
5
5
|
export function JobExecutionTimeText({time}: {time: JobExecutionTime}) {
|
|
6
6
|
useTimeTick();
|
|
@@ -9,26 +9,5 @@ export function JobExecutionTimeText({time}: {time: JobExecutionTime}) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function formatJobExecutionTime(time: JobExecutionTime): string {
|
|
12
|
-
|
|
13
|
-
return formatDuration(Date.now() - Date.parse(time.fromIso));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return formatElapsedDuration(time.elapsed);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function formatElapsedDuration(duration: {
|
|
20
|
-
days?: number | undefined;
|
|
21
|
-
hours?: number | undefined;
|
|
22
|
-
minutes?: number | undefined;
|
|
23
|
-
seconds?: number | undefined;
|
|
24
|
-
}): string {
|
|
25
|
-
const days = duration.days ?? 0;
|
|
26
|
-
const hours = duration.hours ?? 0;
|
|
27
|
-
const minutes = duration.minutes ?? 0;
|
|
28
|
-
const seconds = duration.seconds ?? 0;
|
|
29
|
-
|
|
30
|
-
if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
31
|
-
if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
32
|
-
if (minutes > 0) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
33
|
-
return `${seconds}s`;
|
|
12
|
+
return formatJobExecutionTimeLabel(time);
|
|
34
13
|
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
export function formatJobExecutionTimeLabel(time: JobExecutionTime): string {
|
|
9
9
|
switch (time.state) {
|
|
10
10
|
case 'live':
|
|
11
|
-
return humanDuration(time.fromIso);
|
|
11
|
+
return humanDuration(time.fromIso) || '0s';
|
|
12
12
|
case 'fixed':
|
|
13
13
|
return formatFixedDurationLabel(time.elapsed);
|
|
14
14
|
default: {
|
|
@@ -12,15 +12,25 @@ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
|
12
12
|
const CONNECTION_ID = '33333333-3333-4333-8333-333333333333';
|
|
13
13
|
|
|
14
14
|
describe('ProjectWorkflowsPage', () => {
|
|
15
|
-
test('renders workflow definitions and
|
|
15
|
+
test('renders workflow definitions and their panel regions', async () => {
|
|
16
16
|
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
17
17
|
|
|
18
18
|
renderWorkflowsPage();
|
|
19
19
|
|
|
20
|
-
expect(await screen.
|
|
21
|
-
expect(screen.
|
|
20
|
+
expect((await screen.findAllByText('Deploy production'))[0]).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByRole('heading', {name: 'Workflows'})).toHaveClass('sr-only');
|
|
22
|
+
expect(
|
|
23
|
+
screen.queryByText('Synced workflow definitions for this project source.'),
|
|
24
|
+
).not.toBeInTheDocument();
|
|
22
25
|
expect(screen.getAllByText('.shipfox/workflows/deploy.yml')[0]).toBeInTheDocument();
|
|
23
|
-
|
|
26
|
+
const sourcePanel = screen
|
|
27
|
+
.getByRole('region', {name: 'Project source'})
|
|
28
|
+
.closest('[data-slot="panel"]');
|
|
29
|
+
const definitionsPanel = screen.getByRole('region', {name: 'Workflow definitions'});
|
|
30
|
+
expect(sourcePanel).toBeInTheDocument();
|
|
31
|
+
expect(definitionsPanel).toBeInTheDocument();
|
|
32
|
+
expect(sourcePanel).not.toBe(definitionsPanel);
|
|
33
|
+
expect(screen.getByRole('table').closest('[data-slot="panel"]')).toBe(definitionsPanel);
|
|
24
34
|
// Source strip resolves connection display_name from the integrations
|
|
25
35
|
// workspace cache; external_repository_id renders as a Code chip.
|
|
26
36
|
expect(await screen.findByText('Acme GitHub')).toBeInTheDocument();
|
|
@@ -68,6 +68,10 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
68
68
|
|
|
69
69
|
return (
|
|
70
70
|
<div className="flex w-full flex-col gap-section">
|
|
71
|
+
<Header variant="h1" className="sr-only">
|
|
72
|
+
Workflows
|
|
73
|
+
</Header>
|
|
74
|
+
|
|
71
75
|
{projectQuery.isPending ? (
|
|
72
76
|
<div className="flex flex-col gap-cluster">
|
|
73
77
|
<Skeleton className="h-28 w-1/3" />
|
|
@@ -89,13 +93,6 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
89
93
|
|
|
90
94
|
{projectQuery.data ? (
|
|
91
95
|
<>
|
|
92
|
-
<header className="flex flex-col gap-tight">
|
|
93
|
-
<Header variant="h2">Workflows</Header>
|
|
94
|
-
<Text size="sm" className="text-foreground-neutral-muted">
|
|
95
|
-
Synced workflow definitions for this project source.
|
|
96
|
-
</Text>
|
|
97
|
-
</header>
|
|
98
|
-
|
|
99
96
|
<SourceStrip
|
|
100
97
|
connectionId={projectQuery.data.source.connectionId}
|
|
101
98
|
externalRepositoryId={projectQuery.data.source.externalRepositoryId}
|
|
@@ -181,7 +178,7 @@ function WorkflowDefinitionsList({
|
|
|
181
178
|
|
|
182
179
|
if (isError && definitions.length === 0) {
|
|
183
180
|
return (
|
|
184
|
-
<Panel>
|
|
181
|
+
<Panel role="region" aria-label="Workflow definitions">
|
|
185
182
|
<LoadErrorState
|
|
186
183
|
title="Couldn't load workflows"
|
|
187
184
|
description="Definitions could not be loaded. Source metadata remains visible."
|
|
@@ -195,7 +192,7 @@ function WorkflowDefinitionsList({
|
|
|
195
192
|
|
|
196
193
|
if (definitions.length === 0) {
|
|
197
194
|
return (
|
|
198
|
-
<Panel>
|
|
195
|
+
<Panel role="region" aria-label="Workflow definitions">
|
|
199
196
|
<WorkflowEmptyState sync={sync} />
|
|
200
197
|
</Panel>
|
|
201
198
|
);
|
|
@@ -203,7 +200,7 @@ function WorkflowDefinitionsList({
|
|
|
203
200
|
|
|
204
201
|
return (
|
|
205
202
|
<>
|
|
206
|
-
<Panel>
|
|
203
|
+
<Panel role="region" aria-label="Workflow definitions">
|
|
207
204
|
<div className="hidden md:block">
|
|
208
205
|
<Table>
|
|
209
206
|
<TableHeader>
|