@nocobase/app-template-default 1.0.0-beta.89 → 1.0.0-beta.91
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/.agents/rules/shell.md +4 -0
- package/LICENSE.txt +129 -0
- package/client/lib/navigation-transform.ts +29 -1
- package/client/locales/system/en-US.ts +4 -0
- package/client/locales/system/zh-CN.ts +4 -0
- package/client/pages/home/index.tsx +66 -51
- package/client/pages/settings/database-explorer/en-US.ts +33 -0
- package/client/pages/settings/database-explorer/index.tsx +358 -0
- package/client/pages/settings/database-explorer/routes.ts +14 -0
- package/client/pages/settings/database-explorer/zh-CN.ts +30 -0
- package/client/plugins.ts +8 -1
- package/client/routes.ts +2 -1
- package/config.example.yml +17 -11
- package/index.html +8 -11
- package/package.json +26 -26
- package/server/config/auth.ts +1 -1
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +16 -8
- package/server/config/queue.ts +14 -13
- package/server/config/realtime.ts +4 -0
- package/tests/client/client-routes.test.ts +5 -3
- package/tests/client/database-explorer-integration.test.tsx +30 -0
- package/tests/client/database-explorer.test.tsx +152 -0
- package/tests/client/foundation-integration.test.ts +71 -29
- package/tests/client/home.test.tsx +26 -85
- package/tests/client/layout-i18n.test.tsx +83 -2
- package/tests/client/settings-navigation.test.ts +20 -0
- package/tests/helpers/authenticated-client.tsx +7 -0
- package/tests/project/bootstrap-loading.test.ts +60 -0
- package/tests/server/app-server.test.ts +18 -12
- package/tests/server/auth-email-config.test.ts +21 -0
- package/tests/server/backends-config.test.ts +118 -0
- package/tests/server/concurrency.test.ts +1 -1
- package/tests/server/config.test.ts +22 -14
- package/tests/server/lifecycle.test.ts +2 -2
- package/tests/server/realtime-config.test.ts +114 -0
- package/client/pages/home/capabilities.ts +0 -62
- package/client/pages/home/prompt-dialog.tsx +0 -122
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { ApiClientError, useApiClient } from '@nocobase/app-client';
|
|
2
|
+
import { DatabaseExplorerClient } from '@nocobase/app-plugin-database-explorer/client';
|
|
3
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Link, useSearchParams } from 'react-router';
|
|
12
|
+
import { PageContainer } from '#components/page-container';
|
|
13
|
+
import { PageHeader } from '#components/page-header';
|
|
14
|
+
import { Button } from '#components/ui/button';
|
|
15
|
+
import { Input } from '#components/ui/input';
|
|
16
|
+
import {
|
|
17
|
+
Select,
|
|
18
|
+
SelectContent,
|
|
19
|
+
SelectItem,
|
|
20
|
+
SelectTrigger,
|
|
21
|
+
SelectValue,
|
|
22
|
+
} from '#components/ui/select';
|
|
23
|
+
import {
|
|
24
|
+
Table,
|
|
25
|
+
TableBody,
|
|
26
|
+
TableCell,
|
|
27
|
+
TableHead,
|
|
28
|
+
TableHeader,
|
|
29
|
+
TableRow,
|
|
30
|
+
} from '#components/ui/table';
|
|
31
|
+
|
|
32
|
+
/** A keyed read boundary prevents a late response from replacing a newer selection. */
|
|
33
|
+
function Read<T>({
|
|
34
|
+
load,
|
|
35
|
+
children,
|
|
36
|
+
}: {
|
|
37
|
+
load: () => Promise<T>;
|
|
38
|
+
children: (data: T) => ReactNode;
|
|
39
|
+
}) {
|
|
40
|
+
const { t } = useTranslation();
|
|
41
|
+
const [attempt, setAttempt] = useState(0);
|
|
42
|
+
const [result, setResult] = useState<{ data: T } | { error: unknown }>();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let active = true;
|
|
45
|
+
load().then(
|
|
46
|
+
(data) => {
|
|
47
|
+
if (active) setResult({ data });
|
|
48
|
+
},
|
|
49
|
+
(error: unknown) => {
|
|
50
|
+
if (active) setResult({ error });
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
return () => {
|
|
54
|
+
active = false;
|
|
55
|
+
};
|
|
56
|
+
}, [load, attempt]);
|
|
57
|
+
if (!result)
|
|
58
|
+
return (
|
|
59
|
+
<p role='status' className='p-4 text-sm text-muted-foreground'>
|
|
60
|
+
{t('databaseExplorer.loading')}
|
|
61
|
+
</p>
|
|
62
|
+
);
|
|
63
|
+
if ('error' in result) {
|
|
64
|
+
const status =
|
|
65
|
+
result.error instanceof ApiClientError ? result.error.status : undefined;
|
|
66
|
+
const key =
|
|
67
|
+
status === 401 ? 'expired' : status === 403 ? 'forbidden' : 'failed';
|
|
68
|
+
return (
|
|
69
|
+
<div role='alert' className='space-y-3 p-4'>
|
|
70
|
+
<p>{t(`databaseExplorer.${key}`)}</p>
|
|
71
|
+
{status === 401 ? (
|
|
72
|
+
<Link to='/login'>{t('databaseExplorer.signIn')}</Link>
|
|
73
|
+
) : status !== 403 ? (
|
|
74
|
+
<Button
|
|
75
|
+
variant='outline'
|
|
76
|
+
onClick={() => {
|
|
77
|
+
setResult(undefined);
|
|
78
|
+
setAttempt((value) => value + 1);
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{t('databaseExplorer.retry')}
|
|
82
|
+
</Button>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return children(result.data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function DatabaseExplorerPage() {
|
|
91
|
+
const { t } = useTranslation();
|
|
92
|
+
const api = useApiClient();
|
|
93
|
+
const client = useMemo(() => new DatabaseExplorerClient(api), [api]);
|
|
94
|
+
const load = useCallback(() => client.connections(), [client]);
|
|
95
|
+
const [params, setParams] = useSearchParams();
|
|
96
|
+
return (
|
|
97
|
+
<PageContainer className='space-y-6'>
|
|
98
|
+
<PageHeader
|
|
99
|
+
title={t('databaseExplorer.title')}
|
|
100
|
+
description={t('databaseExplorer.description')}
|
|
101
|
+
/>
|
|
102
|
+
<Read load={load}>
|
|
103
|
+
{({ items, default: defaultConnection }) => {
|
|
104
|
+
if (!items.length)
|
|
105
|
+
return <p>{t('databaseExplorer.emptyConnections')}</p>;
|
|
106
|
+
const connection =
|
|
107
|
+
params.get('connection') ?? defaultConnection ?? items[0].name;
|
|
108
|
+
return (
|
|
109
|
+
<div className='space-y-6'>
|
|
110
|
+
<div className='max-w-sm space-y-2'>
|
|
111
|
+
<label
|
|
112
|
+
htmlFor='database-connection'
|
|
113
|
+
className='text-sm font-medium'
|
|
114
|
+
>
|
|
115
|
+
{t('databaseExplorer.connections')}
|
|
116
|
+
</label>
|
|
117
|
+
<Select
|
|
118
|
+
value={connection}
|
|
119
|
+
onValueChange={(value) => {
|
|
120
|
+
if (value) setParams({ connection: value });
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<SelectTrigger id='database-connection' className='w-full'>
|
|
124
|
+
<SelectValue />
|
|
125
|
+
</SelectTrigger>
|
|
126
|
+
<SelectContent>
|
|
127
|
+
{items.map((item) => (
|
|
128
|
+
<SelectItem key={item.name} value={item.name}>
|
|
129
|
+
{item.name} · {item.dialect}
|
|
130
|
+
{item.isDefault
|
|
131
|
+
? ` · ${t('databaseExplorer.defaultConnection')}`
|
|
132
|
+
: ''}
|
|
133
|
+
</SelectItem>
|
|
134
|
+
))}
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</div>
|
|
138
|
+
<ConnectionTables
|
|
139
|
+
key={connection}
|
|
140
|
+
client={client}
|
|
141
|
+
connection={connection}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
146
|
+
</Read>
|
|
147
|
+
</PageContainer>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ConnectionTables({
|
|
152
|
+
client,
|
|
153
|
+
connection,
|
|
154
|
+
}: {
|
|
155
|
+
client: DatabaseExplorerClient;
|
|
156
|
+
connection: string;
|
|
157
|
+
}) {
|
|
158
|
+
const { t } = useTranslation();
|
|
159
|
+
const [params, setParams] = useSearchParams();
|
|
160
|
+
const selected = params.get('table');
|
|
161
|
+
const query = params.get('q') ?? '';
|
|
162
|
+
const load = useCallback(
|
|
163
|
+
() => client.allCollections(connection),
|
|
164
|
+
[client, connection],
|
|
165
|
+
);
|
|
166
|
+
return (
|
|
167
|
+
<div className='grid min-w-0 gap-6 lg:grid-cols-[minmax(14rem,1fr)_minmax(0,3fr)]'>
|
|
168
|
+
<section
|
|
169
|
+
aria-label={t('databaseExplorer.tables')}
|
|
170
|
+
className='min-w-0 space-y-3'
|
|
171
|
+
>
|
|
172
|
+
<h2 className='font-heading text-base font-semibold'>
|
|
173
|
+
{t('databaseExplorer.tables')}
|
|
174
|
+
</h2>
|
|
175
|
+
<Input
|
|
176
|
+
aria-label={t('databaseExplorer.searchTables')}
|
|
177
|
+
placeholder={t('databaseExplorer.searchTables')}
|
|
178
|
+
value={query}
|
|
179
|
+
onChange={(event) => {
|
|
180
|
+
setParams(
|
|
181
|
+
(previous) => {
|
|
182
|
+
const next = new URLSearchParams(previous);
|
|
183
|
+
next.set('q', event.target.value);
|
|
184
|
+
return next;
|
|
185
|
+
},
|
|
186
|
+
{ replace: true },
|
|
187
|
+
);
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
<Read load={load}>
|
|
191
|
+
{({ items, truncated }) => {
|
|
192
|
+
const matches = items.filter((item) =>
|
|
193
|
+
`${item.name} ${item.tableName} ${item.title ?? ''}`
|
|
194
|
+
.toLocaleLowerCase()
|
|
195
|
+
.includes(query.toLocaleLowerCase()),
|
|
196
|
+
);
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
{truncated ? (
|
|
200
|
+
<p role='status' className='text-sm text-muted-foreground'>
|
|
201
|
+
{t('databaseExplorer.truncated')}
|
|
202
|
+
</p>
|
|
203
|
+
) : null}
|
|
204
|
+
{matches.length ? (
|
|
205
|
+
<ul className='max-h-96 space-y-1 overflow-auto rounded-lg border p-2'>
|
|
206
|
+
{matches.map((item) => (
|
|
207
|
+
<li key={item.name}>
|
|
208
|
+
<Button
|
|
209
|
+
variant={
|
|
210
|
+
selected === item.name ? 'secondary' : 'ghost'
|
|
211
|
+
}
|
|
212
|
+
className='h-auto w-full justify-start whitespace-normal break-all text-left'
|
|
213
|
+
aria-pressed={selected === item.name}
|
|
214
|
+
onClick={() => {
|
|
215
|
+
setParams((previous) => {
|
|
216
|
+
const next = new URLSearchParams(previous);
|
|
217
|
+
next.set('connection', connection);
|
|
218
|
+
next.set('table', item.name);
|
|
219
|
+
return next;
|
|
220
|
+
});
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{item.title
|
|
224
|
+
? `${item.title} (${item.name})`
|
|
225
|
+
: item.name}
|
|
226
|
+
</Button>
|
|
227
|
+
</li>
|
|
228
|
+
))}
|
|
229
|
+
</ul>
|
|
230
|
+
) : (
|
|
231
|
+
<p className='text-sm text-muted-foreground'>
|
|
232
|
+
{t('databaseExplorer.emptyTables')}
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
</>
|
|
236
|
+
);
|
|
237
|
+
}}
|
|
238
|
+
</Read>
|
|
239
|
+
</section>
|
|
240
|
+
<section
|
|
241
|
+
aria-label={t('databaseExplorer.fields')}
|
|
242
|
+
className='min-w-0 space-y-3'
|
|
243
|
+
>
|
|
244
|
+
{selected ? (
|
|
245
|
+
<CollectionFields
|
|
246
|
+
key={selected}
|
|
247
|
+
client={client}
|
|
248
|
+
connection={connection}
|
|
249
|
+
collection={selected}
|
|
250
|
+
/>
|
|
251
|
+
) : (
|
|
252
|
+
<p className='rounded-lg border border-dashed p-6 text-sm text-muted-foreground'>
|
|
253
|
+
{t('databaseExplorer.selectTable')}
|
|
254
|
+
</p>
|
|
255
|
+
)}
|
|
256
|
+
</section>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function CollectionFields({
|
|
262
|
+
client,
|
|
263
|
+
connection,
|
|
264
|
+
collection,
|
|
265
|
+
}: {
|
|
266
|
+
client: DatabaseExplorerClient;
|
|
267
|
+
connection: string;
|
|
268
|
+
collection: string;
|
|
269
|
+
}) {
|
|
270
|
+
const { t } = useTranslation();
|
|
271
|
+
const [query, setQuery] = useState('');
|
|
272
|
+
const load = useCallback(
|
|
273
|
+
() => client.collection(connection, collection),
|
|
274
|
+
[client, connection, collection],
|
|
275
|
+
);
|
|
276
|
+
return (
|
|
277
|
+
<>
|
|
278
|
+
<h2 className='break-all font-heading text-base font-semibold'>
|
|
279
|
+
{collection}
|
|
280
|
+
</h2>
|
|
281
|
+
<Input
|
|
282
|
+
aria-label={t('databaseExplorer.searchFields')}
|
|
283
|
+
placeholder={t('databaseExplorer.searchFields')}
|
|
284
|
+
value={query}
|
|
285
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
286
|
+
/>
|
|
287
|
+
<Read load={load}>
|
|
288
|
+
{({ collection: artifact }) => {
|
|
289
|
+
const fields = (artifact.collection.fields ?? []).filter((field) =>
|
|
290
|
+
`${field.name} ${field.title ?? ''}`
|
|
291
|
+
.toLocaleLowerCase()
|
|
292
|
+
.includes(query.toLocaleLowerCase()),
|
|
293
|
+
);
|
|
294
|
+
return (
|
|
295
|
+
<>
|
|
296
|
+
{artifact.warnings.length ? (
|
|
297
|
+
<p role='status' className='text-sm text-muted-foreground'>
|
|
298
|
+
{t('databaseExplorer.warnings')}
|
|
299
|
+
</p>
|
|
300
|
+
) : null}
|
|
301
|
+
{fields.length ? (
|
|
302
|
+
<div className='overflow-hidden rounded-lg border'>
|
|
303
|
+
<Table>
|
|
304
|
+
<TableHeader>
|
|
305
|
+
<TableRow>
|
|
306
|
+
{[
|
|
307
|
+
'name',
|
|
308
|
+
'type',
|
|
309
|
+
'nullable',
|
|
310
|
+
'primaryKey',
|
|
311
|
+
'target',
|
|
312
|
+
].map((key) => (
|
|
313
|
+
<TableHead key={key}>
|
|
314
|
+
{t(`databaseExplorer.${key}`)}
|
|
315
|
+
</TableHead>
|
|
316
|
+
))}
|
|
317
|
+
</TableRow>
|
|
318
|
+
</TableHeader>
|
|
319
|
+
<TableBody>
|
|
320
|
+
{fields.map((field) => (
|
|
321
|
+
<TableRow key={field.name}>
|
|
322
|
+
<TableCell className='font-medium'>
|
|
323
|
+
{field.name}
|
|
324
|
+
{field.title ? (
|
|
325
|
+
<span className='block text-muted-foreground'>
|
|
326
|
+
{field.title}
|
|
327
|
+
</span>
|
|
328
|
+
) : null}
|
|
329
|
+
</TableCell>
|
|
330
|
+
<TableCell>{field.type}</TableCell>
|
|
331
|
+
<TableCell>
|
|
332
|
+
{t(
|
|
333
|
+
`databaseExplorer.${field.nullable === undefined ? 'unknown' : field.nullable ? 'yes' : 'no'}`,
|
|
334
|
+
)}
|
|
335
|
+
</TableCell>
|
|
336
|
+
<TableCell>
|
|
337
|
+
{t(
|
|
338
|
+
`databaseExplorer.${field.primaryKey ? 'yes' : 'no'}`,
|
|
339
|
+
)}
|
|
340
|
+
</TableCell>
|
|
341
|
+
<TableCell>{field.target ?? '—'}</TableCell>
|
|
342
|
+
</TableRow>
|
|
343
|
+
))}
|
|
344
|
+
</TableBody>
|
|
345
|
+
</Table>
|
|
346
|
+
</div>
|
|
347
|
+
) : (
|
|
348
|
+
<p className='text-sm text-muted-foreground'>
|
|
349
|
+
{t('databaseExplorer.emptyFields')}
|
|
350
|
+
</p>
|
|
351
|
+
)}
|
|
352
|
+
</>
|
|
353
|
+
);
|
|
354
|
+
}}
|
|
355
|
+
</Read>
|
|
356
|
+
</>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineAppRoutes } from '@nocobase/app-client/plugins';
|
|
2
|
+
import { DATABASE_EXPLORER_ACCESS } from '@nocobase/app-plugin-database-explorer/client';
|
|
3
|
+
|
|
4
|
+
export default defineAppRoutes([
|
|
5
|
+
{
|
|
6
|
+
parent: 'settingsSystem',
|
|
7
|
+
name: 'databaseExplorer',
|
|
8
|
+
path: '/database-explorer',
|
|
9
|
+
auth: 'required',
|
|
10
|
+
authz: DATABASE_EXPLORER_ACCESS,
|
|
11
|
+
breadcrumb: { title: 'databaseExplorer.title' },
|
|
12
|
+
componentLoader: () => import('./index.js'),
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
title: '数据库浏览器',
|
|
3
|
+
description: '查看应用的数据库连接、数据表和字段。',
|
|
4
|
+
connections: '数据库连接',
|
|
5
|
+
defaultConnection: '默认',
|
|
6
|
+
tables: '数据表',
|
|
7
|
+
searchTables: '搜索数据表',
|
|
8
|
+
fields: '字段',
|
|
9
|
+
searchFields: '搜索字段',
|
|
10
|
+
name: '名称',
|
|
11
|
+
type: '类型',
|
|
12
|
+
nullable: '允许为空',
|
|
13
|
+
primaryKey: '主键',
|
|
14
|
+
target: '关联表',
|
|
15
|
+
yes: '是',
|
|
16
|
+
no: '否',
|
|
17
|
+
unknown: '未指定',
|
|
18
|
+
emptyConnections: '尚未配置数据库连接。',
|
|
19
|
+
emptyTables: '没有找到数据表。',
|
|
20
|
+
emptyFields: '没有找到字段。',
|
|
21
|
+
selectTable: '选择数据表以查看字段。',
|
|
22
|
+
loading: '正在加载…',
|
|
23
|
+
failed: '无法加载数据库信息。',
|
|
24
|
+
forbidden: '你没有浏览此数据库的权限。',
|
|
25
|
+
expired: '登录已过期,请重新登录后继续。',
|
|
26
|
+
signIn: '登录',
|
|
27
|
+
retry: '重试',
|
|
28
|
+
truncated: '当前仅加载了部分数据表,请缩小数据库范围后查看其余数据表。',
|
|
29
|
+
warnings: '部分字段未能完整解析,显示的结构可能不完整。',
|
|
30
|
+
};
|
package/client/plugins.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import foundation from '@nocobase/app-plugin-foundation/client';
|
|
2
|
+
import applicationManifest from '../package.json' with { type: 'json' };
|
|
2
3
|
import defaultAccess from '@nocobase/app-plugin-authz-default-access/client';
|
|
3
4
|
import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/client';
|
|
4
5
|
import restrictionRules from '@nocobase/app-plugin-authz-restriction-rules/client';
|
|
@@ -35,7 +36,13 @@ const clientPlugins: AppClientPlugins = defineClientPlugins([
|
|
|
35
36
|
file(),
|
|
36
37
|
scheduler(),
|
|
37
38
|
mail(),
|
|
38
|
-
|
|
39
|
+
// The manifest is what the capability guide lists: every plugin this application declares.
|
|
40
|
+
foundation({
|
|
41
|
+
settingsGroup: true,
|
|
42
|
+
manifest: applicationManifest,
|
|
43
|
+
guideSettingsPath: '/settings',
|
|
44
|
+
navigation: false,
|
|
45
|
+
}),
|
|
39
46
|
]);
|
|
40
47
|
|
|
41
48
|
export default clientPlugins;
|
package/client/routes.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import databaseExplorerRoutes from './pages/settings/database-explorer/routes.js';
|
|
1
2
|
import { createAppClientConfig } from '@nocobase/app-client';
|
|
2
3
|
import { readAppClientRuntimeConfig } from '@nocobase/app-client/runtime';
|
|
3
4
|
import { includeNotificationTemplateRoutes } from './pages/settings/notification-routes.js';
|
|
@@ -75,7 +76,7 @@ export function createApplicationRoutes(
|
|
|
75
76
|
): readonly AppClientRouteContribution[] {
|
|
76
77
|
const config = createAppClientConfig({ rawConfig });
|
|
77
78
|
return includeNotificationTemplateRoutes(
|
|
78
|
-
[appRoutes, settingsShellRoutes],
|
|
79
|
+
[appRoutes, settingsShellRoutes, databaseExplorerRoutes],
|
|
79
80
|
config.get<boolean>('notificationTemplates.invitationConfigured') === true,
|
|
80
81
|
);
|
|
81
82
|
}
|
package/config.example.yml
CHANGED
|
@@ -22,13 +22,19 @@
|
|
|
22
22
|
# max: 600
|
|
23
23
|
# window: 1m
|
|
24
24
|
|
|
25
|
-
# Realtime
|
|
25
|
+
# Realtime defaults to inMemory when broker is omitted. An explicit broker requires adapter.
|
|
26
|
+
# For local-only selection: realtime: { broker: { adapter: inMemory } }.
|
|
27
|
+
# For multiple processes, install @nocobase/app-plugin-realtime-redis and register its
|
|
28
|
+
# @nocobase/app-plugin-realtime-redis/server definition in server/plugins.ts on every instance.
|
|
29
|
+
# Realtime events are transient; clients refetch after subscription acknowledgement.
|
|
26
30
|
# realtime:
|
|
27
|
-
#
|
|
28
|
-
#
|
|
31
|
+
# broker:
|
|
32
|
+
# adapter: redis
|
|
33
|
+
# setupTimeoutMs: 5000
|
|
29
34
|
# connection:
|
|
30
35
|
# host: 127.0.0.1
|
|
31
36
|
# port: 6379
|
|
37
|
+
# commandTimeout: 5000
|
|
32
38
|
# namespace: my-app # Defaults to the application name; keep applications isolated.
|
|
33
39
|
# authorizationTimeoutMs: 5000
|
|
34
40
|
# sessionCheckIntervalMs: 30000
|
|
@@ -116,8 +122,7 @@ auth:
|
|
|
116
122
|
# Set app.publicOrigin before explicitly enabling automatic password recovery.
|
|
117
123
|
# passwordReset:
|
|
118
124
|
# enabled: true
|
|
119
|
-
#
|
|
120
|
-
# mailer: system # omit to use mailers.default
|
|
125
|
+
# mailer: system # omit to preserve existing default selection
|
|
121
126
|
# Existing inline auth.email and AUTH_EMAIL_* remain supported for compatibility.
|
|
122
127
|
emailAndPassword:
|
|
123
128
|
enabled: true
|
|
@@ -190,12 +195,13 @@ snowflake:
|
|
|
190
195
|
# Upgrading deployments must set adapter: redis explicitly; templates no longer supply Redis defaults.
|
|
191
196
|
# jobs:
|
|
192
197
|
# default: redis
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
198
|
+
# backends:
|
|
199
|
+
# redis:
|
|
200
|
+
# adapter: redis
|
|
201
|
+
# connection:
|
|
202
|
+
# host: 127.0.0.1
|
|
203
|
+
# port: 6379
|
|
204
|
+
# # namespace: my-app # defaults to the application name; set it when deployments share one Redis
|
|
199
205
|
|
|
200
206
|
# The jobs configuration the Scheduler plugin's schedules run on, by name. Left out, they follow jobs.default.
|
|
201
207
|
# scheduler:
|
package/index.html
CHANGED
|
@@ -32,9 +32,6 @@
|
|
|
32
32
|
transform-origin: 32px 32px;
|
|
33
33
|
animation: app-bootstrap-spin 1.1s linear infinite;
|
|
34
34
|
}
|
|
35
|
-
.app-bootstrap-loading span {
|
|
36
|
-
color: oklch(0.556 0 0);
|
|
37
|
-
}
|
|
38
35
|
|
|
39
36
|
html,
|
|
40
37
|
body {
|
|
@@ -60,11 +57,6 @@
|
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
@media (prefers-color-scheme: dark) {
|
|
64
|
-
.app-bootstrap-loading span {
|
|
65
|
-
color: oklch(0.708 0 0);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
60
|
@media (prefers-reduced-motion: reduce) {
|
|
69
61
|
.app-bootstrap-loading .nocobase-loading-orbit {
|
|
70
62
|
animation: none;
|
|
@@ -76,10 +68,16 @@
|
|
|
76
68
|
<body>
|
|
77
69
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
78
70
|
<div id="root">
|
|
71
|
+
<!--
|
|
72
|
+
The language is unknown until the client starts, so this placeholder shows no text. A screen reader still
|
|
73
|
+
perceives it as an indeterminate progress bar, a role it announces in its own language, named after the
|
|
74
|
+
brand in <title>, which reads the same in every language. The client's localized loading, a polite status,
|
|
75
|
+
replaces it once the language is ready, so nothing is announced twice.
|
|
76
|
+
-->
|
|
79
77
|
<div
|
|
80
78
|
class="app-bootstrap-loading"
|
|
81
|
-
role="
|
|
82
|
-
aria-label="
|
|
79
|
+
role="progressbar"
|
|
80
|
+
aria-label="NocoBase"
|
|
83
81
|
>
|
|
84
82
|
<svg
|
|
85
83
|
data-slot="loading-indicator"
|
|
@@ -114,7 +112,6 @@
|
|
|
114
112
|
</g>
|
|
115
113
|
</g>
|
|
116
114
|
</svg>
|
|
117
|
-
<span>Loading application…</span>
|
|
118
115
|
</div>
|
|
119
116
|
</div>
|
|
120
117
|
<script type="module" src="/client/index.tsx"></script>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.91",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.91"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@noble/hashes": "2.3.0",
|
|
77
77
|
"@nocobase/app-client": "^4.0.0-beta.9",
|
|
78
78
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
79
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
79
|
+
"@nocobase/app-skills": "0.1.0-beta.61",
|
|
80
80
|
"@nocobase/app-testing": "0.1.0-beta.8",
|
|
81
81
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
82
82
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
@@ -143,40 +143,40 @@
|
|
|
143
143
|
"better-auth": "1.7.5",
|
|
144
144
|
"hono": "^4.13.2",
|
|
145
145
|
"@nocobase/concurrency": "^0.1.0-beta.1",
|
|
146
|
-
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.
|
|
147
|
-
"@nocobase/app-plugin-agents": "^2.0.0-beta.
|
|
146
|
+
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.3",
|
|
147
|
+
"@nocobase/app-plugin-agents": "^2.0.0-beta.7",
|
|
148
148
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.21",
|
|
149
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.
|
|
150
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.
|
|
149
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.20",
|
|
150
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.37",
|
|
151
151
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.19",
|
|
152
|
-
"@nocobase/app-plugin-file": "^1.0.0-beta.
|
|
153
|
-
"@nocobase/app-plugin-foundation": "^0.1.0-beta.
|
|
154
|
-
"@nocobase/app-plugin-i18n": "^1.0.0-beta.
|
|
155
|
-
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.
|
|
156
|
-
"@nocobase/app-plugin-mail": "^1.0.0-beta.
|
|
157
|
-
"@nocobase/app-plugin-notification": "^2.0.0-beta.
|
|
158
|
-
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.
|
|
159
|
-
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.
|
|
160
|
-
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.
|
|
161
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
162
|
-
"@nocobase/app-server": "^5.0.0-beta.
|
|
152
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.30",
|
|
153
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.18",
|
|
154
|
+
"@nocobase/app-plugin-i18n": "^1.0.0-beta.22",
|
|
155
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.18",
|
|
156
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.22",
|
|
157
|
+
"@nocobase/app-plugin-notification": "^2.0.0-beta.6",
|
|
158
|
+
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.3",
|
|
159
|
+
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.19",
|
|
160
|
+
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.24",
|
|
161
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.17",
|
|
162
|
+
"@nocobase/app-server": "^5.0.0-beta.2",
|
|
163
163
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
164
|
-
"@nocobase/db": "^1.0.0-beta.
|
|
164
|
+
"@nocobase/db": "^1.0.0-beta.26",
|
|
165
165
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
166
166
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
167
167
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
168
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
169
|
-
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.
|
|
170
|
-
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.
|
|
168
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.20",
|
|
169
|
+
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.19",
|
|
170
|
+
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.18",
|
|
171
171
|
"@nocobase/authorization": "^1.0.0-beta.16",
|
|
172
|
-
"@nocobase/queue": "^1.0.0-beta.
|
|
173
|
-
"@nocobase/jobs": "^1.0.0-beta.
|
|
172
|
+
"@nocobase/queue": "^1.0.0-beta.11",
|
|
173
|
+
"@nocobase/jobs": "^1.0.0-beta.6",
|
|
174
174
|
"@nocobase/lifecycle": "^0.1.0-beta.4",
|
|
175
|
-
"@nocobase/i18n": "^2.0.0-beta.
|
|
175
|
+
"@nocobase/i18n": "^2.0.0-beta.2",
|
|
176
176
|
"@nocobase/caching": "^1.0.0-beta.4",
|
|
177
177
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
178
178
|
"@nocobase/app-cli": "^1.0.0-beta.32",
|
|
179
|
-
"@nocobase/realtime": "^1.0.0-beta.
|
|
179
|
+
"@nocobase/realtime": "^1.0.0-beta.2"
|
|
180
180
|
},
|
|
181
181
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
182
182
|
"imports": {
|
package/server/config/auth.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { bearer, deviceAuthorization, username } from 'better-auth/plugins';
|
|
|
12
12
|
*/
|
|
13
13
|
const CLI_CLIENT_IDS: readonly string[] = ['nocobase-cli'];
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// Select a named sender with passwordReset.mailer; defineAuthConfig retains AUTH_EMAIL_* for legacy inline senders.
|
|
16
16
|
// Automatic recovery requires passwordReset.enabled: true; leave the switch absent for custom callback compatibility.
|
|
17
17
|
const auth: AppConfigFactory<AuthConfig> = defineAuthConfig({
|
|
18
18
|
defaults: {
|
package/server/config/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import queue from './queue.js';
|
|
|
18
18
|
import jobs from './jobs.js';
|
|
19
19
|
import scheduler from './scheduler.js';
|
|
20
20
|
import caching from './caching.js';
|
|
21
|
+
import realtime from './realtime.js';
|
|
21
22
|
import i18n from './i18n.js';
|
|
22
23
|
import app from './app.js';
|
|
23
24
|
import api from './api.js';
|
|
@@ -43,6 +44,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
43
44
|
jobs: ReturnType<typeof jobs>;
|
|
44
45
|
scheduler: ReturnType<typeof scheduler>;
|
|
45
46
|
caching: ReturnType<typeof caching>;
|
|
47
|
+
realtime: ReturnType<typeof realtime>;
|
|
46
48
|
i18n: ReturnType<typeof i18n>;
|
|
47
49
|
app: ReturnType<typeof app>;
|
|
48
50
|
api: ReturnType<typeof api>;
|
|
@@ -67,6 +69,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
67
69
|
jobs,
|
|
68
70
|
scheduler,
|
|
69
71
|
caching,
|
|
72
|
+
realtime,
|
|
70
73
|
i18n,
|
|
71
74
|
app,
|
|
72
75
|
api,
|