@package-broker/ui 2.0.0-rc.1 → 2.0.0-rc.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 (129) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/api-types.d.ts +50 -5
  3. package/dist/api-types.d.ts.map +1 -1
  4. package/dist/api.d.ts +1 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +6 -0
  7. package/dist/api.js.map +1 -1
  8. package/dist/app.d.ts +8 -14
  9. package/dist/app.d.ts.map +1 -1
  10. package/dist/app.js +22 -20
  11. package/dist/app.js.map +1 -1
  12. package/dist/assets/access.6859f43998ca.js +1 -0
  13. package/dist/assets/app.e8434f3343de.js +9 -0
  14. package/dist/assets/app.f15f8f21f88b.css +2 -0
  15. package/dist/assets/audit.576009342714.js +1 -0
  16. package/dist/assets/chunk.297a2d571f9c.js +4 -0
  17. package/dist/assets/chunk.3704953b4a66.js +1 -0
  18. package/dist/assets/chunk.6e23a75a1324.js +1 -0
  19. package/dist/assets/chunk.7cb6f6b4daaa.js +1 -0
  20. package/dist/assets/chunk.855a50f45830.js +1 -0
  21. package/dist/assets/chunk.862544cf5835.js +2 -0
  22. package/dist/assets/chunk.976a2ff4338e.js +1 -0
  23. package/dist/assets/chunk.bb25fc845269.js +1 -0
  24. package/dist/assets/chunk.bf48b04e6a00.js +1 -0
  25. package/dist/assets/chunk.ce96495b8b55.js +1 -0
  26. package/dist/assets/chunk.f922af39a279.js +1 -0
  27. package/dist/assets/import.e2a1e35fcb8d.js +1 -0
  28. package/dist/assets/index.html +6 -2
  29. package/dist/assets/manifest.json +80 -8
  30. package/dist/assets/packages.a8726e5144de.js +1 -0
  31. package/dist/assets/projects.711deb86b7c9.js +1 -0
  32. package/dist/assets/setup.dfed30ecfbad.js +1 -0
  33. package/dist/assets/setup.html +3 -3
  34. package/dist/assets/sources.d0ba0048bd96.js +1 -0
  35. package/dist/assets/tokens.f48d235e8d25.js +4 -0
  36. package/dist/auth.d.ts +4 -0
  37. package/dist/auth.d.ts.map +1 -1
  38. package/dist/auth.js +4 -0
  39. package/dist/auth.js.map +1 -1
  40. package/dist/components/product-footer-content.d.ts +21 -0
  41. package/dist/components/product-footer-content.d.ts.map +1 -0
  42. package/dist/components/product-footer-content.js +30 -0
  43. package/dist/components/product-footer-content.js.map +1 -0
  44. package/dist/components/product-footer.d.ts +3 -0
  45. package/dist/components/product-footer.d.ts.map +1 -0
  46. package/dist/components/product-footer.js +6 -0
  47. package/dist/components/product-footer.js.map +1 -0
  48. package/dist/manifest.d.ts +32 -0
  49. package/dist/manifest.d.ts.map +1 -1
  50. package/dist/manifest.js +41 -0
  51. package/dist/manifest.js.map +1 -1
  52. package/dist/page-loader.d.ts +5 -0
  53. package/dist/page-loader.d.ts.map +1 -0
  54. package/dist/page-loader.js +39 -0
  55. package/dist/page-loader.js.map +1 -0
  56. package/dist/pages/access/grant-form.d.ts +48 -0
  57. package/dist/pages/access/grant-form.d.ts.map +1 -0
  58. package/dist/pages/access/grant-form.js +153 -0
  59. package/dist/pages/access/grant-form.js.map +1 -0
  60. package/dist/pages/access/permission-descriptions.d.ts +2 -0
  61. package/dist/pages/access/permission-descriptions.d.ts.map +1 -0
  62. package/dist/pages/access/permission-descriptions.js +36 -0
  63. package/dist/pages/access/permission-descriptions.js.map +1 -0
  64. package/dist/pages/access/role-catalogue.d.ts +22 -0
  65. package/dist/pages/access/role-catalogue.d.ts.map +1 -0
  66. package/dist/pages/access/role-catalogue.js +40 -0
  67. package/dist/pages/access/role-catalogue.js.map +1 -0
  68. package/dist/pages/access.d.ts +6 -0
  69. package/dist/pages/access.d.ts.map +1 -1
  70. package/dist/pages/access.js +66 -31
  71. package/dist/pages/access.js.map +1 -1
  72. package/dist/pages/packages/source-actions.d.ts +48 -0
  73. package/dist/pages/packages/source-actions.d.ts.map +1 -0
  74. package/dist/pages/packages/source-actions.js +312 -0
  75. package/dist/pages/packages/source-actions.js.map +1 -0
  76. package/dist/pages/packages.d.ts.map +1 -1
  77. package/dist/pages/packages.js +3 -2
  78. package/dist/pages/packages.js.map +1 -1
  79. package/dist/pages/projects/project-form-model.d.ts +25 -0
  80. package/dist/pages/projects/project-form-model.d.ts.map +1 -0
  81. package/dist/pages/projects/project-form-model.js +69 -0
  82. package/dist/pages/projects/project-form-model.js.map +1 -0
  83. package/dist/pages/projects/project-form.d.ts +14 -0
  84. package/dist/pages/projects/project-form.d.ts.map +1 -0
  85. package/dist/pages/projects/project-form.js +91 -0
  86. package/dist/pages/projects/project-form.js.map +1 -0
  87. package/dist/pages/projects.d.ts +3 -2
  88. package/dist/pages/projects.d.ts.map +1 -1
  89. package/dist/pages/projects.js +28 -37
  90. package/dist/pages/projects.js.map +1 -1
  91. package/dist/pages/sources/source-form.d.ts +3 -1
  92. package/dist/pages/sources/source-form.d.ts.map +1 -1
  93. package/dist/pages/sources/source-form.js +18 -16
  94. package/dist/pages/sources/source-form.js.map +1 -1
  95. package/dist/pages/sources/sync-copy.d.ts +5 -0
  96. package/dist/pages/sources/sync-copy.d.ts.map +1 -0
  97. package/dist/pages/sources/sync-copy.js +11 -0
  98. package/dist/pages/sources/sync-copy.js.map +1 -0
  99. package/dist/pages/sources.d.ts +2 -0
  100. package/dist/pages/sources.d.ts.map +1 -1
  101. package/dist/pages/sources.js +54 -15
  102. package/dist/pages/sources.js.map +1 -1
  103. package/dist/pages/tokens/client-config.d.ts +3 -0
  104. package/dist/pages/tokens/client-config.d.ts.map +1 -1
  105. package/dist/pages/tokens/client-config.js +12 -2
  106. package/dist/pages/tokens/client-config.js.map +1 -1
  107. package/dist/pages/tokens/issue-token-form.d.ts +27 -0
  108. package/dist/pages/tokens/issue-token-form.d.ts.map +1 -0
  109. package/dist/pages/tokens/issue-token-form.js +270 -0
  110. package/dist/pages/tokens/issue-token-form.js.map +1 -0
  111. package/dist/pages/tokens/principal-form.d.ts +8 -0
  112. package/dist/pages/tokens/principal-form.d.ts.map +1 -0
  113. package/dist/pages/tokens/principal-form.js +151 -0
  114. package/dist/pages/tokens/principal-form.js.map +1 -0
  115. package/dist/pages/tokens.d.ts +47 -6
  116. package/dist/pages/tokens.d.ts.map +1 -1
  117. package/dist/pages/tokens.js +317 -86
  118. package/dist/pages/tokens.js.map +1 -1
  119. package/dist/setup.d.ts.map +1 -1
  120. package/dist/setup.js +4 -1
  121. package/dist/setup.js.map +1 -1
  122. package/dist/token-pending.d.ts +4 -0
  123. package/dist/token-pending.d.ts.map +1 -0
  124. package/dist/token-pending.js +12 -0
  125. package/dist/token-pending.js.map +1 -0
  126. package/package.json +3 -3
  127. package/dist/assets/app.0348e5888f99.js +0 -15
  128. package/dist/assets/app.e8cea8b32d3a.css +0 -2
  129. package/dist/assets/setup.98343039898f.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { pendingStorage } from '../token-pending.js';
2
3
  import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
3
4
  import { useServices } from '../auth.js';
4
5
  import { loadPending, savePending } from '../pending.js';
@@ -15,7 +16,10 @@ import { Badge } from '../components/ui/badge.js';
15
16
  import { LoadedListFilters } from '../components/ui/toolbar.js';
16
17
  import { ListState } from '../components/ui/list-state.js';
17
18
  import { ConfirmAction } from '../components/ui/confirm-action.js';
19
+ import { IssueTokenForm, accessLabels } from './tokens/issue-token-form.js';
20
+ import { PrincipalForm } from './tokens/principal-form.js';
18
21
  import { tokenClientConfig } from './tokens/client-config.js';
22
+ export const dayLabel = (days) => `${days} ${days === 1 ? 'day' : 'days'}`;
19
23
  export function lifetimeChoices(policy) {
20
24
  return [...new Set([30, 90, 180, 365, policy.maxDays, policy.defaultDays])]
21
25
  .filter((days) => days > 0 && days <= policy.maxDays)
@@ -27,6 +31,7 @@ export function tokensReducer(state, action) {
27
31
  ...state,
28
32
  secret: null,
29
33
  secretProject: null,
34
+ result: null,
30
35
  issuedId: null,
31
36
  oldPersonalId: null,
32
37
  pending: null,
@@ -36,16 +41,6 @@ export function tokensReducer(state, action) {
36
41
  };
37
42
  return state;
38
43
  }
39
- function pendingStorage(storage) {
40
- return {
41
- getItem: (key) => storage.getItem(`tokens:${key}`),
42
- setItem: (key, value) => storage.setItem(`tokens:${key}`, value),
43
- removeItem: (key) => storage.removeItem(`tokens:${key}`),
44
- };
45
- }
46
- export function clearPendingTokenIssue(storage) {
47
- loadPending(pendingStorage(storage), Date.now());
48
- }
49
44
  function record(value) {
50
45
  return value !== null && typeof value === 'object' && !Array.isArray(value);
51
46
  }
@@ -72,6 +67,13 @@ function restoredIssue(value) {
72
67
  target: value.target,
73
68
  project: value.project,
74
69
  key: value.key,
70
+ ...(value.refresh === true ? { refresh: true } : {}),
71
+ ...(['personal', 'project', 'organization'].includes(String(value.listTarget))
72
+ ? {
73
+ listTarget: value.listTarget,
74
+ listProject: typeof value.listProject === 'string' ? value.listProject : null,
75
+ }
76
+ : {}),
75
77
  ...(typeof value.rotate === 'string' ? { rotate: value.rotate } : {}),
76
78
  ...(typeof value.replace === 'string' ? { replace: value.replace } : {}),
77
79
  draft: {
@@ -105,17 +107,48 @@ function publicToken(t) {
105
107
  revokedAt: t.revokedAt,
106
108
  };
107
109
  }
110
+ export function inResultScope(result, project) {
111
+ if (!result)
112
+ return true;
113
+ if (result.target === 'project')
114
+ return project.slug === result.project;
115
+ if (result.target === 'personal')
116
+ return result.draft.projectMode !== 'selected' || result.draft.projectIds.includes(project.id);
117
+ return true;
118
+ }
119
+ export function hasSetupAccess(state, project) {
120
+ const result = state.result;
121
+ if (!result)
122
+ return false;
123
+ const permissions = result.target === 'personal'
124
+ ? project.permissions
125
+ : state.principals.find((p) => p.id === result.draft.servicePrincipalId)?.permissions;
126
+ return (permissions?.includes('project:packages:read') === true ||
127
+ (result.draft.scope !== 'read' &&
128
+ project.cap !== 'read' &&
129
+ permissions?.includes('project:packages:write') === true));
130
+ }
108
131
  export function createTokensController(services, deps) {
109
132
  let state = {
110
133
  target: 'personal',
111
134
  project: null,
135
+ issuanceProject: null,
112
136
  items: [],
113
137
  next: null,
114
138
  principals: [],
115
139
  principalsNext: null,
116
140
  principalsRestricted: false,
141
+ principalsLoading: true,
142
+ principalsError: false,
143
+ organizationPermissions: [],
144
+ issuanceDraft: null,
145
+ issuancePrincipal: null,
146
+ issuanceNonce: 0,
117
147
  names: {},
118
148
  projectSlugs: {},
149
+ projects: [],
150
+ projectsError: false,
151
+ result: null,
119
152
  lifetime: null,
120
153
  userId: null,
121
154
  secret: null,
@@ -132,6 +165,8 @@ export function createTokensController(services, deps) {
132
165
  let epoch = 0;
133
166
  let listing = 0;
134
167
  let navigation = 0;
168
+ const policyQueue = [];
169
+ let policyWorkers = 0;
135
170
  const listeners = new Set();
136
171
  const set = (next) => {
137
172
  state = { ...state, ...next };
@@ -157,6 +192,7 @@ export function createTokensController(services, deps) {
157
192
  busy: true,
158
193
  secret: null,
159
194
  secretProject: null,
195
+ result: null,
160
196
  issuedId: null,
161
197
  oldPersonalId: null,
162
198
  pending: null,
@@ -203,10 +239,15 @@ export function createTokensController(services, deps) {
203
239
  if (result.ok) {
204
240
  set({
205
241
  secret: result.data.token,
206
- secretProject: target === 'project' ? project : null,
242
+ secretProject: target === 'project'
243
+ ? project
244
+ : d.projectMode === 'selected' && d.projectIds.length === 1
245
+ ? (state.projectSlugs[d.projectIds[0]] ?? null)
246
+ : null,
247
+ result: action,
207
248
  oldPersonalId: action.replace ?? null,
208
249
  issuedId: result.data.id,
209
- notice: 'Save this token now; it is shown once.',
250
+ notice: 'Token created.',
210
251
  });
211
252
  }
212
253
  else if (result.code === 'reauth_required') {
@@ -234,6 +275,9 @@ export function createTokensController(services, deps) {
234
275
  }
235
276
  const controller = {
236
277
  snapshot: () => state,
278
+ announce(notice) {
279
+ set({ notice });
280
+ },
237
281
  subscribe(listener) {
238
282
  listeners.add(listener);
239
283
  return () => {
@@ -244,7 +288,12 @@ export function createTokensController(services, deps) {
244
288
  epoch++;
245
289
  navigation++;
246
290
  listing++;
247
- set(tokensReducer(state, { type: 'navigate' }));
291
+ policyQueue.length = 0;
292
+ set({
293
+ ...tokensReducer(state, { type: 'navigate' }),
294
+ principalsLoading: false,
295
+ projects: state.projects.map((p) => p.policyState === 'loading' ? { ...p, policyState: 'idle' } : p),
296
+ });
248
297
  },
249
298
  dispose() {
250
299
  controller.navigate();
@@ -254,24 +303,33 @@ export function createTokensController(services, deps) {
254
303
  if (state.busy)
255
304
  return;
256
305
  epoch++;
257
- set({ secret: null, issuedId: null, oldPersonalId: null, notice: null, renewal: null });
306
+ set({
307
+ secret: null,
308
+ secretProject: null,
309
+ result: null,
310
+ issuedId: null,
311
+ oldPersonalId: null,
312
+ notice: null,
313
+ renewal: null,
314
+ issuanceDraft: state.pending === null ? null : state.issuanceDraft,
315
+ });
258
316
  },
259
317
  restore() {
260
318
  const loaded = loadPending(storage, deps.now());
261
319
  const action = loaded.ok ? restoredIssue(loaded.action?.body) : null;
262
320
  if (action !== null)
263
- set({ target: action.target, project: action.project, pending: action, secret: null });
321
+ set({ pending: action, issuanceDraft: action.refresh ? action : null, secret: null });
264
322
  },
265
323
  async start(target, project) {
324
+ set({ issuanceProject: project });
266
325
  controller.restore();
267
326
  const action = state.pending;
268
- await controller.load(action?.target ?? target, action === null ? project : action.project);
327
+ await controller.load(action?.listTarget ?? action?.target ?? target, action ? (action.listTarget ? (action.listProject ?? null) : action.project) : project);
269
328
  },
270
329
  async selectTarget(target, project) {
271
330
  if (state.busy)
272
331
  return;
273
- epoch++;
274
- set(tokensReducer(state, { type: 'navigate' }));
332
+ set({ pending: null, renewal: null });
275
333
  await controller.load(target, project);
276
334
  },
277
335
  decline() {
@@ -282,11 +340,40 @@ export function createTokensController(services, deps) {
282
340
  if (action === null || state.busy || state.loading)
283
341
  return;
284
342
  if (state.lifetime === null) {
285
- await controller.load(action.target, action.project);
343
+ await controller.load();
286
344
  if (state.pending !== action || state.lifetime === null || state.loading)
287
345
  return;
288
346
  }
289
347
  await send(action);
348
+ if (action.refresh && state.secret !== null)
349
+ await controller.load();
350
+ },
351
+ async issueFor(target, project, draft) {
352
+ const action = {
353
+ target,
354
+ project,
355
+ draft,
356
+ key: deps.newKey(),
357
+ refresh: true,
358
+ listTarget: state.target,
359
+ listProject: state.project,
360
+ };
361
+ set({ issuanceDraft: action });
362
+ await send(action);
363
+ if (state.secret !== null)
364
+ await controller.load();
365
+ },
366
+ configureProject(slug) {
367
+ const result = state.result;
368
+ const project = state.projects.find((p) => p.slug === slug);
369
+ if (!project ||
370
+ !result ||
371
+ (result.target === 'project' && result.project !== slug) ||
372
+ (result.target === 'personal' &&
373
+ result.draft.projectMode === 'selected' &&
374
+ !result.draft.projectIds.includes(project.id)))
375
+ return;
376
+ set({ secretProject: slug });
290
377
  },
291
378
  async issue(draft) {
292
379
  await send({
@@ -316,6 +403,9 @@ export function createTokensController(services, deps) {
316
403
  }
317
404
  set({ renewal: token, secret: null, issuedId: null, oldPersonalId: null, notice: null });
318
405
  },
406
+ invalidProjects() {
407
+ set({ notice: 'Choose between 1 and 200 projects.' });
408
+ },
319
409
  registryOrigin: () => new URL(deps.origin).origin,
320
410
  clientConfig() {
321
411
  return state.secret === null || state.secretProject === null
@@ -338,7 +428,9 @@ export function createTokensController(services, deps) {
338
428
  catch {
339
429
  if (version === epoch)
340
430
  set({
341
- notice: kind === undefined ? 'Unable to copy token.' : 'Unable to copy configuration.',
431
+ notice: kind === undefined
432
+ ? 'Could not copy. Select and copy the token manually.'
433
+ : 'Could not copy. Select and copy the configuration manually.',
342
434
  });
343
435
  }
344
436
  },
@@ -349,7 +441,7 @@ export function createTokensController(services, deps) {
349
441
  project,
350
442
  loading: true,
351
443
  listError: false,
352
- notice: null,
444
+ notice: state.secret === null ? null : state.notice,
353
445
  items: [],
354
446
  next: null,
355
447
  lifetime: null,
@@ -375,36 +467,106 @@ export function createTokensController(services, deps) {
375
467
  },
376
468
  async loadPrincipals(after) {
377
469
  const version = navigation;
378
- const result = await services.client.request('get', '/_pb/service-principals', {
379
- query: { after },
470
+ set({ principalsLoading: true, principalsError: false });
471
+ const principals = [];
472
+ let cursor = after;
473
+ do {
474
+ const result = await services.client.request('get', '/_pb/service-principals', {
475
+ query: { after: cursor },
476
+ });
477
+ if (version !== navigation)
478
+ return;
479
+ if (!result.ok) {
480
+ set({
481
+ principalsLoading: false,
482
+ principalsError: result.status !== 403,
483
+ principalsRestricted: result.status === 403,
484
+ principals: [],
485
+ principalsNext: null,
486
+ });
487
+ return;
488
+ }
489
+ principals.push(...result.data.items.map((p) => ({
490
+ id: p.id,
491
+ name: p.name,
492
+ projectId: p.projectId,
493
+ status: p.status,
494
+ revision: p.revision,
495
+ createdByUserId: p.createdByUserId,
496
+ permissions: p.permissions,
497
+ })));
498
+ cursor = result.data.next ?? undefined;
499
+ } while (cursor !== undefined);
500
+ set({
501
+ principals,
502
+ principalsNext: null,
503
+ principalsRestricted: false,
504
+ principalsLoading: false,
380
505
  });
381
- if (version !== navigation)
382
- return;
383
- if (result.ok)
506
+ },
507
+ loadProjectPolicies(ids, retry = false) {
508
+ const marked = new Set();
509
+ for (const id of ids) {
510
+ const project = state.projects.find((p) => p.id === id);
511
+ if (!project ||
512
+ project.policyState === 'loading' ||
513
+ project.policyState === 'loaded' ||
514
+ (!retry && project.policyState === 'error'))
515
+ continue;
516
+ marked.add(id);
517
+ policyQueue.push(id);
518
+ }
519
+ if (marked.size)
384
520
  set({
385
- principals: result.data.items.map((p) => ({
386
- id: p.id,
387
- name: p.name,
388
- projectId: p.projectId,
389
- status: p.status,
390
- revision: p.revision,
391
- createdByUserId: p.createdByUserId,
392
- permissions: p.permissions,
393
- })),
394
- principalsNext: result.data.next,
395
- principalsRestricted: false,
521
+ projects: state.projects.map((p) => marked.has(p.id) ? { ...p, policyState: 'loading' } : p),
396
522
  });
397
- else if (result.status === 403)
398
- set({ principals: [], principalsNext: null, principalsRestricted: true });
523
+ async function work() {
524
+ policyWorkers++;
525
+ try {
526
+ while (policyQueue.length) {
527
+ const id = policyQueue.shift();
528
+ const project = state.projects.find((p) => p.id === id);
529
+ if (!project)
530
+ continue;
531
+ const version = navigation;
532
+ const detail = await services.client.request('get', '/_pb/projects/{project}', {
533
+ params: { project: project.slug },
534
+ });
535
+ const authority = await services.client.request('get', '/_pb/projects/{project}/permissions', { params: { project: project.slug } });
536
+ if (version !== navigation)
537
+ continue;
538
+ set({
539
+ projects: state.projects.map((p) => p.id === id
540
+ ? {
541
+ ...p,
542
+ cap: detail.ok ? detail.data.tokenMaxScope : null,
543
+ permissions: authority.ok ? authority.data.project : null,
544
+ policyState: detail.ok && authority.ok ? 'loaded' : 'error',
545
+ }
546
+ : p),
547
+ });
548
+ }
549
+ }
550
+ finally {
551
+ policyWorkers--;
552
+ }
553
+ }
554
+ while (policyQueue.length && policyWorkers < 4)
555
+ void work();
556
+ },
557
+ choosePrincipal(id, project, organization = project === null) {
558
+ set({
559
+ issuancePrincipal: id,
560
+ issuanceProject: organization ? '__organization' : (project ?? ''),
561
+ issuanceNonce: state.issuanceNonce + 1,
562
+ });
399
563
  },
400
564
  async identity() {
401
565
  const version = navigation;
402
- const result = await services.client.request('get', '/_pb/session');
403
- if (version !== navigation)
404
- return;
405
- if (result.ok)
406
- set({ userId: result.data.user.id });
566
+ const session = services.client.request('get', '/_pb/session');
407
567
  const projectSlugs = {};
568
+ const catalogue = [];
569
+ let projectsError = false;
408
570
  let projectAfter;
409
571
  do {
410
572
  const projects = await services.client.request('get', '/_pb/projects', {
@@ -412,16 +574,36 @@ export function createTokensController(services, deps) {
412
574
  });
413
575
  if (version !== navigation)
414
576
  return;
415
- if (!projects.ok)
577
+ if (!projects.ok) {
578
+ projectsError = true;
416
579
  break;
417
- for (const project of projects.data.items)
580
+ }
581
+ for (const project of projects.data.items) {
418
582
  projectSlugs[project.id] = project.slug;
583
+ if (project.status === 'archived')
584
+ continue;
585
+ catalogue.push({
586
+ id: project.id,
587
+ slug: project.slug,
588
+ name: project.name ?? project.slug,
589
+ cap: null,
590
+ permissions: null,
591
+ policyState: 'idle',
592
+ });
593
+ }
419
594
  projectAfter = projects.data.next ?? undefined;
595
+ set({ projectSlugs: { ...projectSlugs }, projects: [...catalogue], projectsError });
420
596
  } while (projectAfter !== undefined);
421
- set({ projectSlugs });
597
+ set({ projectSlugs, projects: catalogue, projectsError });
598
+ const result = await session;
599
+ if (version !== navigation)
600
+ return;
601
+ if (result.ok)
602
+ set({ userId: result.data.user.id });
422
603
  const permissions = await services.client.request('get', '/_pb/me/permissions');
423
604
  if (version !== navigation)
424
605
  return;
606
+ set({ organizationPermissions: permissions.ok ? permissions.data.organization : [] });
425
607
  if (!permissions.ok || !permissions.data.organization.includes('org:users:read')) {
426
608
  set({ names: {} });
427
609
  return;
@@ -489,7 +671,8 @@ export function createTokensController(services, deps) {
489
671
  failure(result);
490
672
  else {
491
673
  await controller.loadPrincipals();
492
- set({ notice: 'Service principal created.' });
674
+ set({ notice: 'Service principal created. Create a token to use it.' });
675
+ return result.data;
493
676
  }
494
677
  },
495
678
  async suspendPrincipal(principal) {
@@ -518,7 +701,9 @@ export function createTokensController(services, deps) {
518
701
  }
519
702
  export function filterTokens(items, filters) {
520
703
  return items.filter((item) => item.name.toLowerCase().includes(filters.q.toLowerCase()) &&
521
- (!filters.scope || item.scope === filters.scope) &&
704
+ (!filters.scope ||
705
+ item.scope === filters.scope ||
706
+ accessLabels[item.scope] === filters.scope) &&
522
707
  (!filters.status || item.status === filters.status));
523
708
  }
524
709
  export function TokenList({ state: s, controller: c }) {
@@ -532,7 +717,7 @@ export function TokenList({ state: s, controller: c }) {
532
717
  : 'Organization';
533
718
  return (_jsxs(_Fragment, { children: [_jsx(LoadedListFilters, { values: filters, fields: [
534
719
  { key: 'q', label: 'Name (this page)' },
535
- { key: 'scope', label: 'Scope', options: ['read', 'write', 'full'] },
720
+ { key: 'scope', label: 'Scope', options: Object.values(accessLabels) },
536
721
  { key: 'status', label: 'Status', options: ['active', 'expired', 'revoked'] },
537
722
  ], onChange: setFilters }), _jsxs("div", { className: "my-3 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("p", { className: "text-sm text-neutral-600", children: [items.length, " tokens on this page \u00B7", ' ', items.filter((token) => token.status === 'active').length, " active"] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.loading || s.busy, onClick: () => void c.load(), children: "Reload tokens" })] }), s.loading ? (_jsx(ListState, { kind: "loading", title: "Loading tokens\u2026" })) : s.listError ? (_jsx(ListState, { kind: "error", title: "Unable to load tokens." })) : items.length === 0 ? (_jsx(ListState, { kind: filtered ? 'filtered-empty' : 'empty', title: filtered ? 'No tokens match these filters on this page.' : 'No tokens on this page.' })) : (_jsx(DataTable, { label: "Tokens", items: items, columns: [
538
723
  {
@@ -551,7 +736,7 @@ export function TokenList({ state: s, controller: c }) {
551
736
  key: 'scope',
552
737
  label: 'Scope',
553
738
  value: (token) => token.scope,
554
- render: (token) => _jsx(Badge, { variant: "outline", children: token.scope }),
739
+ render: (token) => _jsx(Badge, { variant: "outline", children: accessLabels[token.scope] }),
555
740
  },
556
741
  {
557
742
  key: 'project',
@@ -599,8 +784,25 @@ export function TokenList({ state: s, controller: c }) {
599
784
  },
600
785
  ] })), s.next === null ? null : (_jsx(Button, { type: "button", disabled: s.busy || s.loading, onClick: () => void c.load(s.target, s.project, s.next), children: "Next tokens" })), _jsx("p", { className: "mt-3 text-xs text-neutral-600", children: "Secrets are never listed. Only tokens in your accessible scope appear here." })] }));
601
786
  }
787
+ function focusResult(node) {
788
+ node?.focus();
789
+ }
790
+ function SetupProjectAccess({ state: s, controller: c }) {
791
+ const [setupCandidate, setSetupCandidate] = useState('');
792
+ const candidate = s.projects.find((p) => p.id === setupCandidate);
793
+ useEffect(() => {
794
+ if (s.secret && candidate?.policyState === 'loaded' && hasSetupAccess(s, candidate))
795
+ c.configureProject(candidate.slug);
796
+ }, [c, s.secret, candidate]);
797
+ return (_jsxs(_Fragment, { children: [' ', s.projects.some((p) => p.permissions === null) && (_jsxs("details", { children: [_jsx("summary", { className: "min-h-11 cursor-pointer", children: "Check another project's package access" }), _jsx(Label, { htmlFor: "setup-candidate", children: "Project to configure" }), _jsxs(Select, { id: "setup-candidate", className: "min-h-11", value: setupCandidate, onChange: (event) => {
798
+ setSetupCandidate(event.target.value);
799
+ c.loadProjectPolicies([event.target.value]);
800
+ }, children: [_jsx("option", { value: "", children: "Choose a project" }), s.projects
801
+ .filter((p) => inResultScope(s.result, p))
802
+ .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), candidate?.policyState === 'loading' && _jsx("p", { children: "Loading project access\u2026" }), candidate?.policyState === 'error' && (_jsx(Button, { type: "button", onClick: () => c.loadProjectPolicies([candidate.id], true), children: "Try again loading project access" })), candidate?.policyState === 'loaded' && !hasSetupAccess(s, candidate) && (_jsx("p", { children: "This token does not have package access to this project." }))] }))] }));
803
+ }
602
804
  export function TokensView({ state: s, controller: c }) {
603
- const personal = s.renewal?.kind === 'pat' || s.target === 'personal';
805
+ const personal = s.renewal !== null ? s.renewal.kind === 'pat' : s.target === 'personal';
604
806
  const policy = s.lifetime?.[personal ? 'personal' : 'service'];
605
807
  const renewal = s.renewal;
606
808
  const clientConfig = c.clientConfig();
@@ -609,26 +811,20 @@ export function TokensView({ state: s, controller: c }) {
609
811
  : s.target === 'project'
610
812
  ? Object.keys(s.projectSlugs).find((id) => s.projectSlugs[id] === s.project)
611
813
  : null;
612
- const principalChoices = s.principalsRestricted
613
- ? [
614
- ...new Map(s.items
615
- .filter((t) => t.kind === 'svc' && t.status === 'active' && t.servicePrincipalId !== null)
616
- .map((t) => [
617
- t.servicePrincipalId,
618
- {
619
- id: t.servicePrincipalId,
620
- name: t.servicePrincipalId,
621
- projectId: t.projectId,
622
- },
623
- ])).values(),
624
- ]
625
- : s.principals.filter((p) => p.status === 'active');
814
+ const principalChoices = s.principals.filter((p) => p.status === 'active');
626
815
  function issue(event) {
627
816
  event.preventDefault();
628
817
  if (s.busy)
629
818
  return;
630
819
  const form = event.currentTarget;
631
820
  const data = new FormData(form);
821
+ const selectedIds = data.getAll('projectIds').map(String);
822
+ if (personal &&
823
+ data.get('projectMode') === 'selected' &&
824
+ (selectedIds.length < 1 || selectedIds.length > 200)) {
825
+ c.invalidProjects();
826
+ return;
827
+ }
632
828
  const scope = renewal?.kind === 'svc' ? renewal.scope : String(data.get('scope'));
633
829
  void c.issue({
634
830
  name: String(data.get('name')),
@@ -636,28 +832,49 @@ export function TokensView({ state: s, controller: c }) {
636
832
  lifetimeDays: Number(data.get('lifetimeDays')),
637
833
  servicePrincipalId: String(data.get('principal') ?? ''),
638
834
  projectMode: data.get('projectMode') === 'selected' ? 'selected' : 'all',
639
- projectIds: String(data.get('projectIds') ?? '')
640
- .split(/\s+/)
641
- .filter(Boolean),
835
+ projectIds: data.getAll('projectIds').map(String),
642
836
  overlapDays: Number(data.get('overlap') ?? 0),
643
837
  });
644
- form.reset();
645
838
  }
646
- return (_jsxs("section", { "aria-labelledby": "page-heading", className: "space-y-4", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Tokens" }), _jsx("p", { className: "text-neutral-600", children: "Credentials for Composer and npm clients. Management writes use your signed-in session. Secrets are shown once." }), _jsx(Label, { htmlFor: "token-list", children: "Token list" }), _jsxs(Select, { id: "token-list", value: s.target, disabled: s.busy, onChange: (e) => {
647
- void c.selectTarget(e.target.value, s.project);
648
- }, children: [_jsx("option", { value: "personal", children: "My personal tokens" }), s.project === null ? null : _jsx("option", { value: "project", children: "Project tokens" }), _jsx("option", { value: "organization", children: "Organization tokens" })] }), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-medium", children: "Token lifetimes and access" }), _jsx("p", { className: "mt-3", children: "Your personal tokens are revoked when you leave the organization or your account is suspended. Access stops at once; the tokens are marked revoked shortly after." }), _jsx("p", { children: "Service tokens keep working when the person who created them leaves. Project owners can see, renew and revoke every service token of their project." }), _jsxs("dl", { className: "detail-list mt-3", children: [_jsx("dt", { children: "Personal maximum lifetime" }), _jsx("dd", { children: "1 year (the operator can set less)" }), _jsx("dt", { children: "Service maximum lifetime" }), _jsx("dd", { children: "1 year for every scope (the operator can change it)" })] })] }), s.notice === null || s.listError ? null : (_jsx(Alert, { children: _jsx(AlertDescription, { children: s.notice }) })), s.pending === null ? null : (_jsxs("section", { "aria-label": "Confirm token issue", className: "space-y-2", children: [_jsx("h2", { children: "Confirm token issue" }), _jsx("p", { children: "Your identity was checked. Issue this token now?" }), _jsx(Button, { type: "button", disabled: s.busy || s.loading || s.lifetime === null, onClick: () => void c.confirm(), children: "Confirm token issue" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.decline(), children: "Cancel" })] })), s.secret === null ? null : (_jsxs("section", { "aria-label": "Token secret", className: "detail-group space-y-3 border-emerald-200", children: [_jsx("h2", { children: "Token secret \u2014 shown once" }), _jsx("p", { children: "Copy it now. This value will not be shown again. Closing this panel keeps the token active." }), _jsx("pre", { className: "rounded-md border bg-neutral-50 p-3 overflow-x-auto whitespace-pre-wrap break-all", children: _jsx("code", { children: s.secret }) }), _jsx(Button, { type: "button", onClick: () => void c.copy(), children: "Copy token" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.closeSecret(), children: "Close secret panel" }), clientConfig === null ? (_jsxs("p", { children: ["Choose a project this token can access to configure Composer or npm. Open that project\u2019s registry at ", c.registryOrigin(), "/<project> for Composer or", ' ', c.registryOrigin(), "/<project>/npm/ for npm."] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: "Composer" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.composer }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('composer'), children: "Copy Composer configuration" }), _jsx("h3", { children: "npm (.npmrc)" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.npm }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('npm'), children: "Copy npm configuration" })] })), s.oldPersonalId !== null ? (_jsx(ConfirmAction, { action: "Revoke old personal token", target: s.oldPersonalId, scope: "My personal tokens", description: "Clients using the old token will lose access immediately. This cannot be undone. Update clients to the new token before revoking.", disabled: s.busy, onConfirm: () => void c.revoke(s.oldPersonalId, true) })) : null] })), s.pending !== null || s.secret !== null ? null : (_jsxs("details", { open: renewal !== null, className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: renewal === null ? 'Issue token' : 'Renew token' }), _jsxs("form", { onSubmit: issue, className: "grid gap-2", children: [_jsx("h2", { children: renewal === null ? 'Issue token' : 'Renew token' }), _jsx(Label, { htmlFor: "token-name", children: "Name" }), _jsx(Input, { id: "token-name", name: "name", defaultValue: renewal?.name ?? '', required: true }), _jsx(Label, { htmlFor: "token-scope", children: "Scope" }), _jsxs(Select, { id: "token-scope", name: "scope", defaultValue: renewal?.scope ?? 'read', disabled: renewal?.kind === 'svc', children: [_jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "write", children: "Write" }), _jsx("option", { value: "full", children: "Full" })] }), _jsx(Label, { htmlFor: "token-lifetime", children: "Lifetime" }), _jsx(Select, { id: "token-lifetime", name: "lifetimeDays", defaultValue: policy?.defaultDays, required: true, children: policy === undefined
839
+ return (_jsxs("section", { "aria-labelledby": "page-heading", className: "space-y-4", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Tokens" }), _jsx("p", { className: "text-neutral-600", children: "Connect your tools and CI pipelines to your packages." }), s.notice === null || s.listError ? null : (_jsx(Alert, { role: "status", "aria-live": "polite", children: _jsx(AlertDescription, { children: s.notice }) })), s.pending === null ? null : (_jsxs("section", { "aria-label": "Confirm token issue", className: "space-y-2", children: [_jsx("h2", { children: "Confirm token issue" }), _jsx("p", { children: "Your identity was checked. Issue this token now?" }), _jsx(Button, { type: "button", disabled: s.busy || s.loading || s.lifetime === null, onClick: () => void c.confirm(), children: "Confirm token issue" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.decline(), children: "Cancel" })] })), s.secret === null ? null : (_jsxs("section", { "aria-label": "Token secret", className: "detail-group space-y-3 border-emerald-200", children: [_jsx("h2", { tabIndex: -1, ref: focusResult, children: "Token created" }), s.result && (_jsxs("p", { className: "break-words", children: [s.result.draft.name, " \u00B7", ' ', s.result.target === 'project'
840
+ ? (s.projects.find((p) => p.slug === s.result?.project)?.name ??
841
+ 'Project name unavailable')
842
+ : s.result.target === 'organization'
843
+ ? 'Organization-wide'
844
+ : s.result.draft.projectMode === 'all'
845
+ ? 'All accessible projects'
846
+ : s.result.draft.projectIds.length === 1
847
+ ? (s.projects.find((p) => p.id === s.result?.draft.projectIds[0])?.name ??
848
+ '1 selected project')
849
+ : `${s.result.draft.projectIds.length} selected projects`, ' ', "\u00B7 ", accessLabels[s.result.draft.scope], " \u00B7 Expires after", ' ', dayLabel(s.result.draft.lifetimeDays)] })), _jsx("p", { children: "Copy this token now. You cannot view it again. Closing this panel keeps it active." }), _jsx("pre", { className: "rounded-md border bg-neutral-50 p-3 overflow-x-auto whitespace-pre-wrap break-all", children: _jsx("code", { role: "region", "aria-label": "Created token", children: s.secret }) }), _jsx(Button, { className: "min-h-11", type: "button", onClick: () => void c.copy(), children: "Copy token" }), _jsx(Button, { type: "button", variant: "outline", className: "min-h-11", onClick: () => {
850
+ c.closeSecret();
851
+ requestAnimationFrame(() => document.getElementById('new-token')?.focus());
852
+ }, children: "Done" }), _jsx(SetupProjectAccess, { state: s, controller: c }), _jsx(Label, { htmlFor: "configure-project", children: "Configure for project" }), _jsxs(Select, { className: "min-h-11", id: "configure-project", value: s.secretProject ?? '', onChange: (event) => c.configureProject(event.target.value), children: [_jsx("option", { value: "", children: "Choose a project" }), s.projects
853
+ .filter((p) => hasSetupAccess(s, p))
854
+ .filter((p) => inResultScope(s.result, p))
855
+ .map((p) => (_jsx("option", { value: p.slug, children: p.name }, p.id)))] }), clientConfig === null ? (_jsx("p", { children: "Choose a project this token can access to configure Composer or npm." })) : (_jsxs(_Fragment, { children: [_jsx("p", { children: "Composer sends this credential to this registry host. The token still limits which projects it can access." }), [
856
+ ['Composer', ['composer', 'composerAuth']],
857
+ ['npm', ['npm']],
858
+ ['CI environment', ['ciComposer', 'ciNpm']],
859
+ ].map(([title, kinds], index) => (_jsxs("details", { open: index === 0, className: "min-w-0", children: [_jsx("summary", { className: "min-h-11 cursor-pointer", children: title }), kinds.map((kind) => (_jsxs("div", { children: [_jsx("p", { children: {
860
+ composer: 'Composer repository command',
861
+ composerAuth: 'Merge this into your local auth.json. Do not commit credentials.',
862
+ npm: 'Use this on your computer. For CI, use the environment-variable version below.',
863
+ ciComposer: 'Save PACKAGE_BROKER_TOKEN as a secret in your CI settings. These examples install packages; they do not publish them.',
864
+ ciNpm: 'Save this as .npmrc, then run npm ci.',
865
+ }[kind] }), _jsx("pre", { className: "whitespace-pre-wrap break-all rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig[kind] }) }), _jsx(Button, { type: "button", variant: "outline", className: "min-h-11", onClick: () => void c.copy(kind), children: {
866
+ composer: 'Copy Composer configuration',
867
+ composerAuth: 'Copy auth.json',
868
+ npm: 'Copy npm configuration',
869
+ ciComposer: 'Copy CI Composer',
870
+ ciNpm: 'Copy CI npm',
871
+ }[kind] })] }, kind)))] }, title)))] })), s.oldPersonalId !== null ? (_jsx(ConfirmAction, { action: "Revoke old personal token", target: s.oldPersonalId, scope: "My personal tokens", description: "Clients using the old token will lose access immediately. This cannot be undone. Update clients to the new token before revoking.", disabled: s.busy, onConfirm: () => void c.revoke(s.oldPersonalId, true) })) : null] })), _jsx("div", { hidden: s.pending !== null || s.secret !== null || renewal !== null, children: _jsx(IssueTokenForm, { state: s, controller: c }) }), s.pending !== null || s.secret !== null || renewal === null ? null : (_jsxs("details", { open: renewal !== null, className: "detail-group", children: [_jsx("summary", { className: "min-h-11 cursor-pointer font-semibold", children: renewal === null ? 'Issue token' : 'Renew token' }), _jsxs("form", { onSubmit: issue, className: "grid gap-2", children: [_jsx(Label, { htmlFor: "token-name", children: "Name" }), _jsx(Input, { id: "token-name", name: "name", defaultValue: renewal?.name ?? '', required: true }), _jsx(Label, { htmlFor: "token-scope", children: "Scope" }), _jsxs(Select, { id: "token-scope", name: "scope", defaultValue: renewal?.scope ?? 'read', disabled: renewal?.kind === 'svc', children: [_jsx("option", { value: "read", children: "Install and download" }), _jsx("option", { value: "write", children: "Include publish permission" }), _jsx("option", { value: "full", children: "Include grants, sources and audit access" })] }), _jsx(Label, { htmlFor: "token-lifetime", children: "Lifetime" }), _jsx(Select, { id: "token-lifetime", name: "lifetimeDays", defaultValue: policy?.defaultDays, required: true, children: policy === undefined
649
872
  ? null
650
- : lifetimeChoices(policy).map((days) => (_jsxs("option", { value: days, children: [days, " days"] }, days))) }), personal ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-project-mode", children: "Projects" }), _jsxs(Select, { id: "token-project-mode", name: "projectMode", defaultValue: renewal?.projectMode ?? 'all', children: [_jsx("option", { value: "all", children: "All accessible projects" }), _jsx("option", { value: "selected", children: "Selected projects" })] }), _jsx(Label, { htmlFor: "token-project-ids", children: "Selected project IDs (space separated; re-enter when renewing)" }), _jsx(Input, { id: "token-project-ids", name: "projectIds", required: renewal?.projectMode === 'selected' })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-principal", children: "Service principal" }), _jsxs(Select, { id: "token-principal", name: "principal", defaultValue: renewal?.servicePrincipalId ?? '', disabled: renewal?.kind === 'svc', required: renewal?.kind !== 'svc', children: [_jsx("option", { value: "", disabled: true, children: "Select a principal" }), principalChoices
873
+ : lifetimeChoices(policy).map((days) => (_jsx("option", { value: days, children: dayLabel(days) }, days))) }), personal ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-project-mode", children: "Projects" }), _jsxs(Select, { id: "token-project-mode", name: "projectMode", defaultValue: renewal?.projectMode ?? 'all', children: [_jsx("option", { value: "all", children: "All accessible projects" }), _jsx("option", { value: "selected", children: "Selected projects" })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: "Choose projects again when renewing a selected token" }), s.projects.map((p) => (_jsxs("label", { className: "flex min-h-11 items-center gap-2", children: [_jsx("input", { type: "checkbox", name: "projectIds", value: p.id }), p.name] }, p.id)))] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-principal", children: "Service principal" }), _jsxs(Select, { id: "token-principal", name: "principal", defaultValue: renewal?.servicePrincipalId ?? '', disabled: renewal?.kind === 'svc', required: renewal?.kind !== 'svc', children: [_jsx("option", { value: "", disabled: true, children: "Select a principal" }), principalChoices
651
874
  .filter((p) => p.projectId === projectId)
652
- .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), s.principalsRestricted ? (_jsx("p", { children: "Principal discovery is limited to principals referenced by the tokens you can see." })) : null] })), renewal?.kind !== 'svc' ? null : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-overlap", children: "Overlap" }), _jsxs(Select, { id: "token-overlap", name: "overlap", defaultValue: "0", children: [_jsx("option", { value: "0", children: "None" }), _jsx("option", { value: "1", children: "1 day" }), _jsx("option", { value: "7", children: "7 days" })] }), _jsx("p", { children: "The old token stops at the end of the overlap or when the new token expires, whichever is first." })] })), _jsx(Button, { type: "submit", disabled: s.busy || s.loading || policy === undefined, children: renewal === null ? 'Issue token' : 'Renew token' }), renewal === null ? null : (_jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => c.closeSecret(), children: "Cancel renewal" }))] }, `${s.target}:${renewal?.id ?? ''}:${policy?.defaultDays}`)] }, `${s.target}:${renewal?.id ?? ''}`)), _jsx(TokenList, { state: s, controller: c }, `${s.target}:${s.project}`), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: "Service principals" }), _jsx("h2", { children: "Service principals" }), _jsxs("form", { className: "grid gap-2", onSubmit: (event) => {
653
- event.preventDefault();
654
- if (s.busy)
655
- return;
656
- const form = event.currentTarget;
657
- const data = new FormData(form);
658
- void c.createPrincipal(String(data.get('name')), String(data.get('project')), String(data.get('permissions')).split(/\s+/).filter(Boolean));
659
- form.reset();
660
- }, children: [_jsx(Label, { htmlFor: "principal-name", children: "Principal name" }), _jsx(Input, { id: "principal-name", name: "name", required: true }), _jsx(Label, { htmlFor: "principal-project", children: "Project ID (empty for organization)" }), _jsx(Input, { id: "principal-project", name: "project" }), _jsx(Label, { htmlFor: "principal-permissions", children: "Permissions (space separated)" }), _jsx(Input, { id: "principal-permissions", name: "permissions" }), _jsx(Button, { type: "submit", disabled: s.busy, children: "Create service principal" })] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => void c.loadPrincipals(), children: "Reload service principals" }), s.principalsRestricted ? (_jsx(ListState, { kind: "placeholder", title: "Service principal discovery is restricted." })) : s.principals.length === 0 ? (_jsx(ListState, { kind: "empty", title: "No service principals loaded." })) : (_jsx(DataTable, { label: "Service principals", items: s.principals, columns: [
875
+ .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), s.principalsRestricted ? (_jsx("p", { children: "Ask an organization administrator to create a service identity for this project." })) : null] })), renewal?.kind !== 'svc' ? null : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-overlap", children: "Overlap" }), _jsxs(Select, { id: "token-overlap", name: "overlap", defaultValue: "0", children: [_jsx("option", { value: "0", children: "None" }), _jsx("option", { value: "1", children: "1 day" }), _jsx("option", { value: "7", children: "7 days" })] }), _jsx("p", { children: "The old token stops at the end of the overlap or when the new token expires, whichever is first." })] })), _jsx(Button, { type: "submit", disabled: s.busy || s.loading || policy === undefined, children: renewal === null ? 'Issue token' : 'Renew token' }), renewal === null ? null : (_jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => c.closeSecret(), children: "Cancel renewal" }))] }, `${s.target}:${renewal?.id ?? ''}:${policy?.defaultDays}`)] }, `${s.target}:${renewal?.id ?? ''}`)), _jsx(Label, { htmlFor: "token-list", children: "Token list" }), _jsxs(Select, { id: "token-list", value: s.target, disabled: s.busy, onChange: (e) => {
876
+ void c.selectTarget(e.target.value, s.project);
877
+ }, children: [_jsx("option", { value: "personal", children: "My personal tokens" }), s.project === null ? null : _jsx("option", { value: "project", children: "Project tokens" }), _jsx("option", { value: "organization", children: "Organization tokens" })] }), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { children: "How token access works" }), _jsx("p", { children: "Personal tokens use your access and stop working if you leave or your account is suspended. Service tokens use a service identity and keep working when their creator leaves. Each project's access policy can limit a token further." })] }), _jsx(TokenList, { state: s, controller: c }, `${s.target}:${s.project}`), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "min-h-11 cursor-pointer font-semibold", children: "Service principals" }), _jsx(PrincipalForm, { state: s, controller: c }), _jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => void c.loadPrincipals(), children: "Reload service principals" }), s.principalsRestricted ? (_jsx(ListState, { kind: "placeholder", title: "You cannot browse service principals here." })) : s.principalsLoading ? (_jsx(ListState, { kind: "loading", title: "Loading service principals\u2026" })) : s.principals.length === 0 ? (_jsx(ListState, { kind: "empty", title: "No service principals loaded." })) : (_jsx(DataTable, { label: "Service principals", items: s.principals, columns: [
661
878
  {
662
879
  key: 'name',
663
880
  label: 'Name',
@@ -687,13 +904,18 @@ export function bindTokensLifecycle(controller, events, onNavigate) {
687
904
  onNavigate();
688
905
  };
689
906
  const clear = () => controller.navigate();
907
+ const show = (event) => {
908
+ clear();
909
+ if (event.persisted === true)
910
+ onNavigate();
911
+ };
690
912
  events.addEventListener('hashchange', change);
691
913
  events.addEventListener('pagehide', clear);
692
- events.addEventListener('pageshow', clear);
914
+ events.addEventListener('pageshow', show);
693
915
  return () => {
694
916
  events.removeEventListener('hashchange', change);
695
917
  events.removeEventListener('pagehide', clear);
696
- events.removeEventListener('pageshow', clear);
918
+ events.removeEventListener('pageshow', show);
697
919
  controller.dispose();
698
920
  };
699
921
  }
@@ -733,10 +955,19 @@ export function TokensPage() {
733
955
  if (window.location.hash.startsWith('#/tokens'))
734
956
  void controller.start(project === null ? 'personal' : 'project', project);
735
957
  }
958
+ function reload() {
959
+ if (!window.location.hash.startsWith('#/tokens'))
960
+ return;
961
+ void controller.identity();
962
+ void controller.loadPrincipals();
963
+ }
736
964
  change();
737
965
  void controller.identity();
738
966
  void controller.loadPrincipals();
739
- return bindTokensLifecycle(controller, window, change);
967
+ return bindTokensLifecycle(controller, window, () => {
968
+ change();
969
+ reload();
970
+ });
740
971
  }, [controller]);
741
972
  useEffect(() => bindTokensReplay(controller, services), [controller, services]);
742
973
  return _jsx(TokensView, { state: state, controller: controller });