@package-broker/ui 0.19.8 → 2.0.0-rc.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.
Files changed (207) hide show
  1. package/LICENSE +661 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/api-types.d.ts +9987 -0
  4. package/dist/api-types.d.ts.map +1 -0
  5. package/dist/api-types.js +6 -0
  6. package/dist/api-types.js.map +1 -0
  7. package/dist/api.d.ts +88 -0
  8. package/dist/api.d.ts.map +1 -0
  9. package/dist/api.js +179 -0
  10. package/dist/api.js.map +1 -0
  11. package/dist/app.d.ts +76 -0
  12. package/dist/app.d.ts.map +1 -0
  13. package/dist/app.js +239 -0
  14. package/dist/app.js.map +1 -0
  15. package/dist/assets/app.0348e5888f99.js +15 -0
  16. package/dist/assets/app.e8cea8b32d3a.css +2 -0
  17. package/dist/assets/index.html +13 -0
  18. package/dist/assets/manifest.json +26 -0
  19. package/dist/assets/setup.98343039898f.js +1 -0
  20. package/dist/assets/setup.html +14 -0
  21. package/dist/auth.d.ts +170 -0
  22. package/dist/auth.d.ts.map +1 -0
  23. package/dist/auth.js +551 -0
  24. package/dist/auth.js.map +1 -0
  25. package/dist/components/ui/alert.d.ts +10 -0
  26. package/dist/components/ui/alert.d.ts.map +1 -0
  27. package/dist/components/ui/alert.js +22 -0
  28. package/dist/components/ui/alert.js.map +1 -0
  29. package/dist/components/ui/badge.d.ts +8 -0
  30. package/dist/components/ui/badge.d.ts.map +1 -0
  31. package/dist/components/ui/badge.js +18 -0
  32. package/dist/components/ui/badge.js.map +1 -0
  33. package/dist/components/ui/button.d.ts +13 -0
  34. package/dist/components/ui/button.d.ts.map +1 -0
  35. package/dist/components/ui/button.js +27 -0
  36. package/dist/components/ui/button.js.map +1 -0
  37. package/dist/components/ui/card.d.ts +10 -0
  38. package/dist/components/ui/card.d.ts.map +1 -0
  39. package/dist/components/ui/card.js +21 -0
  40. package/dist/components/ui/card.js.map +1 -0
  41. package/dist/components/ui/confirm-action.d.ts +12 -0
  42. package/dist/components/ui/confirm-action.d.ts.map +1 -0
  43. package/dist/components/ui/confirm-action.js +19 -0
  44. package/dist/components/ui/confirm-action.js.map +1 -0
  45. package/dist/components/ui/dialog.d.ts +17 -0
  46. package/dist/components/ui/dialog.d.ts.map +1 -0
  47. package/dist/components/ui/dialog.js +180 -0
  48. package/dist/components/ui/dialog.js.map +1 -0
  49. package/dist/components/ui/dropdown-menu.d.ts +8 -0
  50. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  51. package/dist/components/ui/dropdown-menu.js +71 -0
  52. package/dist/components/ui/dropdown-menu.js.map +1 -0
  53. package/dist/components/ui/id-text.d.ts +11 -0
  54. package/dist/components/ui/id-text.d.ts.map +1 -0
  55. package/dist/components/ui/id-text.js +23 -0
  56. package/dist/components/ui/id-text.js.map +1 -0
  57. package/dist/components/ui/input.d.ts +5 -0
  58. package/dist/components/ui/input.d.ts.map +1 -0
  59. package/dist/components/ui/input.js +6 -0
  60. package/dist/components/ui/input.js.map +1 -0
  61. package/dist/components/ui/label.d.ts +4 -0
  62. package/dist/components/ui/label.d.ts.map +1 -0
  63. package/dist/components/ui/label.js +7 -0
  64. package/dist/components/ui/label.js.map +1 -0
  65. package/dist/components/ui/list-state.d.ts +7 -0
  66. package/dist/components/ui/list-state.d.ts.map +1 -0
  67. package/dist/components/ui/list-state.js +5 -0
  68. package/dist/components/ui/list-state.js.map +1 -0
  69. package/dist/components/ui/overlay-host.d.ts +6 -0
  70. package/dist/components/ui/overlay-host.d.ts.map +1 -0
  71. package/dist/components/ui/overlay-host.js +36 -0
  72. package/dist/components/ui/overlay-host.js.map +1 -0
  73. package/dist/components/ui/row-actions.d.ts +17 -0
  74. package/dist/components/ui/row-actions.d.ts.map +1 -0
  75. package/dist/components/ui/row-actions.js +21 -0
  76. package/dist/components/ui/row-actions.js.map +1 -0
  77. package/dist/components/ui/select.d.ts +3 -0
  78. package/dist/components/ui/select.d.ts.map +1 -0
  79. package/dist/components/ui/select.js +6 -0
  80. package/dist/components/ui/select.js.map +1 -0
  81. package/dist/components/ui/status-badge.d.ts +28 -0
  82. package/dist/components/ui/status-badge.d.ts.map +1 -0
  83. package/dist/components/ui/status-badge.js +50 -0
  84. package/dist/components/ui/status-badge.js.map +1 -0
  85. package/dist/components/ui/table.d.ts +27 -0
  86. package/dist/components/ui/table.d.ts.map +1 -0
  87. package/dist/components/ui/table.js +81 -0
  88. package/dist/components/ui/table.js.map +1 -0
  89. package/dist/components/ui/tabs.d.ts +7 -0
  90. package/dist/components/ui/tabs.d.ts.map +1 -0
  91. package/dist/components/ui/tabs.js +14 -0
  92. package/dist/components/ui/tabs.js.map +1 -0
  93. package/dist/components/ui/textarea.d.ts +3 -0
  94. package/dist/components/ui/textarea.d.ts.map +1 -0
  95. package/dist/components/ui/textarea.js +14 -0
  96. package/dist/components/ui/textarea.js.map +1 -0
  97. package/dist/components/ui/time.d.ts +5 -0
  98. package/dist/components/ui/time.d.ts.map +1 -0
  99. package/dist/components/ui/time.js +14 -0
  100. package/dist/components/ui/time.js.map +1 -0
  101. package/dist/components/ui/toolbar.d.ts +20 -0
  102. package/dist/components/ui/toolbar.d.ts.map +1 -0
  103. package/dist/components/ui/toolbar.js +42 -0
  104. package/dist/components/ui/toolbar.js.map +1 -0
  105. package/dist/components/vulnerability-status.d.ts +8 -0
  106. package/dist/components/vulnerability-status.d.ts.map +1 -0
  107. package/dist/components/vulnerability-status.js +49 -0
  108. package/dist/components/vulnerability-status.js.map +1 -0
  109. package/dist/index.d.ts +2 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +2 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/lib/use-merged-refs.d.ts +3 -0
  114. package/dist/lib/use-merged-refs.d.ts.map +1 -0
  115. package/dist/lib/use-merged-refs.js +22 -0
  116. package/dist/lib/use-merged-refs.js.map +1 -0
  117. package/dist/lib/utils.d.ts +4 -0
  118. package/dist/lib/utils.d.ts.map +1 -0
  119. package/dist/lib/utils.js +11 -0
  120. package/dist/lib/utils.js.map +1 -0
  121. package/dist/main.d.ts +2 -0
  122. package/dist/main.d.ts.map +1 -0
  123. package/dist/main.js +9 -0
  124. package/dist/main.js.map +1 -0
  125. package/dist/manifest.d.ts +8 -0
  126. package/dist/manifest.d.ts.map +1 -0
  127. package/dist/manifest.js +21 -0
  128. package/dist/manifest.js.map +1 -0
  129. package/dist/page-hash.d.ts +23 -0
  130. package/dist/page-hash.d.ts.map +1 -0
  131. package/dist/page-hash.js +45 -0
  132. package/dist/page-hash.js.map +1 -0
  133. package/dist/pages/access.d.ts +69 -0
  134. package/dist/pages/access.d.ts.map +1 -0
  135. package/dist/pages/access.js +391 -0
  136. package/dist/pages/access.js.map +1 -0
  137. package/dist/pages/audit.d.ts +63 -0
  138. package/dist/pages/audit.d.ts.map +1 -0
  139. package/dist/pages/audit.js +182 -0
  140. package/dist/pages/audit.js.map +1 -0
  141. package/dist/pages/import.d.ts +41 -0
  142. package/dist/pages/import.d.ts.map +1 -0
  143. package/dist/pages/import.js +274 -0
  144. package/dist/pages/import.js.map +1 -0
  145. package/dist/pages/management.d.ts +47 -0
  146. package/dist/pages/management.d.ts.map +1 -0
  147. package/dist/pages/management.js +141 -0
  148. package/dist/pages/management.js.map +1 -0
  149. package/dist/pages/packages.d.ts +74 -0
  150. package/dist/pages/packages.d.ts.map +1 -0
  151. package/dist/pages/packages.js +505 -0
  152. package/dist/pages/packages.js.map +1 -0
  153. package/dist/pages/projects.d.ts +58 -0
  154. package/dist/pages/projects.d.ts.map +1 -0
  155. package/dist/pages/projects.js +357 -0
  156. package/dist/pages/projects.js.map +1 -0
  157. package/dist/pages/sources/add-package-dialog.d.ts +10 -0
  158. package/dist/pages/sources/add-package-dialog.d.ts.map +1 -0
  159. package/dist/pages/sources/add-package-dialog.js +19 -0
  160. package/dist/pages/sources/add-package-dialog.js.map +1 -0
  161. package/dist/pages/sources/proxy-message.d.ts +2 -0
  162. package/dist/pages/sources/proxy-message.d.ts.map +1 -0
  163. package/dist/pages/sources/proxy-message.js +2 -0
  164. package/dist/pages/sources/proxy-message.js.map +1 -0
  165. package/dist/pages/sources/source-form.d.ts +29 -0
  166. package/dist/pages/sources/source-form.d.ts.map +1 -0
  167. package/dist/pages/sources/source-form.js +463 -0
  168. package/dist/pages/sources/source-form.js.map +1 -0
  169. package/dist/pages/sources/sync-poll.d.ts +20 -0
  170. package/dist/pages/sources/sync-poll.d.ts.map +1 -0
  171. package/dist/pages/sources/sync-poll.js +71 -0
  172. package/dist/pages/sources/sync-poll.js.map +1 -0
  173. package/dist/pages/sources.d.ts +325 -0
  174. package/dist/pages/sources.d.ts.map +1 -0
  175. package/dist/pages/sources.js +1132 -0
  176. package/dist/pages/sources.js.map +1 -0
  177. package/dist/pages/tokens/client-config.d.ts +5 -0
  178. package/dist/pages/tokens/client-config.d.ts.map +1 -0
  179. package/dist/pages/tokens/client-config.js +9 -0
  180. package/dist/pages/tokens/client-config.js.map +1 -0
  181. package/dist/pages/tokens.d.ts +127 -0
  182. package/dist/pages/tokens.d.ts.map +1 -0
  183. package/dist/pages/tokens.js +744 -0
  184. package/dist/pages/tokens.js.map +1 -0
  185. package/dist/pending.d.ts +33 -0
  186. package/dist/pending.d.ts.map +1 -0
  187. package/dist/pending.js +156 -0
  188. package/dist/pending.js.map +1 -0
  189. package/dist/qr.d.ts +14 -0
  190. package/dist/qr.d.ts.map +1 -0
  191. package/dist/qr.js +322 -0
  192. package/dist/qr.js.map +1 -0
  193. package/dist/setup.d.ts +49 -0
  194. package/dist/setup.d.ts.map +1 -0
  195. package/dist/setup.js +390 -0
  196. package/dist/setup.js.map +1 -0
  197. package/dist/test-fixtures/api-types.d.ts +228 -0
  198. package/dist/test-fixtures/api-types.d.ts.map +1 -0
  199. package/dist/test-fixtures/api-types.js +6 -0
  200. package/dist/test-fixtures/api-types.js.map +1 -0
  201. package/package.json +37 -40
  202. package/public/index.html +11 -0
  203. package/README.md +0 -11
  204. package/dist/assets/main-3NLwl1eV.js +0 -111
  205. package/dist/assets/main-CsTEYyDl.css +0 -1
  206. package/dist/config.js +0 -3
  207. package/dist/index.html +0 -22
@@ -0,0 +1,1132 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { PROXY_DEMAND_MESSAGE } from './sources/proxy-message.js';
3
+ import { pollSync, syncPollResult } from './sources/sync-poll.js';
4
+ import { AddPackageDialog } from './sources/add-package-dialog.js';
5
+ import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
6
+ import { responseHeader } from '../api.js';
7
+ import { useServices } from '../auth.js';
8
+ import { StatusBadge } from '../components/ui/status-badge.js';
9
+ import { IdText } from '../components/ui/id-text.js';
10
+ import { Time } from '../components/ui/time.js';
11
+ import { Badge } from '../components/ui/badge.js';
12
+ import { LoadedListFilters } from '../components/ui/toolbar.js';
13
+ import { ListState } from '../components/ui/list-state.js';
14
+ import { ConfirmAction } from '../components/ui/confirm-action.js';
15
+ import { RowActions } from '../components/ui/row-actions.js';
16
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '../components/ui/dialog.js';
17
+ import { Button } from '../components/ui/button.js';
18
+ import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card.js';
19
+ import { Input } from '../components/ui/input.js';
20
+ import { Label } from '../components/ui/label.js';
21
+ import { Select } from '../components/ui/select.js';
22
+ import { DataTable } from '../components/ui/table.js';
23
+ import { buildListFilterHash } from '../page-hash.js';
24
+ import { SourceForm, SOURCE_TYPE_LABELS, sourceFieldErrors, scopeNamePatterns, } from './sources/source-form.js';
25
+ const CREDENTIAL_FIELDS = {
26
+ http_basic: ['username', 'password'],
27
+ bearer: ['token'],
28
+ header: ['name', 'value'],
29
+ query: ['param', 'value'],
30
+ github_app: ['appId', 'privateKeyPem', 'webhookSecret'],
31
+ npm_token: ['token'],
32
+ ssh_key: ['privateKeyPem', 'knownHosts'],
33
+ };
34
+ const CREDENTIAL_KINDS = Object.keys(CREDENTIAL_FIELDS);
35
+ export function sourceCredentialKinds(type, { scope, github, vcsProviders }) {
36
+ switch (type) {
37
+ case 'packagist_proxy':
38
+ return ['bearer'];
39
+ case 'npmjs_proxy':
40
+ case 'hosted':
41
+ return [];
42
+ case 'composer_repository':
43
+ return ['http_basic', 'bearer', 'header', 'query', 'npm_token'];
44
+ case 'npm_registry':
45
+ return ['npm_token', 'http_basic', 'bearer', 'header'];
46
+ case 'vcs':
47
+ if (github)
48
+ return scope === 'organization' ? ['bearer', 'github_app'] : ['bearer'];
49
+ // The server accepts ssh_key only on the Node runtime, which is the only one reporting the ssh provider.
50
+ return [
51
+ 'bearer',
52
+ 'http_basic',
53
+ 'header',
54
+ 'npm_token',
55
+ ...(vcsProviders?.includes('ssh') ? ['ssh_key'] : []),
56
+ ];
57
+ }
58
+ }
59
+ function credentialRules(source, vcsProviders) {
60
+ return {
61
+ scope: source.projectId === null ? 'organization' : 'project',
62
+ github: source.githubAuthentication !== undefined,
63
+ vcsProviders,
64
+ };
65
+ }
66
+ function acceptsPackage(source) {
67
+ return (['packagist_proxy', 'npmjs_proxy', 'npm_registry'].includes(source.type) &&
68
+ 'attached' in source &&
69
+ source.attached === true);
70
+ }
71
+ export function sourceCredentialKind(body) {
72
+ if (body.type === 'composer_repository')
73
+ return 'http_basic';
74
+ if (body.type === 'npm_registry')
75
+ return 'npm_token';
76
+ if (body.type === 'vcs') {
77
+ const config = body.config;
78
+ if (config?.provider === 'ssh')
79
+ return 'ssh_key';
80
+ if (config?.provider === 'github' && config.installationId !== undefined)
81
+ return 'github_app';
82
+ }
83
+ return 'bearer';
84
+ }
85
+ function isProxy(source) {
86
+ return ['packagist_proxy', 'npmjs_proxy'].includes(source.type);
87
+ }
88
+ const ENABLE_ADMIN_MESSAGE = 'An organization admin must enable this source.';
89
+ const ENABLE_CONFLICT_MESSAGE = 'Register package names, then enable the source.';
90
+ export function suggestedScopePatterns(source, registered) {
91
+ const config = source?.config;
92
+ if (source?.type !== 'npm_registry' || !Array.isArray(config?.scopes))
93
+ return [];
94
+ return scopeNamePatterns(config.scopes.join('\n')).filter((pattern) => !registered.includes(pattern));
95
+ }
96
+ export function displayUrl(value) {
97
+ if (value === null)
98
+ return null;
99
+ try {
100
+ const url = new URL(value);
101
+ if (!['https:', 'http:', 'ssh:', 'git:'].includes(url.protocol))
102
+ return null;
103
+ url.username = '';
104
+ url.password = '';
105
+ url.search = '';
106
+ url.hash = '';
107
+ return url.toString();
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ export function createSourcesModel(services, project, now = () => new Date().toISOString()) {
114
+ const { client } = services;
115
+ let state = {
116
+ ownership: 'organization',
117
+ editor: null,
118
+ fieldErrors: {},
119
+ formError: null,
120
+ available: [],
121
+ availableNext: null,
122
+ availableLoading: false,
123
+ availableError: false,
124
+ items: [],
125
+ next: null,
126
+ loading: true,
127
+ listError: false,
128
+ settingsVersion: 0,
129
+ vcsProviders: undefined,
130
+ busy: false,
131
+ error: null,
132
+ notice: null,
133
+ createdSource: null,
134
+ credentialSource: null,
135
+ credentialVersion: 0,
136
+ credentialKind: 'bearer',
137
+ credentialKinds: CREDENTIAL_KINDS,
138
+ packageSource: null,
139
+ packageFieldError: null,
140
+ packageAlert: null,
141
+ savedCredential: null,
142
+ names: null,
143
+ preview: null,
144
+ order: [],
145
+ etag: null,
146
+ canEnableProjectSource: false,
147
+ enableDenied: [],
148
+ enableConflicts: [],
149
+ };
150
+ const listeners = new Set();
151
+ const set = (patch) => {
152
+ state = { ...state, ...patch };
153
+ for (const listener of listeners)
154
+ listener();
155
+ };
156
+ let permissionsRequest;
157
+ let stopSyncPolling = () => { };
158
+ let syncGeneration = 0;
159
+ let listVersion = 0;
160
+ let availableVersion = 0;
161
+ let namesVersion = 0;
162
+ let previewVersion = 0;
163
+ function failure(result, credential = false) {
164
+ if (result.ok)
165
+ return false;
166
+ if (result.code === 'reauth_required') {
167
+ services.requireReauth(credential ? { method: result.request.method, path: result.request.path } : result.request, { credential, secretFields: [] });
168
+ set({
169
+ enableDenied: [],
170
+ error: credential
171
+ ? 'Reauthenticate, then enter the credential again.'
172
+ : 'Reauthentication is required.',
173
+ });
174
+ }
175
+ else {
176
+ set({ error: 'The request could not be saved or loaded. Refresh and try again.' });
177
+ }
178
+ return true;
179
+ }
180
+ async function load(more = false) {
181
+ if (more && (state.loading || state.next === null))
182
+ return;
183
+ const version = ++listVersion;
184
+ const after = more ? state.next : null;
185
+ const previousEtag = state.etag;
186
+ ++previewVersion;
187
+ set({
188
+ loading: true,
189
+ listError: false,
190
+ error: null,
191
+ etag: null,
192
+ preview: null,
193
+ order: [],
194
+ enableDenied: [],
195
+ enableConflicts: [],
196
+ notice: null,
197
+ });
198
+ const query = { limit: '100', ...(after === null ? {} : { after }) };
199
+ const [permissions, result] = await Promise.all([
200
+ project !== null || state.ownership !== 'organization'
201
+ ? (permissionsRequest ??= client.request('get', '/_pb/me/permissions').then((result) => {
202
+ if (!result.ok)
203
+ permissionsRequest = undefined;
204
+ return result;
205
+ }))
206
+ : null,
207
+ project === null
208
+ ? client.request('get', '/_pb/sources', {
209
+ query: {
210
+ ...query,
211
+ ...(state.ownership === 'organization' ? {} : { ownership: state.ownership }),
212
+ },
213
+ })
214
+ : client.request('get', '/_pb/projects/{project}/sources', {
215
+ params: { project },
216
+ query,
217
+ }),
218
+ ]);
219
+ if (version !== listVersion)
220
+ return;
221
+ const listNeedsReauth = !result.ok && result.code === 'reauth_required';
222
+ if (permissions &&
223
+ !permissions.ok &&
224
+ !listNeedsReauth &&
225
+ (permissions.status !== 403 || permissions.code === 'reauth_required'))
226
+ failure(permissions);
227
+ if (failure(result) || !result.ok) {
228
+ set({ loading: false, listError: true });
229
+ return;
230
+ }
231
+ set({
232
+ canEnableProjectSource: permissions?.ok === true && permissions.data.organization.includes('org:sources:write'),
233
+ });
234
+ const etag = responseHeader(result, 'etag') ?? null;
235
+ if (project !== null && more && etag !== previousEtag) {
236
+ await load();
237
+ return;
238
+ }
239
+ const page = result.data.items.map((item) => ({ ...item, url: displayUrl(item.url) }));
240
+ const items = more ? [...state.items, ...page] : page;
241
+ const attached = items.flatMap((item) => 'attached' in item &&
242
+ item.attached === true &&
243
+ 'precedence' in item &&
244
+ typeof item.precedence === 'number'
245
+ ? [{ id: item.id, precedence: item.precedence }]
246
+ : []);
247
+ set({
248
+ items,
249
+ settingsVersion: state.settingsVersion + 1,
250
+ next: result.data.next,
251
+ vcsProviders: result.data.vcsProviders,
252
+ loading: false,
253
+ etag,
254
+ order: attached.sort((a, b) => a.precedence - b.precedence).map(({ id }) => id),
255
+ });
256
+ }
257
+ async function loadAvailable(more = false) {
258
+ if (project === null || (more && (state.availableLoading || state.availableNext === null)))
259
+ return;
260
+ const version = ++availableVersion;
261
+ const after = more ? state.availableNext : null;
262
+ set({ availableLoading: true, availableError: false });
263
+ const result = await client.request('get', '/_pb/sources', {
264
+ query: { limit: '100', ...(after === null ? {} : { after }) },
265
+ });
266
+ if (version !== availableVersion)
267
+ return;
268
+ if (!result.ok) {
269
+ set({ available: [], availableNext: null, availableLoading: false, availableError: true });
270
+ return;
271
+ }
272
+ const page = result.data.items.map((item) => ({
273
+ ...item,
274
+ attached: false,
275
+ url: displayUrl(item.url),
276
+ }));
277
+ set({
278
+ available: more ? [...state.available, ...page] : page,
279
+ availableNext: result.data.next,
280
+ availableLoading: false,
281
+ });
282
+ }
283
+ async function findLoadedSource(source) {
284
+ while (!state.items.some((item) => item.id === source) &&
285
+ state.next !== null &&
286
+ !state.listError) {
287
+ await load(true);
288
+ }
289
+ return state.items.find((item) => item.id === source);
290
+ }
291
+ async function revealCreated(source, body) {
292
+ const created = await findLoadedSource(source);
293
+ if (created)
294
+ set({ createdSource: source, notice: `Next steps for ${created.name}` });
295
+ if (created && !isProxy(created) && created.type !== 'hosted') {
296
+ const kinds = sourceCredentialKinds(created.type, credentialRules(created, state.vcsProviders));
297
+ const preferred = body
298
+ ? sourceCredentialKind(body)
299
+ : created.githubAuthentication === 'app'
300
+ ? 'github_app'
301
+ : kinds[0];
302
+ set({
303
+ credentialSource: source,
304
+ credentialVersion: state.credentialVersion + 1,
305
+ credentialKinds: kinds,
306
+ credentialKind: preferred !== undefined && kinds.includes(preferred) ? preferred : kinds[0],
307
+ });
308
+ }
309
+ }
310
+ async function names(source, more = false) {
311
+ const previous = state.names;
312
+ if (!more)
313
+ set({ credentialSource: null, error: null });
314
+ if (more && (previous?.source !== source || previous.next === null))
315
+ return;
316
+ const version = ++namesVersion;
317
+ const after = more ? previous?.next : null;
318
+ const result = await client.request('get', '/_pb/sources/{source}/names', {
319
+ params: { source },
320
+ query: { limit: '100', ...(after == null ? {} : { after }) },
321
+ });
322
+ if (version !== namesVersion || failure(result) || !result.ok)
323
+ return;
324
+ set({
325
+ names: {
326
+ ...result.data,
327
+ source,
328
+ items: more ? [...previous.items, ...result.data.items] : result.data.items,
329
+ },
330
+ });
331
+ }
332
+ async function mutate(request, after, handled = () => false) {
333
+ if (state.busy)
334
+ return;
335
+ set({ busy: true, error: null, notice: null });
336
+ try {
337
+ const result = await request();
338
+ if (!handled(result) && !failure(result))
339
+ await after();
340
+ }
341
+ finally {
342
+ set({ busy: false });
343
+ }
344
+ }
345
+ return {
346
+ project,
347
+ dispose() {
348
+ ++syncGeneration;
349
+ stopSyncPolling();
350
+ },
351
+ snapshot: () => state,
352
+ enableNeedsAdmin(source) {
353
+ return (source.projectId !== null &&
354
+ (!state.canEnableProjectSource || state.enableDenied.includes(source.id)));
355
+ },
356
+ subscribe(listener) {
357
+ listeners.add(listener);
358
+ return () => {
359
+ listeners.delete(listener);
360
+ };
361
+ },
362
+ load,
363
+ async setOwnership(ownership) {
364
+ if (project !== null || state.busy)
365
+ return;
366
+ set({ ownership, items: [], next: null });
367
+ await load();
368
+ },
369
+ loadAvailable,
370
+ names,
371
+ openEditor(source = null) {
372
+ if (state.busy)
373
+ return;
374
+ if (source && project !== null && source.projectId === null)
375
+ return;
376
+ set({
377
+ editor: source ?? 'create',
378
+ fieldErrors: {},
379
+ formError: null,
380
+ error: null,
381
+ credentialSource: null,
382
+ savedCredential: null,
383
+ });
384
+ },
385
+ closeEditor() {
386
+ if (state.busy)
387
+ return;
388
+ set({ editor: null, fieldErrors: {}, formError: null });
389
+ },
390
+ async saveSource(body) {
391
+ if (state.busy || state.editor === null)
392
+ return;
393
+ const editor = state.editor;
394
+ if (editor !== 'create' && Object.keys(body).length === 0) {
395
+ set({ editor: null, fieldErrors: {}, formError: null });
396
+ return;
397
+ }
398
+ set({ busy: true, error: null, formError: null, fieldErrors: {} });
399
+ try {
400
+ const result = editor === 'create'
401
+ ? project === null
402
+ ? await client.request('post', '/_pb/sources', { body: body })
403
+ : await client.request('post', '/_pb/projects/{project}/sources', {
404
+ params: { project },
405
+ body: body,
406
+ })
407
+ : project === null
408
+ ? await client.request('patch', '/_pb/sources/{source}', {
409
+ params: { source: editor.id },
410
+ body,
411
+ })
412
+ : await client.request('patch', '/_pb/projects/{project}/sources/{source}', {
413
+ params: { project, source: editor.id },
414
+ body,
415
+ });
416
+ if (!result.ok) {
417
+ failure(result);
418
+ set({ fieldErrors: sourceFieldErrors(result.details), formError: state.error });
419
+ return;
420
+ }
421
+ set({
422
+ editor: null,
423
+ fieldErrors: {},
424
+ formError: null,
425
+ ...(editor === 'create' && project === null && state.ownership === 'project'
426
+ ? { ownership: 'organization' }
427
+ : {}),
428
+ });
429
+ await load();
430
+ if (editor === 'create' && result.data && 'id' in result.data) {
431
+ await revealCreated(result.data.id, body);
432
+ }
433
+ }
434
+ finally {
435
+ set({ busy: false });
436
+ }
437
+ },
438
+ attachment(source, attached) {
439
+ if (project === null)
440
+ return Promise.resolve();
441
+ return mutate(() => attached
442
+ ? client.request('put', '/_pb/projects/{project}/sources/{source}/attachment', {
443
+ params: { project, source },
444
+ body: {},
445
+ })
446
+ : client.request('delete', '/_pb/projects/{project}/sources/{source}/attachment', {
447
+ params: { project, source },
448
+ }), () => load());
449
+ },
450
+ async replayed(result) {
451
+ const prefix = project === null ? '/_pb/sources' : `/_pb/projects/${encodeURIComponent(project)}/sources`;
452
+ const namesChange = /^\/_pb\/sources\/[^/]+\/names(?:\/[^/]+)?$/.test(result.path);
453
+ const enableChange = /^\/_pb\/sources\/([^/]+)\/enable$/.exec(result.path);
454
+ if (result.path !== prefix &&
455
+ !result.path.startsWith(`${prefix}/`) &&
456
+ !namesChange &&
457
+ !enableChange)
458
+ return;
459
+ const selectedNames = state.names?.source;
460
+ set({ enableDenied: [], enableConflicts: [] });
461
+ if (result.ok)
462
+ set({
463
+ editor: null,
464
+ fieldErrors: {},
465
+ formError: null,
466
+ ...(result.createdSourceId && project === null && state.ownership === 'project'
467
+ ? { ownership: 'organization' }
468
+ : {}),
469
+ });
470
+ await load();
471
+ if (enableChange && !result.ok) {
472
+ if (result.status === 403 &&
473
+ result.code === 'forbidden' &&
474
+ state.items.some((source) => source.id === enableChange[1] && source.projectId !== null)) {
475
+ set({ enableDenied: [enableChange[1]], notice: ENABLE_ADMIN_MESSAGE });
476
+ }
477
+ else if (result.status === 409 && result.code === 'conflict') {
478
+ set({ enableConflicts: [enableChange[1]], notice: ENABLE_CONFLICT_MESSAGE });
479
+ }
480
+ else {
481
+ set({ error: 'The request could not be saved or loaded. Refresh and try again.' });
482
+ }
483
+ }
484
+ if (result.ok && result.createdSourceId)
485
+ await revealCreated(result.createdSourceId);
486
+ if (selectedNames !== undefined)
487
+ await names(selectedNames);
488
+ },
489
+ moveSource(source, direction) {
490
+ if (state.loading || state.busy || state.next !== null)
491
+ return;
492
+ const order = [...state.order];
493
+ const index = order.indexOf(source);
494
+ const target = index + direction;
495
+ if (index < 0 || target < 0 || target >= order.length)
496
+ return;
497
+ [order[index], order[target]] = [order[target], order[index]];
498
+ set({ order });
499
+ },
500
+ async saveOrder() {
501
+ if (project === null ||
502
+ state.loading ||
503
+ state.busy ||
504
+ state.next !== null ||
505
+ state.etag === null)
506
+ return;
507
+ const etag = state.etag;
508
+ const sourceIds = [...state.order];
509
+ set({ busy: true, error: null, preview: null });
510
+ ++previewVersion;
511
+ try {
512
+ const result = await client.request('put', '/_pb/projects/{project}/sources/order', {
513
+ params: { project },
514
+ headers: { 'If-Match': etag },
515
+ body: { sourceIds },
516
+ });
517
+ if (!result.ok && result.status === 412) {
518
+ await load();
519
+ set({ error: 'Source ordering changed. Review the reloaded list before saving again.' });
520
+ }
521
+ else if (!failure(result)) {
522
+ await load();
523
+ }
524
+ }
525
+ finally {
526
+ set({ busy: false });
527
+ }
528
+ },
529
+ closeCredential() {
530
+ if (state.busy)
531
+ return;
532
+ set({ credentialSource: null });
533
+ },
534
+ closeNames() {
535
+ if (state.busy)
536
+ return;
537
+ ++namesVersion;
538
+ set({ names: null });
539
+ },
540
+ chooseCredential(source) {
541
+ if (state.busy)
542
+ return;
543
+ const selected = state.items.find((item) => item.id === source);
544
+ const kinds = selected
545
+ ? sourceCredentialKinds(selected.type, credentialRules(selected, state.vcsProviders))
546
+ : ['bearer'];
547
+ if (kinds.length === 0)
548
+ return;
549
+ ++namesVersion;
550
+ set({
551
+ credentialSource: source,
552
+ names: null,
553
+ savedCredential: null,
554
+ credentialVersion: state.credentialVersion + 1,
555
+ credentialKind: selected?.githubAuthentication === 'app' && kinds.includes('github_app')
556
+ ? 'github_app'
557
+ : kinds[0],
558
+ credentialKinds: kinds,
559
+ error: null,
560
+ });
561
+ },
562
+ openPackage(source) {
563
+ if (state.busy || project === null || !acceptsPackage(source) || source.status === 'paused')
564
+ return;
565
+ set({ packageSource: source, packageFieldError: null, packageAlert: null, notice: null });
566
+ },
567
+ closePackage() {
568
+ if (!state.busy)
569
+ set({ packageSource: null, packageFieldError: null, packageAlert: null });
570
+ },
571
+ async addPackage(name) {
572
+ const source = state.packageSource;
573
+ if (state.busy || project === null || !source || !acceptsPackage(source))
574
+ return;
575
+ name = name.trim();
576
+ if (!name || name.length > 428) {
577
+ set({ packageFieldError: 'Enter a package name of 1 to 428 characters.' });
578
+ return;
579
+ }
580
+ set({ busy: true, packageFieldError: null, packageAlert: null, notice: null });
581
+ try {
582
+ const result = await client.request('post', '/_pb/projects/{project}/sources/{source}/packages', {
583
+ params: { project, source: source.id },
584
+ body: { name },
585
+ });
586
+ if (result.ok) {
587
+ set({
588
+ packageSource: null,
589
+ notice: result.status === 202
590
+ ? `Queued: ${result.data.name} will appear in Packages shortly`
591
+ : 'Already available',
592
+ });
593
+ }
594
+ else if (result.status === 400 && result.code === 'invalid_request') {
595
+ set({ packageFieldError: 'Enter a valid package name for this source.' });
596
+ }
597
+ else if (result.status === 400) {
598
+ set({
599
+ packageFieldError: 'This source does not support that package. Check the name or use another source.',
600
+ });
601
+ }
602
+ else if (result.code === 'forbidden') {
603
+ set({ packageAlert: 'You do not have permission to add packages to this project.' });
604
+ }
605
+ else if (result.status === 502) {
606
+ set({
607
+ packageAlert: 'The upstream registry refused or failed the request. Check the source credential or try again later.',
608
+ });
609
+ }
610
+ else if (result.status === 404) {
611
+ set({
612
+ packageAlert: 'The package or attached source could not be found. Check the name and refresh sources.',
613
+ });
614
+ }
615
+ else if (result.status === 503) {
616
+ set({ packageAlert: 'Package requests are temporarily unavailable. Try again shortly.' });
617
+ }
618
+ else if (result.status === 409) {
619
+ set({ packageAlert: 'This source is paused. Enable it before adding a package.' });
620
+ }
621
+ else {
622
+ failure(result);
623
+ set({ packageAlert: state.error, error: null });
624
+ }
625
+ }
626
+ finally {
627
+ set({ busy: false });
628
+ }
629
+ },
630
+ async credential(source, kind, material) {
631
+ if (state.busy)
632
+ return;
633
+ set({
634
+ busy: true,
635
+ error: null,
636
+ credentialSource: source,
637
+ credentialVersion: state.credentialVersion + 1,
638
+ savedCredential: null,
639
+ });
640
+ try {
641
+ const result = project === null
642
+ ? await client.request('put', '/_pb/sources/{source}/credential', {
643
+ params: { source },
644
+ body: { kind, material },
645
+ })
646
+ : await client.request('put', '/_pb/projects/{project}/sources/{source}/credential', {
647
+ params: { project, source },
648
+ body: { kind, material },
649
+ });
650
+ if (!failure(result, true)) {
651
+ set({
652
+ savedCredential: { source, at: now() },
653
+ notice: 'Credential set.',
654
+ items: state.items.map((item) => item.id === source ? { ...item, hasCredential: true } : item),
655
+ });
656
+ }
657
+ }
658
+ finally {
659
+ set({ busy: false });
660
+ }
661
+ },
662
+ edit(source, pollIntervalSeconds, removalPolicy) {
663
+ if (!Number.isSafeInteger(pollIntervalSeconds) || pollIntervalSeconds <= 0) {
664
+ set({ error: 'Polling interval must be a positive whole number.' });
665
+ return Promise.resolve();
666
+ }
667
+ const body = { pollIntervalSeconds, removalPolicy };
668
+ return mutate(() => project === null
669
+ ? client.request('patch', '/_pb/sources/{source}', { params: { source }, body })
670
+ : client.request('patch', '/_pb/projects/{project}/sources/{source}', {
671
+ params: { project, source },
672
+ body,
673
+ }), () => load());
674
+ },
675
+ syncNow(source) {
676
+ if (state.busy ||
677
+ source.status === 'paused' ||
678
+ (project !== null && source.projectId === null))
679
+ return Promise.resolve();
680
+ stopSyncPolling();
681
+ const generation = ++syncGeneration;
682
+ return mutate(() => project === null
683
+ ? client.request('post', '/_pb/sources/{source}/sync', {
684
+ params: { source: source.id },
685
+ })
686
+ : client.request('post', '/_pb/projects/{project}/sources/{source}/sync', {
687
+ params: { project, source: source.id },
688
+ }), async () => {
689
+ if (generation !== syncGeneration)
690
+ return;
691
+ const refresh = async () => {
692
+ const version = listVersion;
693
+ const ownership = state.ownership;
694
+ let after = null;
695
+ do {
696
+ const query = {
697
+ limit: '100',
698
+ ...(after === null ? {} : { after }),
699
+ };
700
+ const result = project === null
701
+ ? await client.request('get', '/_pb/sources', {
702
+ query: { ...query, ...(ownership === 'organization' ? {} : { ownership }) },
703
+ })
704
+ : await client.request('get', '/_pb/projects/{project}/sources', {
705
+ params: { project },
706
+ query,
707
+ });
708
+ if (generation !== syncGeneration || version !== listVersion || !result.ok)
709
+ return true;
710
+ const current = result.data.items.find((item) => item.id === source.id);
711
+ if (current) {
712
+ set({
713
+ items: state.items.map((item) => item.id === source.id ? { ...current, url: displayUrl(current.url) } : item),
714
+ });
715
+ return syncPollResult(source, current);
716
+ }
717
+ after = result.data.next;
718
+ } while (after !== null);
719
+ return true;
720
+ };
721
+ let complete;
722
+ if (state.loading && state.items.length === 0) {
723
+ await load();
724
+ const current = await findLoadedSource(source.id);
725
+ complete = state.listError || !current || syncPollResult(source, current);
726
+ }
727
+ else {
728
+ complete = await refresh();
729
+ }
730
+ if (generation !== syncGeneration)
731
+ return;
732
+ set({ notice: 'Sync requested.' });
733
+ if (complete !== true && generation === syncGeneration)
734
+ stopSyncPolling = pollSync(refresh);
735
+ });
736
+ },
737
+ async togglePause(source) {
738
+ if (project !== null && source.projectId === null)
739
+ return;
740
+ if (source.status === 'paused' && !isProxy(source)) {
741
+ if (state.busy ||
742
+ (source.projectId !== null &&
743
+ (!state.canEnableProjectSource || state.enableDenied.includes(source.id))))
744
+ return;
745
+ set({ enableConflicts: state.enableConflicts.filter((id) => id !== source.id) });
746
+ await mutate(() => client.request('post', '/_pb/sources/{source}/enable', {
747
+ params: { source: source.id },
748
+ }), () => load(), (result) => {
749
+ if (source.projectId !== null &&
750
+ !result.ok &&
751
+ result.status === 403 &&
752
+ result.code === 'forbidden') {
753
+ set({
754
+ enableDenied: [...new Set([...state.enableDenied, source.id])],
755
+ notice: ENABLE_ADMIN_MESSAGE,
756
+ });
757
+ return true;
758
+ }
759
+ if (!result.ok && result.status === 409 && result.code === 'conflict') {
760
+ set({
761
+ enableConflicts: [...new Set([...state.enableConflicts, source.id])],
762
+ notice: ENABLE_CONFLICT_MESSAGE,
763
+ });
764
+ return true;
765
+ }
766
+ return false;
767
+ });
768
+ return;
769
+ }
770
+ const action = source.status === 'paused' ? 'resume' : 'pause';
771
+ return mutate(() => project === null
772
+ ? client.request('post', `/_pb/sources/{source}/${action}`, {
773
+ params: { source: source.id },
774
+ })
775
+ : client.request('post', `/_pb/projects/{project}/sources/{source}/${action}`, {
776
+ params: { project, source: source.id },
777
+ }), () => load());
778
+ },
779
+ addName(source, pattern) {
780
+ if (pattern.length === 0 || pattern.length > 50 || !/^[a-z0-9._~@!/*-]+$/.test(pattern)) {
781
+ set({ error: 'Enter a registered pattern of 1 to 50 supported characters.' });
782
+ return Promise.resolve();
783
+ }
784
+ return mutate(() => client.request('post', '/_pb/sources/{source}/names', {
785
+ params: { source },
786
+ body: { pattern },
787
+ }), () => names(source));
788
+ },
789
+ removeName(source, name) {
790
+ return mutate(() => client.request('delete', '/_pb/sources/{source}/names/{name}', {
791
+ params: { source, name },
792
+ }), async () => {
793
+ await names(source);
794
+ await load();
795
+ });
796
+ },
797
+ async resolve(name) {
798
+ const version = ++previewVersion;
799
+ set({ preview: null, error: null });
800
+ if (project === null || name.length === 0 || name.length > 214) {
801
+ set({ error: 'Choose a project and enter a package name of 1 to 214 characters.' });
802
+ return;
803
+ }
804
+ const result = await client.request('get', '/_pb/projects/{project}/resolve', {
805
+ params: { project },
806
+ query: { name },
807
+ });
808
+ if (version === previewVersion && !failure(result) && result.ok)
809
+ set({ preview: result.data });
810
+ },
811
+ };
812
+ }
813
+ export function submitCredential(model, source, form) {
814
+ const kind = form.elements.namedItem('kind')?.value;
815
+ const fields = CREDENTIAL_KINDS.includes(kind) ? CREDENTIAL_FIELDS[kind] : null;
816
+ const material = fields === null
817
+ ? null
818
+ : JSON.stringify(Object.fromEntries(fields.map((field) => [field, form.elements.namedItem(field)?.value ?? ''])));
819
+ form.reset();
820
+ if (material === null)
821
+ return Promise.resolve();
822
+ return model.credential(source, kind, material);
823
+ }
824
+ export function CredentialFields({ kind, busy }) {
825
+ return (_jsx(_Fragment, { children: CREDENTIAL_FIELDS[kind].map((field) => (_jsxs("div", { children: [_jsx(Label, { htmlFor: `credential-${field}`, children: field }), _jsx(Input, { id: `credential-${field}`, name: field, type: "password", autoComplete: "off", defaultValue: "", maxLength: 16384, required: true, disabled: busy })] }, field))) }));
826
+ }
827
+ function CredentialEditor({ model, source, busy, initialKind, kinds, }) {
828
+ const [kind, setKind] = useState(initialKind);
829
+ return (_jsxs("form", { className: "grid gap-3", autoComplete: "off", onSubmit: (event) => {
830
+ event.preventDefault();
831
+ if (busy)
832
+ return;
833
+ void submitCredential(model, source, event.currentTarget);
834
+ }, children: [_jsx(Label, { htmlFor: "credential-kind", children: "Credential kind" }), _jsx(Select, { id: "credential-kind", name: "kind", value: kind, disabled: busy, onChange: (event) => setKind(event.currentTarget.value), children: kinds.map((value) => (_jsx("option", { value: value, children: value }, value))) }), _jsx(CredentialFields, { kind: kind, busy: busy }, kind), _jsx(Button, { type: "submit", className: "w-fit", disabled: busy, children: "Save credential" })] }));
835
+ }
836
+ function formValue(form, name) {
837
+ return new FormData(form).get(name)?.toString() ?? '';
838
+ }
839
+ export const SYNC_ERROR_LABELS = {
840
+ network: 'Network failure',
841
+ timeout: 'Sync timed out',
842
+ http_error: 'HTTP error',
843
+ unauthorized: 'Authorization failed',
844
+ rate_limited: 'Rate limited',
845
+ not_found: 'Not found',
846
+ invalid_response: 'Invalid response',
847
+ tag_limit_exceeded: 'Tag limit exceeded',
848
+ mass_absence: 'Mass package absence',
849
+ too_large: 'Response too large',
850
+ policy_denied: 'Policy denied',
851
+ credential_unavailable: 'Credential unavailable',
852
+ internal: 'Internal error',
853
+ };
854
+ function SyncStatus({ source }) {
855
+ const errorLabel = source.lastError !== null && Object.hasOwn(SYNC_ERROR_LABELS, source.lastError)
856
+ ? SYNC_ERROR_LABELS[source.lastError]
857
+ : null;
858
+ return (_jsxs("div", { className: "grid gap-1 text-sm", children: [_jsxs("p", { children: ["Last listing: ", _jsx(Time, { value: source.lastSuccessAt, fallback: "Never" })] }), _jsxs("p", { children: ["Last listing error:", ' ', source.lastError === null ? ('None') : (_jsxs(_Fragment, { children: [errorLabel && `${errorLabel} · `, _jsx("code", { children: source.lastError })] }))] }), source.sync && (_jsxs(_Fragment, { children: [_jsxs("p", { children: ["Latest sync: ", source.sync.queued, " queued \u00B7 ", source.sync.running, " running \u00B7", ' ', source.sync.retrying, " retrying \u00B7 ", source.sync.failed, " failed \u00B7 ", source.sync.completed, ' ', "completed"] }), source.sync.errors.slice(0, 3).map(({ code, count }) => (_jsxs("p", { children: [SYNC_ERROR_LABELS[code], " \u00B7 ", _jsx("code", { children: code }), ": ", count] }, code)))] })), isProxy(source) ? _jsx("p", { children: PROXY_DEMAND_MESSAGE }) : null, _jsxs("p", { children: ["Next poll: ", _jsx(Time, { value: source.nextPollAt, fallback: "None" })] })] }));
859
+ }
860
+ export function SourceActions({ source, model, busy, }) {
861
+ const [pauseSource, setPauseSource] = useState(null);
862
+ useEffect(() => {
863
+ if (pauseSource && (pauseSource.id !== source.id || pauseSource.status !== source.status))
864
+ setPauseSource(null);
865
+ }, [pauseSource, source.id, source.status]);
866
+ const owned = model.project === null ? source.projectId === null : source.projectId !== null;
867
+ const organizationProject = model.project === null && source.projectId !== null;
868
+ const enableDenied = (owned || organizationProject) &&
869
+ source.status === 'paused' &&
870
+ !isProxy(source) &&
871
+ model.enableNeedsAdmin(source);
872
+ const enableConflict = model.snapshot().enableConflicts.includes(source.id);
873
+ const explanationId = `source-actions-note-${source.id}`;
874
+ const explanation = !owned && !organizationProject
875
+ ? 'Manage this organization source in organization scope.'
876
+ : [enableDenied && ENABLE_ADMIN_MESSAGE, enableConflict && ENABLE_CONFLICT_MESSAGE]
877
+ .filter(Boolean)
878
+ .join(' ');
879
+ const actions = owned
880
+ ? [
881
+ { id: 'edit', label: 'Edit', disabled: busy, onSelect: () => model.openEditor(source) },
882
+ {
883
+ id: 'sync',
884
+ label: 'Sync now',
885
+ disabled: busy || source.status === 'paused',
886
+ ...(source.status === 'paused' ? { describedBy: `sync-paused-${source.id}` } : {}),
887
+ onSelect: () => void model.syncNow(source),
888
+ },
889
+ ]
890
+ : [];
891
+ const overflow = [];
892
+ if (owned || organizationProject) {
893
+ if (!(organizationProject && (source.status !== 'paused' || isProxy(source)))) {
894
+ overflow.push(source.status === 'paused'
895
+ ? {
896
+ id: 'enable',
897
+ label: 'Enable',
898
+ disabled: busy || enableDenied,
899
+ ...(enableDenied || enableConflict ? { describedBy: explanationId } : {}),
900
+ onSelect: () => void model.togglePause(source),
901
+ }
902
+ : {
903
+ id: 'pause',
904
+ label: 'Pause source…',
905
+ disabled: busy,
906
+ onSelect: () => setPauseSource(source),
907
+ });
908
+ }
909
+ if (owned &&
910
+ sourceCredentialKinds(source.type, credentialRules(source, model.snapshot().vcsProviders))
911
+ .length > 0)
912
+ overflow.push({
913
+ id: 'credential',
914
+ label: source.type === 'packagist_proxy' ? 'Add GitHub token (optional)' : 'Set credential',
915
+ disabled: busy,
916
+ onSelect: () => model.chooseCredential(source.id),
917
+ });
918
+ }
919
+ else {
920
+ overflow.push({
921
+ id: 'manage',
922
+ label: 'Manage source',
923
+ disabled: true,
924
+ describedBy: explanationId,
925
+ onSelect: () => { },
926
+ });
927
+ }
928
+ if (model.project !== null && acceptsPackage(source))
929
+ overflow.push({
930
+ id: 'add-package',
931
+ label: 'Add package',
932
+ disabled: busy || source.status === 'paused',
933
+ ...(source.status === 'paused' ? { describedBy: `add-package-paused-${source.id}` } : {}),
934
+ onSelect: () => model.openPackage(source),
935
+ });
936
+ overflow.push({
937
+ id: 'names',
938
+ label: 'Registered names',
939
+ disabled: busy,
940
+ onSelect: () => void model.names(source.id),
941
+ });
942
+ return (_jsxs(_Fragment, { children: [_jsx(RowActions, { entityLabel: source.name, actions: actions, overflow: overflow }), owned && source.status === 'paused' ? (_jsx("p", { id: `sync-paused-${source.id}`, children: "Enable before syncing." })) : null, model.project !== null && acceptsPackage(source) && source.status === 'paused' ? (_jsx("p", { id: `add-package-paused-${source.id}`, children: "Enable before adding a package." })) : null, explanation ? _jsx("p", { id: explanationId, children: explanation }) : null, _jsx(ConfirmAction, { action: "Pause", target: `${source.name} (${source.id})`, scope: model.project === null ? 'Organization' : `Project: ${model.project}`, description: "Pausing this source stops it from resolving packages in this scope. You can enable it again later.", disabled: busy, open: pauseSource?.id === source.id &&
943
+ pauseSource.status === source.status &&
944
+ source.status !== 'paused', onOpenChange: (open) => {
945
+ if (!open)
946
+ setPauseSource(null);
947
+ }, hideTrigger: true, onConfirm: () => void model.togglePause(source) })] }));
948
+ }
949
+ export function filterSources(items, filters) {
950
+ return items.filter((item) => item.name.toLowerCase().includes(filters.q.toLowerCase()) &&
951
+ (!filters.type || item.type === filters.type) &&
952
+ (!filters.scope ||
953
+ (filters.scope === 'organization' ? item.projectId === null : item.projectId !== null)));
954
+ }
955
+ export function SourcesView({ model }) {
956
+ const state = useSyncExternalStore(model.subscribe, model.snapshot, model.snapshot);
957
+ const [filters, setFilters] = useState({ q: '', type: '', scope: '' });
958
+ const filtered = !!(filters.q || filters.type || filters.scope);
959
+ const created = state.items.find((item) => item.id === state.createdSource);
960
+ const scopePatterns = useMemo(() => {
961
+ const names = state.names;
962
+ if (names === null)
963
+ return [];
964
+ return suggestedScopePatterns(state.items.find((source) => source.id === names.source), names.items.map((item) => item.pattern));
965
+ }, [state.items, state.names]);
966
+ const membershipKnown = !state.loading && !state.listError && state.next === null;
967
+ const credentialForPackagist = state.items.find((source) => source.id === state.credentialSource)?.type === 'packagist_proxy';
968
+ const combined = [
969
+ ...state.items,
970
+ ...(membershipKnown
971
+ ? state.available.filter((candidate) => !state.items.some((item) => item.id === candidate.id))
972
+ : []),
973
+ ];
974
+ const items = filterSources(combined, filters).sort((a, b) => {
975
+ const position = (id) => {
976
+ const index = state.order.indexOf(id);
977
+ return index < 0 ? Infinity : index;
978
+ };
979
+ return position(a.id) - position(b.id);
980
+ });
981
+ return (_jsxs("section", { "aria-labelledby": "page-heading", className: "grid gap-6", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Sources" }), _jsx(Button, { type: "button", disabled: state.busy, onClick: () => model.openEditor(), children: "Add source" })] }), state.editor !== null ? (_jsx(SourceForm, { source: state.editor === 'create' ? null : state.editor, busy: state.busy, errors: state.fieldErrors, alert: state.formError, onSubmit: (body) => {
982
+ if (state.editor === 'create')
983
+ setFilters({ q: '', type: '', scope: '' });
984
+ void model.saveSource(body);
985
+ }, onClose: () => model.closeEditor(), vcsProviders: state.vcsProviders })) : null, _jsx("p", { className: "text-neutral-600", children: "Upstream registries and private repositories. Attached sources are tried in precedence order." }), model.project === null ? (_jsxs(Label, { children: ["Ownership", _jsxs(Select, { value: state.ownership, disabled: state.busy, onChange: (event) => void model.setOwnership(event.target.value), children: [_jsx("option", { value: "organization", children: "Organization" }), _jsx("option", { value: "project", children: "Project-owned" }), _jsx("option", { value: "all", children: "All" })] })] })) : null, _jsx(LoadedListFilters, { values: {
986
+ ...filters,
987
+ scope: filters.scope === 'organization'
988
+ ? 'Organization'
989
+ : filters.scope === 'project'
990
+ ? 'Project'
991
+ : '',
992
+ type: SOURCE_TYPE_LABELS[filters.type] ?? '',
993
+ }, fields: [
994
+ { key: 'q', label: 'Name (loaded sources)' },
995
+ {
996
+ key: 'type',
997
+ label: 'Type',
998
+ options: [...new Set(combined.map((item) => SOURCE_TYPE_LABELS[item.type]))].sort(),
999
+ },
1000
+ ...(model.project === null
1001
+ ? []
1002
+ : [{ key: 'scope', label: 'Source scope', options: ['Organization', 'Project'] }]),
1003
+ ], onChange: (next) => setFilters({
1004
+ q: next.q ?? '',
1005
+ scope: model.project === null
1006
+ ? ''
1007
+ : next.scope === 'Organization'
1008
+ ? 'organization'
1009
+ : next.scope === 'Project'
1010
+ ? 'project'
1011
+ : '',
1012
+ type: Object.keys(SOURCE_TYPE_LABELS).find((key) => SOURCE_TYPE_LABELS[key] === next.type) ?? '',
1013
+ }) }), _jsx("p", { role: "status", "aria-live": "polite", children: state.notice }), created?.status === 'paused' && !isProxy(created) ? (_jsxs("p", { children: ["Next steps for ", created.name, ":", ' ', model.enableNeedsAdmin(created)
1014
+ ? ENABLE_ADMIN_MESSAGE
1015
+ : state.names?.source === created.id && state.names.items.length > 0
1016
+ ? 'Enable the source.'
1017
+ : 'Register names, then Enable the source.'] })) : null, state.error && !state.listError ? _jsx(ListState, { kind: "error", title: state.error }) : null, state.listError ? (_jsxs(ListState, { kind: "error", title: "Sources could not be loaded.", children: [state.error ? _jsx("p", { children: state.error }) : null, _jsx(Button, { type: "button", className: "w-fit", disabled: state.loading || state.busy, onClick: () => void model.load(), children: "Retry sources" })] })) : null, !state.listError && !state.availableError ? (_jsx(Button, { type: "button", className: "w-fit", variant: "outline", disabled: state.loading || state.busy, onClick: () => {
1018
+ void model.load();
1019
+ void model.loadAvailable();
1020
+ }, children: "Refresh sources" })) : null, model.project !== null && !membershipKnown && state.available.length > 0 ? (_jsx("p", { className: "text-sm text-neutral-600", children: "Load all project sources before attaching organization sources." })) : null, state.availableLoading ? (_jsx(ListState, { kind: "loading", title: "Loading organization sources\u2026" })) : null, state.availableError ? (_jsx(ListState, { kind: "error", title: "Organization sources could not be loaded.", children: _jsx(Button, { type: "button", className: "w-fit", disabled: state.availableLoading || state.busy, onClick: () => void model.loadAvailable(), children: "Retry organization sources" }) })) : null, state.loading ? (_jsx(ListState, { kind: "loading", title: "Loading sources\u2026" })) : !state.listError && items.length === 0 ? (_jsx(ListState, { kind: filtered ? 'filtered-empty' : 'empty', title: filtered ? 'No sources match these filters.' : 'No sources.' })) : null, !state.loading && !state.listError && items.length ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-neutral-600", children: [items.length, " sources loaded", filtered ? ' matching filters' : ''] }), _jsx(DataTable, { label: "Sources in precedence order", items: items, columns: [
1021
+ {
1022
+ key: 'name',
1023
+ label: 'Source',
1024
+ value: (source) => source.name,
1025
+ render: (source) => (_jsxs("div", { className: "grid gap-1", children: [_jsx("strong", { children: source.name }), 'projectName' in source && source.projectName !== null ? (_jsx("span", { children: `Project: ${source.projectName}` })) : null, _jsxs("span", { className: "text-xs text-neutral-600", children: [source.ecosystem === 'composer' ? 'Composer' : 'npm', " \u00B7", ' ', SOURCE_TYPE_LABELS[source.type]] }), _jsx(StatusBadge, { status: source.status })] })),
1026
+ },
1027
+ {
1028
+ key: 'precedence',
1029
+ label: 'Precedence',
1030
+ value: (source) => state.order.indexOf(source.id) < 0 ? '—' : state.order.indexOf(source.id) + 1,
1031
+ render: (source) => {
1032
+ const index = state.order.indexOf(source.id);
1033
+ return index < 0 ? ('—') : (_jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [_jsx("strong", { children: index + 1 }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", "aria-label": `Move up ${source.name}`, disabled: state.busy || state.next !== null || index === 0, onClick: () => model.moveSource(source.id, -1), children: "Move up" }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", "aria-label": `Move down ${source.name}`, disabled: state.busy || state.next !== null || index === state.order.length - 1, onClick: () => model.moveSource(source.id, 1), children: "Move down" })] }));
1034
+ },
1035
+ },
1036
+ {
1037
+ key: 'scope',
1038
+ label: 'Attached to',
1039
+ value: (source) => source.projectId === null ? 'Organization source' : 'Project source',
1040
+ render: (source) => (_jsxs("div", { className: "grid gap-1", children: [_jsx("span", { children: source.projectId === null ? 'Organization source' : 'Project source' }), model.project === null && 'attachedProjectCount' in source ? (_jsx(Badge, { variant: "secondary", children: `Attached to ${source.attachedProjectCount} projects` })) : null, 'attached' in source ? (_jsx("span", { className: "text-xs text-neutral-600", children: source.attached
1041
+ ? 'Attached to this project'
1042
+ : 'Not attached to this project' })) : null] })),
1043
+ },
1044
+ {
1045
+ key: 'credential',
1046
+ label: 'Credentials',
1047
+ value: (source) => (source.hasCredential ? 'Set · write-only' : 'None'),
1048
+ render: (source) => source.hasCredential ? _jsx(Badge, { variant: "outline", children: "Set \u00B7 write-only" }) : 'None',
1049
+ },
1050
+ {
1051
+ key: 'sync',
1052
+ label: 'Sync status',
1053
+ value: (source) => source.lastSuccessAt ?? '',
1054
+ render: (source) => _jsx(SyncStatus, { source: source }),
1055
+ },
1056
+ {
1057
+ key: 'id',
1058
+ label: 'Source ID',
1059
+ value: (source) => source.id,
1060
+ priority: 3,
1061
+ render: (source) => _jsx(IdText, { value: source.id, label: "source ID" }),
1062
+ },
1063
+ {
1064
+ key: 'actions',
1065
+ label: 'Actions',
1066
+ value: () => '',
1067
+ sortable: false,
1068
+ render: (source) => (_jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [model.project !== null && 'attached' in source ? (source.attached ? (_jsx(ConfirmAction, { action: "Detach", target: `${source.name} (${source.id})`, scope: `Project: ${model.project}`, description: "Detaching this source stops this project from resolving packages through it. You can attach it again later.", disabled: state.busy, onConfirm: () => void model.attachment(source.id, false) })) : (_jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: state.busy, onClick: () => void model.attachment(source.id, true), children: "Attach" }))) : null, _jsx(SourceActions, { source: source, model: model, busy: state.busy })] })),
1069
+ },
1070
+ ] })] })) : null, state.next !== null ? (_jsx(Button, { type: "button", className: "w-fit", disabled: state.loading || state.busy, onClick: () => {
1071
+ void model.load(true);
1072
+ }, children: "Load more sources" })) : null, state.availableNext !== null ? (_jsx(Button, { type: "button", className: "w-fit", disabled: state.availableLoading || state.busy, onClick: () => void model.loadAvailable(true), children: "Load more organization sources" })) : null, state.packageSource !== null ? (_jsx(AddPackageDialog, { source: state.packageSource, busy: state.busy, fieldError: state.packageFieldError, alert: state.packageAlert, onSubmit: (name) => void model.addPackage(name), onClose: () => model.closePackage() }, state.packageSource.id)) : null, state.credentialSource !== null ? (_jsx(Dialog, { open: true, onOpenChange: (open) => {
1073
+ if (!open)
1074
+ model.closeCredential();
1075
+ }, children: _jsxs(DialogContent, { variant: "source", onCancel: (event) => {
1076
+ if (state.busy)
1077
+ event.preventDefault();
1078
+ }, children: [_jsxs(DialogHeader, { className: "shrink-0 p-6 pr-16", children: [_jsx(DialogTitle, { children: credentialForPackagist ? 'Add GitHub token (optional)' : 'Set credential' }), _jsxs(DialogDescription, { children: ["Credentials are write-only. Save a new credential to rotate a secret.", credentialForPackagist
1079
+ ? ' This optional bearer token is used only for GitHub archive downloads and their rate limits. Enable Use GitHub token in Edit after saving it.'
1080
+ : ''] })] }), _jsxs("div", { className: "grid min-h-0 gap-3 overflow-y-auto px-6 pb-6", children: [_jsx(IdText, { value: state.credentialSource, label: "source ID" }), _jsx(CredentialEditor, { model: model, source: state.credentialSource, initialKind: state.credentialKind, kinds: state.credentialKinds, busy: state.busy }, state.credentialVersion), state.error ? _jsx("p", { role: "alert", children: state.error }) : null, state.savedCredential?.source === state.credentialSource ? (_jsxs("p", { children: ["credential set \u00B7 ", _jsx(Time, { value: state.savedCredential.at })] })) : null] })] }) })) : null, state.savedCredential && state.credentialSource === null ? (_jsxs("p", { children: ["credential set \u00B7 ", state.savedCredential.source, " \u00B7", ' ', _jsx(Time, { value: state.savedCredential.at })] })) : null, state.names ? (_jsx(Dialog, { open: true, onOpenChange: (open) => {
1081
+ if (!open)
1082
+ model.closeNames();
1083
+ }, children: _jsxs(DialogContent, { variant: "source", onCancel: (event) => {
1084
+ if (state.busy)
1085
+ event.preventDefault();
1086
+ }, children: [_jsxs(DialogHeader, { className: "shrink-0 p-6 pr-16", children: [_jsx(DialogTitle, { children: "Registered names" }), _jsx(DialogDescription, { children: "Manage the package patterns registered for this source." })] }), _jsxs("div", { className: "grid min-h-0 gap-3 overflow-y-auto px-6 pb-6", children: [state.error ? _jsx("p", { role: "alert", children: state.error }) : null, _jsx(IdText, { value: state.names.source, label: "source ID" }), _jsx("ul", { children: state.names.items.map((name) => (_jsxs("li", { className: "flex items-center gap-3", children: [_jsx("span", { children: name.pattern }), _jsx(ConfirmAction, { action: `Remove ${name.pattern}`, target: name.pattern, scope: model.project === null ? 'Organization' : `Project: ${model.project}`, description: "Removing this registered pattern can stop the source from resolving matching packages. Add the pattern again to restore it.", disabled: state.busy, onConfirm: () => void model.removeName(name.sourceId, name.id) })] }, name.id))) }), state.names.next !== null ? (_jsx(Button, { type: "button", disabled: state.busy, onClick: () => {
1087
+ void model.names(state.names.source, true);
1088
+ }, children: "Load more names" })) : null, _jsxs("form", { className: "grid gap-3", onSubmit: (event) => {
1089
+ event.preventDefault();
1090
+ if (state.busy)
1091
+ return;
1092
+ const form = event.currentTarget;
1093
+ const pattern = formValue(form, 'pattern');
1094
+ form.reset();
1095
+ void model.addName(state.names.source, pattern);
1096
+ }, children: [_jsx(Label, { htmlFor: "registered-pattern", children: "Registered pattern" }), _jsx(Input, { id: "registered-pattern", name: "pattern", maxLength: 50, required: true, defaultValue: scopePatterns[0] ?? '' }, `${state.names.source}:${state.names.items.map((item) => item.pattern).join(',')}`), scopePatterns.map((pattern) => (_jsxs("p", { className: "text-sm", children: ["Register ", pattern, " before enabling this source."] }, pattern))), _jsx(Button, { type: "submit", className: "w-fit", disabled: state.busy, children: "Add name" })] })] })] }) })) : null, model.project !== null &&
1097
+ !state.listError &&
1098
+ state.items.some((item) => 'attached' in item && item.attached) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Source order" }) }), _jsxs(CardContent, { className: "grid gap-3", children: [_jsx("p", { children: "Earlier sources take precedence. Only attached sources are ordered." }), state.next !== null ? _jsx("p", { children: "Load all sources before changing their order." }) : null, _jsx(Button, { type: "button", className: "w-fit", disabled: state.loading || state.busy || state.next !== null || state.etag === null, onClick: () => {
1099
+ void model.saveOrder();
1100
+ }, children: "Save source order" })] })] })) : null, model.project !== null ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Who wins" }) }), _jsxs(CardContent, { className: "grid gap-3", children: [_jsxs("form", { className: "grid gap-3", onSubmit: (event) => {
1101
+ event.preventDefault();
1102
+ if (state.busy)
1103
+ return;
1104
+ void model.resolve(formValue(event.currentTarget, 'name'));
1105
+ }, children: [_jsx(Label, { htmlFor: "resolve-name", children: "Package name" }), _jsx(Input, { id: "resolve-name", name: "name", maxLength: 214, required: true }), _jsx(Button, { type: "submit", className: "w-fit", disabled: state.busy, children: "Preview" })] }), state.preview ? (_jsxs("div", { "aria-live": "polite", children: [_jsx("p", { children: state.preview.name }), _jsxs("p", { children: ["Winning source: ", _jsx(IdText, { value: state.preview.winner, label: "winning source ID" })] }), _jsx("ol", { children: state.preview.candidates.map((candidate) => (_jsxs("li", { children: [candidate.sourceName, " \u00B7", ' ', _jsx(IdText, { value: candidate.sourceId, label: "candidate source ID" }), " \u00B7", ' ', candidate.eligible ? 'Eligible' : 'Ineligible'] }, candidate.sourceId))) })] })) : null] })] })) : null] }));
1106
+ }
1107
+ function projectFromHash() {
1108
+ if (typeof window === 'undefined')
1109
+ return null;
1110
+ const project = new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('project');
1111
+ return new URLSearchParams(buildListFilterHash('sources', { project }).split('?')[1] ?? '').get('project');
1112
+ }
1113
+ export function SourcesPage() {
1114
+ const services = useServices();
1115
+ const [project, setProject] = useState(projectFromHash);
1116
+ useEffect(() => {
1117
+ const changed = () => setProject(projectFromHash());
1118
+ window.addEventListener('hashchange', changed);
1119
+ return () => window.removeEventListener('hashchange', changed);
1120
+ }, []);
1121
+ const model = useMemo(() => createSourcesModel(services, project), [services, project]);
1122
+ useEffect(() => {
1123
+ void model.load();
1124
+ void model.loadAvailable();
1125
+ return () => model.dispose();
1126
+ }, [model]);
1127
+ useEffect(() => services.subscribeReplay?.((result) => {
1128
+ void model.replayed(result);
1129
+ }), [services, model]);
1130
+ return _jsx(SourcesView, { model: model }, project ?? 'organization');
1131
+ }
1132
+ //# sourceMappingURL=sources.js.map