@shipfox/client-runners 0.2.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/main.ts +1 -0
- package/.storybook/preview.css +4 -0
- package/.storybook/preview.tsx +64 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +112 -0
- package/LICENSE +21 -0
- package/dist/components/create-manual-registration-token-form.d.ts +10 -0
- package/dist/components/create-manual-registration-token-form.d.ts.map +1 -0
- package/dist/components/create-manual-registration-token-form.js +206 -0
- package/dist/components/create-manual-registration-token-form.js.map +1 -0
- package/dist/components/create-provisioner-token-form.d.ts +10 -0
- package/dist/components/create-provisioner-token-form.d.ts.map +1 -0
- package/dist/components/create-provisioner-token-form.js +206 -0
- package/dist/components/create-provisioner-token-form.js.map +1 -0
- package/dist/components/form-errors.d.ts +6 -0
- package/dist/components/form-errors.d.ts.map +1 -0
- package/dist/components/form-errors.js +9 -0
- package/dist/components/form-errors.js.map +1 -0
- package/dist/components/manual-registration-token-errors.d.ts +2 -0
- package/dist/components/manual-registration-token-errors.d.ts.map +1 -0
- package/dist/components/manual-registration-token-errors.js +14 -0
- package/dist/components/manual-registration-token-errors.js.map +1 -0
- package/dist/components/manual-registration-token-format.d.ts +5 -0
- package/dist/components/manual-registration-token-format.d.ts.map +1 -0
- package/dist/components/manual-registration-token-format.js +14 -0
- package/dist/components/manual-registration-token-format.js.map +1 -0
- package/dist/components/manual-registration-token-list.d.ts +8 -0
- package/dist/components/manual-registration-token-list.d.ts.map +1 -0
- package/dist/components/manual-registration-token-list.js +293 -0
- package/dist/components/manual-registration-token-list.js.map +1 -0
- package/dist/components/manual-registration-tokens-settings-section.d.ts +4 -0
- package/dist/components/manual-registration-tokens-settings-section.d.ts.map +1 -0
- package/dist/components/manual-registration-tokens-settings-section.js +109 -0
- package/dist/components/manual-registration-tokens-settings-section.js.map +1 -0
- package/dist/components/provisioner-token-errors.d.ts +2 -0
- package/dist/components/provisioner-token-errors.d.ts.map +1 -0
- package/dist/components/provisioner-token-errors.js +13 -0
- package/dist/components/provisioner-token-errors.js.map +1 -0
- package/dist/components/provisioner-token-form-errors.d.ts +6 -0
- package/dist/components/provisioner-token-form-errors.d.ts.map +1 -0
- package/dist/components/provisioner-token-form-errors.js +9 -0
- package/dist/components/provisioner-token-form-errors.js.map +1 -0
- package/dist/components/provisioner-token-format.d.ts +21 -0
- package/dist/components/provisioner-token-format.d.ts.map +1 -0
- package/dist/components/provisioner-token-format.js +36 -0
- package/dist/components/provisioner-token-format.js.map +1 -0
- package/dist/components/provisioner-token-list.d.ts +9 -0
- package/dist/components/provisioner-token-list.d.ts.map +1 -0
- package/dist/components/provisioner-token-list.js +344 -0
- package/dist/components/provisioner-token-list.js.map +1 -0
- package/dist/components/provisioner-tokens-settings-section.d.ts +4 -0
- package/dist/components/provisioner-tokens-settings-section.d.ts.map +1 -0
- package/dist/components/provisioner-tokens-settings-section.js +117 -0
- package/dist/components/provisioner-tokens-settings-section.js.map +1 -0
- package/dist/components/token-date.d.ts +6 -0
- package/dist/components/token-date.d.ts.map +1 -0
- package/dist/components/token-date.js +27 -0
- package/dist/components/token-date.js.map +1 -0
- package/dist/components/token-expiration-select.d.ts +11 -0
- package/dist/components/token-expiration-select.d.ts.map +1 -0
- package/dist/components/token-expiration-select.js +68 -0
- package/dist/components/token-expiration-select.js.map +1 -0
- package/dist/components/token-name.d.ts +4 -0
- package/dist/components/token-name.d.ts.map +1 -0
- package/dist/components/token-name.js +24 -0
- package/dist/components/token-name.js.map +1 -0
- package/dist/components/token-settings-sections.stories.d.ts +26 -0
- package/dist/components/token-settings-sections.stories.d.ts.map +1 -0
- package/dist/components/token-settings-sections.stories.js +370 -0
- package/dist/components/token-settings-sections.stories.js.map +1 -0
- package/dist/hooks/api/manual-registration-tokens.d.ts +83 -0
- package/dist/hooks/api/manual-registration-tokens.d.ts.map +1 -0
- package/dist/hooks/api/manual-registration-tokens.js +53 -0
- package/dist/hooks/api/manual-registration-tokens.js.map +1 -0
- package/dist/hooks/api/provisioner-tokens.d.ts +125 -0
- package/dist/hooks/api/provisioner-tokens.d.ts.map +1 -0
- package/dist/hooks/api/provisioner-tokens.js +81 -0
- package/dist/hooks/api/provisioner-tokens.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +88 -0
- package/src/components/create-manual-registration-token-form.tsx +197 -0
- package/src/components/create-provisioner-token-form.tsx +193 -0
- package/src/components/form-errors.test.ts +24 -0
- package/src/components/form-errors.ts +9 -0
- package/src/components/manual-registration-token-errors.ts +13 -0
- package/src/components/manual-registration-token-format.test.ts +48 -0
- package/src/components/manual-registration-token-format.ts +18 -0
- package/src/components/manual-registration-token-list.tsx +246 -0
- package/src/components/manual-registration-tokens-settings-section.test.tsx +251 -0
- package/src/components/manual-registration-tokens-settings-section.tsx +110 -0
- package/src/components/provisioner-token-errors.ts +12 -0
- package/src/components/provisioner-token-form-errors.test.ts +35 -0
- package/src/components/provisioner-token-form-errors.ts +9 -0
- package/src/components/provisioner-token-format.test.ts +96 -0
- package/src/components/provisioner-token-format.ts +40 -0
- package/src/components/provisioner-token-list.tsx +280 -0
- package/src/components/provisioner-tokens-settings-section.test.tsx +362 -0
- package/src/components/provisioner-tokens-settings-section.tsx +114 -0
- package/src/components/token-date.tsx +27 -0
- package/src/components/token-expiration-select.tsx +64 -0
- package/src/components/token-name.tsx +19 -0
- package/src/components/token-settings-sections.stories.tsx +360 -0
- package/src/hooks/api/manual-registration-tokens.test.ts +87 -0
- package/src/hooks/api/manual-registration-tokens.ts +71 -0
- package/src/hooks/api/provisioner-tokens.test.ts +127 -0
- package/src/hooks/api/provisioner-tokens.ts +102 -0
- package/src/index.ts +4 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +74 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type {ManualRegistrationTokenDto, ProvisionerTokenDto} from '@shipfox/api-runners-dto';
|
|
2
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
3
|
+
import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
|
|
4
|
+
import {Toaster} from '@shipfox/react-ui/toast';
|
|
5
|
+
import {Code} from '@shipfox/react-ui/typography';
|
|
6
|
+
import type {Decorator, Meta, StoryObj} from '@storybook/react';
|
|
7
|
+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
8
|
+
import type {ReactNode} from 'react';
|
|
9
|
+
import {within} from 'storybook/test';
|
|
10
|
+
import {CreatedManualRegistrationTokenPanel} from './create-manual-registration-token-form.js';
|
|
11
|
+
import {CreatedProvisionerTokenPanel} from './create-provisioner-token-form.js';
|
|
12
|
+
import {
|
|
13
|
+
EmptyManualRegistrationTokens,
|
|
14
|
+
ManualRegistrationTokenList,
|
|
15
|
+
ManualRegistrationTokenTableSkeleton,
|
|
16
|
+
} from './manual-registration-token-list.js';
|
|
17
|
+
import {WorkspaceManualRegistrationTokensSettingsSection} from './manual-registration-tokens-settings-section.js';
|
|
18
|
+
import {
|
|
19
|
+
EmptyProvisionerTokens,
|
|
20
|
+
ProvisionerTokenList,
|
|
21
|
+
ProvisionerTokenTableSkeleton,
|
|
22
|
+
} from './provisioner-token-list.js';
|
|
23
|
+
import {WorkspaceProvisionerTokensSettingsSection} from './provisioner-tokens-settings-section.js';
|
|
24
|
+
|
|
25
|
+
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
26
|
+
const NOW = '2026-06-26T12:00:00.000Z';
|
|
27
|
+
const EIGHT_MINUTES_AGO = '2026-06-26T11:52:00.000Z';
|
|
28
|
+
const EXPIRES_AT = '2026-09-24T12:00:00.000Z';
|
|
29
|
+
const CREATED_AT = '2026-06-20T12:00:00.000Z';
|
|
30
|
+
|
|
31
|
+
type Scenario = 'populated' | 'empty' | 'loading' | 'errors';
|
|
32
|
+
|
|
33
|
+
interface TokenSettingsSectionsStoryProps {
|
|
34
|
+
scenario: Scenario;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const withQueryClient: Decorator = (Story) => {
|
|
38
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
39
|
+
return (
|
|
40
|
+
<QueryClientProvider client={queryClient}>
|
|
41
|
+
<RelativeTimeProvider>
|
|
42
|
+
<Story />
|
|
43
|
+
</RelativeTimeProvider>
|
|
44
|
+
<Toaster />
|
|
45
|
+
</QueryClientProvider>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function TokenSettingsSectionsStory({scenario}: TokenSettingsSectionsStoryProps) {
|
|
50
|
+
configureApiClient({
|
|
51
|
+
baseUrl: 'https://api.example.test',
|
|
52
|
+
fetchImpl: fetchForScenario(scenario),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<StorySurface>
|
|
57
|
+
<WorkspaceManualRegistrationTokensSettingsSection workspaceId={WORKSPACE_ID} />
|
|
58
|
+
<WorkspaceProvisionerTokensSettingsSection workspaceId={WORKSPACE_ID} />
|
|
59
|
+
</StorySurface>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const meta = {
|
|
64
|
+
title: 'Runners/TokenSettingsSections',
|
|
65
|
+
component: TokenSettingsSectionsStory,
|
|
66
|
+
parameters: {layout: 'fullscreen'},
|
|
67
|
+
decorators: [withQueryClient],
|
|
68
|
+
args: {scenario: 'populated'},
|
|
69
|
+
} satisfies Meta<typeof TokenSettingsSectionsStory>;
|
|
70
|
+
|
|
71
|
+
export default meta;
|
|
72
|
+
type Story = StoryObj<typeof meta>;
|
|
73
|
+
|
|
74
|
+
export const Playground: Story = {
|
|
75
|
+
play: async ({canvasElement}) => {
|
|
76
|
+
const canvas = within(canvasElement);
|
|
77
|
+
await canvas.findAllByText('Deploy runner');
|
|
78
|
+
await canvas.findAllByText('Docker autoscaler');
|
|
79
|
+
await canvas.findAllByText('Connected');
|
|
80
|
+
await canvas.findAllByText('Never connected');
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const DataStates: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<StorySurface>
|
|
87
|
+
<StateExample label="Manual empty">
|
|
88
|
+
<EmptyManualRegistrationTokens />
|
|
89
|
+
</StateExample>
|
|
90
|
+
<StateExample label="Manual loading">
|
|
91
|
+
<ManualRegistrationTokenTableSkeleton />
|
|
92
|
+
</StateExample>
|
|
93
|
+
<StateExample label="Provisioner empty">
|
|
94
|
+
<EmptyProvisionerTokens />
|
|
95
|
+
</StateExample>
|
|
96
|
+
<StateExample label="Provisioner loading">
|
|
97
|
+
<ProvisionerTokenTableSkeleton />
|
|
98
|
+
</StateExample>
|
|
99
|
+
</StorySurface>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Errors: Story = {
|
|
104
|
+
args: {scenario: 'errors'},
|
|
105
|
+
play: async ({canvasElement}) => {
|
|
106
|
+
const canvas = within(canvasElement);
|
|
107
|
+
await canvas.findByText("Couldn't load manual registration tokens");
|
|
108
|
+
await canvas.findByText("Couldn't load provisioner registration tokens");
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Statuses: Story = {
|
|
113
|
+
render: () => (
|
|
114
|
+
<StorySurface>
|
|
115
|
+
<ProvisionerTokenList
|
|
116
|
+
workspaceId={WORKSPACE_ID}
|
|
117
|
+
tokens={[
|
|
118
|
+
provisionerToken({
|
|
119
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
120
|
+
name: 'Docker autoscaler',
|
|
121
|
+
last_seen_at: NOW,
|
|
122
|
+
}),
|
|
123
|
+
provisionerToken({
|
|
124
|
+
id: '44444444-4444-4444-8444-444444444444',
|
|
125
|
+
name: 'Kubernetes spot pool',
|
|
126
|
+
last_seen_at: EIGHT_MINUTES_AGO,
|
|
127
|
+
}),
|
|
128
|
+
provisionerToken({
|
|
129
|
+
id: '55555555-5555-4555-8555-555555555555',
|
|
130
|
+
name: 'Buildkite migration pool',
|
|
131
|
+
last_seen_at: null,
|
|
132
|
+
}),
|
|
133
|
+
]}
|
|
134
|
+
activeIds={new Set(['33333333-3333-4333-8333-333333333333'])}
|
|
135
|
+
/>
|
|
136
|
+
</StorySurface>
|
|
137
|
+
),
|
|
138
|
+
play: async ({canvasElement}) => {
|
|
139
|
+
const canvas = within(canvasElement);
|
|
140
|
+
await canvas.findAllByText('Connected');
|
|
141
|
+
await canvas.findAllByText('Last seen');
|
|
142
|
+
await canvas.findAllByText('Never connected');
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const Content: Story = {
|
|
147
|
+
render: () => (
|
|
148
|
+
<StorySurface>
|
|
149
|
+
<StateExample label="Existing tokens">
|
|
150
|
+
<div className="flex flex-col gap-24">
|
|
151
|
+
<ManualRegistrationTokenList
|
|
152
|
+
workspaceId={WORKSPACE_ID}
|
|
153
|
+
tokens={[
|
|
154
|
+
manualToken({name: 'Deploy runner', prefix: 'sf_mrt_deploy'}),
|
|
155
|
+
manualToken({
|
|
156
|
+
id: '66666666-6666-4666-8666-666666666666',
|
|
157
|
+
name: 'macOS build host',
|
|
158
|
+
prefix: 'sf_mrt_macos',
|
|
159
|
+
expires_at: null,
|
|
160
|
+
}),
|
|
161
|
+
]}
|
|
162
|
+
/>
|
|
163
|
+
<ProvisionerTokenList
|
|
164
|
+
workspaceId={WORKSPACE_ID}
|
|
165
|
+
tokens={provisionerTokens()}
|
|
166
|
+
activeIds={new Set(['33333333-3333-4333-8333-333333333333'])}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</StateExample>
|
|
170
|
+
<StateExample label="Long names">
|
|
171
|
+
<div className="flex flex-col gap-24">
|
|
172
|
+
<ManualRegistrationTokenList
|
|
173
|
+
workspaceId={WORKSPACE_ID}
|
|
174
|
+
tokens={[
|
|
175
|
+
manualToken({
|
|
176
|
+
name: 'self-hosted-runner-for-production-release-candidate-validation-on-metal',
|
|
177
|
+
prefix: 'sf_mrt_release_candidate_validation',
|
|
178
|
+
}),
|
|
179
|
+
]}
|
|
180
|
+
/>
|
|
181
|
+
<ProvisionerTokenList
|
|
182
|
+
workspaceId={WORKSPACE_ID}
|
|
183
|
+
tokens={[
|
|
184
|
+
provisionerToken({
|
|
185
|
+
name: 'docker-provisioner-for-west-coast-gpu-burst-capacity-and-fallback',
|
|
186
|
+
prefix: 'sf_pt_west_coast_gpu_burst',
|
|
187
|
+
last_seen_at: EIGHT_MINUTES_AGO,
|
|
188
|
+
}),
|
|
189
|
+
]}
|
|
190
|
+
activeIds={new Set()}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</StateExample>
|
|
194
|
+
</StorySurface>
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const CreatedTokenPanels: Story = {
|
|
199
|
+
render: () => (
|
|
200
|
+
<StorySurface>
|
|
201
|
+
<StateExample label="Manual token reveal">
|
|
202
|
+
<CreatedManualRegistrationTokenPanel
|
|
203
|
+
token={{
|
|
204
|
+
id: '77777777-7777-4777-8777-777777777777',
|
|
205
|
+
raw_token: 'sf_mrt_4nJvNrs23ExampleManualTokenValue',
|
|
206
|
+
prefix: 'sf_mrt_4nJv',
|
|
207
|
+
name: 'Deploy runner',
|
|
208
|
+
workspace_id: WORKSPACE_ID,
|
|
209
|
+
expires_at: EXPIRES_AT,
|
|
210
|
+
created_at: CREATED_AT,
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
</StateExample>
|
|
214
|
+
<StateExample label="Provisioner token reveal">
|
|
215
|
+
<CreatedProvisionerTokenPanel
|
|
216
|
+
token={{
|
|
217
|
+
...provisionerToken({name: 'Docker autoscaler'}),
|
|
218
|
+
raw_token: 'sf_pt_Rv8YwrExampleProvisionerTokenValue',
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
</StateExample>
|
|
222
|
+
</StorySurface>
|
|
223
|
+
),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function StorySurface({children}: {children: ReactNode}) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="min-h-screen bg-background-neutral-background p-24">
|
|
229
|
+
<div className="mx-auto flex w-full max-w-[920px] flex-col gap-32">{children}</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function StateExample({label, children}: {label: string; children: ReactNode}) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="flex flex-col gap-8">
|
|
237
|
+
<Code variant="label" className="text-foreground-neutral-subtle">
|
|
238
|
+
{label}
|
|
239
|
+
</Code>
|
|
240
|
+
<div className="rounded-8 border border-border-neutral-base bg-background-neutral-base p-16">
|
|
241
|
+
{children}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function fetchForScenario(scenario: Scenario): typeof fetch {
|
|
248
|
+
return (input) => {
|
|
249
|
+
const url = requestUrl(input);
|
|
250
|
+
if (scenario === 'loading') return new Promise<Response>(() => undefined);
|
|
251
|
+
if (url.pathname.endsWith('/runners/manual-registration-tokens')) {
|
|
252
|
+
if (scenario === 'errors') return Promise.resolve(errorResponse());
|
|
253
|
+
return Promise.resolve(
|
|
254
|
+
jsonResponse({
|
|
255
|
+
manual_registration_tokens: scenario === 'empty' ? [] : [manualToken()],
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (url.pathname.endsWith('/provisioners/tokens')) {
|
|
260
|
+
if (scenario === 'errors') return Promise.resolve(errorResponse());
|
|
261
|
+
return Promise.resolve(
|
|
262
|
+
jsonResponse({
|
|
263
|
+
tokens: scenario === 'empty' ? [] : provisionerTokens(),
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (url.pathname.endsWith('/provisioners/active')) {
|
|
268
|
+
return Promise.resolve(
|
|
269
|
+
jsonResponse({
|
|
270
|
+
provisioners:
|
|
271
|
+
scenario === 'empty'
|
|
272
|
+
? []
|
|
273
|
+
: [
|
|
274
|
+
{
|
|
275
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
276
|
+
name: 'Docker autoscaler',
|
|
277
|
+
prefix: 'sf_pt_docker',
|
|
278
|
+
last_seen_at: NOW,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return Promise.resolve(jsonResponse({}, {status: 404}));
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function provisionerTokens(): ProvisionerTokenDto[] {
|
|
289
|
+
return [
|
|
290
|
+
provisionerToken({
|
|
291
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
292
|
+
name: 'Docker autoscaler',
|
|
293
|
+
prefix: 'sf_pt_docker',
|
|
294
|
+
last_seen_at: NOW,
|
|
295
|
+
}),
|
|
296
|
+
provisionerToken({
|
|
297
|
+
id: '44444444-4444-4444-8444-444444444444',
|
|
298
|
+
name: 'Kubernetes spot pool',
|
|
299
|
+
prefix: 'sf_pt_spot',
|
|
300
|
+
last_seen_at: EIGHT_MINUTES_AGO,
|
|
301
|
+
}),
|
|
302
|
+
provisionerToken({
|
|
303
|
+
id: '55555555-5555-4555-8555-555555555555',
|
|
304
|
+
name: 'Buildkite migration pool',
|
|
305
|
+
prefix: 'sf_pt_migrate',
|
|
306
|
+
last_seen_at: null,
|
|
307
|
+
}),
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function manualToken(
|
|
312
|
+
overrides: Partial<ManualRegistrationTokenDto> = {},
|
|
313
|
+
): ManualRegistrationTokenDto {
|
|
314
|
+
return {
|
|
315
|
+
id: '22222222-2222-4222-8222-222222222222',
|
|
316
|
+
workspace_id: WORKSPACE_ID,
|
|
317
|
+
prefix: 'sf_mrt_deploy',
|
|
318
|
+
name: 'Deploy runner',
|
|
319
|
+
expires_at: EXPIRES_AT,
|
|
320
|
+
revoked_at: null,
|
|
321
|
+
created_at: CREATED_AT,
|
|
322
|
+
updated_at: CREATED_AT,
|
|
323
|
+
...overrides,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function provisionerToken(overrides: Partial<ProvisionerTokenDto> = {}): ProvisionerTokenDto {
|
|
328
|
+
return {
|
|
329
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
330
|
+
workspace_id: WORKSPACE_ID,
|
|
331
|
+
prefix: 'sf_pt_docker',
|
|
332
|
+
name: 'Docker autoscaler',
|
|
333
|
+
created_by_user_id: '99999999-9999-4999-8999-999999999999',
|
|
334
|
+
revoked_by_user_id: null,
|
|
335
|
+
expires_at: EXPIRES_AT,
|
|
336
|
+
revoked_at: null,
|
|
337
|
+
last_seen_at: NOW,
|
|
338
|
+
created_at: CREATED_AT,
|
|
339
|
+
updated_at: CREATED_AT,
|
|
340
|
+
...overrides,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function requestUrl(input: RequestInfo | URL): URL {
|
|
345
|
+
if (typeof input === 'string') return new URL(input);
|
|
346
|
+
if (input instanceof URL) return input;
|
|
347
|
+
return new URL(input.url);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function jsonResponse(body: unknown, init: ResponseInit = {}) {
|
|
351
|
+
return new Response(JSON.stringify(body), {
|
|
352
|
+
status: 200,
|
|
353
|
+
headers: {'content-type': 'application/json'},
|
|
354
|
+
...init,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function errorResponse() {
|
|
359
|
+
return jsonResponse({code: 'server-error'}, {status: 500, statusText: 'Server error'});
|
|
360
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
+
import {
|
|
3
|
+
createManualRegistrationToken,
|
|
4
|
+
revokeManualRegistrationToken,
|
|
5
|
+
} from './manual-registration-tokens.js';
|
|
6
|
+
|
|
7
|
+
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
|
|
8
|
+
return new Response(JSON.stringify(body), {
|
|
9
|
+
headers: {'content-type': 'application/json'},
|
|
10
|
+
status: 200,
|
|
11
|
+
...init,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('createManualRegistrationToken', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('posts token creation body', async () => {
|
|
21
|
+
let requestBody: unknown;
|
|
22
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
23
|
+
requestBody = await (input as Request).clone().json();
|
|
24
|
+
return jsonResponse(
|
|
25
|
+
{
|
|
26
|
+
id: '44444444-4444-4444-8444-444444444444',
|
|
27
|
+
raw_token: 'sf_mrt_raw-created-token',
|
|
28
|
+
prefix: 'sf_mrt_raw-c',
|
|
29
|
+
name: 'Local runner',
|
|
30
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
31
|
+
expires_at: '2026-05-09T00:00:00.000Z',
|
|
32
|
+
created_at: '2026-05-08T00:00:00.000Z',
|
|
33
|
+
},
|
|
34
|
+
{status: 201},
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
configureApiClient({fetchImpl});
|
|
38
|
+
const body = {name: 'Local runner', ttl_seconds: 86_400};
|
|
39
|
+
|
|
40
|
+
const result = await createManualRegistrationToken({
|
|
41
|
+
workspaceId: '11111111-1111-4111-8111-111111111111',
|
|
42
|
+
body,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
46
|
+
expect(result.raw_token).toBe('sf_mrt_raw-created-token');
|
|
47
|
+
expect(request.url).toBe(
|
|
48
|
+
'https://api.example.test/workspaces/11111111-1111-4111-8111-111111111111/runners/manual-registration-tokens',
|
|
49
|
+
);
|
|
50
|
+
expect(request.method).toBe('POST');
|
|
51
|
+
expect(requestBody).toEqual(body);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('revokeManualRegistrationToken', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('posts to the token revoke endpoint', async () => {
|
|
61
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
62
|
+
jsonResponse({
|
|
63
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
64
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
65
|
+
prefix: 'sf_mrt_abcde',
|
|
66
|
+
name: 'Deploy runner',
|
|
67
|
+
expires_at: '2026-05-09T00:00:00.000Z',
|
|
68
|
+
revoked_at: '2026-05-08T01:00:00.000Z',
|
|
69
|
+
created_at: '2026-05-08T00:00:00.000Z',
|
|
70
|
+
updated_at: '2026-05-08T01:00:00.000Z',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
configureApiClient({fetchImpl});
|
|
74
|
+
|
|
75
|
+
const result = await revokeManualRegistrationToken({
|
|
76
|
+
workspaceId: '11111111-1111-4111-8111-111111111111',
|
|
77
|
+
tokenId: '33333333-3333-4333-8333-333333333333',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
81
|
+
expect(result.revoked_at).toBe('2026-05-08T01:00:00.000Z');
|
|
82
|
+
expect(request.url).toBe(
|
|
83
|
+
'https://api.example.test/workspaces/11111111-1111-4111-8111-111111111111/runners/manual-registration-tokens/33333333-3333-4333-8333-333333333333/revoke',
|
|
84
|
+
);
|
|
85
|
+
expect(request.method).toBe('POST');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateManualRegistrationTokenBodyDto,
|
|
3
|
+
CreateManualRegistrationTokenResponseDto,
|
|
4
|
+
ListManualRegistrationTokensResponseDto,
|
|
5
|
+
RevokeManualRegistrationTokenResponseDto,
|
|
6
|
+
} from '@shipfox/api-runners-dto';
|
|
7
|
+
import {apiRequest} from '@shipfox/client-api';
|
|
8
|
+
import {useMutation, useQuery} from '@tanstack/react-query';
|
|
9
|
+
|
|
10
|
+
export const manualRegistrationTokenQueryKeys = {
|
|
11
|
+
all: ['manual-registration-tokens'] as const,
|
|
12
|
+
list: (workspaceId: string) =>
|
|
13
|
+
[...manualRegistrationTokenQueryKeys.all, 'list', workspaceId] as const,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function listManualRegistrationTokens({
|
|
17
|
+
workspaceId,
|
|
18
|
+
signal,
|
|
19
|
+
}: {
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}) {
|
|
23
|
+
return await apiRequest<ListManualRegistrationTokensResponseDto>(
|
|
24
|
+
`/workspaces/${workspaceId}/runners/manual-registration-tokens`,
|
|
25
|
+
{signal},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createManualRegistrationToken({
|
|
30
|
+
workspaceId,
|
|
31
|
+
body,
|
|
32
|
+
}: {
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
body: CreateManualRegistrationTokenBodyDto;
|
|
35
|
+
}) {
|
|
36
|
+
return await apiRequest<CreateManualRegistrationTokenResponseDto>(
|
|
37
|
+
`/workspaces/${workspaceId}/runners/manual-registration-tokens`,
|
|
38
|
+
{method: 'POST', body},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function revokeManualRegistrationToken({
|
|
43
|
+
workspaceId,
|
|
44
|
+
tokenId,
|
|
45
|
+
}: {
|
|
46
|
+
workspaceId: string;
|
|
47
|
+
tokenId: string;
|
|
48
|
+
}) {
|
|
49
|
+
return await apiRequest<RevokeManualRegistrationTokenResponseDto>(
|
|
50
|
+
`/workspaces/${workspaceId}/runners/manual-registration-tokens/${tokenId}/revoke`,
|
|
51
|
+
{method: 'POST'},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useManualRegistrationTokensQuery(workspaceId: string | undefined) {
|
|
56
|
+
return useQuery({
|
|
57
|
+
queryKey: workspaceId
|
|
58
|
+
? manualRegistrationTokenQueryKeys.list(workspaceId)
|
|
59
|
+
: [...manualRegistrationTokenQueryKeys.all, 'list'],
|
|
60
|
+
enabled: Boolean(workspaceId),
|
|
61
|
+
queryFn: ({signal}) => listManualRegistrationTokens({workspaceId: workspaceId ?? '', signal}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useCreateManualRegistrationTokenMutation() {
|
|
66
|
+
return useMutation({mutationFn: createManualRegistrationToken});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function useRevokeManualRegistrationTokenMutation() {
|
|
70
|
+
return useMutation({mutationFn: revokeManualRegistrationToken});
|
|
71
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
+
import {
|
|
3
|
+
createProvisionerToken,
|
|
4
|
+
listActiveProvisioners,
|
|
5
|
+
listProvisionerTokens,
|
|
6
|
+
revokeProvisionerToken,
|
|
7
|
+
} from './provisioner-tokens.js';
|
|
8
|
+
|
|
9
|
+
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
|
|
10
|
+
return new Response(JSON.stringify(body), {
|
|
11
|
+
headers: {'content-type': 'application/json'},
|
|
12
|
+
status: 200,
|
|
13
|
+
...init,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
18
|
+
const tokenId = '33333333-3333-4333-8333-333333333333';
|
|
19
|
+
|
|
20
|
+
describe('provisioner token transports', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('lists provisioner tokens', async () => {
|
|
26
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({tokens: []}));
|
|
27
|
+
configureApiClient({fetchImpl});
|
|
28
|
+
|
|
29
|
+
const result = await listProvisionerTokens({workspaceId});
|
|
30
|
+
|
|
31
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
32
|
+
expect(result.tokens).toEqual([]);
|
|
33
|
+
expect(request.url).toBe(
|
|
34
|
+
`https://api.example.test/workspaces/${workspaceId}/provisioners/tokens`,
|
|
35
|
+
);
|
|
36
|
+
expect(request.method).toBe('GET');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('posts token creation body', async () => {
|
|
40
|
+
let requestBody: unknown;
|
|
41
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
42
|
+
requestBody = await (input as Request).clone().json();
|
|
43
|
+
return jsonResponse(
|
|
44
|
+
{
|
|
45
|
+
id: tokenId,
|
|
46
|
+
raw_token: 'sf_pt_raw-created-token',
|
|
47
|
+
prefix: 'sf_pt_raw-c',
|
|
48
|
+
name: 'Docker provisioner',
|
|
49
|
+
workspace_id: workspaceId,
|
|
50
|
+
created_by_user_id: '22222222-2222-4222-8222-222222222222',
|
|
51
|
+
revoked_by_user_id: null,
|
|
52
|
+
expires_at: '2026-05-09T00:00:00.000Z',
|
|
53
|
+
revoked_at: null,
|
|
54
|
+
last_seen_at: null,
|
|
55
|
+
created_at: '2026-05-08T00:00:00.000Z',
|
|
56
|
+
updated_at: '2026-05-08T00:00:00.000Z',
|
|
57
|
+
},
|
|
58
|
+
{status: 201},
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
configureApiClient({fetchImpl});
|
|
62
|
+
const body = {name: 'Docker provisioner', ttl_seconds: 86_400};
|
|
63
|
+
|
|
64
|
+
const result = await createProvisionerToken({workspaceId, body});
|
|
65
|
+
|
|
66
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
67
|
+
expect(result.raw_token).toBe('sf_pt_raw-created-token');
|
|
68
|
+
expect(request.url).toBe(
|
|
69
|
+
`https://api.example.test/workspaces/${workspaceId}/provisioners/tokens`,
|
|
70
|
+
);
|
|
71
|
+
expect(request.method).toBe('POST');
|
|
72
|
+
expect(requestBody).toEqual(body);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('posts to the token revoke endpoint', async () => {
|
|
76
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
77
|
+
jsonResponse({
|
|
78
|
+
id: tokenId,
|
|
79
|
+
workspace_id: workspaceId,
|
|
80
|
+
prefix: 'sf_pt_abcde',
|
|
81
|
+
name: 'Docker provisioner',
|
|
82
|
+
created_by_user_id: '22222222-2222-4222-8222-222222222222',
|
|
83
|
+
revoked_by_user_id: '22222222-2222-4222-8222-222222222222',
|
|
84
|
+
expires_at: '2026-05-09T00:00:00.000Z',
|
|
85
|
+
revoked_at: '2026-05-08T01:00:00.000Z',
|
|
86
|
+
last_seen_at: null,
|
|
87
|
+
created_at: '2026-05-08T00:00:00.000Z',
|
|
88
|
+
updated_at: '2026-05-08T01:00:00.000Z',
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
configureApiClient({fetchImpl});
|
|
92
|
+
|
|
93
|
+
const result = await revokeProvisionerToken({workspaceId, tokenId});
|
|
94
|
+
|
|
95
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
96
|
+
expect(result.revoked_at).toBe('2026-05-08T01:00:00.000Z');
|
|
97
|
+
expect(request.url).toBe(
|
|
98
|
+
`https://api.example.test/workspaces/${workspaceId}/provisioners/tokens/${tokenId}/revoke`,
|
|
99
|
+
);
|
|
100
|
+
expect(request.method).toBe('POST');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('lists active provisioners', async () => {
|
|
104
|
+
const fetchImpl = vi.fn().mockResolvedValue(
|
|
105
|
+
jsonResponse({
|
|
106
|
+
provisioners: [
|
|
107
|
+
{
|
|
108
|
+
id: tokenId,
|
|
109
|
+
name: 'Docker provisioner',
|
|
110
|
+
prefix: 'sf_pt_abcde',
|
|
111
|
+
last_seen_at: '2026-05-08T01:00:00.000Z',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
configureApiClient({fetchImpl});
|
|
117
|
+
|
|
118
|
+
const result = await listActiveProvisioners({workspaceId});
|
|
119
|
+
|
|
120
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
121
|
+
expect(result.provisioners).toHaveLength(1);
|
|
122
|
+
expect(request.url).toBe(
|
|
123
|
+
`https://api.example.test/workspaces/${workspaceId}/provisioners/active`,
|
|
124
|
+
);
|
|
125
|
+
expect(request.method).toBe('GET');
|
|
126
|
+
});
|
|
127
|
+
});
|