@peanut-admin/admin 0.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/admin-core/src/access/access.ts +27 -0
- package/admin-core/src/access/permission-policy.ts +37 -0
- package/admin-core/src/api/client.ts +200 -0
- package/admin-core/src/api/problem.ts +67 -0
- package/admin-core/src/api/refresh.ts +122 -0
- package/admin-core/src/auth/stores.ts +121 -0
- package/admin-core/src/auth/tenant-session.ts +32 -0
- package/admin-core/src/generated/api.d.ts +22106 -0
- package/admin-core/src/governance/audit.ts +56 -0
- package/admin-core/src/governance/catalog.ts +86 -0
- package/admin-core/src/governance/index.ts +26 -0
- package/admin-core/src/governance/menu.ts +63 -0
- package/admin-core/src/governance/roles.ts +144 -0
- package/admin-core/src/governance/types.ts +64 -0
- package/admin-core/src/index.ts +122 -0
- package/admin-core/src/lifecycle/tenant.ts +59 -0
- package/admin-core/src/module/contribution.ts +124 -0
- package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
- package/admin-core/src/module/tenant-modules.ts +20 -0
- package/admin-core/src/runtime/config.ts +55 -0
- package/admin-core/src/runtime/errors.ts +81 -0
- package/admin-core/src/runtime/guard.ts +40 -0
- package/admin-core/src/runtime/navigation.ts +105 -0
- package/admin-core/src/runtime/overrides.ts +214 -0
- package/admin-core/src/targets/store.ts +153 -0
- package/admin-shell/src/config.ts +84 -0
- package/admin-shell/src/deployment-mode.ts +36 -0
- package/admin-shell/src/index.ts +44 -0
- package/admin-shell/src/layout.ts +332 -0
- package/admin-shell/src/overrides.ts +53 -0
- package/admin-shell/src/states.ts +93 -0
- package/admin-shell/src/tabs.ts +31 -0
- package/admin-shell/src/targets.ts +128 -0
- package/admin-shell/src/theme.ts +15 -0
- package/client-core/src/index.ts +415 -0
- package/client-nuxt/src/index.ts +48 -0
- package/client-uniapp/src/index.ts +50 -0
- package/file-media/src/FileAssetSelector.vue +117 -0
- package/file-media/src/FileMediaPage.vue +158 -0
- package/file-media/src/contracts.ts +220 -0
- package/file-media/src/index.ts +19 -0
- package/file-media/src/runtime.ts +210 -0
- package/import-export/src/ImportExportPage.vue +155 -0
- package/import-export/src/contracts.ts +96 -0
- package/import-export/src/index.ts +3 -0
- package/import-export/src/runtime.ts +128 -0
- package/integration-security/src/IntegrationSecurityPage.vue +402 -0
- package/integration-security/src/contracts.ts +171 -0
- package/integration-security/src/index.ts +3 -0
- package/integration-security/src/runtime.ts +180 -0
- package/notification-sms/src/NotificationInboxPage.vue +266 -0
- package/notification-sms/src/contracts.ts +195 -0
- package/notification-sms/src/index.ts +4 -0
- package/notification-sms/src/runtime.ts +143 -0
- package/ops-console/src/OpsConsolePage.vue +337 -0
- package/ops-console/src/contracts.ts +169 -0
- package/ops-console/src/index.ts +3 -0
- package/ops-console/src/runtime.ts +199 -0
- package/package.json +139 -0
- package/reference-codes/src/ReferenceCodesPage.vue +942 -0
- package/reference-codes/src/contracts.ts +484 -0
- package/reference-codes/src/index.ts +53 -0
- package/reference-codes/src/runtime.ts +855 -0
- package/settings/src/SettingsPage.vue +536 -0
- package/settings/src/contracts.ts +331 -0
- package/settings/src/index.ts +45 -0
- package/settings/src/runtime.ts +545 -0
- package/task-job/src/TaskJobPage.vue +120 -0
- package/task-job/src/contracts.ts +117 -0
- package/task-job/src/index.ts +2 -0
- package/task-job/src/runtime.ts +105 -0
- package/testing/src/index.ts +141 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAdminOverrideSlot,
|
|
3
|
+
} from '@peanut-admin/admin/core'
|
|
4
|
+
import type {
|
|
5
|
+
AdminOverrideRegistry,
|
|
6
|
+
ApiAudience,
|
|
7
|
+
} from '@peanut-admin/admin/core'
|
|
8
|
+
import type { Component } from 'vue'
|
|
9
|
+
|
|
10
|
+
import { AdminShell, PlatformShell } from './layout'
|
|
11
|
+
|
|
12
|
+
export const WORKSPACE_SHELL_OVERRIDE_KEY = 'peanut.shell.service.workspace-component' as const
|
|
13
|
+
|
|
14
|
+
export type WorkspaceShellResolver = (audience: ApiAudience) => Component
|
|
15
|
+
|
|
16
|
+
const defaultWorkspaceShell: WorkspaceShellResolver = audience => (
|
|
17
|
+
audience === 'tenant' ? AdminShell : PlatformShell
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const isWorkspaceShellResolver = (value: unknown): value is WorkspaceShellResolver => (
|
|
21
|
+
typeof value === 'function'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const isVueComponent = (value: unknown): value is Component => {
|
|
25
|
+
if (typeof value === 'function') return true
|
|
26
|
+
if (typeof value !== 'object' || value === null) return false
|
|
27
|
+
const component = value as Record<string, unknown>
|
|
28
|
+
return typeof component.setup === 'function'
|
|
29
|
+
|| typeof component.render === 'function'
|
|
30
|
+
|| typeof component.template === 'string'
|
|
31
|
+
|| typeof component.__asyncLoader === 'function'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const ADMIN_SHELL_OVERRIDE_SLOTS = [
|
|
35
|
+
defineAdminOverrideSlot({
|
|
36
|
+
key: WORKSPACE_SHELL_OVERRIDE_KEY,
|
|
37
|
+
kind: 'service',
|
|
38
|
+
contractVersion: '1.0.0',
|
|
39
|
+
defaultValue: defaultWorkspaceShell,
|
|
40
|
+
validate: isWorkspaceShellResolver,
|
|
41
|
+
}),
|
|
42
|
+
] as const
|
|
43
|
+
|
|
44
|
+
export type AdminShellOverrideRegistry = AdminOverrideRegistry<typeof ADMIN_SHELL_OVERRIDE_SLOTS>
|
|
45
|
+
|
|
46
|
+
export const resolveWorkspaceShell = (
|
|
47
|
+
registry: AdminShellOverrideRegistry,
|
|
48
|
+
audience: ApiAudience,
|
|
49
|
+
): Component => {
|
|
50
|
+
const component = registry.get(WORKSPACE_SHELL_OVERRIDE_KEY)(audience)
|
|
51
|
+
if (!isVueComponent(component)) throw new Error('ADMIN_SHELL_OVERRIDE_RESULT_INVALID')
|
|
52
|
+
return component
|
|
53
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ElButton } from 'element-plus'
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
|
+
import type { PropType } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface StateDefaults {
|
|
6
|
+
title: string
|
|
7
|
+
message: string
|
|
8
|
+
actionLabel?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const createStateComponent = (name: string, state: string, defaults: StateDefaults) => defineComponent({
|
|
12
|
+
name,
|
|
13
|
+
props: {
|
|
14
|
+
title: { type: String, default: defaults.title },
|
|
15
|
+
message: { type: String, default: defaults.message },
|
|
16
|
+
requestId: { type: String, default: null },
|
|
17
|
+
retryAfter: { type: String as PropType<string | null>, default: null },
|
|
18
|
+
actionLabel: { type: String, default: defaults.actionLabel ?? null },
|
|
19
|
+
onAction: { type: Function as PropType<() => void>, default: null },
|
|
20
|
+
},
|
|
21
|
+
emits: {
|
|
22
|
+
action: () => true,
|
|
23
|
+
},
|
|
24
|
+
setup(props, { emit, slots }) {
|
|
25
|
+
return () => h('section', {
|
|
26
|
+
class: ['pa-state', `pa-state--${name.replace(/State$/, '').toLowerCase()}`],
|
|
27
|
+
'data-state': state,
|
|
28
|
+
role: 'status',
|
|
29
|
+
'aria-live': 'polite',
|
|
30
|
+
}, [
|
|
31
|
+
h('h2', { class: 'pa-state__title' }, props.title),
|
|
32
|
+
h('p', { class: 'pa-state__message' }, props.message),
|
|
33
|
+
props.requestId === null
|
|
34
|
+
? null
|
|
35
|
+
: h('p', { class: 'pa-state__request-id' }, `Request ID: ${props.requestId}`),
|
|
36
|
+
props.retryAfter === null
|
|
37
|
+
? null
|
|
38
|
+
: h('p', { class: 'pa-state__retry-after' }, `Retry after: ${props.retryAfter}`),
|
|
39
|
+
slots.default?.(),
|
|
40
|
+
props.actionLabel === null
|
|
41
|
+
? null
|
|
42
|
+
: h(ElButton, {
|
|
43
|
+
onClick: () => {
|
|
44
|
+
emit('action')
|
|
45
|
+
},
|
|
46
|
+
}, () => props.actionLabel),
|
|
47
|
+
])
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const EmptyState = createStateComponent('EmptyState', 'empty', {
|
|
52
|
+
title: 'No data',
|
|
53
|
+
message: 'There is nothing to display.',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const ForbiddenState = createStateComponent('ForbiddenState', 'forbidden', {
|
|
57
|
+
title: 'Access denied',
|
|
58
|
+
message: 'You do not have permission to view this page.',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export const NotFoundState = createStateComponent('NotFoundState', 'not-found', {
|
|
62
|
+
title: 'Not found',
|
|
63
|
+
message: 'The requested resource is unavailable.',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const ModuleUnavailableState = createStateComponent('ModuleUnavailableState', 'module-unavailable', {
|
|
67
|
+
title: 'Module unavailable',
|
|
68
|
+
message: 'This module is currently unavailable.',
|
|
69
|
+
actionLabel: 'Retry',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const ConflictState = createStateComponent('ConflictState', 'conflict', {
|
|
73
|
+
title: 'Content changed',
|
|
74
|
+
message: 'Reload the latest version before continuing.',
|
|
75
|
+
actionLabel: 'Reload',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export const RateLimitState = createStateComponent('RateLimitState', 'rate-limit', {
|
|
79
|
+
title: 'Too many requests',
|
|
80
|
+
message: 'Wait before trying again.',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export const ServiceUnavailableState = createStateComponent('ServiceUnavailableState', 'service-unavailable', {
|
|
84
|
+
title: 'Service unavailable',
|
|
85
|
+
message: 'The service is temporarily unavailable.',
|
|
86
|
+
actionLabel: 'Retry',
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export const SessionExpiredState = createStateComponent('SessionExpiredState', 'session-expired', {
|
|
90
|
+
title: 'Session expired',
|
|
91
|
+
message: 'Sign in again to continue.',
|
|
92
|
+
actionLabel: 'Sign in',
|
|
93
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Framework-neutral route data required by an admin workspace tab. */
|
|
2
|
+
export interface ShellTabRoute {
|
|
3
|
+
name: unknown
|
|
4
|
+
fullPath: string
|
|
5
|
+
query?: unknown
|
|
6
|
+
meta?: {
|
|
7
|
+
locale?: string
|
|
8
|
+
ignoreCache?: boolean | undefined
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ShellTab {
|
|
13
|
+
title: string
|
|
14
|
+
name: string
|
|
15
|
+
fullPath: string
|
|
16
|
+
query?: unknown
|
|
17
|
+
ignoreCache?: boolean | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ShellTabState {
|
|
21
|
+
tagList: ShellTab[]
|
|
22
|
+
cacheTabList: Set<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const tabFromRoute = (route: ShellTabRoute): ShellTab => ({
|
|
26
|
+
title: route.meta?.locale || '',
|
|
27
|
+
name: String(route.name),
|
|
28
|
+
fullPath: route.fullPath,
|
|
29
|
+
query: route.query,
|
|
30
|
+
ignoreCache: route.meta?.ignoreCache,
|
|
31
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* eslint-disable vue/one-component-per-file */
|
|
2
|
+
|
|
3
|
+
import type { TargetCandidate, TypedTarget } from '@peanut-admin/admin/core'
|
|
4
|
+
import { ElOption, ElPagination, ElSelect } from 'element-plus'
|
|
5
|
+
import { computed, defineComponent, h } from 'vue'
|
|
6
|
+
import type { Component, PropType } from 'vue'
|
|
7
|
+
|
|
8
|
+
const SelectComponent = ElSelect as unknown as Component
|
|
9
|
+
const OptionComponent = ElOption as unknown as Component
|
|
10
|
+
const PaginationComponent = ElPagination as unknown as Component
|
|
11
|
+
|
|
12
|
+
const targetKey = (target: TypedTarget): string => JSON.stringify([
|
|
13
|
+
target.target_resource_key,
|
|
14
|
+
target.target_role,
|
|
15
|
+
target.target_id,
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export const TargetSelector = defineComponent({
|
|
19
|
+
name: 'TargetSelector',
|
|
20
|
+
props: {
|
|
21
|
+
modelValue: {
|
|
22
|
+
type: Array as PropType<readonly TypedTarget[]>,
|
|
23
|
+
default: () => [],
|
|
24
|
+
},
|
|
25
|
+
candidates: {
|
|
26
|
+
type: Array as PropType<readonly TargetCandidate[]>,
|
|
27
|
+
default: () => [],
|
|
28
|
+
},
|
|
29
|
+
multiple: { type: Boolean, default: false },
|
|
30
|
+
loading: { type: Boolean, default: false },
|
|
31
|
+
disabled: { type: Boolean, default: false },
|
|
32
|
+
placeholder: { type: String, default: 'Select target' },
|
|
33
|
+
page: { type: Number, default: 1 },
|
|
34
|
+
pageSize: { type: Number, default: 20 },
|
|
35
|
+
total: { type: Number, default: 0 },
|
|
36
|
+
},
|
|
37
|
+
emits: {
|
|
38
|
+
'update:modelValue': (targets: readonly TypedTarget[]) => Array.isArray(targets),
|
|
39
|
+
search: (query: string) => typeof query === 'string',
|
|
40
|
+
'page-change': (page: number) => Number.isInteger(page) && page >= 1,
|
|
41
|
+
},
|
|
42
|
+
setup(props, { emit }) {
|
|
43
|
+
const candidateMap = computed(() => new Map(
|
|
44
|
+
props.candidates.map(candidate => [targetKey(candidate), candidate]),
|
|
45
|
+
))
|
|
46
|
+
const selectedKeys = computed(() => props.modelValue.map(targetKey))
|
|
47
|
+
const updateSelection = (value: unknown): void => {
|
|
48
|
+
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
49
|
+
(candidate): candidate is string => typeof candidate === 'string',
|
|
50
|
+
)
|
|
51
|
+
const targets = values.flatMap(key => {
|
|
52
|
+
const candidate = candidateMap.value.get(key)
|
|
53
|
+
return candidate === undefined
|
|
54
|
+
? []
|
|
55
|
+
: [{
|
|
56
|
+
target_resource_key: candidate.target_resource_key,
|
|
57
|
+
target_role: candidate.target_role,
|
|
58
|
+
target_id: candidate.target_id,
|
|
59
|
+
}]
|
|
60
|
+
})
|
|
61
|
+
emit('update:modelValue', props.multiple ? targets : targets.slice(0, 1))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return () => h('div', { class: 'pa-target-selector' }, [
|
|
65
|
+
h(SelectComponent, {
|
|
66
|
+
modelValue: props.multiple ? selectedKeys.value : (selectedKeys.value[0] ?? null),
|
|
67
|
+
'onUpdate:modelValue': updateSelection,
|
|
68
|
+
multiple: props.multiple,
|
|
69
|
+
filterable: true,
|
|
70
|
+
remote: true,
|
|
71
|
+
remoteMethod: (query: string) => emit('search', query),
|
|
72
|
+
loading: props.loading,
|
|
73
|
+
disabled: props.disabled,
|
|
74
|
+
placeholder: props.placeholder,
|
|
75
|
+
class: 'pa-target-selector__select',
|
|
76
|
+
}, () => props.candidates.map(candidate => h(OptionComponent, {
|
|
77
|
+
key: targetKey(candidate),
|
|
78
|
+
value: targetKey(candidate),
|
|
79
|
+
label: candidate.label,
|
|
80
|
+
}))),
|
|
81
|
+
props.total <= props.pageSize
|
|
82
|
+
? null
|
|
83
|
+
: h(PaginationComponent, {
|
|
84
|
+
class: 'pa-target-selector__pagination',
|
|
85
|
+
currentPage: props.page,
|
|
86
|
+
pageSize: props.pageSize,
|
|
87
|
+
total: props.total,
|
|
88
|
+
layout: 'prev, pager, next',
|
|
89
|
+
'onUpdate:currentPage': (page: number) => emit('page-change', page),
|
|
90
|
+
}),
|
|
91
|
+
])
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export type TargetScopeMode = 'zero' | 'single' | 'multiple' | 'aggregate'
|
|
96
|
+
|
|
97
|
+
export const TargetScopeSummary = defineComponent({
|
|
98
|
+
name: 'TargetScopeSummary',
|
|
99
|
+
props: {
|
|
100
|
+
mode: {
|
|
101
|
+
type: String as PropType<TargetScopeMode>,
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
availableCount: { type: Number, required: true },
|
|
105
|
+
selectedCount: { type: Number, default: 0 },
|
|
106
|
+
digest: { type: String, default: null },
|
|
107
|
+
},
|
|
108
|
+
setup(props) {
|
|
109
|
+
const message = computed(() => {
|
|
110
|
+
switch (props.mode) {
|
|
111
|
+
case 'zero':
|
|
112
|
+
return 'No available targets'
|
|
113
|
+
case 'single':
|
|
114
|
+
return '1 available target'
|
|
115
|
+
case 'aggregate':
|
|
116
|
+
return `Read-only aggregate across ${props.availableCount} targets`
|
|
117
|
+
default:
|
|
118
|
+
return `${props.selectedCount} of ${props.availableCount} targets selected`
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return () => h('div', {
|
|
123
|
+
class: ['pa-target-scope-summary', `is-${props.mode}`],
|
|
124
|
+
role: 'status',
|
|
125
|
+
'aria-label': 'Target scope',
|
|
126
|
+
}, message.value)
|
|
127
|
+
},
|
|
128
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const SHELL_THEME_TOKENS = {
|
|
2
|
+
headerHeight: '--pa-shell-header-height',
|
|
3
|
+
sidebarWidth: '--pa-shell-sidebar-width',
|
|
4
|
+
sidebarCollapsedWidth: '--pa-shell-sidebar-collapsed-width',
|
|
5
|
+
contentMaxWidth: '--pa-shell-content-max-width',
|
|
6
|
+
surfaceColor: '--pa-shell-surface-color',
|
|
7
|
+
borderColor: '--pa-shell-border-color',
|
|
8
|
+
textColor: '--pa-shell-text-color',
|
|
9
|
+
mutedTextColor: '--pa-shell-muted-text-color',
|
|
10
|
+
focusColor: '--pa-shell-focus-color',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export type ShellThemeToken = typeof SHELL_THEME_TOKENS[keyof typeof SHELL_THEME_TOKENS]
|
|
14
|
+
|
|
15
|
+
export type ShellSlotName = 'header' | 'sidebar' | 'breadcrumb' | 'tabs' | 'default'
|