@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.
- package/apps/agents-server/next.config.ts +8 -1
- package/apps/agents-server/playwright.config.ts +2 -0
- package/apps/agents-server/src/app/admin/_components/AdminConfigurationShell.tsx +13 -7
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +225 -0
- package/apps/agents-server/src/app/admin/code-runners/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/database/DatabaseAdminClient.tsx +38 -0
- package/apps/agents-server/src/app/admin/database/DatabaseAdminStudioSurface.tsx +42 -0
- package/apps/agents-server/src/app/admin/database/page.tsx +33 -0
- package/apps/agents-server/src/app/admin/environment/EnvironmentVariablesClient.tsx +259 -0
- package/apps/agents-server/src/app/admin/environment/page.tsx +21 -0
- package/apps/agents-server/src/app/admin/logs/LogsClient.tsx +78 -0
- package/apps/agents-server/src/app/admin/logs/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +64 -33
- package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +15 -2
- package/apps/agents-server/src/app/admin/servers/page.tsx +3 -3
- package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +12 -2
- package/apps/agents-server/src/app/api/admin/code-runners/route.ts +104 -0
- package/apps/agents-server/src/app/api/admin/database/studio/route.ts +113 -0
- package/apps/agents-server/src/app/api/admin/environment/route.ts +65 -0
- package/apps/agents-server/src/app/api/admin/logs/route.ts +24 -0
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +79 -3
- package/apps/agents-server/src/app/api/admin/servers/route.ts +36 -1
- package/apps/agents-server/src/app/layout.tsx +1 -0
- package/apps/agents-server/src/app/page.tsx +101 -1
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +27 -4
- package/apps/agents-server/src/database/$provideClientSql.ts +2 -6
- package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +273 -0
- package/apps/agents-server/src/database/resolvePostgresConnectionString.ts +26 -0
- package/apps/agents-server/src/database/sqlite/$provideAgentsServerSqliteDatabase.ts +83 -0
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +20 -71
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +5 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +5 -0
- package/apps/agents-server/src/languages/translations/english.yaml +5 -0
- package/apps/agents-server/src/tools/$provideServer.ts +27 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +20 -1
- package/apps/agents-server/src/utils/session.ts +123 -2
- package/apps/agents-server/src/utils/vpsConfiguration.ts +550 -0
- package/esm/index.es.js +1 -1
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +3 -1
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +9 -398
- package/src/book-components/Chat/utils/renderMarkdown.ts +323 -8
- package/src/other/templates/getTemplatesPipelineCollection.ts +712 -829
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +1 -1
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
- 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 {
|
|
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
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|
|
2
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
3
3
|
import { ServersClient } from './ServersClient';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Admin page for viewing same-instance registered servers.
|
|
7
7
|
*/
|
|
8
8
|
export default async function AdminServersPage() {
|
|
9
|
-
if (!(await
|
|
9
|
+
if (!(await isUserAdmin())) {
|
|
10
10
|
return <ForbiddenPage />;
|
|
11
11
|
}
|
|
12
12
|
|