@peanut-admin/admin 0.1.0-alpha.2

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 (67) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/api/client.ts +200 -0
  4. package/admin-core/src/api/problem.ts +67 -0
  5. package/admin-core/src/api/refresh.ts +122 -0
  6. package/admin-core/src/auth/stores.ts +121 -0
  7. package/admin-core/src/generated/api.d.ts +22106 -0
  8. package/admin-core/src/governance/audit.ts +56 -0
  9. package/admin-core/src/governance/catalog.ts +86 -0
  10. package/admin-core/src/governance/index.ts +26 -0
  11. package/admin-core/src/governance/menu.ts +63 -0
  12. package/admin-core/src/governance/roles.ts +144 -0
  13. package/admin-core/src/governance/types.ts +64 -0
  14. package/admin-core/src/index.ts +105 -0
  15. package/admin-core/src/lifecycle/tenant.ts +59 -0
  16. package/admin-core/src/module/contribution.ts +124 -0
  17. package/admin-core/src/runtime/config.ts +55 -0
  18. package/admin-core/src/runtime/errors.ts +81 -0
  19. package/admin-core/src/runtime/guard.ts +40 -0
  20. package/admin-core/src/runtime/navigation.ts +105 -0
  21. package/admin-core/src/runtime/overrides.ts +214 -0
  22. package/admin-core/src/targets/store.ts +153 -0
  23. package/admin-shell/src/config.ts +84 -0
  24. package/admin-shell/src/index.ts +40 -0
  25. package/admin-shell/src/layout.ts +332 -0
  26. package/admin-shell/src/overrides.ts +53 -0
  27. package/admin-shell/src/states.ts +93 -0
  28. package/admin-shell/src/targets.ts +128 -0
  29. package/admin-shell/src/theme.ts +15 -0
  30. package/client-core/src/index.ts +325 -0
  31. package/client-nuxt/src/index.ts +40 -0
  32. package/client-uniapp/src/index.ts +50 -0
  33. package/file-media/src/FileAssetSelector.vue +117 -0
  34. package/file-media/src/FileMediaPage.vue +158 -0
  35. package/file-media/src/contracts.ts +220 -0
  36. package/file-media/src/index.ts +19 -0
  37. package/file-media/src/runtime.ts +210 -0
  38. package/import-export/src/ImportExportPage.vue +155 -0
  39. package/import-export/src/contracts.ts +96 -0
  40. package/import-export/src/index.ts +3 -0
  41. package/import-export/src/runtime.ts +128 -0
  42. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  43. package/integration-security/src/contracts.ts +171 -0
  44. package/integration-security/src/index.ts +3 -0
  45. package/integration-security/src/runtime.ts +180 -0
  46. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  47. package/notification-sms/src/contracts.ts +195 -0
  48. package/notification-sms/src/index.ts +4 -0
  49. package/notification-sms/src/runtime.ts +143 -0
  50. package/ops-console/src/OpsConsolePage.vue +337 -0
  51. package/ops-console/src/contracts.ts +169 -0
  52. package/ops-console/src/index.ts +3 -0
  53. package/ops-console/src/runtime.ts +199 -0
  54. package/package.json +108 -0
  55. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  56. package/reference-codes/src/contracts.ts +484 -0
  57. package/reference-codes/src/index.ts +53 -0
  58. package/reference-codes/src/runtime.ts +855 -0
  59. package/settings/src/SettingsPage.vue +536 -0
  60. package/settings/src/contracts.ts +331 -0
  61. package/settings/src/index.ts +45 -0
  62. package/settings/src/runtime.ts +545 -0
  63. package/task-job/src/TaskJobPage.vue +120 -0
  64. package/task-job/src/contracts.ts +117 -0
  65. package/task-job/src/index.ts +2 -0
  66. package/task-job/src/runtime.ts +105 -0
  67. package/testing/src/index.ts +141 -0
@@ -0,0 +1,536 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ EmptyState,
4
+ ForbiddenState,
5
+ ModuleUnavailableState,
6
+ PageContent,
7
+ PageHeader,
8
+ PageToolbar,
9
+ SessionExpiredState,
10
+ } from '@peanut-admin/admin/shell'
11
+ import { ElButton, ElInput, ElInputNumber, ElSwitch } from 'element-plus'
12
+ import { computed, onMounted } from 'vue'
13
+
14
+ import { settingEditorKind } from './contracts'
15
+ import type { SettingRecord, SettingScalar } from './contracts'
16
+ import { useSettingsRuntime } from './runtime'
17
+
18
+ const runtime = useSettingsRuntime()
19
+ const state = runtime.state
20
+ const canManage = computed(() => runtime.canManage())
21
+ const hasPendingMutation = computed(() => state.pendingResources.size > 0)
22
+
23
+ const keyOf = (record: SettingRecord): string => `${record.moduleKey}/${record.settingKey}`
24
+ const headingId = (record: SettingRecord): string => (
25
+ `setting-heading-${record.moduleKey.length}-${record.moduleKey}-${record.settingKey.length}-${record.settingKey}`
26
+ )
27
+ const commandLabel = (command: string, record: SettingRecord): string => (
28
+ `${command} ${record.name} (${keyOf(record)})`
29
+ )
30
+ const enumOptions = (record: SettingRecord): readonly SettingScalar[] => (
31
+ (record.schema.enum ?? []) as readonly SettingScalar[]
32
+ )
33
+ const enumIndex = (record: SettingRecord): number => enumOptions(record).findIndex(
34
+ option => option === state.forms[keyOf(record)]?.value,
35
+ )
36
+ const hasBooleanValue = (record: SettingRecord): boolean => typeof state.forms[keyOf(record)]?.value === 'boolean'
37
+ const numberValue = (record: SettingRecord): number | null => {
38
+ const value = state.forms[keyOf(record)]?.value
39
+ return typeof value === 'number' ? value : null
40
+ }
41
+ const updateEnum = (record: SettingRecord, event: Event): void => {
42
+ const target = event.currentTarget
43
+ if (!(target instanceof HTMLSelectElement)) return
44
+ const value = enumOptions(record)[Number(target.value)]
45
+ if (value !== undefined) runtime.updateForm(keyOf(record), value)
46
+ }
47
+ const typeLabel = (record: SettingRecord): string => record.secret
48
+ ? 'secret'
49
+ : typeof record.schema.type === 'string' ? record.schema.type : record.schema.type.join(' | ')
50
+ const sourceLabel = (record: SettingRecord): string => record.sourceScope ?? 'not configured'
51
+ const dateLabel = (value: string | null): string => value ?? 'none'
52
+ const canSave = (record: SettingRecord): boolean => {
53
+ if (!canManage.value || runtime.isPending(keyOf(record))) return false
54
+ if (state.forms[keyOf(record)]?.dirty !== true) return false
55
+ if (!record.secret) return true
56
+ const value = state.forms[keyOf(record)]?.value
57
+ return typeof value === 'string' && value !== ''
58
+ }
59
+
60
+ const load = async (): Promise<void> => {
61
+ try {
62
+ await runtime.load()
63
+ } catch {
64
+ return
65
+ }
66
+ }
67
+
68
+ const save = async (key: string): Promise<void> => {
69
+ try {
70
+ await runtime.save(key)
71
+ } catch {
72
+ return
73
+ }
74
+ }
75
+
76
+ const unset = async (key: string): Promise<void> => {
77
+ try {
78
+ await runtime.unset(key)
79
+ } catch {
80
+ return
81
+ }
82
+ }
83
+
84
+ const reload = async (key: string): Promise<void> => {
85
+ try {
86
+ await runtime.reload(key)
87
+ } catch {
88
+ return
89
+ }
90
+ }
91
+
92
+ onMounted(load)
93
+ </script>
94
+
95
+ <template>
96
+ <PageContent class="settings-page">
97
+ <PageHeader>
98
+ Settings
99
+ <template #actions>
100
+ <ElButton
101
+ aria-label="Reload settings"
102
+ :aria-disabled="hasPendingMutation"
103
+ :disabled="hasPendingMutation"
104
+ :loading="state.loading"
105
+ @click="load"
106
+ >
107
+ Reload
108
+ </ElButton>
109
+ </template>
110
+ </PageHeader>
111
+
112
+ <PageToolbar
113
+ v-if="!state.errors.page"
114
+ label="Settings view"
115
+ >
116
+ <span>{{ state.records.length }} settings</span>
117
+ </PageToolbar>
118
+
119
+ <SessionExpiredState
120
+ v-if="state.errors.page?.status === 401"
121
+ v-bind="state.errors.page.requestId === null ? {} : { requestId: state.errors.page.requestId }"
122
+ :message="state.errors.page.message"
123
+ />
124
+
125
+ <ForbiddenState
126
+ v-else-if="state.errors.page?.status === 403"
127
+ v-bind="state.errors.page.requestId === null ? {} : { requestId: state.errors.page.requestId }"
128
+ :message="state.errors.page.message"
129
+ />
130
+
131
+ <EmptyState
132
+ v-else-if="state.errors.page?.status === 404"
133
+ data-settings-state="not-found"
134
+ title="Settings not available"
135
+ v-bind="state.errors.page.requestId === null ? {} : { requestId: state.errors.page.requestId }"
136
+ :message="state.errors.page.message"
137
+ />
138
+
139
+ <ModuleUnavailableState
140
+ v-else-if="state.errors.page?.status === 503"
141
+ v-bind="state.errors.page.requestId === null ? {} : { requestId: state.errors.page.requestId }"
142
+ :message="state.errors.page.message"
143
+ @action="load"
144
+ />
145
+
146
+ <section
147
+ v-else-if="state.errors.page"
148
+ class="settings-page__request-error"
149
+ data-settings-state="request-error"
150
+ role="alert"
151
+ aria-live="polite"
152
+ >
153
+ <h2>Unable to load settings</h2>
154
+ <p>{{ state.errors.page.message }}</p>
155
+ <p v-if="state.errors.page.requestId !== null">
156
+ Request ID: {{ state.errors.page.requestId }}
157
+ </p>
158
+ <ElButton @click="load">
159
+ Retry
160
+ </ElButton>
161
+ </section>
162
+
163
+ <EmptyState
164
+ v-else-if="!state.loading && state.groups.length === 0"
165
+ title="No settings"
166
+ message="No settings are available for this Tenant."
167
+ />
168
+
169
+ <section
170
+ v-for="group in state.groups"
171
+ :key="group.moduleKey"
172
+ class="settings-group"
173
+ :data-settings-group="group.moduleKey"
174
+ >
175
+ <h2>{{ group.moduleKey }}</h2>
176
+
177
+ <article
178
+ v-for="record in group.definitions"
179
+ :key="keyOf(record)"
180
+ :aria-labelledby="headingId(record)"
181
+ class="setting-item"
182
+ :data-setting-key="keyOf(record)"
183
+ >
184
+ <header class="setting-item__header">
185
+ <div>
186
+ <h3 :id="headingId(record)">
187
+ {{ record.name }}
188
+ </h3>
189
+ <p>{{ record.description }}</p>
190
+ </div>
191
+ <span class="setting-item__type">{{ typeLabel(record) }}</span>
192
+ </header>
193
+
194
+ <dl class="setting-item__metadata">
195
+ <div>
196
+ <dt>Source</dt>
197
+ <dd>{{ sourceLabel(record) }}</dd>
198
+ </div>
199
+ <div>
200
+ <dt>Configured</dt>
201
+ <dd>{{ record.configured ? 'yes' : 'no' }}</dd>
202
+ </div>
203
+ <div>
204
+ <dt>Revision</dt>
205
+ <dd>{{ record.revision }}</dd>
206
+ </div>
207
+ <div>
208
+ <dt>Effective</dt>
209
+ <dd>{{ dateLabel(record.effectiveAt) }}</dd>
210
+ </div>
211
+ <div>
212
+ <dt>Expires</dt>
213
+ <dd>{{ dateLabel(record.expiresAt) }}</dd>
214
+ </div>
215
+ </dl>
216
+
217
+ <div
218
+ v-if="settingEditorKind(record) === 'boolean'"
219
+ class="setting-item__editor"
220
+ data-editor-kind="boolean"
221
+ >
222
+ <label
223
+ v-if="hasBooleanValue(record)"
224
+ :for="`setting-${keyOf(record)}`"
225
+ >
226
+ {{ record.name }}
227
+ </label>
228
+ <span v-else>{{ record.name }}</span>
229
+ <ElSwitch
230
+ v-if="hasBooleanValue(record)"
231
+ :id="`setting-${keyOf(record)}`"
232
+ :model-value="Boolean(state.forms[keyOf(record)]?.value)"
233
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
234
+ @update:model-value="runtime.updateForm(keyOf(record), $event)"
235
+ />
236
+ <div
237
+ v-else
238
+ class="setting-item__boolean-unconfigured"
239
+ :data-boolean-unconfigured="keyOf(record)"
240
+ role="status"
241
+ >
242
+ <span>Not configured</span>
243
+ <ElButton
244
+ :aria-label="commandLabel('Set false for', record)"
245
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
246
+ @click="runtime.updateForm(keyOf(record), false)"
247
+ >
248
+ Use false
249
+ </ElButton>
250
+ <ElButton
251
+ :aria-label="commandLabel('Set true for', record)"
252
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
253
+ @click="runtime.updateForm(keyOf(record), true)"
254
+ >
255
+ Use true
256
+ </ElButton>
257
+ </div>
258
+ </div>
259
+
260
+ <div
261
+ v-else-if="settingEditorKind(record) === 'number'"
262
+ class="setting-item__editor"
263
+ data-editor-kind="number"
264
+ >
265
+ <label :for="`setting-${keyOf(record)}`">{{ record.name }}</label>
266
+ <ElInputNumber
267
+ :id="`setting-${keyOf(record)}`"
268
+ :model-value="numberValue(record)"
269
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
270
+ controls-position="right"
271
+ @update:model-value="runtime.updateForm(keyOf(record), $event)"
272
+ />
273
+ </div>
274
+
275
+ <div
276
+ v-else-if="settingEditorKind(record) === 'enum'"
277
+ class="setting-item__editor"
278
+ data-editor-kind="enum"
279
+ >
280
+ <label :for="`setting-${keyOf(record)}`">{{ record.name }}</label>
281
+ <select
282
+ :id="`setting-${keyOf(record)}`"
283
+ :value="enumIndex(record)"
284
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
285
+ @change="updateEnum(record, $event)"
286
+ >
287
+ <option
288
+ v-for="(option, index) in enumOptions(record)"
289
+ :key="`${typeof option}:${String(option)}`"
290
+ :value="index"
291
+ >
292
+ {{ String(option) }}
293
+ </option>
294
+ </select>
295
+ </div>
296
+
297
+ <div
298
+ v-else-if="settingEditorKind(record) === 'string'"
299
+ class="setting-item__editor"
300
+ data-editor-kind="string"
301
+ >
302
+ <label :for="`setting-${keyOf(record)}`">{{ record.name }}</label>
303
+ <ElInput
304
+ :id="`setting-${keyOf(record)}`"
305
+ :model-value="String(state.forms[keyOf(record)]?.value ?? '')"
306
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
307
+ @update:model-value="runtime.updateForm(keyOf(record), $event)"
308
+ />
309
+ </div>
310
+
311
+ <div
312
+ v-else-if="settingEditorKind(record) === 'secret'"
313
+ class="setting-item__editor"
314
+ data-editor-kind="secret"
315
+ >
316
+ <label :for="`setting-${keyOf(record)}`">{{ record.name }}</label>
317
+ <ElInput
318
+ :id="`setting-${keyOf(record)}`"
319
+ autocomplete="new-password"
320
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
321
+ :model-value="String(state.forms[keyOf(record)]?.value ?? '')"
322
+ placeholder="Enter a new value"
323
+ :type="runtime.isSecretVisible(keyOf(record)) ? 'text' : 'password'"
324
+ @update:model-value="runtime.updateForm(keyOf(record), $event)"
325
+ />
326
+ <ElButton
327
+ :aria-label="commandLabel(runtime.isSecretVisible(keyOf(record)) ? 'Hide typed value for' : 'Show typed value for', record)"
328
+ :disabled="!canManage || runtime.isPending(keyOf(record))"
329
+ text
330
+ @click="runtime.setSecretVisible(keyOf(record), !runtime.isSecretVisible(keyOf(record)))"
331
+ >
332
+ {{ runtime.isSecretVisible(keyOf(record)) ? 'Hide typed value' : 'Show typed value' }}
333
+ </ElButton>
334
+ </div>
335
+
336
+ <div
337
+ v-else
338
+ class="setting-item__unsupported"
339
+ data-editor-kind="unsupported"
340
+ role="status"
341
+ >
342
+ This setting type is read-only.
343
+ </div>
344
+
345
+ <div
346
+ v-if="state.errors[keyOf(record)]"
347
+ class="setting-item__error"
348
+ role="alert"
349
+ >
350
+ {{ state.errors[keyOf(record)]?.message }}
351
+ </div>
352
+
353
+ <div
354
+ v-if="state.conflicts[keyOf(record)]"
355
+ class="setting-item__conflict"
356
+ role="alert"
357
+ >
358
+ <span>{{ state.conflicts[keyOf(record)]?.message }}</span>
359
+ <ElButton
360
+ :aria-label="commandLabel('Reload', record)"
361
+ :aria-disabled="hasPendingMutation"
362
+ :disabled="hasPendingMutation"
363
+ @click="reload(keyOf(record))"
364
+ >
365
+ Reload current value
366
+ </ElButton>
367
+ </div>
368
+
369
+ <div
370
+ v-if="settingEditorKind(record) !== 'unsupported'"
371
+ class="setting-item__actions"
372
+ >
373
+ <ElButton
374
+ :aria-label="commandLabel('Save', record)"
375
+ type="primary"
376
+ :disabled="!canSave(record)"
377
+ @click="save(keyOf(record))"
378
+ >
379
+ Save
380
+ </ElButton>
381
+ <ElButton
382
+ :aria-label="commandLabel('Unset', record)"
383
+ :disabled="!canManage || runtime.isPending(keyOf(record)) || !state.etags[keyOf(record)]"
384
+ @click="unset(keyOf(record))"
385
+ >
386
+ Unset
387
+ </ElButton>
388
+ </div>
389
+ </article>
390
+ </section>
391
+ </PageContent>
392
+ </template>
393
+
394
+ <style scoped>
395
+ .settings-page {
396
+ min-width: 0;
397
+ }
398
+
399
+ .settings-page__request-error {
400
+ padding: 16px 0;
401
+ }
402
+
403
+ .settings-page__request-error h2,
404
+ .settings-page__request-error p {
405
+ margin: 0 0 8px;
406
+ letter-spacing: 0;
407
+ overflow-wrap: anywhere;
408
+ }
409
+
410
+ .settings-group {
411
+ margin-block: 24px;
412
+ }
413
+
414
+ .settings-group > h2 {
415
+ margin: 0 0 12px;
416
+ font-size: 18px;
417
+ letter-spacing: 0;
418
+ overflow-wrap: anywhere;
419
+ }
420
+
421
+ .setting-item {
422
+ display: grid;
423
+ min-width: 0;
424
+ gap: 16px;
425
+ padding: 18px 0;
426
+ border-top: 1px solid var(--el-border-color-lighter);
427
+ }
428
+
429
+ .setting-item__header {
430
+ display: flex;
431
+ min-width: 0;
432
+ align-items: flex-start;
433
+ justify-content: space-between;
434
+ gap: 16px;
435
+ }
436
+
437
+ .setting-item__header h3,
438
+ .setting-item__header p {
439
+ margin: 0;
440
+ letter-spacing: 0;
441
+ overflow-wrap: anywhere;
442
+ }
443
+
444
+ .setting-item__header h3 {
445
+ font-size: 16px;
446
+ }
447
+
448
+ .setting-item__header p {
449
+ margin-top: 4px;
450
+ color: var(--el-text-color-secondary);
451
+ }
452
+
453
+ .setting-item__type {
454
+ flex: none;
455
+ color: var(--el-text-color-secondary);
456
+ font-size: 13px;
457
+ }
458
+
459
+ .setting-item__metadata {
460
+ display: grid;
461
+ grid-template-columns: repeat(5, minmax(0, 1fr));
462
+ gap: 12px;
463
+ margin: 0;
464
+ }
465
+
466
+ .setting-item__metadata div {
467
+ min-width: 0;
468
+ }
469
+
470
+ .setting-item__metadata dt {
471
+ color: var(--el-text-color-secondary);
472
+ font-size: 12px;
473
+ }
474
+
475
+ .setting-item__metadata dd {
476
+ margin: 3px 0 0;
477
+ overflow-wrap: anywhere;
478
+ }
479
+
480
+ .setting-item__editor {
481
+ display: grid;
482
+ grid-template-columns: minmax(120px, 180px) minmax(0, 420px) auto;
483
+ align-items: center;
484
+ gap: 12px;
485
+ min-width: 0;
486
+ }
487
+
488
+ .setting-item__unsupported,
489
+ .setting-item__error,
490
+ .setting-item__conflict {
491
+ padding: 12px;
492
+ border-radius: 6px;
493
+ background: var(--el-fill-color-light);
494
+ }
495
+
496
+ .setting-item__error,
497
+ .setting-item__conflict {
498
+ color: var(--el-color-danger);
499
+ }
500
+
501
+ .setting-item__conflict {
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: space-between;
505
+ gap: 12px;
506
+ }
507
+
508
+ .setting-item__actions {
509
+ display: flex;
510
+ flex-wrap: wrap;
511
+ gap: 8px;
512
+ }
513
+
514
+ @media (max-width: 720px) {
515
+ .setting-item__header,
516
+ .setting-item__conflict {
517
+ align-items: stretch;
518
+ flex-direction: column;
519
+ }
520
+
521
+ .setting-item__metadata {
522
+ grid-template-columns: repeat(2, minmax(0, 1fr));
523
+ }
524
+
525
+ .setting-item__editor {
526
+ grid-template-columns: minmax(0, 1fr);
527
+ }
528
+
529
+ .setting-item__editor :deep(.el-input),
530
+ .setting-item__editor :deep(.el-input-number),
531
+ .setting-item__editor select {
532
+ width: 100%;
533
+ max-width: 100%;
534
+ }
535
+ }
536
+ </style>