@rapidmx/web-client 0.19.0 → 0.21.0
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/README.md +1 -1
- package/apps/shared/components/admin/diagnostics/HostCard.tsx +2 -0
- package/apps/shared/components/admin/diagnostics/PressureTiles.tsx +108 -0
- package/apps/shared/components/admin/diagnostics/PvcTable.tsx +20 -6
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +21 -2
- package/apps/shared/components/admin/layout/AdminShell.tsx +2 -2
- package/apps/shared/components/admin/settings/PluginsManager.tsx +508 -35
- package/apps/shared/components/admin/settings/pluginPreferences.ts +34 -0
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +2 -2
- package/apps/shared/components/layout/AppShell.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +18 -9
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +24 -0
- package/apps/shared/components/mail/listPreferences.ts +3 -3
- package/apps/www/index.tsx +62 -12
- package/dist/apps/shared/components/admin/diagnostics/HostCard.js +2 -1
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.d.ts +22 -0
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.js +52 -0
- package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +9 -4
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +35 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +267 -35
- package/dist/apps/shared/components/admin/settings/pluginPreferences.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/pluginPreferences.js +35 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +2 -2
- package/dist/apps/shared/components/layout/AppShell.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.d.ts +10 -3
- package/dist/apps/shared/components/mail/ConversationList.js +9 -5
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +20 -1
- package/dist/apps/shared/components/mail/listPreferences.d.ts +1 -1
- package/dist/apps/www/index.js +51 -10
- package/package.json +2 -2
|
@@ -37,6 +37,7 @@ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
|
37
37
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
38
38
|
import { notify } from "../../../notifications/store.js";
|
|
39
39
|
import { isElevationRequired } from "../elevation.js";
|
|
40
|
+
import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
|
|
40
41
|
|
|
41
42
|
const INPUT_CLASS =
|
|
42
43
|
"w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -91,6 +92,72 @@ interface PendingChange {
|
|
|
91
92
|
uid?: string;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/** An action the toolbar above the installed plugins applies to several plugins at once. */
|
|
96
|
+
type BulkKind = "upgrade" | "disable" | "uninstall";
|
|
97
|
+
|
|
98
|
+
const BULK_WORDS: Record<BulkKind, { doing: string; done: string }> = {
|
|
99
|
+
upgrade: { doing: "Upgrading", done: "upgraded" },
|
|
100
|
+
disable: { doing: "Disabling", done: "disabled" },
|
|
101
|
+
uninstall: { doing: "Uninstalling", done: "uninstalled" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** How far a bulk action has got. */
|
|
105
|
+
interface BulkProgress {
|
|
106
|
+
kind: BulkKind;
|
|
107
|
+
/** How many plugins were finished, failures included. */
|
|
108
|
+
done: number;
|
|
109
|
+
total: number;
|
|
110
|
+
/** The plugin being changed now. */
|
|
111
|
+
current: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Why some plugins of a bulk action were left as they were. */
|
|
115
|
+
interface BulkFailure {
|
|
116
|
+
title: string;
|
|
117
|
+
reasons: string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pluralPlugins = (count: number): string => `${count} ${count === 1 ? "plugin" : "plugins"}`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `plugins` in the order a bulk action has to change them in, since a plugin that others require can't be disabled or
|
|
124
|
+
* uninstalled while they're enabled, and one that requires others may need them at their new versions first: each
|
|
125
|
+
* plugin comes after the ones it requires (`dependenciesFirst`, for an upgrade) or before them (for the rest).
|
|
126
|
+
*/
|
|
127
|
+
function orderByRequirements(plugins: Plugin[], dependenciesFirst: boolean): Plugin[] {
|
|
128
|
+
const byName: Map<string, Plugin> = new Map(plugins.map((plugin) => [plugin.name, plugin]));
|
|
129
|
+
const ordered: Plugin[] = [];
|
|
130
|
+
const seen: Set<string> = new Set();
|
|
131
|
+
const visit = (plugin: Plugin) => {
|
|
132
|
+
if (seen.has(plugin.name)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
seen.add(plugin.name);
|
|
136
|
+
for (const required of Object.keys(plugin.manifest.requires ?? {})) {
|
|
137
|
+
const dependency: Plugin | undefined = byName.get(required);
|
|
138
|
+
if (dependency) {
|
|
139
|
+
visit(dependency);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ordered.push(plugin);
|
|
143
|
+
};
|
|
144
|
+
plugins.forEach(visit);
|
|
145
|
+
return dependenciesFirst ? ordered : ordered.reverse();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The bar that runs across the top of the window while plugins are being changed, so it's plain that something is still
|
|
150
|
+
* happening: it slides endlessly, since how long a registry lookup or an install takes isn't known. It stands still
|
|
151
|
+
* (as a faded full bar) when the user asks for less motion.
|
|
152
|
+
*/
|
|
153
|
+
function PluginProgressBar({ label }: { label: string }) {
|
|
154
|
+
return (
|
|
155
|
+
<div role="progressbar" aria-label={label} className="fixed inset-x-0 top-0 z-[1100] h-1 overflow-hidden bg-primary/20">
|
|
156
|
+
<div className="rr-progress-indeterminate h-full w-1/3 rounded-pill bg-primary motion-reduce:w-full motion-reduce:opacity-40" />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
94
161
|
/** Classes for a plugin table's header and body cells. */
|
|
95
162
|
const TH_CLASS = "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border";
|
|
96
163
|
const TD_CLASS = "py-3 px-2.5 border-b border-border align-top";
|
|
@@ -139,6 +206,21 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
139
206
|
const [listStale, setListStale] = useState(false);
|
|
140
207
|
/** How many times status has been retried while none has ever been read. */
|
|
141
208
|
const [statusRetries, setStatusRetries] = useState(0);
|
|
209
|
+
/** Whether prerelease versions are offered (a preference of this browser). */
|
|
210
|
+
const [allowPrerelease, setAllowPrereleaseState] = useState<boolean>(getAllowPrerelease);
|
|
211
|
+
/** The same, for `refreshUpdates()`, which must stay the same function for the effects that depend on it. */
|
|
212
|
+
const allowPrereleaseRef = useRef(allowPrerelease);
|
|
213
|
+
/** Identifies the latest update check, so a slower earlier one can't replace what it found. */
|
|
214
|
+
const updatesRequest = useRef(0);
|
|
215
|
+
/** The plugins ticked for a bulk action. */
|
|
216
|
+
const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
|
|
217
|
+
/** Set while a bulk action runs. */
|
|
218
|
+
const [bulk, setBulk] = useState<BulkProgress | null>(null);
|
|
219
|
+
const [bulkFailure, setBulkFailure] = useState<BulkFailure | null>(null);
|
|
220
|
+
/** The plugins waiting for the administrator to confirm uninstalling them together. */
|
|
221
|
+
const [confirmingBulkUninstall, setConfirmingBulkUninstall] = useState<Plugin[] | null>(null);
|
|
222
|
+
/** How many changes (or the lookups that come before them) are waiting on the server. */
|
|
223
|
+
const [working, setWorking] = useState(0);
|
|
142
224
|
const statusRequest = useRef<Promise<void> | null>(null);
|
|
143
225
|
/** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
|
|
144
226
|
const seenPurges = useRef<Map<string, PluginPurgeState>>(new Map());
|
|
@@ -169,13 +251,43 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
169
251
|
return statusRequest.current;
|
|
170
252
|
}, []);
|
|
171
253
|
|
|
172
|
-
|
|
254
|
+
/** Checks the registry for newer versions. Resolves what it found, or `undefined` when it couldn't check. */
|
|
255
|
+
const refreshUpdates = useCallback((): Promise<PluginUpdateInfo[] | undefined> => {
|
|
256
|
+
const token: number = ++updatesRequest.current;
|
|
173
257
|
// Also advisory: a registry that can't be reached just means no update badges.
|
|
174
|
-
return getPluginUpdates()
|
|
175
|
-
.then((list)
|
|
176
|
-
|
|
258
|
+
return getPluginUpdates({ prerelease: allowPrereleaseRef.current })
|
|
259
|
+
.then((list): PluginUpdateInfo[] | undefined => {
|
|
260
|
+
if (token === updatesRequest.current) {
|
|
261
|
+
setUpdates(new Map(list.map((info) => [info.uid, info])));
|
|
262
|
+
}
|
|
263
|
+
return list;
|
|
264
|
+
})
|
|
265
|
+
.catch((): PluginUpdateInfo[] | undefined => {
|
|
266
|
+
if (token === updatesRequest.current) {
|
|
267
|
+
setUpdates(new Map());
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
});
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
/** Keeps `work` counted as in progress, which is what the bar across the top of the window shows. */
|
|
274
|
+
const track = useCallback(async <R,>(work: Promise<R>): Promise<R> => {
|
|
275
|
+
setWorking((count) => count + 1);
|
|
276
|
+
try {
|
|
277
|
+
return await work;
|
|
278
|
+
} finally {
|
|
279
|
+
setWorking((count) => count - 1);
|
|
280
|
+
}
|
|
177
281
|
}, []);
|
|
178
282
|
|
|
283
|
+
function changePrerelease(allow: boolean) {
|
|
284
|
+
setAllowPrerelease(allow);
|
|
285
|
+
allowPrereleaseRef.current = allow;
|
|
286
|
+
setAllowPrereleaseState(allow);
|
|
287
|
+
// What counts as an update depends on it.
|
|
288
|
+
void refreshUpdates();
|
|
289
|
+
}
|
|
290
|
+
|
|
179
291
|
/** Re-reads the list, for changes that also installed or enabled other plugins. */
|
|
180
292
|
const reload = useCallback(() => {
|
|
181
293
|
return listPlugins()
|
|
@@ -283,12 +395,18 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
283
395
|
setBusy(plugin.uid, false);
|
|
284
396
|
}
|
|
285
397
|
|
|
398
|
+
/** Disables an installed plugin, resolving why it couldn't be, or `null`. */
|
|
399
|
+
function disable(plugin: Plugin): Promise<string | null> {
|
|
400
|
+
return attempt(
|
|
401
|
+
async () => applied(await track(updatePlugin(plugin.uid, { version: plugin.version, enabled: false }))),
|
|
402
|
+
`Could not disable ${plugin.manifest.displayName}.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
286
406
|
function toggle(plugin: Plugin) {
|
|
287
407
|
const displayName: string = plugin.manifest.displayName;
|
|
288
408
|
if (plugin.enabled) {
|
|
289
|
-
return onRow(plugin, () =>
|
|
290
|
-
attempt(async () => applied(await updatePlugin(plugin.uid, { version: plugin.version, enabled: false })), `Could not disable ${displayName}.`),
|
|
291
|
-
);
|
|
409
|
+
return onRow(plugin, () => disable(plugin));
|
|
292
410
|
}
|
|
293
411
|
// Enabling a plugin also installs or enables the plugins it requires, so it's previewed like any other change.
|
|
294
412
|
// The installed version is planned from its stored manifest, so this doesn't need the registry. A preview that
|
|
@@ -300,7 +418,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
300
418
|
plugin.packageVersion,
|
|
301
419
|
displayName,
|
|
302
420
|
async (plan) => {
|
|
303
|
-
applied(await updatePlugin(plugin.uid, { version: plugin.version, enabled: true, expectedPlan: expectedPlanOf(plan) }));
|
|
421
|
+
applied(await track(updatePlugin(plugin.uid, { version: plugin.version, enabled: true, expectedPlan: expectedPlanOf(plan) })));
|
|
304
422
|
if (plan.install.length > 0 || plan.enable.length > 0) {
|
|
305
423
|
void reload();
|
|
306
424
|
}
|
|
@@ -325,11 +443,16 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
325
443
|
displayName: string,
|
|
326
444
|
apply: (plan: PluginChangePlan) => Promise<void>,
|
|
327
445
|
failure: string,
|
|
328
|
-
options: {
|
|
446
|
+
options: {
|
|
447
|
+
uid?: string;
|
|
448
|
+
/** Part of a bulk action, which has no one to confirm the other plugins the change would also install or enable:
|
|
449
|
+
* such a change isn't made, and says why. */
|
|
450
|
+
unattended?: boolean;
|
|
451
|
+
} = {},
|
|
329
452
|
): Promise<string | null> {
|
|
330
453
|
let plan: PluginChangePlan;
|
|
331
454
|
try {
|
|
332
|
-
plan = await planPluginChange(name, packageVersion);
|
|
455
|
+
plan = await track(planPluginChange(name, packageVersion));
|
|
333
456
|
} catch (err) {
|
|
334
457
|
return errorMessage(err, failure);
|
|
335
458
|
}
|
|
@@ -337,6 +460,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
337
460
|
return `${displayName} ${plan.plugin.version} can't be installed. ${plan.conflicts.join(" ")}`;
|
|
338
461
|
}
|
|
339
462
|
if (plan.install.length > 0 || plan.enable.length > 0) {
|
|
463
|
+
if (options.unattended) {
|
|
464
|
+
return `${displayName} ${plan.plugin.version} also needs other plugins installed or enabled, so it wasn't changed. Change it on its own to review them.`;
|
|
465
|
+
}
|
|
340
466
|
setConfirming({ displayName, plan, apply, failure, uid: options.uid });
|
|
341
467
|
return null;
|
|
342
468
|
}
|
|
@@ -349,7 +475,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
349
475
|
packageVersion,
|
|
350
476
|
displayName,
|
|
351
477
|
async (plan) => {
|
|
352
|
-
const result = await addPlugin(name, packageVersion, expectedPlanOf(plan));
|
|
478
|
+
const result = await track(addPlugin(name, packageVersion, expectedPlanOf(plan)));
|
|
353
479
|
applied(...result.dependencies, result.plugin);
|
|
354
480
|
// Adding a plugin cancels the deletion of its data that was waiting for the servers to stop running it.
|
|
355
481
|
for (const warning of result.warnings ?? []) {
|
|
@@ -360,24 +486,24 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
360
486
|
);
|
|
361
487
|
}
|
|
362
488
|
|
|
363
|
-
function changeVersion(plugin: Plugin, packageVersion: string) {
|
|
489
|
+
function changeVersion(plugin: Plugin, packageVersion: string, unattended = false) {
|
|
364
490
|
const failure = `Could not upgrade ${plugin.manifest.displayName}.`;
|
|
365
491
|
if (!plugin.enabled) {
|
|
366
492
|
// A disabled plugin stays disabled, so nothing it requires is installed or enabled - there's nothing to preview.
|
|
367
|
-
return attempt(async () => applied(await updatePlugin(plugin.uid, { version: plugin.version, packageVersion })), failure);
|
|
493
|
+
return attempt(async () => applied(await track(updatePlugin(plugin.uid, { version: plugin.version, packageVersion }))), failure);
|
|
368
494
|
}
|
|
369
495
|
return planned(
|
|
370
496
|
plugin.name,
|
|
371
497
|
packageVersion,
|
|
372
498
|
plugin.manifest.displayName,
|
|
373
499
|
async (plan) => {
|
|
374
|
-
applied(await updatePlugin(plugin.uid, { version: plugin.version, packageVersion, expectedPlan: expectedPlanOf(plan) }));
|
|
500
|
+
applied(await track(updatePlugin(plugin.uid, { version: plugin.version, packageVersion, expectedPlan: expectedPlanOf(plan) })));
|
|
375
501
|
if (plan.install.length > 0 || plan.enable.length > 0) {
|
|
376
502
|
void reload();
|
|
377
503
|
}
|
|
378
504
|
},
|
|
379
505
|
failure,
|
|
380
|
-
{ uid: plugin.uid },
|
|
506
|
+
{ uid: plugin.uid, unattended },
|
|
381
507
|
);
|
|
382
508
|
}
|
|
383
509
|
|
|
@@ -391,7 +517,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
391
517
|
return onRow(purge, async () => {
|
|
392
518
|
let latest: string;
|
|
393
519
|
try {
|
|
394
|
-
latest = (await planPluginChange(purge.name)).plugin.version;
|
|
520
|
+
latest = (await track(planPluginChange(purge.name, undefined, { prerelease: allowPrerelease }))).plugin.version;
|
|
395
521
|
} catch (err) {
|
|
396
522
|
return errorMessage(err, `Could not install ${displayName}.`);
|
|
397
523
|
}
|
|
@@ -415,6 +541,130 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
415
541
|
setBusy(purge.uid, false);
|
|
416
542
|
}
|
|
417
543
|
|
|
544
|
+
/** Lists a data deletion the server just scheduled. Shown at once; the status polling that follows replaces it with
|
|
545
|
+
* what the servers report. */
|
|
546
|
+
function showScheduledPurge(scheduled: PluginPurgeInfo | undefined) {
|
|
547
|
+
setStatus((prev) =>
|
|
548
|
+
prev && scheduled ? { ...prev, purges: [...(prev.purges ?? []).filter((purge) => purge.uid !== scheduled.uid), scheduled] } : prev,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Uninstalls an installed plugin, and with `purgeData` deletes the data it stored once no server runs it. Resolves
|
|
553
|
+
* why it couldn't be, or `null`. */
|
|
554
|
+
async function uninstall(plugin: Plugin, purgeData: boolean): Promise<string | null> {
|
|
555
|
+
try {
|
|
556
|
+
const result = await removePlugin(plugin.uid, { purgeData });
|
|
557
|
+
setPlugins((prev) => prev.filter((other) => other.uid !== plugin.uid));
|
|
558
|
+
if (result.purgeScheduled) {
|
|
559
|
+
showScheduledPurge(result.purge);
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
} catch (err) {
|
|
563
|
+
return isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, `Could not uninstall ${plugin.manifest.displayName}.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Applies `kind` to `targets` one after another, in the order their requirements allow, and says what became of
|
|
569
|
+
* them. A plugin that can't be changed is left as it was and the rest carry on. For an upgrade, `versions` holds the
|
|
570
|
+
* version each plugin goes to; for an uninstall, `purgeData` also deletes what each stored. Deleting data needs the
|
|
571
|
+
* administrator to have confirmed their identity recently, which no plugin will get past if one is refused for it,
|
|
572
|
+
* so the rest aren't tried.
|
|
573
|
+
*/
|
|
574
|
+
async function runBulk(kind: BulkKind, targets: Plugin[], options: { versions?: ReadonlyMap<string, string>; purgeData?: boolean } = {}) {
|
|
575
|
+
const { versions = new Map<string, string>(), purgeData = false } = options;
|
|
576
|
+
const ordered: Plugin[] = orderByRequirements(targets, kind === "upgrade");
|
|
577
|
+
const reasons: string[] = [];
|
|
578
|
+
const failed: Set<string> = new Set();
|
|
579
|
+
setError(null);
|
|
580
|
+
setRetryEnableUid(null);
|
|
581
|
+
setBulkFailure(null);
|
|
582
|
+
await track(
|
|
583
|
+
(async () => {
|
|
584
|
+
for (const [index, plugin] of ordered.entries()) {
|
|
585
|
+
setBulk({ kind, done: index, total: ordered.length, current: plugin.manifest.displayName });
|
|
586
|
+
const problem: string | null =
|
|
587
|
+
kind === "upgrade"
|
|
588
|
+
? await changeVersion(plugin, versions.get(plugin.uid)!, true)
|
|
589
|
+
: kind === "disable"
|
|
590
|
+
? await disable(plugin)
|
|
591
|
+
: await uninstall(plugin, purgeData);
|
|
592
|
+
if (!problem) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
failed.add(plugin.uid);
|
|
596
|
+
reasons.push(`${plugin.manifest.displayName}: ${problem}`);
|
|
597
|
+
if (problem === ELEVATION_MESSAGE) {
|
|
598
|
+
for (const untried of ordered.slice(index + 1)) {
|
|
599
|
+
failed.add(untried.uid);
|
|
600
|
+
reasons.push(`${untried.manifest.displayName}: Not attempted.`);
|
|
601
|
+
}
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
})(),
|
|
606
|
+
);
|
|
607
|
+
setBulk(null);
|
|
608
|
+
setSelected(failed);
|
|
609
|
+
const succeeded: number = ordered.length - failed.size;
|
|
610
|
+
if (kind === "uninstall" && succeeded > 0) {
|
|
611
|
+
watchRollout();
|
|
612
|
+
}
|
|
613
|
+
if (succeeded > 0) {
|
|
614
|
+
notify({
|
|
615
|
+
kind: "success",
|
|
616
|
+
title: `${pluralPlugins(succeeded)} ${BULK_WORDS[kind].done}`,
|
|
617
|
+
message: purgeData ? "Their data is deleted once every server has stopped running them. Adding a plugin again before then cancels its deletion." : undefined,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (reasons.length > 0) {
|
|
621
|
+
setBulkFailure({ title: `${pluralPlugins(reasons.length)} couldn't be ${BULK_WORDS[kind].done}:`, reasons });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Upgrades every plugin that has an update, whatever is ticked. Checks the registry first, so it's never working
|
|
626
|
+
* from an out-of-date list of updates. */
|
|
627
|
+
async function upgradeAll() {
|
|
628
|
+
setError(null);
|
|
629
|
+
setBulkFailure(null);
|
|
630
|
+
const found: PluginUpdateInfo[] | undefined = await track(refreshUpdates());
|
|
631
|
+
if (!found) {
|
|
632
|
+
setError("Could not check for plugin updates.");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const versions: Map<string, string> = new Map();
|
|
636
|
+
for (const info of found) {
|
|
637
|
+
if (info.updateAvailable && info.latestVersion) {
|
|
638
|
+
versions.set(info.uid, info.latestVersion);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const targets: Plugin[] = plugins.filter((plugin) => versions.has(plugin.uid));
|
|
642
|
+
if (targets.length === 0) {
|
|
643
|
+
const unchecked: number = found.filter((info) => info.error).length;
|
|
644
|
+
notify(
|
|
645
|
+
unchecked > 0
|
|
646
|
+
? { kind: "warning", title: "No updates found", message: `${pluralPlugins(unchecked)} couldn't be checked against the registry.` }
|
|
647
|
+
: { kind: "info", title: "All plugins are up to date" },
|
|
648
|
+
);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
await runBulk("upgrade", targets, { versions });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const toggleSelected = (uid: string) =>
|
|
655
|
+
setSelected((prev) => {
|
|
656
|
+
const next = new Set(prev);
|
|
657
|
+
if (!next.delete(uid)) {
|
|
658
|
+
next.add(uid);
|
|
659
|
+
}
|
|
660
|
+
return next;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const selectedPlugins: Plugin[] = plugins.filter((plugin) => selected.has(plugin.uid));
|
|
664
|
+
const allSelected: boolean = plugins.length > 0 && selectedPlugins.length === plugins.length;
|
|
665
|
+
const selectedUpgrades: Plugin[] = selectedPlugins.filter((plugin) => updates.get(plugin.uid)?.updateAvailable);
|
|
666
|
+
const selectedEnabled: Plugin[] = selectedPlugins.filter((plugin) => plugin.enabled);
|
|
667
|
+
|
|
418
668
|
// Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
|
|
419
669
|
const retryEnable: Plugin | undefined = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
|
|
420
670
|
|
|
@@ -426,9 +676,11 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
426
676
|
Add by name
|
|
427
677
|
</Button>
|
|
428
678
|
);
|
|
679
|
+
const bulkBusy: boolean = bulk !== null;
|
|
429
680
|
|
|
430
681
|
return (
|
|
431
682
|
<>
|
|
683
|
+
{(working > 0 || pending) && <PluginProgressBar label={working > 0 ? "Working on plugins" : "Applying plugin changes"} />}
|
|
432
684
|
{embedded ? (
|
|
433
685
|
<p className="text-sm text-text-muted mb-5 max-w-3xl">
|
|
434
686
|
Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run
|
|
@@ -448,6 +700,36 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
448
700
|
</>
|
|
449
701
|
)}
|
|
450
702
|
|
|
703
|
+
<label className="flex items-start gap-2 text-sm mb-5 max-w-3xl cursor-pointer">
|
|
704
|
+
<input
|
|
705
|
+
type="checkbox"
|
|
706
|
+
className="mt-0.5 h-4 w-4 shrink-0"
|
|
707
|
+
checked={allowPrerelease}
|
|
708
|
+
onChange={(e) => changePrerelease(e.target.checked)}
|
|
709
|
+
/>
|
|
710
|
+
<span className="flex flex-col gap-0.5">
|
|
711
|
+
<span className="font-semibold">Allow pre-release versions</span>
|
|
712
|
+
<span className="text-xs text-text-muted">
|
|
713
|
+
Also offer beta and other pre-release builds, such as 1.0.0-beta.2, as updates and as versions to
|
|
714
|
+
install. They can be unstable. This is remembered in this browser.
|
|
715
|
+
</span>
|
|
716
|
+
</span>
|
|
717
|
+
</label>
|
|
718
|
+
|
|
719
|
+
{bulkFailure && (
|
|
720
|
+
<Alert>
|
|
721
|
+
<div>
|
|
722
|
+
{bulkFailure.title}
|
|
723
|
+
<ul className="list-disc pl-5 mt-1">
|
|
724
|
+
{bulkFailure.reasons.map((reason) => (
|
|
725
|
+
<li key={reason} className="break-words">
|
|
726
|
+
{reason}
|
|
727
|
+
</li>
|
|
728
|
+
))}
|
|
729
|
+
</ul>
|
|
730
|
+
</div>
|
|
731
|
+
</Alert>
|
|
732
|
+
)}
|
|
451
733
|
{error && (
|
|
452
734
|
<Alert>
|
|
453
735
|
{error}
|
|
@@ -489,6 +771,75 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
489
771
|
<p className="text-sm text-text-muted">No plugins installed.</p>
|
|
490
772
|
) : (
|
|
491
773
|
<div className="overflow-x-auto">
|
|
774
|
+
{plugins.length > 0 && (
|
|
775
|
+
<div role="toolbar" aria-label="Actions for several plugins" className="flex flex-wrap items-center gap-2 py-2">
|
|
776
|
+
<label className="flex items-center gap-2 text-sm mr-2 cursor-pointer">
|
|
777
|
+
<input
|
|
778
|
+
type="checkbox"
|
|
779
|
+
className="h-4 w-4 shrink-0"
|
|
780
|
+
aria-label="Select all plugins"
|
|
781
|
+
checked={allSelected}
|
|
782
|
+
disabled={bulkBusy}
|
|
783
|
+
ref={(input) => {
|
|
784
|
+
if (input) {
|
|
785
|
+
input.indeterminate = selectedPlugins.length > 0 && !allSelected;
|
|
786
|
+
}
|
|
787
|
+
}}
|
|
788
|
+
onChange={(e) => setSelected(e.target.checked ? new Set(plugins.map((plugin) => plugin.uid)) : new Set())}
|
|
789
|
+
/>
|
|
790
|
+
<span>{selectedPlugins.length > 0 ? `${selectedPlugins.length} selected` : "Select all"}</span>
|
|
791
|
+
</label>
|
|
792
|
+
<Button
|
|
793
|
+
type="button"
|
|
794
|
+
variant="secondary"
|
|
795
|
+
className="!w-auto"
|
|
796
|
+
aria-label="Upgrade selected plugins"
|
|
797
|
+
disabled={bulkBusy || selectedUpgrades.length === 0}
|
|
798
|
+
onClick={() =>
|
|
799
|
+
void runBulk("upgrade", selectedUpgrades, {
|
|
800
|
+
versions: new Map(selectedUpgrades.map((plugin) => [plugin.uid, updates.get(plugin.uid)!.latestVersion!])),
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
>
|
|
804
|
+
Upgrade{selectedUpgrades.length > 0 ? ` (${selectedUpgrades.length})` : ""}
|
|
805
|
+
</Button>
|
|
806
|
+
<Button
|
|
807
|
+
type="button"
|
|
808
|
+
variant="secondary"
|
|
809
|
+
className="!w-auto"
|
|
810
|
+
aria-label="Disable selected plugins"
|
|
811
|
+
disabled={bulkBusy || selectedEnabled.length === 0}
|
|
812
|
+
onClick={() => void runBulk("disable", selectedEnabled)}
|
|
813
|
+
>
|
|
814
|
+
Disable{selectedEnabled.length > 0 ? ` (${selectedEnabled.length})` : ""}
|
|
815
|
+
</Button>
|
|
816
|
+
<Button
|
|
817
|
+
type="button"
|
|
818
|
+
variant="secondary"
|
|
819
|
+
className="!w-auto"
|
|
820
|
+
aria-label="Uninstall selected plugins"
|
|
821
|
+
disabled={bulkBusy || selectedPlugins.length === 0}
|
|
822
|
+
onClick={() => setConfirmingBulkUninstall(selectedPlugins)}
|
|
823
|
+
>
|
|
824
|
+
Uninstall{selectedPlugins.length > 0 ? ` (${selectedPlugins.length})` : ""}
|
|
825
|
+
</Button>
|
|
826
|
+
<Button
|
|
827
|
+
type="button"
|
|
828
|
+
className="!w-auto ml-auto"
|
|
829
|
+
aria-label="Upgrade all plugins"
|
|
830
|
+
loading={bulk?.kind === "upgrade"}
|
|
831
|
+
disabled={bulkBusy}
|
|
832
|
+
onClick={() => void upgradeAll()}
|
|
833
|
+
>
|
|
834
|
+
Upgrade all
|
|
835
|
+
</Button>
|
|
836
|
+
{bulk && (
|
|
837
|
+
<span aria-live="polite" className="basis-full text-xs text-text-muted">
|
|
838
|
+
{BULK_WORDS[bulk.kind].doing} {bulk.current} ({bulk.done + 1} of {bulk.total})…
|
|
839
|
+
</span>
|
|
840
|
+
)}
|
|
841
|
+
</div>
|
|
842
|
+
)}
|
|
492
843
|
<table role="table" className="w-full text-sm border-collapse block lg:table">
|
|
493
844
|
<thead role="rowgroup" className="hidden lg:table-header-group">
|
|
494
845
|
<tr role="row">
|
|
@@ -510,7 +861,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
510
861
|
{plugins.map((plugin) => {
|
|
511
862
|
const update: PluginUpdateInfo | undefined = updates.get(plugin.uid);
|
|
512
863
|
const latest: string | undefined = update?.updateAvailable ? update.latestVersion : undefined;
|
|
513
|
-
const busy: boolean = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid;
|
|
864
|
+
const busy: boolean = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid || bulkBusy;
|
|
514
865
|
const requires: string[] = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
|
|
515
866
|
const requiredBy: string[] = plugins
|
|
516
867
|
.filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
|
|
@@ -518,15 +869,27 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
518
869
|
return (
|
|
519
870
|
<tr key={plugin.uid} role="row" className={INSTALLED_ROW_CLASS}>
|
|
520
871
|
<td role="cell" className={`${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`}>
|
|
521
|
-
<div className="
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
872
|
+
<div className="flex items-start gap-3">
|
|
873
|
+
<input
|
|
874
|
+
type="checkbox"
|
|
875
|
+
className="mt-1 h-4 w-4 shrink-0"
|
|
876
|
+
aria-label={`Select ${plugin.manifest.displayName}`}
|
|
877
|
+
checked={selected.has(plugin.uid)}
|
|
878
|
+
disabled={bulkBusy}
|
|
879
|
+
onChange={() => toggleSelected(plugin.uid)}
|
|
880
|
+
/>
|
|
881
|
+
<div className="min-w-0">
|
|
882
|
+
<div className="font-semibold">{plugin.manifest.displayName}</div>
|
|
883
|
+
<div className="text-xs text-text-muted">{plugin.name}</div>
|
|
884
|
+
{plugin.manifest.description && (
|
|
885
|
+
<div className="text-xs text-text-muted mt-1 max-w-md">{plugin.manifest.description}</div>
|
|
886
|
+
)}
|
|
887
|
+
{requires.length > 0 && <div className="text-xs text-text-muted mt-1">Requires: {requires.join(", ")}</div>}
|
|
888
|
+
{requiredBy.length > 0 && (
|
|
889
|
+
<div className="text-xs text-text-muted mt-1">Required by: {requiredBy.join(", ")}</div>
|
|
890
|
+
)}
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
530
893
|
</td>
|
|
531
894
|
<td role="cell" className={`${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`}>
|
|
532
895
|
<div>{plugin.packageVersion}</div>
|
|
@@ -596,6 +959,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
596
959
|
<PluginBrowser
|
|
597
960
|
headingLevel={SectionHeading}
|
|
598
961
|
plugins={plugins}
|
|
962
|
+
prerelease={allowPrerelease}
|
|
599
963
|
onInstall={(result) => install(result.name, result.version, result.name)}
|
|
600
964
|
// The browser only offers an upgrade for a result it matched to one of these same `plugins`.
|
|
601
965
|
onUpgrade={(uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid)!, packageVersion)}
|
|
@@ -603,6 +967,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
603
967
|
|
|
604
968
|
<AddPluginModal
|
|
605
969
|
open={adding}
|
|
970
|
+
prerelease={allowPrerelease}
|
|
606
971
|
onClose={() => setAdding(false)}
|
|
607
972
|
onAdd={async (name, packageVersion, displayName) => {
|
|
608
973
|
const problem = await install(name, packageVersion, displayName);
|
|
@@ -615,6 +980,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
615
980
|
{upgrading && (
|
|
616
981
|
<ChangeVersionModal
|
|
617
982
|
plugin={upgrading}
|
|
983
|
+
prerelease={allowPrerelease}
|
|
618
984
|
onClose={() => setUpgrading(null)}
|
|
619
985
|
onSave={async (packageVersion) => {
|
|
620
986
|
const problem = await changeVersion(upgrading, packageVersion);
|
|
@@ -633,6 +999,17 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
633
999
|
onDone={() => setConfirming(null)}
|
|
634
1000
|
/>
|
|
635
1001
|
)}
|
|
1002
|
+
{confirmingBulkUninstall && (
|
|
1003
|
+
<BulkUninstallModal
|
|
1004
|
+
plugins={confirmingBulkUninstall}
|
|
1005
|
+
onClose={() => setConfirmingBulkUninstall(null)}
|
|
1006
|
+
onConfirm={(purgeData) => {
|
|
1007
|
+
const targets = confirmingBulkUninstall;
|
|
1008
|
+
setConfirmingBulkUninstall(null);
|
|
1009
|
+
void runBulk("uninstall", targets, { purgeData });
|
|
1010
|
+
}}
|
|
1011
|
+
/>
|
|
1012
|
+
)}
|
|
636
1013
|
{configuring && (
|
|
637
1014
|
<SettingsModal
|
|
638
1015
|
plugin={configuring}
|
|
@@ -653,11 +1030,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
653
1030
|
setRemoving(null);
|
|
654
1031
|
setPlugins((prev) => prev.filter((plugin) => plugin.uid !== uid));
|
|
655
1032
|
if (result.purgeScheduled) {
|
|
656
|
-
|
|
657
|
-
const scheduled = result.purge;
|
|
658
|
-
setStatus((prev) =>
|
|
659
|
-
prev && scheduled ? { ...prev, purges: [...(prev.purges ?? []).filter((purge) => purge.uid !== scheduled.uid), scheduled] } : prev,
|
|
660
|
-
);
|
|
1033
|
+
showScheduledPurge(result.purge);
|
|
661
1034
|
notify({
|
|
662
1035
|
kind: "info",
|
|
663
1036
|
title: `${displayName} uninstalled`,
|
|
@@ -678,11 +1051,14 @@ const ALL_NAMESPACES = "";
|
|
|
678
1051
|
function PluginBrowser({
|
|
679
1052
|
headingLevel: Heading,
|
|
680
1053
|
plugins,
|
|
1054
|
+
prerelease,
|
|
681
1055
|
onInstall,
|
|
682
1056
|
onUpgrade,
|
|
683
1057
|
}: {
|
|
684
1058
|
headingLevel: "h2" | "h3";
|
|
685
1059
|
plugins: Plugin[];
|
|
1060
|
+
/** Whether prerelease versions count as a plugin's newest. */
|
|
1061
|
+
prerelease: boolean;
|
|
686
1062
|
/** Resolves why the plugin couldn't be installed, or `null`. */
|
|
687
1063
|
onInstall: (result: PluginSearchResult) => Promise<string | null>;
|
|
688
1064
|
onUpgrade: (uid: string, packageVersion: string) => Promise<void>;
|
|
@@ -700,12 +1076,15 @@ function PluginBrowser({
|
|
|
700
1076
|
.catch(() => setNamespaces([]));
|
|
701
1077
|
}, []);
|
|
702
1078
|
|
|
1079
|
+
// What was found was found under the other setting, so it isn't shown as though it still held.
|
|
1080
|
+
useEffect(() => setResults(null), [prerelease]);
|
|
1081
|
+
|
|
703
1082
|
async function search(e: FormEvent) {
|
|
704
1083
|
e.preventDefault();
|
|
705
1084
|
setSearching(true);
|
|
706
1085
|
setError(null);
|
|
707
1086
|
try {
|
|
708
|
-
setResults(await searchPlugins(namespace || undefined));
|
|
1087
|
+
setResults(await searchPlugins(namespace || undefined, { prerelease }));
|
|
709
1088
|
} catch (err) {
|
|
710
1089
|
setResults(null);
|
|
711
1090
|
setError(errorMessage(err, "Could not search for plugins."));
|
|
@@ -996,10 +1375,13 @@ function UninstalledPluginRow({ purge, busy, onRetry, onInstall }: { purge: Plug
|
|
|
996
1375
|
|
|
997
1376
|
function AddPluginModal({
|
|
998
1377
|
open,
|
|
1378
|
+
prerelease,
|
|
999
1379
|
onClose,
|
|
1000
1380
|
onAdd,
|
|
1001
1381
|
}: {
|
|
1002
1382
|
open: boolean;
|
|
1383
|
+
/** Whether prerelease versions are listed, and count as the package's newest. */
|
|
1384
|
+
prerelease: boolean;
|
|
1003
1385
|
onClose: () => void;
|
|
1004
1386
|
/** Resolves why the plugin couldn't be added, or `null`. */
|
|
1005
1387
|
onAdd: (name: string, packageVersion: string, displayName: string) => Promise<string | null>;
|
|
@@ -1040,7 +1422,7 @@ function AddPluginModal({
|
|
|
1040
1422
|
setLooking(true);
|
|
1041
1423
|
setError(null);
|
|
1042
1424
|
try {
|
|
1043
|
-
const found = await lookupPluginPackage(packageName, packageVersion);
|
|
1425
|
+
const found = await lookupPluginPackage(packageName, packageVersion, { prerelease });
|
|
1044
1426
|
if (token === lookupToken.current) {
|
|
1045
1427
|
setLookup(found);
|
|
1046
1428
|
setSelectedVersion(found.selected.version);
|
|
@@ -1161,10 +1543,13 @@ function AddPluginModal({
|
|
|
1161
1543
|
|
|
1162
1544
|
function ChangeVersionModal({
|
|
1163
1545
|
plugin,
|
|
1546
|
+
prerelease,
|
|
1164
1547
|
onClose,
|
|
1165
1548
|
onSave,
|
|
1166
1549
|
}: {
|
|
1167
1550
|
plugin: Plugin;
|
|
1551
|
+
/** Whether prerelease versions are listed, and count as the package's newest. */
|
|
1552
|
+
prerelease: boolean;
|
|
1168
1553
|
onClose: () => void;
|
|
1169
1554
|
/** Resolves why the version couldn't be changed, or `null`. */
|
|
1170
1555
|
onSave: (packageVersion: string) => Promise<string | null>;
|
|
@@ -1176,13 +1561,13 @@ function ChangeVersionModal({
|
|
|
1176
1561
|
const [busy, setBusy] = useState(false);
|
|
1177
1562
|
|
|
1178
1563
|
useEffect(() => {
|
|
1179
|
-
lookupPluginPackage(plugin.name)
|
|
1564
|
+
lookupPluginPackage(plugin.name, undefined, { prerelease })
|
|
1180
1565
|
.then((found) => {
|
|
1181
1566
|
setVersions(found.package.versions);
|
|
1182
1567
|
setLatest(found.package.latest);
|
|
1183
1568
|
})
|
|
1184
1569
|
.catch((err) => setError(errorMessage(err, "Could not load the available versions.")));
|
|
1185
|
-
}, [plugin.name]);
|
|
1570
|
+
}, [plugin.name, prerelease]);
|
|
1186
1571
|
|
|
1187
1572
|
async function save() {
|
|
1188
1573
|
setBusy(true);
|
|
@@ -1530,6 +1915,94 @@ function SettingField({
|
|
|
1530
1915
|
);
|
|
1531
1916
|
}
|
|
1532
1917
|
|
|
1918
|
+
/** The word typed to confirm deleting the data of several plugins at once, in place of a plugin's name. */
|
|
1919
|
+
const BULK_DELETE_WORD = "delete";
|
|
1920
|
+
|
|
1921
|
+
/** Asks for confirmation before several plugins are uninstalled at once, listing them, with the choice to delete the
|
|
1922
|
+
* data they stored too. Deleting data is permanent, so it also takes `BULK_DELETE_WORD` typed out. */
|
|
1923
|
+
function BulkUninstallModal({ plugins, onClose, onConfirm }: { plugins: Plugin[]; onClose: () => void; onConfirm: (purgeData: boolean) => void }) {
|
|
1924
|
+
const [purgeData, setPurgeData] = useState(false);
|
|
1925
|
+
const [typed, setTyped] = useState("");
|
|
1926
|
+
const helpId = useId();
|
|
1927
|
+
const single: boolean = plugins.length === 1;
|
|
1928
|
+
const confirmed: boolean = !purgeData || typed.trim().toLowerCase() === BULK_DELETE_WORD;
|
|
1929
|
+
|
|
1930
|
+
return (
|
|
1931
|
+
<Modal open onClose={onClose} title={`Uninstall ${pluralPlugins(plugins.length)}?`}>
|
|
1932
|
+
<form
|
|
1933
|
+
onSubmit={(event) => {
|
|
1934
|
+
event.preventDefault();
|
|
1935
|
+
if (confirmed) {
|
|
1936
|
+
onConfirm(purgeData);
|
|
1937
|
+
}
|
|
1938
|
+
}}
|
|
1939
|
+
>
|
|
1940
|
+
<p className="text-sm mb-2">The servers stop running {single ? "this plugin" : "these plugins"} after they restart:</p>
|
|
1941
|
+
<ul className="text-sm list-disc pl-5 mb-4">
|
|
1942
|
+
{plugins.map((plugin) => (
|
|
1943
|
+
<li key={plugin.uid}>{plugin.manifest.displayName}</li>
|
|
1944
|
+
))}
|
|
1945
|
+
</ul>
|
|
1946
|
+
<p className="text-sm mb-4">
|
|
1947
|
+
{purgeData
|
|
1948
|
+
? `Once every server has stopped running ${single ? "it" : "them"}, all the data ${single ? "it" : "they"} stored is deleted.`
|
|
1949
|
+
: `Data ${single ? "it" : "they"} stored stays in the database, and adding ${single ? "the plugin" : "a plugin"} again brings it back.`}
|
|
1950
|
+
</p>
|
|
1951
|
+
<div className="mb-4">
|
|
1952
|
+
<label className="flex items-start gap-2 text-sm font-semibold cursor-pointer">
|
|
1953
|
+
<input
|
|
1954
|
+
type="checkbox"
|
|
1955
|
+
className="mt-0.5 h-4 w-4 shrink-0 accent-danger"
|
|
1956
|
+
checked={purgeData}
|
|
1957
|
+
aria-describedby={helpId}
|
|
1958
|
+
onChange={(e) => setPurgeData(e.target.checked)}
|
|
1959
|
+
/>
|
|
1960
|
+
<span>Also delete all data {single ? "this plugin" : "these plugins"} stored</span>
|
|
1961
|
+
</label>
|
|
1962
|
+
<div id={helpId} className="mt-2 pl-6 text-xs text-text-muted">
|
|
1963
|
+
<p>What is deleted, for each plugin:</p>
|
|
1964
|
+
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
|
1965
|
+
<li>the database collections and tables its features use, with everything in them</li>
|
|
1966
|
+
<li>its saved settings</li>
|
|
1967
|
+
<li>its downloaded package and cached pages on the servers</li>
|
|
1968
|
+
<li>whatever else it cleans up itself, such as files it stored or data kept outside the database</li>
|
|
1969
|
+
</ul>
|
|
1970
|
+
<p className="mt-2 text-danger font-semibold">This can't be undone.</p>
|
|
1971
|
+
</div>
|
|
1972
|
+
</div>
|
|
1973
|
+
{purgeData && (
|
|
1974
|
+
<>
|
|
1975
|
+
<p className="text-xs text-text-muted mb-3">
|
|
1976
|
+
Nothing is deleted while a server is still running a plugin. Adding a plugin again before its data is deleted cancels the deletion.
|
|
1977
|
+
</p>
|
|
1978
|
+
<label className="block text-sm mb-4">
|
|
1979
|
+
<span>
|
|
1980
|
+
Type <strong>{BULK_DELETE_WORD}</strong> to confirm
|
|
1981
|
+
</span>
|
|
1982
|
+
<input
|
|
1983
|
+
type="text"
|
|
1984
|
+
className={`${INPUT_CLASS} mt-1.5`}
|
|
1985
|
+
value={typed}
|
|
1986
|
+
autoComplete="off"
|
|
1987
|
+
spellCheck={false}
|
|
1988
|
+
onChange={(e) => setTyped(e.target.value)}
|
|
1989
|
+
/>
|
|
1990
|
+
</label>
|
|
1991
|
+
</>
|
|
1992
|
+
)}
|
|
1993
|
+
<div className="flex gap-2 justify-end">
|
|
1994
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={onClose}>
|
|
1995
|
+
Cancel
|
|
1996
|
+
</Button>
|
|
1997
|
+
<Button type="submit" className={purgeData ? DANGER_BUTTON_CLASS : "!w-auto"} disabled={!confirmed}>
|
|
1998
|
+
{purgeData ? `Uninstall ${pluralPlugins(plugins.length)} and delete data` : `Uninstall ${pluralPlugins(plugins.length)}`}
|
|
1999
|
+
</Button>
|
|
2000
|
+
</div>
|
|
2001
|
+
</form>
|
|
2002
|
+
</Modal>
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1533
2006
|
function RemoveModal({ plugin, onClose, onRemoved }: { plugin: Plugin; onClose: () => void; onRemoved: (result: RemovePluginResult) => void }) {
|
|
1534
2007
|
const [error, setError] = useState<string | null>(null);
|
|
1535
2008
|
const [busy, setBusy] = useState(false);
|