@promptbook/cli 0.112.0-93 → 0.112.0-96

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 (52) hide show
  1. package/apps/agents-server/next.config.ts +8 -1
  2. package/apps/agents-server/playwright.config.ts +2 -0
  3. package/apps/agents-server/src/app/admin/_components/AdminConfigurationShell.tsx +13 -7
  4. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +225 -0
  5. package/apps/agents-server/src/app/admin/code-runners/page.tsx +14 -0
  6. package/apps/agents-server/src/app/admin/database/DatabaseAdminClient.tsx +38 -0
  7. package/apps/agents-server/src/app/admin/database/DatabaseAdminStudioSurface.tsx +42 -0
  8. package/apps/agents-server/src/app/admin/database/page.tsx +33 -0
  9. package/apps/agents-server/src/app/admin/environment/EnvironmentVariablesClient.tsx +259 -0
  10. package/apps/agents-server/src/app/admin/environment/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/logs/LogsClient.tsx +78 -0
  12. package/apps/agents-server/src/app/admin/logs/page.tsx +14 -0
  13. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +64 -33
  14. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  15. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +15 -2
  16. package/apps/agents-server/src/app/admin/servers/page.tsx +3 -3
  17. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +12 -2
  18. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +104 -0
  19. package/apps/agents-server/src/app/api/admin/database/studio/route.ts +113 -0
  20. package/apps/agents-server/src/app/api/admin/environment/route.ts +65 -0
  21. package/apps/agents-server/src/app/api/admin/logs/route.ts +24 -0
  22. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +79 -3
  23. package/apps/agents-server/src/app/api/admin/servers/route.ts +36 -1
  24. package/apps/agents-server/src/app/layout.tsx +1 -0
  25. package/apps/agents-server/src/app/page.tsx +101 -1
  26. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +27 -4
  27. package/apps/agents-server/src/database/$provideClientSql.ts +2 -6
  28. package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +273 -0
  29. package/apps/agents-server/src/database/resolvePostgresConnectionString.ts +26 -0
  30. package/apps/agents-server/src/database/sqlite/$provideAgentsServerSqliteDatabase.ts +83 -0
  31. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +20 -71
  32. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +5 -0
  33. package/apps/agents-server/src/languages/translations/czech.yaml +5 -0
  34. package/apps/agents-server/src/languages/translations/english.yaml +5 -0
  35. package/apps/agents-server/src/tools/$provideServer.ts +27 -0
  36. package/apps/agents-server/src/utils/serverRegistry.ts +20 -1
  37. package/apps/agents-server/src/utils/session.ts +123 -2
  38. package/apps/agents-server/src/utils/vpsConfiguration.ts +550 -0
  39. package/esm/index.es.js +1 -1
  40. package/esm/index.es.js.map +1 -1
  41. package/esm/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  42. package/esm/src/version.d.ts +1 -1
  43. package/package.json +3 -1
  44. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +9 -398
  45. package/src/book-components/Chat/utils/renderMarkdown.ts +323 -8
  46. package/src/other/templates/getTemplatesPipelineCollection.ts +712 -829
  47. package/src/version.ts +2 -2
  48. package/src/versions.txt +2 -0
  49. package/umd/index.umd.js +1 -1
  50. package/umd/index.umd.js.map +1 -1
  51. package/umd/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
  52. package/umd/src/version.d.ts +1 -1
@@ -0,0 +1,259 @@
1
+ 'use client';
2
+
3
+ import { EyeOff, Loader2, Save, ServerCog } from 'lucide-react';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { Card } from '../../../components/Homepage/Card';
6
+
7
+ /**
8
+ * One environment variable returned by the admin API.
9
+ */
10
+ type EnvironmentVariableRecord = {
11
+ readonly key: string;
12
+ readonly value: string;
13
+ readonly isSensitive: boolean;
14
+ readonly isDefined: boolean;
15
+ };
16
+
17
+ /**
18
+ * Environment API response consumed by the client page.
19
+ */
20
+ type EnvironmentVariablesResponse = {
21
+ readonly envFilePath: string;
22
+ readonly variables: ReadonlyArray<EnvironmentVariableRecord>;
23
+ readonly canEdit: boolean;
24
+ readonly error?: string;
25
+ readonly applyResult?: {
26
+ readonly isAvailable: boolean;
27
+ readonly output: string;
28
+ } | null;
29
+ };
30
+
31
+ /**
32
+ * Shared input styling for environment rows.
33
+ */
34
+ const INPUT_CLASS_NAME =
35
+ 'w-full rounded-md border border-gray-300 px-3 py-2 text-sm font-mono text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:bg-gray-50 disabled:text-gray-500';
36
+
37
+ /**
38
+ * Placeholder shown instead of sensitive environment values.
39
+ */
40
+ const HIDDEN_ENVIRONMENT_VALUE = '********';
41
+
42
+ /**
43
+ * Browser UI for standalone VPS environment variables.
44
+ */
45
+ export function EnvironmentVariablesClient() {
46
+ const [envFilePath, setEnvFilePath] = useState('');
47
+ const [variables, setVariables] = useState<EnvironmentVariableRecord[]>([]);
48
+ const [draftValues, setDraftValues] = useState<Record<string, string>>({});
49
+ const [canEdit, setCanEdit] = useState(false);
50
+ const [isLoading, setIsLoading] = useState(true);
51
+ const [isSaving, setIsSaving] = useState(false);
52
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
53
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
54
+ const [applyOutput, setApplyOutput] = useState<string | null>(null);
55
+
56
+ const hasChanges = useMemo(
57
+ () => variables.some((variable) => draftValues[variable.key] !== variable.value),
58
+ [draftValues, variables],
59
+ );
60
+
61
+ useEffect(() => {
62
+ void loadVariables();
63
+ }, []);
64
+
65
+ /**
66
+ * Loads environment variables from the admin API.
67
+ */
68
+ async function loadVariables(): Promise<void> {
69
+ try {
70
+ setIsLoading(true);
71
+ setErrorMessage(null);
72
+ const response = await fetch('/api/admin/environment', { cache: 'no-store' });
73
+ const payload = (await response.json()) as EnvironmentVariablesResponse;
74
+
75
+ if (!response.ok) {
76
+ throw new Error(payload.error || 'Failed to load environment variables.');
77
+ }
78
+
79
+ setEnvFilePath(payload.envFilePath);
80
+ setVariables([...payload.variables]);
81
+ setDraftValues(Object.fromEntries(payload.variables.map((variable) => [variable.key, variable.value])));
82
+ setCanEdit(payload.canEdit);
83
+ } catch (error) {
84
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to load environment variables.');
85
+ } finally {
86
+ setIsLoading(false);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Persists current environment drafts.
92
+ *
93
+ * @param applyRuntimeConfiguration - Whether to run the VPS apply step after saving.
94
+ */
95
+ async function saveVariables(applyRuntimeConfiguration: boolean): Promise<void> {
96
+ try {
97
+ setIsSaving(true);
98
+ setErrorMessage(null);
99
+ setSuccessMessage(null);
100
+ setApplyOutput(null);
101
+
102
+ const response = await fetch('/api/admin/environment', {
103
+ method: 'PATCH',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ },
107
+ body: JSON.stringify({
108
+ variables: draftValues,
109
+ applyRuntimeConfiguration,
110
+ }),
111
+ });
112
+ const payload = (await response.json()) as EnvironmentVariablesResponse;
113
+
114
+ if (!response.ok) {
115
+ throw new Error(payload.error || 'Failed to save environment variables.');
116
+ }
117
+
118
+ setEnvFilePath(payload.envFilePath);
119
+ setVariables([...payload.variables]);
120
+ setDraftValues(Object.fromEntries(payload.variables.map((variable) => [variable.key, variable.value])));
121
+ setCanEdit(payload.canEdit);
122
+ setSuccessMessage('Environment variables were saved.');
123
+ setApplyOutput(payload.applyResult?.output || null);
124
+ } catch (error) {
125
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to save environment variables.');
126
+ } finally {
127
+ setIsSaving(false);
128
+ }
129
+ }
130
+
131
+ return (
132
+ <div className="space-y-6">
133
+ <Card className="hover:border-gray-200 hover:shadow-md">
134
+ <div className="space-y-2 text-sm text-slate-600">
135
+ <p>
136
+ These values are stored in the VPS-wide <span className="font-mono">.env</span> file and affect
137
+ the whole installed Agents Server process.
138
+ </p>
139
+ <p>
140
+ Sensitive values are always masked. To change a secret, type a new value; leaving the stars in
141
+ place keeps the current secret.
142
+ </p>
143
+ {envFilePath ? <p className="font-mono text-xs text-slate-500">{envFilePath}</p> : null}
144
+ </div>
145
+ </Card>
146
+
147
+ {!canEdit && (
148
+ <div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
149
+ You can view environment variables as an administrator. Editing is restricted to the super admin
150
+ authenticated with <span className="font-mono">ADMIN_PASSWORD</span>.
151
+ </div>
152
+ )}
153
+
154
+ {errorMessage && (
155
+ <div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
156
+ {errorMessage}
157
+ </div>
158
+ )}
159
+ {successMessage && (
160
+ <div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
161
+ {successMessage}
162
+ </div>
163
+ )}
164
+ {applyOutput && (
165
+ <pre className="max-h-72 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
166
+ {applyOutput}
167
+ </pre>
168
+ )}
169
+
170
+ <Card className="hover:border-gray-200 hover:shadow-md">
171
+ {isLoading ? (
172
+ <div className="py-10 text-center text-sm text-gray-500">Loading environment variables...</div>
173
+ ) : (
174
+ <div className="overflow-x-auto rounded-xl border border-gray-200">
175
+ <table className="min-w-full table-fixed divide-y divide-gray-200 text-sm">
176
+ <colgroup>
177
+ <col className="w-[18rem]" />
178
+ <col />
179
+ <col className="w-[10rem]" />
180
+ </colgroup>
181
+ <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-500">
182
+ <tr>
183
+ <th className="px-4 py-3 text-left font-semibold">Variable</th>
184
+ <th className="px-4 py-3 text-left font-semibold">Value</th>
185
+ <th className="px-4 py-3 text-left font-semibold">Status</th>
186
+ </tr>
187
+ </thead>
188
+ <tbody className="divide-y divide-gray-200 bg-white">
189
+ {variables.map((variable) => (
190
+ <tr key={variable.key}>
191
+ <td className="px-4 py-3 align-top font-mono text-sm font-semibold text-slate-800">
192
+ {variable.key}
193
+ </td>
194
+ <td className="px-4 py-3 align-top">
195
+ <div className="relative">
196
+ <input
197
+ type={variable.isSensitive ? 'password' : 'text'}
198
+ value={draftValues[variable.key] ?? ''}
199
+ onChange={(event) =>
200
+ setDraftValues((currentDraftValues) => ({
201
+ ...currentDraftValues,
202
+ [variable.key]: event.target.value,
203
+ }))
204
+ }
205
+ disabled={!canEdit || isSaving}
206
+ className={`${INPUT_CLASS_NAME} ${
207
+ variable.isSensitive ? 'pr-10 tracking-wider' : ''
208
+ }`}
209
+ placeholder={variable.isSensitive ? HIDDEN_ENVIRONMENT_VALUE : ''}
210
+ />
211
+ {variable.isSensitive && (
212
+ <EyeOff className="pointer-events-none absolute right-3 top-2.5 h-4 w-4 text-slate-400" />
213
+ )}
214
+ </div>
215
+ </td>
216
+ <td className="px-4 py-3 align-top">
217
+ <span
218
+ className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${
219
+ variable.isDefined
220
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
221
+ : 'border-slate-200 bg-slate-50 text-slate-500'
222
+ }`}
223
+ >
224
+ {variable.isDefined ? 'Configured' : 'Empty'}
225
+ </span>
226
+ </td>
227
+ </tr>
228
+ ))}
229
+ </tbody>
230
+ </table>
231
+ </div>
232
+ )}
233
+ </Card>
234
+
235
+ {canEdit && (
236
+ <div className="flex flex-wrap items-center gap-3">
237
+ <button
238
+ type="button"
239
+ onClick={() => void saveVariables(false)}
240
+ disabled={isSaving || !hasChanges}
241
+ className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
242
+ >
243
+ {isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
244
+ Save .env
245
+ </button>
246
+ <button
247
+ type="button"
248
+ onClick={() => void saveVariables(true)}
249
+ disabled={isSaving}
250
+ className="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
251
+ >
252
+ {isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ServerCog className="h-4 w-4" />}
253
+ Save and apply VPS configuration
254
+ </button>
255
+ </div>
256
+ )}
257
+ </div>
258
+ );
259
+ }
@@ -0,0 +1,21 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
+ import { AdminConfigurationShell } from '../_components/AdminConfigurationShell';
4
+ import { EnvironmentVariablesClient } from './EnvironmentVariablesClient';
5
+
6
+ /**
7
+ * Page for viewing and editing standalone VPS `.env` variables.
8
+ */
9
+ export default async function EnvironmentVariablesPage() {
10
+ const isAdmin = await isUserAdmin();
11
+
12
+ if (!isAdmin) {
13
+ return <ForbiddenPage />;
14
+ }
15
+
16
+ return (
17
+ <AdminConfigurationShell activePage="environment">
18
+ <EnvironmentVariablesClient />
19
+ </AdminConfigurationShell>
20
+ );
21
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { Loader2, RefreshCcw } from 'lucide-react';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ /**
7
+ * API payload returned by the pm2 logs endpoint.
8
+ */
9
+ type LogsResponse = {
10
+ readonly isAvailable?: boolean;
11
+ readonly output?: string;
12
+ readonly error?: string;
13
+ };
14
+
15
+ /**
16
+ * Client UI for recent pm2 log output.
17
+ */
18
+ export function LogsClient() {
19
+ const [logs, setLogs] = useState('');
20
+ const [isLoading, setIsLoading] = useState(true);
21
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
22
+
23
+ useEffect(() => {
24
+ void loadLogs();
25
+ }, []);
26
+
27
+ /**
28
+ * Refreshes log output from the API.
29
+ */
30
+ async function loadLogs(): Promise<void> {
31
+ try {
32
+ setIsLoading(true);
33
+ setErrorMessage(null);
34
+ const response = await fetch('/api/admin/logs?lines=300', { cache: 'no-store' });
35
+ const payload = (await response.json()) as LogsResponse;
36
+
37
+ if (!response.ok) {
38
+ throw new Error(payload.error || 'Failed to load logs.');
39
+ }
40
+
41
+ setLogs(payload.output || 'No log output returned.');
42
+ } catch (error) {
43
+ setErrorMessage(error instanceof Error ? error.message : 'Failed to load logs.');
44
+ } finally {
45
+ setIsLoading(false);
46
+ }
47
+ }
48
+
49
+ return (
50
+ <div className="container mx-auto space-y-6 px-4 py-8">
51
+ <div className="mt-20 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
52
+ <div>
53
+ <h1 className="text-3xl font-light text-gray-900">Logs</h1>
54
+ <p className="mt-1 text-sm text-gray-500">Recent pm2 output for the standalone Agents Server.</p>
55
+ </div>
56
+ <button
57
+ type="button"
58
+ onClick={() => void loadLogs()}
59
+ disabled={isLoading}
60
+ className="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
61
+ >
62
+ {isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCcw className="h-4 w-4" />}
63
+ Refresh
64
+ </button>
65
+ </div>
66
+
67
+ {errorMessage && (
68
+ <div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
69
+ {errorMessage}
70
+ </div>
71
+ )}
72
+
73
+ <pre className="min-h-[28rem] overflow-auto rounded-xl border border-slate-800 bg-slate-950 p-4 text-xs leading-5 text-slate-100 shadow-sm">
74
+ {isLoading && !logs ? 'Loading logs...' : logs}
75
+ </pre>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,14 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
3
+ import { LogsClient } from './LogsClient';
4
+
5
+ /**
6
+ * Super-admin page for viewing standalone VPS pm2 logs.
7
+ */
8
+ export default async function LogsPage() {
9
+ if (!(await isUserGlobalAdmin())) {
10
+ return <ForbiddenPage />;
11
+ }
12
+
13
+ return <LogsClient />;
14
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { Plus } from 'lucide-react';
4
- import { useCallback } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { useCallback, useEffect } from 'react';
5
6
  import { Card } from '../../../components/Homepage/Card';
6
7
  import { Section } from '../../../components/Homepage/Section';
7
8
  import { useUnsavedChangesGuard } from '../../../components/utils/useUnsavedChangesGuard';
@@ -25,7 +26,9 @@ const PRIMARY_BUTTON_CLASS_NAME =
25
26
  * @private route component of AdminServersPage
26
27
  */
27
28
  export function ServersClient() {
29
+ const searchParams = useSearchParams();
28
30
  const {
31
+ canEdit,
29
32
  currentServer,
30
33
  currentServerId,
31
34
  deleteCurrentServer,
@@ -75,16 +78,39 @@ export function ServersClient() {
75
78
  });
76
79
  }, [allowNextNavigation, deleteCurrentServer]);
77
80
 
81
+ useEffect(() => {
82
+ if (
83
+ canEdit &&
84
+ !loading &&
85
+ servers.length === 0 &&
86
+ searchParams?.get('setup') === '1' &&
87
+ !createServerWizard.isDialogOpen
88
+ ) {
89
+ createServerWizard.openDialog();
90
+ }
91
+ }, [canEdit, createServerWizard, loading, searchParams, servers.length]);
92
+
78
93
  return (
79
94
  <div className="container mx-auto space-y-8 px-4 py-8">
80
95
  <div className="mt-20 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
81
96
  <h1 className="text-3xl font-light text-gray-900">Servers</h1>
82
- <button type="button" onClick={createServerWizard.openDialog} className={PRIMARY_BUTTON_CLASS_NAME}>
83
- <Plus className="h-4 w-4" />
84
- Create new server
85
- </button>
97
+ {canEdit ? (
98
+ <button type="button" onClick={createServerWizard.openDialog} className={PRIMARY_BUTTON_CLASS_NAME}>
99
+ <Plus className="h-4 w-4" />
100
+ Create new server
101
+ </button>
102
+ ) : null}
86
103
  </div>
87
104
 
105
+ {!canEdit ? (
106
+ <Card className="border-amber-200 bg-amber-50 hover:border-amber-200 hover:shadow-md">
107
+ <p className="text-sm text-amber-800">
108
+ You can view servers as an administrator. Editing domains, migrations, and deletion is restricted
109
+ to the super admin authenticated with <span className="font-mono">ADMIN_PASSWORD</span>.
110
+ </p>
111
+ </Card>
112
+ ) : null}
113
+
88
114
  {error ? (
89
115
  <Card className="border-red-200 bg-red-50 hover:border-red-200 hover:shadow-md">
90
116
  <p className="text-sm text-red-700">{error}</p>
@@ -95,6 +121,7 @@ export function ServersClient() {
95
121
  <Card className="hover:border-gray-200 hover:shadow-md">
96
122
  <ServersRegistryTable
97
123
  currentServerId={currentServerId}
124
+ canEdit={canEdit}
98
125
  loading={loading}
99
126
  migratingServerId={migratingServerId}
100
127
  navigatingServerId={navigatingServerId}
@@ -110,35 +137,39 @@ export function ServersClient() {
110
137
  </Card>
111
138
  </Section>
112
139
 
113
- <CreateServerDialog
114
- addAdditionalUser={createServerWizard.addAdditionalUser}
115
- derivedWizardTablePrefix={createServerWizard.derivedWizardTablePrefix}
116
- handleCreateServer={createServerWizard.handleCreateServer}
117
- handleIconUpload={createServerWizard.handleIconUpload}
118
- handleWizardBack={createServerWizard.handleWizardBack}
119
- handleWizardNext={createServerWizard.handleWizardNext}
120
- handleWizardStepSelection={createServerWizard.handleWizardStepSelection}
121
- iconInputRef={createServerWizard.iconInputRef}
122
- isCreatingServer={createServerWizard.isCreatingServer}
123
- isOpen={createServerWizard.isDialogOpen}
124
- isUploadingIcon={createServerWizard.isUploadingIcon}
125
- removeAdditionalUser={createServerWizard.removeAdditionalUser}
126
- requestClose={createServerWizard.requestClose}
127
- resetWizard={createServerWizard.resetWizard}
128
- updateAdditionalUser={createServerWizard.updateAdditionalUser}
129
- updateAdminUser={createServerWizard.updateAdminUser}
130
- updateInitialSetting={createServerWizard.updateInitialSetting}
131
- updateWizardField={createServerWizard.updateWizardField}
132
- wizardError={createServerWizard.wizardError}
133
- wizardState={createServerWizard.wizardState}
134
- wizardStep={createServerWizard.wizardStep}
135
- />
140
+ {canEdit ? (
141
+ <CreateServerDialog
142
+ addAdditionalUser={createServerWizard.addAdditionalUser}
143
+ derivedWizardTablePrefix={createServerWizard.derivedWizardTablePrefix}
144
+ handleCreateServer={createServerWizard.handleCreateServer}
145
+ handleIconUpload={createServerWizard.handleIconUpload}
146
+ handleWizardBack={createServerWizard.handleWizardBack}
147
+ handleWizardNext={createServerWizard.handleWizardNext}
148
+ handleWizardStepSelection={createServerWizard.handleWizardStepSelection}
149
+ iconInputRef={createServerWizard.iconInputRef}
150
+ isCreatingServer={createServerWizard.isCreatingServer}
151
+ isOpen={createServerWizard.isDialogOpen}
152
+ isUploadingIcon={createServerWizard.isUploadingIcon}
153
+ removeAdditionalUser={createServerWizard.removeAdditionalUser}
154
+ requestClose={createServerWizard.requestClose}
155
+ resetWizard={createServerWizard.resetWizard}
156
+ updateAdditionalUser={createServerWizard.updateAdditionalUser}
157
+ updateAdminUser={createServerWizard.updateAdminUser}
158
+ updateInitialSetting={createServerWizard.updateInitialSetting}
159
+ updateWizardField={createServerWizard.updateWizardField}
160
+ wizardError={createServerWizard.wizardError}
161
+ wizardState={createServerWizard.wizardState}
162
+ wizardStep={createServerWizard.wizardStep}
163
+ />
164
+ ) : null}
136
165
 
137
- <DeleteCurrentServerSection
138
- currentServer={currentServer}
139
- deletingServerId={deletingServerId}
140
- onDeleteCurrentServer={handleDeleteCurrentServer}
141
- />
166
+ {canEdit ? (
167
+ <DeleteCurrentServerSection
168
+ currentServer={currentServer}
169
+ deletingServerId={deletingServerId}
170
+ onDeleteCurrentServer={handleDeleteCurrentServer}
171
+ />
172
+ ) : null}
142
173
  </div>
143
174
  );
144
175
  }
@@ -17,6 +17,11 @@ type ManagedServersReadResponse = {
17
17
  */
18
18
  readonly currentServerId: number | null;
19
19
 
20
+ /**
21
+ * Whether the current viewer can mutate server rows.
22
+ */
23
+ readonly canEdit: boolean;
24
+
20
25
  /**
21
26
  * Optional failure message returned by the API.
22
27
  */
@@ -40,6 +40,11 @@ const PRIMARY_BUTTON_CLASS_NAME =
40
40
  * @private function of <ServersClient/>
41
41
  */
42
42
  type ServersRegistryTableProps = {
43
+ /**
44
+ * Whether the viewer can edit server rows.
45
+ */
46
+ readonly canEdit: boolean;
47
+
43
48
  /**
44
49
  * Identifier of the server resolved from the current request domain.
45
50
  */
@@ -107,6 +112,7 @@ type ServersRegistryTableProps = {
107
112
  * @private function of <ServersClient/>
108
113
  */
109
114
  type ServersRegistryTableRowProps = {
115
+ readonly canEdit: boolean;
110
116
  readonly currentServerId: number | null;
111
117
  readonly draft: ServerDraft | undefined;
112
118
  readonly isDirty: boolean;
@@ -163,6 +169,7 @@ function ServerStatusBadge(props: { readonly label: string; readonly tone: 'blue
163
169
  function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
164
170
  const {
165
171
  currentServerId,
172
+ canEdit,
166
173
  draft,
167
174
  isDirty,
168
175
  isMigrating,
@@ -184,6 +191,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
184
191
  value={draft?.name || ''}
185
192
  onChange={(event) => onUpdateServerDraft(server.id, 'name', event.target.value)}
186
193
  className={INPUT_CLASS_NAME}
194
+ disabled={!canEdit}
187
195
  aria-label={`Server name for ${server.name}`}
188
196
  />
189
197
  </td>
@@ -194,6 +202,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
194
202
  onUpdateServerDraft(server.id, 'environment', event.target.value as ManagedServerEnvironment)
195
203
  }
196
204
  className={INPUT_CLASS_NAME}
205
+ disabled={!canEdit}
197
206
  aria-label={`Environment for ${server.name}`}
198
207
  >
199
208
  {MANAGED_SERVER_ENVIRONMENT_OPTIONS.map((environment) => (
@@ -209,6 +218,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
209
218
  value={draft?.domain || ''}
210
219
  onChange={(event) => onUpdateServerDraft(server.id, 'domain', event.target.value)}
211
220
  className={INPUT_CLASS_NAME}
221
+ disabled={!canEdit}
212
222
  aria-label={`Domain for ${server.name}`}
213
223
  />
214
224
  </td>
@@ -218,6 +228,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
218
228
  value={draft?.tablePrefix || ''}
219
229
  onChange={(event) => onUpdateServerDraft(server.id, 'tablePrefix', event.target.value)}
220
230
  className={`${INPUT_CLASS_NAME} font-mono`}
231
+ disabled={!canEdit}
221
232
  aria-label={`Table prefix for ${server.name}`}
222
233
  />
223
234
  </td>
@@ -239,7 +250,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
239
250
  <button
240
251
  type="button"
241
252
  onClick={() => void onSaveServer(server.id)}
242
- disabled={!isDirty || isSaving}
253
+ disabled={!canEdit || !isDirty || isSaving}
243
254
  className={`${PRIMARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
244
255
  >
245
256
  {isSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Save className="h-3.5 w-3.5" />}
@@ -248,7 +259,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
248
259
  <button
249
260
  type="button"
250
261
  onClick={() => void onMigrateServer(server.id)}
251
- disabled={isMigrating}
262
+ disabled={!canEdit || isMigrating}
252
263
  className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
253
264
  >
254
265
  {isMigrating ? (
@@ -288,6 +299,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
288
299
  export function ServersRegistryTable(props: ServersRegistryTableProps) {
289
300
  const {
290
301
  currentServerId,
302
+ canEdit,
291
303
  isServerDraftDirty,
292
304
  loading,
293
305
  migratingServerId,
@@ -344,6 +356,7 @@ export function ServersRegistryTable(props: ServersRegistryTableProps) {
344
356
  <ServersRegistryTableRow
345
357
  key={server.id}
346
358
  currentServerId={currentServerId}
359
+ canEdit={canEdit}
347
360
  draft={serverDrafts[server.id]}
348
361
  isDirty={isServerDraftDirty(server)}
349
362
  isMigrating={migratingServerId === server.id}
@@ -1,12 +1,12 @@
1
1
  import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
- import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
3
  import { ServersClient } from './ServersClient';
4
4
 
5
5
  /**
6
- * Global-admin page for managing same-instance registered servers.
6
+ * Admin page for viewing same-instance registered servers.
7
7
  */
8
8
  export default async function AdminServersPage() {
9
- if (!(await isUserGlobalAdmin())) {
9
+ if (!(await isUserAdmin())) {
10
10
  return <ForbiddenPage />;
11
11
  }
12
12