@hartl-services/medusa-base 0.3.0 → 0.4.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/.medusa/server/src/admin/index.js +741 -36
- package/.medusa/server/src/admin/index.mjs +743 -38
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/route.js +31 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/runs/route.js +16 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/route.js +14 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/settings/route.js +22 -0
- package/.medusa/server/src/api/admin/plugin-features/route.js +6 -4
- package/.medusa/server/src/api/middlewares.js +22 -8
- package/.medusa/server/src/api/plugin-actions.js +221 -0
- package/.medusa/server/src/api/plugin-features-http.js +67 -8
- package/.medusa/server/src/modules/plugin-features/domain/plugin-action-runs.js +92 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +213 -0
- package/.medusa/server/src/modules/plugin-features/domain/secret-box.js +56 -0
- package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928205031.js +16 -0
- package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928215428.js +17 -0
- package/.medusa/server/src/modules/plugin-features/models/plugin-action-run.js +43 -0
- package/.medusa/server/src/modules/plugin-features/models/plugin-setting-state.js +32 -0
- package/.medusa/server/src/modules/plugin-features/service.js +419 -17
- package/.medusa/server/src/public/action-progress.js +67 -0
- package/.medusa/server/src/public/descriptor.js +199 -2
- package/.medusa/server/src/public/effective-features.js +9 -5
- package/.medusa/server/src/public/enforcement.js +18 -8
- package/.medusa/server/src/public/index.js +10 -2
- package/.medusa/server/src/public/module-access.js +14 -0
- package/.medusa/server/src/public/settings.js +17 -0
- package/.medusa/server/src/public/shop-master-data.js +95 -0
- package/.medusa/server/src/utils/discover-complete-cart-participants.js +9 -2
- package/.medusa/server/src/utils/discover-plugin-features.js +47 -9
- package/.medusa/server/src/workflows/hooks/complete-cart-participants.js +28 -16
- package/README.md +393 -12
- package/dist/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +6 -0
- package/dist/admin-components/features/plugin-features/components/PluginActionsSection.js +66 -0
- package/dist/admin-components/features/plugin-features/components/PluginDetailPage.d.ts +2 -0
- package/dist/admin-components/features/plugin-features/components/PluginDetailPage.js +9 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.d.ts +22 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.js +18 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +11 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.js +19 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +12 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.js +34 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +5 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +16 -0
- package/dist/admin-components/features/plugin-features/components/PluginHubCard.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/components/PluginHubCard.js +21 -0
- package/dist/admin-components/features/plugin-features/components/PluginPageShell.d.ts +13 -0
- package/dist/admin-components/features/plugin-features/components/PluginPageShell.js +20 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +39 -0
- package/dist/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -0
- package/dist/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +114 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.js +40 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +392 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.js +128 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +93 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.js +44 -0
- package/dist/admin-components/features/plugin-features/lib/action-runs.d.ts +19 -0
- package/dist/admin-components/features/plugin-features/lib/action-runs.js +74 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +363 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +9 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-paths.d.ts +4 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-paths.js +4 -0
- package/dist/admin-components/features/plugin-features/lib/settings-draft.d.ts +28 -0
- package/dist/admin-components/features/plugin-features/lib/settings-draft.js +86 -0
- package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.js +7 -0
- package/dist/admin-components/i18n/texts.d.ts +90 -0
- package/dist/admin-components/i18n/texts.js +90 -0
- package/dist/admin-components/index.d.ts +14 -0
- package/dist/admin-components/index.js +14 -0
- package/dist/admin-components/lib/errors.d.ts +6 -0
- package/dist/admin-components/lib/errors.js +27 -0
- package/dist/admin-components/lib/sdk.d.ts +2 -0
- package/dist/admin-components/lib/sdk.js +13 -0
- package/dist/public/action-progress.d.ts +25 -0
- package/dist/public/action-progress.js +66 -0
- package/dist/public/descriptor.d.ts +3 -2
- package/dist/public/descriptor.js +198 -1
- package/dist/public/effective-features.d.ts +2 -1
- package/dist/public/effective-features.js +8 -4
- package/dist/public/enforcement.d.ts +7 -0
- package/dist/public/enforcement.js +17 -7
- package/dist/public/index.d.ts +5 -2
- package/dist/public/index.js +9 -1
- package/dist/public/module-access.d.ts +3 -0
- package/dist/public/module-access.js +13 -0
- package/dist/public/settings.d.ts +11 -0
- package/dist/public/settings.js +16 -0
- package/dist/public/shop-master-data.d.ts +25 -0
- package/dist/public/shop-master-data.js +94 -0
- package/dist/public/types.d.ts +97 -1
- package/dist/utils/discover-plugin-features.d.ts +28 -0
- package/dist/utils/discover-plugin-features.js +96 -0
- package/package.json +7 -3
|
@@ -1,35 +1,91 @@
|
|
|
1
1
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import { toast, Container, Heading, Text,
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { toast, Container, Heading, Text, Alert, Button, Label, Switch, Input, Select, Hint, StatusBadge, usePrompt } from "@medusajs/ui";
|
|
4
4
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
|
|
5
5
|
import { pluginFeaturesAdminPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/plugin-features";
|
|
6
6
|
import Medusa from "@medusajs/js-sdk";
|
|
7
|
+
import { Link, useParams } from "react-router-dom";
|
|
8
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
7
9
|
const texts = {
|
|
8
10
|
menu: {
|
|
9
11
|
label: "Hartl Services Plugins"
|
|
10
12
|
},
|
|
11
13
|
page: {
|
|
12
14
|
title: "Hartl Services Plugins",
|
|
13
|
-
description: "Plugins und
|
|
15
|
+
description: "Plugins ein- oder ausschalten und öffnen, um Funktionen, Einstellungen und Aktionen zu verwalten. Änderungen gelten sofort; abgeschaltete Funktionen sind im Shop nicht erreichbar."
|
|
14
16
|
},
|
|
15
17
|
states: {
|
|
16
|
-
loading: "
|
|
17
|
-
error: "
|
|
18
|
+
loading: "Plugins werden geladen…",
|
|
19
|
+
error: "Plugins konnten nicht geladen werden.",
|
|
18
20
|
empty: "Keine Plugins mit Funktionsschaltern installiert.",
|
|
19
|
-
retry: "Erneut laden"
|
|
21
|
+
retry: "Erneut laden",
|
|
22
|
+
notFound: "Dieses Plugin ist nicht installiert."
|
|
20
23
|
},
|
|
21
24
|
plugin: {
|
|
22
|
-
switchLabel: (label) => `${label} aktiv
|
|
25
|
+
switchLabel: (label) => `${label} aktiv`,
|
|
26
|
+
open: "Öffnen",
|
|
27
|
+
openLabel: (label) => `${label} öffnen`
|
|
28
|
+
},
|
|
29
|
+
chips: {
|
|
30
|
+
enabled: "aktiv",
|
|
31
|
+
disabled: "aus",
|
|
32
|
+
configured: "konfiguriert",
|
|
33
|
+
notConfigured: (labels) => `nicht konfiguriert: ${labels}`
|
|
34
|
+
},
|
|
35
|
+
detail: {
|
|
36
|
+
back: "Zurück zu Hartl Services Plugins"
|
|
37
|
+
},
|
|
38
|
+
sections: {
|
|
39
|
+
features: "Funktionen",
|
|
40
|
+
settings: "Einstellungen",
|
|
41
|
+
actions: "Aktionen"
|
|
23
42
|
},
|
|
24
43
|
hints: {
|
|
25
44
|
pluginDisabled: "Das Plugin ist ausgeschaltet. Alle Funktionen sind inaktiv, ihre Einstellungen bleiben erhalten.",
|
|
26
45
|
parentDisabled: (parentLabel) => `Inaktiv, solange „${parentLabel}“ ausgeschaltet ist.`,
|
|
27
46
|
saving: "Wird gespeichert…"
|
|
28
47
|
},
|
|
48
|
+
settings: {
|
|
49
|
+
missing: (labels) => `Nicht konfiguriert: ${labels}`,
|
|
50
|
+
save: "Einstellungen speichern",
|
|
51
|
+
secretPlaceholder: "gesetzt",
|
|
52
|
+
secretClear: "Löschen",
|
|
53
|
+
secretClearPending: "Wird beim Speichern gelöscht; danach gilt der Wert aus der Konfiguration, falls vorhanden.",
|
|
54
|
+
secretClearUndo: "Nicht löschen",
|
|
55
|
+
secretClearLabel: (label) => `${label} löschen`,
|
|
56
|
+
secretClearUndoLabel: (label) => `${label} nicht löschen`,
|
|
57
|
+
invalidates: (labels) => `Beim Ändern muss ${labels} neu eingegeben werden.`,
|
|
58
|
+
required: "Pflichtfeld",
|
|
59
|
+
invalidNumber: "Bitte eine gültige Zahl eingeben.",
|
|
60
|
+
outOfRange: "Der Wert liegt außerhalb des erlaubten Bereichs.",
|
|
61
|
+
numberMin: (min) => `Mindestens ${min}.`,
|
|
62
|
+
numberMax: (max) => `Höchstens ${max}.`,
|
|
63
|
+
numberRange: (min, max) => `Zwischen ${min} und ${max}.`,
|
|
64
|
+
selectPlaceholder: "Bitte auswählen",
|
|
65
|
+
clearHint: "Ein leeres Feld speichern entfernt den gespeicherten Wert (dann gilt Konfiguration oder Standard).",
|
|
66
|
+
source: {
|
|
67
|
+
stored: "gespeichert",
|
|
68
|
+
config: "aus Konfiguration",
|
|
69
|
+
default: "Standard",
|
|
70
|
+
none: "nicht gesetzt"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
actions: {
|
|
74
|
+
confirmTitle: (label) => `${label}?`,
|
|
75
|
+
confirm: "Ausführen",
|
|
76
|
+
cancel: "Abbrechen",
|
|
77
|
+
runningSince: (time) => `läuft seit ${time}`,
|
|
78
|
+
lastCompleted: (time) => `Zuletzt erfolgreich: ${time}`,
|
|
79
|
+
lastFailed: (time) => `Zuletzt fehlgeschlagen: ${time}`
|
|
80
|
+
},
|
|
29
81
|
toasts: {
|
|
30
82
|
saved: "Einstellung wurde gespeichert.",
|
|
31
83
|
saveFailed: "Einstellung konnte nicht gespeichert werden.",
|
|
32
|
-
conflict: "Die Einstellungen wurden zwischenzeitlich geändert und neu geladen."
|
|
84
|
+
conflict: "Die Einstellungen wurden zwischenzeitlich geändert und neu geladen.",
|
|
85
|
+
actionCompleted: (label) => `„${label}“ wurde ausgeführt.`,
|
|
86
|
+
actionStarted: (label) => `„${label}“ wurde gestartet und läuft im Hintergrund.`,
|
|
87
|
+
actionFailed: (label) => `„${label}“ ist fehlgeschlagen.`,
|
|
88
|
+
actionAlreadyRunning: (label) => `„${label}“ läuft bereits; der Fortschritt wird angezeigt.`
|
|
33
89
|
},
|
|
34
90
|
errors: {
|
|
35
91
|
unknown: "Unbekannter Fehler"
|
|
@@ -58,62 +114,679 @@ const getErrorMessage = (error) => {
|
|
|
58
114
|
return void 0;
|
|
59
115
|
};
|
|
60
116
|
const sdk = new Medusa({
|
|
61
|
-
baseUrl: __BACKEND_URL__ ?? "/",
|
|
117
|
+
baseUrl: (typeof __BACKEND_URL__ === "undefined" ? void 0 : __BACKEND_URL__) ?? "/",
|
|
62
118
|
auth: {
|
|
63
|
-
type: __AUTH_TYPE__ ?? "session",
|
|
64
|
-
jwtTokenStorageKey: __JWT_TOKEN_STORAGE_KEY__ || void 0
|
|
119
|
+
type: (typeof __AUTH_TYPE__ === "undefined" ? void 0 : __AUTH_TYPE__) ?? "session",
|
|
120
|
+
jwtTokenStorageKey: (typeof __JWT_TOKEN_STORAGE_KEY__ === "undefined" ? void 0 : __JWT_TOKEN_STORAGE_KEY__) || void 0
|
|
65
121
|
}
|
|
66
122
|
});
|
|
67
123
|
const listPluginFeatures = () => sdk.client.fetch(
|
|
68
124
|
pluginFeaturesAdminPaths.get.list,
|
|
69
125
|
{ method: "GET" }
|
|
70
126
|
);
|
|
127
|
+
const retrievePluginFeatures = (pluginKey) => sdk.client.fetch(
|
|
128
|
+
pluginFeaturesAdminPaths.get.retrieve(pluginKey),
|
|
129
|
+
{ method: "GET" }
|
|
130
|
+
);
|
|
71
131
|
const updatePluginFeatures = (payload) => sdk.client.fetch(
|
|
72
132
|
pluginFeaturesAdminPaths.post.update,
|
|
73
133
|
{ method: "POST", body: payload }
|
|
74
134
|
);
|
|
135
|
+
const updatePluginSettings = (pluginKey, payload) => sdk.client.fetch(
|
|
136
|
+
pluginFeaturesAdminPaths.post.updateSettings(pluginKey),
|
|
137
|
+
{ method: "POST", body: payload }
|
|
138
|
+
);
|
|
139
|
+
const runPluginAction = (pluginKey, actionKey) => sdk.client.fetch(
|
|
140
|
+
pluginFeaturesAdminPaths.post.runAction(pluginKey, actionKey),
|
|
141
|
+
{ method: "POST", body: {} }
|
|
142
|
+
);
|
|
143
|
+
const listPluginActionRuns = (pluginKey, actionKey, limit = 1) => sdk.client.fetch(
|
|
144
|
+
pluginFeaturesAdminPaths.get.actionRuns(pluginKey, actionKey),
|
|
145
|
+
{ method: "GET", query: { limit } }
|
|
146
|
+
);
|
|
75
147
|
const pluginFeatureKeys = {
|
|
76
148
|
all: ["plugin-features"],
|
|
77
|
-
list: () => [...pluginFeatureKeys.all, "list"]
|
|
149
|
+
list: () => [...pluginFeatureKeys.all, "list"],
|
|
150
|
+
details: () => [...pluginFeatureKeys.all, "detail"],
|
|
151
|
+
detail: (pluginKey) => [...pluginFeatureKeys.details(), pluginKey],
|
|
152
|
+
actionRuns: (pluginKey, actionKey) => [...pluginFeatureKeys.all, "action-runs", pluginKey, actionKey]
|
|
78
153
|
};
|
|
79
154
|
const isConflict = (error) => typeof error === "object" && error !== null && "status" in error && error.status === 409;
|
|
155
|
+
const storePlugin = (queryClient, plugin2) => {
|
|
156
|
+
queryClient.setQueryData(
|
|
157
|
+
pluginFeatureKeys.list(),
|
|
158
|
+
(current) => current && {
|
|
159
|
+
plugins: current.plugins.map(
|
|
160
|
+
(entry) => entry.plugin_key === plugin2.plugin_key ? plugin2 : entry
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
queryClient.setQueryData(
|
|
165
|
+
pluginFeatureKeys.detail(plugin2.plugin_key),
|
|
166
|
+
{ plugin: plugin2 }
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
const onSaveError = async (queryClient, error) => {
|
|
170
|
+
if (isConflict(error)) {
|
|
171
|
+
toast.error(texts.toasts.conflict);
|
|
172
|
+
} else {
|
|
173
|
+
toast.error(texts.toasts.saveFailed, {
|
|
174
|
+
description: getErrorMessage(error) ?? texts.errors.unknown
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
await queryClient.invalidateQueries({ queryKey: pluginFeatureKeys.all });
|
|
178
|
+
};
|
|
80
179
|
function usePluginFeatures() {
|
|
81
180
|
return useQuery({
|
|
82
181
|
queryKey: pluginFeatureKeys.list(),
|
|
83
182
|
queryFn: listPluginFeatures
|
|
84
183
|
});
|
|
85
184
|
}
|
|
185
|
+
function usePluginFeature(pluginKey) {
|
|
186
|
+
const queryClient = useQueryClient();
|
|
187
|
+
return useQuery({
|
|
188
|
+
queryKey: pluginFeatureKeys.detail(pluginKey ?? ""),
|
|
189
|
+
queryFn: () => retrievePluginFeatures(pluginKey),
|
|
190
|
+
enabled: Boolean(pluginKey),
|
|
191
|
+
placeholderData: () => {
|
|
192
|
+
const plugin2 = queryClient.getQueryData(pluginFeatureKeys.list())?.plugins.find((entry) => entry.plugin_key === pluginKey);
|
|
193
|
+
return plugin2 ? { plugin: plugin2 } : void 0;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
86
197
|
function useUpdatePluginFeatures() {
|
|
87
198
|
const queryClient = useQueryClient();
|
|
88
199
|
return useMutation({
|
|
89
200
|
mutationFn: updatePluginFeatures,
|
|
90
201
|
onSuccess: ({ plugin: plugin2 }) => {
|
|
202
|
+
storePlugin(queryClient, plugin2);
|
|
203
|
+
toast.success(texts.toasts.saved);
|
|
204
|
+
},
|
|
205
|
+
onError: (error) => onSaveError(queryClient, error)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function useUpdatePluginSettings(pluginKey) {
|
|
209
|
+
const queryClient = useQueryClient();
|
|
210
|
+
return useMutation({
|
|
211
|
+
mutationFn: (payload) => updatePluginSettings(pluginKey, payload),
|
|
212
|
+
onSuccess: ({ plugin: plugin2 }) => {
|
|
213
|
+
storePlugin(queryClient, plugin2);
|
|
214
|
+
toast.success(texts.toasts.saved);
|
|
215
|
+
},
|
|
216
|
+
onError: (error) => onSaveError(queryClient, error)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const PLUGIN_ACTION_RUN_POLL_MS = 2e3;
|
|
220
|
+
function useLatestPluginActionRun(pluginKey, actionKey) {
|
|
221
|
+
return useQuery({
|
|
222
|
+
queryKey: pluginFeatureKeys.actionRuns(pluginKey, actionKey),
|
|
223
|
+
queryFn: () => listPluginActionRuns(pluginKey, actionKey, 1),
|
|
224
|
+
select: (data) => data.runs[0],
|
|
225
|
+
refetchInterval: (query) => query.state.data?.runs[0]?.status === "running" ? PLUGIN_ACTION_RUN_POLL_MS : false
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function useRunPluginAction(pluginKey) {
|
|
229
|
+
const queryClient = useQueryClient();
|
|
230
|
+
return useMutation({
|
|
231
|
+
mutationFn: (action) => runPluginAction(pluginKey, action.key),
|
|
232
|
+
onSuccess: ({ action: run }, action) => {
|
|
91
233
|
queryClient.setQueryData(
|
|
92
|
-
pluginFeatureKeys.
|
|
93
|
-
|
|
94
|
-
plugins: current.plugins.map(
|
|
95
|
-
(entry) => entry.plugin_key === plugin2.plugin_key ? plugin2 : entry
|
|
96
|
-
)
|
|
97
|
-
}
|
|
234
|
+
pluginFeatureKeys.actionRuns(pluginKey, action.key),
|
|
235
|
+
{ runs: [run.run] }
|
|
98
236
|
);
|
|
99
|
-
|
|
237
|
+
if (run.status === "started") {
|
|
238
|
+
toast.info(texts.toasts.actionStarted(action.label));
|
|
239
|
+
} else {
|
|
240
|
+
toast.success(texts.toasts.actionCompleted(action.label));
|
|
241
|
+
}
|
|
100
242
|
},
|
|
101
|
-
onError: async (error) => {
|
|
243
|
+
onError: async (error, action) => {
|
|
102
244
|
if (isConflict(error)) {
|
|
103
|
-
toast.
|
|
245
|
+
toast.info(texts.toasts.actionAlreadyRunning(action.label));
|
|
104
246
|
} else {
|
|
105
|
-
toast.error(texts.toasts.
|
|
247
|
+
toast.error(texts.toasts.actionFailed(action.label), {
|
|
106
248
|
description: getErrorMessage(error) ?? texts.errors.unknown
|
|
107
249
|
});
|
|
108
250
|
}
|
|
109
|
-
await queryClient.invalidateQueries({
|
|
251
|
+
await queryClient.invalidateQueries({
|
|
252
|
+
queryKey: pluginFeatureKeys.actionRuns(pluginKey, action.key)
|
|
253
|
+
});
|
|
110
254
|
}
|
|
111
255
|
});
|
|
112
256
|
}
|
|
257
|
+
const PLUGIN_HUB_PATH = "/settings/hartl-services";
|
|
258
|
+
const pluginDetailPath = (pluginKey) => `${PLUGIN_HUB_PATH}/plugin/${encodeURIComponent(pluginKey)}`;
|
|
259
|
+
const initialEntry = (setting) => {
|
|
260
|
+
if (setting.type === "secret") {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
if (setting.type === "boolean") {
|
|
264
|
+
return setting.value === true;
|
|
265
|
+
}
|
|
266
|
+
return setting.value === null ? "" : String(setting.value);
|
|
267
|
+
};
|
|
268
|
+
const initialSettingsDraft = (settings) => Object.fromEntries(
|
|
269
|
+
settings.map((setting) => [setting.key, initialEntry(setting)])
|
|
270
|
+
);
|
|
271
|
+
const outsideBounds = (setting, value) => setting.min !== null && value < setting.min || setting.max !== null && value > setting.max;
|
|
272
|
+
const numberEntryValue = (entry) => {
|
|
273
|
+
const value = Number(entry.trim());
|
|
274
|
+
return Number.isFinite(value) ? value : null;
|
|
275
|
+
};
|
|
276
|
+
const invalidDraftKeys = (settings, draft, unparsable = []) => settings.filter((setting) => {
|
|
277
|
+
if (setting.type !== "number") {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (unparsable.includes(setting.key)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const entry = draft[setting.key];
|
|
284
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const value = numberEntryValue(entry);
|
|
288
|
+
return value === null || outsideBounds(setting, value);
|
|
289
|
+
}).map((setting) => setting.key);
|
|
290
|
+
const changedSettingValues = (settings, draft, unparsable = []) => {
|
|
291
|
+
const initial = initialSettingsDraft(settings);
|
|
292
|
+
const values = {};
|
|
293
|
+
for (const setting of settings) {
|
|
294
|
+
const entry = draft[setting.key];
|
|
295
|
+
if (entry === void 0 || entry === initial[setting.key]) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (setting.type === "secret") {
|
|
299
|
+
if (entry === null) {
|
|
300
|
+
if (setting.source === "stored") {
|
|
301
|
+
values[setting.key] = null;
|
|
302
|
+
}
|
|
303
|
+
} else if (typeof entry === "string" && entry !== "") {
|
|
304
|
+
values[setting.key] = entry;
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (setting.type === "boolean") {
|
|
309
|
+
values[setting.key] = entry === true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (unparsable.includes(setting.key)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const text = typeof entry === "string" ? entry.trim() : "";
|
|
316
|
+
if (text === "") {
|
|
317
|
+
if (setting.source === "stored") {
|
|
318
|
+
values[setting.key] = null;
|
|
319
|
+
}
|
|
320
|
+
} else if (setting.type === "number") {
|
|
321
|
+
const value = numberEntryValue(text);
|
|
322
|
+
if (value !== null) {
|
|
323
|
+
values[setting.key] = value;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
values[setting.key] = text;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return values;
|
|
330
|
+
};
|
|
331
|
+
function usePluginSettingsForm(plugin2) {
|
|
332
|
+
const mutation = useUpdatePluginSettings(plugin2.plugin_key);
|
|
333
|
+
const [draft, setDraft] = useState(
|
|
334
|
+
() => initialSettingsDraft(plugin2.settings)
|
|
335
|
+
);
|
|
336
|
+
const [unparsable, setUnparsable] = useState([]);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
setDraft(initialSettingsDraft(plugin2.settings));
|
|
339
|
+
setUnparsable([]);
|
|
340
|
+
}, [plugin2.settings, plugin2.settings_version]);
|
|
341
|
+
const changes = useMemo(
|
|
342
|
+
() => changedSettingValues(plugin2.settings, draft, unparsable),
|
|
343
|
+
[plugin2.settings, draft, unparsable]
|
|
344
|
+
);
|
|
345
|
+
const invalid = useMemo(
|
|
346
|
+
() => invalidDraftKeys(plugin2.settings, draft, unparsable),
|
|
347
|
+
[plugin2.settings, draft, unparsable]
|
|
348
|
+
);
|
|
349
|
+
const dirty = Object.keys(changes).length > 0;
|
|
350
|
+
const setValue = (key, value, badInput = false) => {
|
|
351
|
+
setDraft((current) => ({ ...current, [key]: value }));
|
|
352
|
+
setUnparsable(
|
|
353
|
+
(current) => badInput ? current.includes(key) ? current : [...current, key] : current.filter((entry) => entry !== key)
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
const submit = (event) => {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
if (!dirty || invalid.length > 0 || mutation.isPending) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
mutation.mutate({
|
|
362
|
+
expected_version: plugin2.settings_version,
|
|
363
|
+
values: changes
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
draft,
|
|
368
|
+
setValue,
|
|
369
|
+
dirty,
|
|
370
|
+
invalid,
|
|
371
|
+
submit,
|
|
372
|
+
mutation
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
|
|
376
|
+
const missingLabels = (plugin2) => plugin2.missing.map(
|
|
377
|
+
(key) => plugin2.settings.find((setting) => setting.key === key)?.label ?? key
|
|
378
|
+
).join(", ");
|
|
379
|
+
const invalidatesLabels = (plugin2, setting) => setting.invalidates.map(
|
|
380
|
+
(key) => plugin2.settings.find((candidate) => candidate.key === key)?.label ?? key
|
|
381
|
+
).join(", ");
|
|
382
|
+
const boundsText = (setting) => setting.min !== null && setting.max !== null ? texts.settings.numberRange(setting.min, setting.max) : setting.min !== null ? texts.settings.numberMin(setting.min) : setting.max !== null ? texts.settings.numberMax(setting.max) : void 0;
|
|
383
|
+
const SettingField = ({
|
|
384
|
+
pluginKey,
|
|
385
|
+
setting,
|
|
386
|
+
invalidatesLabels: invalidatesLabels2,
|
|
387
|
+
value,
|
|
388
|
+
invalid,
|
|
389
|
+
disabled,
|
|
390
|
+
onChange
|
|
391
|
+
}) => {
|
|
392
|
+
const id = fieldId(pluginKey, setting.key);
|
|
393
|
+
const hintId = `${id}-hint`;
|
|
394
|
+
const clearPending = setting.type === "secret" && value === null;
|
|
395
|
+
const bounds = setting.type === "number" ? boundsText(setting) : void 0;
|
|
396
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
397
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
398
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: id, size: "small", weight: "plus", children: [
|
|
399
|
+
setting.label,
|
|
400
|
+
setting.required && /* @__PURE__ */ jsxs("span", { className: "text-ui-fg-muted", children: [
|
|
401
|
+
" (",
|
|
402
|
+
texts.settings.required,
|
|
403
|
+
")"
|
|
404
|
+
] })
|
|
405
|
+
] }),
|
|
406
|
+
/* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted shrink-0", children: texts.settings.source[setting.source] })
|
|
407
|
+
] }),
|
|
408
|
+
setting.type === "boolean" ? /* @__PURE__ */ jsx(
|
|
409
|
+
Switch,
|
|
410
|
+
{
|
|
411
|
+
id,
|
|
412
|
+
"aria-describedby": hintId,
|
|
413
|
+
checked: value === true,
|
|
414
|
+
disabled,
|
|
415
|
+
onCheckedChange: (checked) => onChange(checked === true)
|
|
416
|
+
}
|
|
417
|
+
) : setting.type === "secret" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
418
|
+
/* @__PURE__ */ jsx(
|
|
419
|
+
Input,
|
|
420
|
+
{
|
|
421
|
+
id,
|
|
422
|
+
"aria-describedby": hintId,
|
|
423
|
+
className: "flex-1",
|
|
424
|
+
type: "password",
|
|
425
|
+
autoComplete: "new-password",
|
|
426
|
+
placeholder: setting.is_set ? texts.settings.secretPlaceholder : "",
|
|
427
|
+
value: typeof value === "string" ? value : "",
|
|
428
|
+
disabled: disabled || clearPending,
|
|
429
|
+
onChange: (event) => onChange(event.target.value)
|
|
430
|
+
}
|
|
431
|
+
),
|
|
432
|
+
setting.source === "stored" && /* @__PURE__ */ jsx(
|
|
433
|
+
Button,
|
|
434
|
+
{
|
|
435
|
+
type: "button",
|
|
436
|
+
size: "small",
|
|
437
|
+
variant: "secondary",
|
|
438
|
+
disabled,
|
|
439
|
+
"aria-label": clearPending ? texts.settings.secretClearUndoLabel(setting.label) : texts.settings.secretClearLabel(setting.label),
|
|
440
|
+
onClick: () => onChange(clearPending ? "" : null),
|
|
441
|
+
children: clearPending ? texts.settings.secretClearUndo : texts.settings.secretClear
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
] }) : setting.type === "select" ? /* @__PURE__ */ jsxs(
|
|
445
|
+
Select,
|
|
446
|
+
{
|
|
447
|
+
value: typeof value === "string" ? value : "",
|
|
448
|
+
disabled,
|
|
449
|
+
onValueChange: (next) => onChange(next),
|
|
450
|
+
children: [
|
|
451
|
+
/* @__PURE__ */ jsx(Select.Trigger, { id, "aria-describedby": hintId, children: /* @__PURE__ */ jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }),
|
|
452
|
+
/* @__PURE__ */ jsx(Select.Content, { children: setting.options.map((option) => /* @__PURE__ */ jsx(Select.Item, { value: option.value, children: option.label }, option.value)) })
|
|
453
|
+
]
|
|
454
|
+
}
|
|
455
|
+
) : setting.type === "number" ? /* @__PURE__ */ jsx(
|
|
456
|
+
Input,
|
|
457
|
+
{
|
|
458
|
+
id,
|
|
459
|
+
"aria-describedby": hintId,
|
|
460
|
+
"aria-invalid": invalid,
|
|
461
|
+
type: "number",
|
|
462
|
+
step: "any",
|
|
463
|
+
min: setting.min ?? void 0,
|
|
464
|
+
max: setting.max ?? void 0,
|
|
465
|
+
value: typeof value === "string" ? value : "",
|
|
466
|
+
disabled,
|
|
467
|
+
onChange: (event) => onChange(event.target.value, event.target.validity.badInput)
|
|
468
|
+
}
|
|
469
|
+
) : /* @__PURE__ */ jsx(
|
|
470
|
+
Input,
|
|
471
|
+
{
|
|
472
|
+
id,
|
|
473
|
+
"aria-describedby": hintId,
|
|
474
|
+
"aria-invalid": invalid,
|
|
475
|
+
type: setting.type === "url" ? "url" : "text",
|
|
476
|
+
value: typeof value === "string" ? value : "",
|
|
477
|
+
disabled,
|
|
478
|
+
onChange: (event) => onChange(event.target.value)
|
|
479
|
+
}
|
|
480
|
+
),
|
|
481
|
+
/* @__PURE__ */ jsxs("div", { id: hintId, className: "grid gap-1", children: [
|
|
482
|
+
setting.description && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description }),
|
|
483
|
+
clearPending && /* @__PURE__ */ jsx(Hint, { variant: "info", children: texts.settings.secretClearPending }),
|
|
484
|
+
invalidatesLabels2 && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels2) }),
|
|
485
|
+
bounds && !invalid && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds }),
|
|
486
|
+
invalid && /* @__PURE__ */ jsx(Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber })
|
|
487
|
+
] })
|
|
488
|
+
] });
|
|
489
|
+
};
|
|
490
|
+
const PluginSettingsForm = ({ plugin: plugin2 }) => {
|
|
491
|
+
const form = usePluginSettingsForm(plugin2);
|
|
492
|
+
const pending = form.mutation.isPending;
|
|
493
|
+
const hasTextFields = plugin2.settings.some(
|
|
494
|
+
(setting) => setting.type !== "boolean" && setting.type !== "secret" && setting.type !== "select"
|
|
495
|
+
);
|
|
496
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [
|
|
497
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4", children: [
|
|
498
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: texts.sections.settings }),
|
|
499
|
+
hasTextFields && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.clearHint })
|
|
500
|
+
] }),
|
|
501
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: form.submit, className: "grid gap-5 px-6 py-5", children: [
|
|
502
|
+
!plugin2.configured && /* @__PURE__ */ jsx(Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin2)) }),
|
|
503
|
+
plugin2.settings.map((setting) => /* @__PURE__ */ jsx(
|
|
504
|
+
SettingField,
|
|
505
|
+
{
|
|
506
|
+
pluginKey: plugin2.plugin_key,
|
|
507
|
+
setting,
|
|
508
|
+
invalidatesLabels: invalidatesLabels(plugin2, setting),
|
|
509
|
+
value: form.draft[setting.key],
|
|
510
|
+
invalid: form.invalid.includes(setting.key),
|
|
511
|
+
disabled: pending,
|
|
512
|
+
onChange: (value, badInput) => form.setValue(setting.key, value, badInput)
|
|
513
|
+
},
|
|
514
|
+
setting.key
|
|
515
|
+
)),
|
|
516
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
|
|
517
|
+
Button,
|
|
518
|
+
{
|
|
519
|
+
type: "submit",
|
|
520
|
+
size: "small",
|
|
521
|
+
isLoading: pending,
|
|
522
|
+
disabled: !form.dirty || form.invalid.length > 0 || pending,
|
|
523
|
+
children: texts.settings.save
|
|
524
|
+
}
|
|
525
|
+
) })
|
|
526
|
+
] })
|
|
527
|
+
] });
|
|
528
|
+
};
|
|
529
|
+
const PluginStatusChips = ({ plugin: plugin2 }) => /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
|
|
530
|
+
plugin2.switchable && /* @__PURE__ */ jsx(StatusBadge, { color: plugin2.enabled ? "green" : "grey", children: plugin2.enabled ? texts.chips.enabled : texts.chips.disabled }),
|
|
531
|
+
plugin2.settings.length > 0 && /* @__PURE__ */ jsx(StatusBadge, { color: plugin2.configured ? "green" : "orange", children: plugin2.configured ? texts.chips.configured : texts.chips.notConfigured(missingLabels(plugin2)) })
|
|
532
|
+
] });
|
|
533
|
+
const PluginHubCard = ({ plugin: plugin2 }) => {
|
|
534
|
+
const mutation = useUpdatePluginFeatures();
|
|
535
|
+
const pending = mutation.isPending;
|
|
536
|
+
const toggle = (enabled) => {
|
|
537
|
+
mutation.mutate({
|
|
538
|
+
plugin_key: plugin2.plugin_key,
|
|
539
|
+
expected_version: plugin2.version,
|
|
540
|
+
enabled,
|
|
541
|
+
features: Object.fromEntries(
|
|
542
|
+
plugin2.features.map((feature) => [feature.key, feature.enabled])
|
|
543
|
+
)
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
return /* @__PURE__ */ jsxs(
|
|
547
|
+
Container,
|
|
548
|
+
{
|
|
549
|
+
className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4",
|
|
550
|
+
"aria-busy": pending,
|
|
551
|
+
children: [
|
|
552
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
553
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: plugin2.label }),
|
|
554
|
+
plugin2.description && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
|
|
555
|
+
/* @__PURE__ */ jsx(PluginStatusChips, { plugin: plugin2 }),
|
|
556
|
+
pending && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving })
|
|
557
|
+
] }),
|
|
558
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [
|
|
559
|
+
plugin2.switchable && /* @__PURE__ */ jsx(
|
|
560
|
+
Switch,
|
|
561
|
+
{
|
|
562
|
+
"aria-label": texts.plugin.switchLabel(plugin2.label),
|
|
563
|
+
checked: plugin2.enabled,
|
|
564
|
+
disabled: pending,
|
|
565
|
+
onCheckedChange: (checked) => toggle(checked === true)
|
|
566
|
+
}
|
|
567
|
+
),
|
|
568
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", asChild: true, children: /* @__PURE__ */ jsx(
|
|
569
|
+
Link,
|
|
570
|
+
{
|
|
571
|
+
to: plugin2.admin_page ?? pluginDetailPath(plugin2.plugin_key),
|
|
572
|
+
"aria-label": texts.plugin.openLabel(plugin2.label),
|
|
573
|
+
children: texts.plugin.open
|
|
574
|
+
}
|
|
575
|
+
) })
|
|
576
|
+
] })
|
|
577
|
+
]
|
|
578
|
+
}
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
const PluginFeaturesSettingsPage = () => {
|
|
582
|
+
const query = usePluginFeatures();
|
|
583
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
584
|
+
/* @__PURE__ */ jsxs(Container, { className: "px-6 py-4", children: [
|
|
585
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: texts.page.title }),
|
|
586
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: texts.page.description })
|
|
587
|
+
] }),
|
|
588
|
+
query.isPending ? /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) }) : query.isError ? /* @__PURE__ */ jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
589
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }),
|
|
590
|
+
/* @__PURE__ */ jsx(
|
|
591
|
+
Button,
|
|
592
|
+
{
|
|
593
|
+
size: "small",
|
|
594
|
+
variant: "secondary",
|
|
595
|
+
isLoading: query.isFetching,
|
|
596
|
+
onClick: () => void query.refetch(),
|
|
597
|
+
children: texts.states.retry
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
] }) : query.data.plugins.length === 0 ? /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.empty }) }) : [
|
|
601
|
+
...query.data.plugins.filter((plugin2) => !plugin2.switchable),
|
|
602
|
+
...query.data.plugins.filter((plugin2) => plugin2.switchable)
|
|
603
|
+
].map((plugin2) => /* @__PURE__ */ jsx(PluginHubCard, { plugin: plugin2 }, plugin2.plugin_key))
|
|
604
|
+
] });
|
|
605
|
+
};
|
|
606
|
+
const config = defineRouteConfig({
|
|
607
|
+
label: texts.menu.label
|
|
608
|
+
});
|
|
609
|
+
const SUMMARY_MAX_LENGTH = 200;
|
|
610
|
+
const cut = (text) => text.length > SUMMARY_MAX_LENGTH ? `${text.slice(0, SUMMARY_MAX_LENGTH - 1)}…` : text;
|
|
611
|
+
const isPrimitive = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
612
|
+
const summarizeActionResult = (result) => {
|
|
613
|
+
if (result === null || result === void 0) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (isPrimitive(result)) {
|
|
617
|
+
return cut(String(result));
|
|
618
|
+
}
|
|
619
|
+
if (typeof result !== "object" || Array.isArray(result)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const record = result;
|
|
623
|
+
for (const key of ["summary", "message"]) {
|
|
624
|
+
const value = record[key];
|
|
625
|
+
if (typeof value === "string" && value.trim()) {
|
|
626
|
+
return cut(value.trim());
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const parts = Object.entries(record).filter(
|
|
630
|
+
(entry) => isPrimitive(entry[1])
|
|
631
|
+
).map(([key, value]) => `${key}: ${String(value)}`);
|
|
632
|
+
return parts.length > 0 ? cut(parts.join(", ")) : null;
|
|
633
|
+
};
|
|
634
|
+
const progressPercent = (progress) => {
|
|
635
|
+
if (!progress || progress.total === null || progress.total <= 0) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
return Math.max(
|
|
639
|
+
0,
|
|
640
|
+
Math.min(100, Math.round(progress.current / progress.total * 100))
|
|
641
|
+
);
|
|
642
|
+
};
|
|
643
|
+
const progressCount = (progress) => {
|
|
644
|
+
if (!progress) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return progress.total === null ? String(progress.current) : `${progress.current} / ${progress.total}`;
|
|
648
|
+
};
|
|
649
|
+
const formatRunTime = (iso) => {
|
|
650
|
+
if (!iso) {
|
|
651
|
+
return "";
|
|
652
|
+
}
|
|
653
|
+
const date = new Date(iso);
|
|
654
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString("de-AT", { dateStyle: "short", timeStyle: "short" });
|
|
655
|
+
};
|
|
656
|
+
const runTransition = (previous, current) => {
|
|
657
|
+
if (!previous || !current || previous.id !== current.id || previous.status !== "running" || current.status === "running") {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
return current.status;
|
|
661
|
+
};
|
|
662
|
+
const RunProgress = ({ run }) => {
|
|
663
|
+
const percent = progressPercent(run.progress);
|
|
664
|
+
const count = progressCount(run.progress);
|
|
665
|
+
const label = run.progress?.label;
|
|
666
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-col gap-y-1", role: "status", children: [
|
|
667
|
+
(label || count) && /* @__PURE__ */ jsx(Text, { size: "small", children: [label, count].filter(Boolean).join(" · ") }),
|
|
668
|
+
percent !== null && /* @__PURE__ */ jsx(
|
|
669
|
+
"div",
|
|
670
|
+
{
|
|
671
|
+
className: "bg-ui-border-base w-full overflow-hidden rounded-full",
|
|
672
|
+
style: { height: 6 },
|
|
673
|
+
role: "progressbar",
|
|
674
|
+
"aria-valuemin": 0,
|
|
675
|
+
"aria-valuemax": 100,
|
|
676
|
+
"aria-valuenow": percent,
|
|
677
|
+
children: /* @__PURE__ */ jsx(
|
|
678
|
+
"div",
|
|
679
|
+
{
|
|
680
|
+
className: "bg-ui-fg-interactive h-full rounded-full",
|
|
681
|
+
style: { width: `${percent}%`, height: "100%" }
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
),
|
|
686
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.actions.runningSince(formatRunTime(run.started_at)) })
|
|
687
|
+
] });
|
|
688
|
+
};
|
|
689
|
+
const RunStatusLine = ({ run }) => {
|
|
690
|
+
if (run.status === "completed") {
|
|
691
|
+
const summary = summarizeActionResult(run.result);
|
|
692
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-1", children: [
|
|
693
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.actions.lastCompleted(
|
|
694
|
+
formatRunTime(run.finished_at ?? run.started_at)
|
|
695
|
+
) }),
|
|
696
|
+
summary && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: summary })
|
|
697
|
+
] });
|
|
698
|
+
}
|
|
699
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-1", children: [
|
|
700
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: texts.actions.lastFailed(
|
|
701
|
+
formatRunTime(run.finished_at ?? run.started_at)
|
|
702
|
+
) }),
|
|
703
|
+
run.message && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: run.message })
|
|
704
|
+
] });
|
|
705
|
+
};
|
|
706
|
+
const PluginActionRow = ({
|
|
707
|
+
pluginKey,
|
|
708
|
+
action,
|
|
709
|
+
starting,
|
|
710
|
+
blocked,
|
|
711
|
+
onRun
|
|
712
|
+
}) => {
|
|
713
|
+
const { data: run } = useLatestPluginActionRun(pluginKey, action.key);
|
|
714
|
+
const previous = useRef(void 0);
|
|
715
|
+
const running = run?.status === "running";
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
const transition = runTransition(previous.current, run);
|
|
718
|
+
previous.current = run;
|
|
719
|
+
if (transition === "completed") {
|
|
720
|
+
toast.success(texts.toasts.actionCompleted(action.label), {
|
|
721
|
+
description: summarizeActionResult(run?.result) ?? void 0
|
|
722
|
+
});
|
|
723
|
+
} else if (transition === "failed") {
|
|
724
|
+
toast.error(texts.toasts.actionFailed(action.label), {
|
|
725
|
+
description: run?.message ?? texts.errors.unknown
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}, [run, action.label]);
|
|
729
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [
|
|
730
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
731
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: action.label }),
|
|
732
|
+
action.description && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: action.description }),
|
|
733
|
+
run && (running ? /* @__PURE__ */ jsx(RunProgress, { run }) : /* @__PURE__ */ jsx(RunStatusLine, { run }))
|
|
734
|
+
] }),
|
|
735
|
+
/* @__PURE__ */ jsx(
|
|
736
|
+
Button,
|
|
737
|
+
{
|
|
738
|
+
size: "small",
|
|
739
|
+
variant: "secondary",
|
|
740
|
+
className: "shrink-0",
|
|
741
|
+
isLoading: starting || running,
|
|
742
|
+
disabled: blocked || starting || running,
|
|
743
|
+
onClick: () => onRun(action),
|
|
744
|
+
children: action.label
|
|
745
|
+
}
|
|
746
|
+
)
|
|
747
|
+
] });
|
|
748
|
+
};
|
|
749
|
+
const PluginActionsSection = ({ plugin: plugin2 }) => {
|
|
750
|
+
const mutation = useRunPluginAction(plugin2.plugin_key);
|
|
751
|
+
const prompt = usePrompt();
|
|
752
|
+
const startingKey = mutation.isPending ? mutation.variables?.key : void 0;
|
|
753
|
+
const run = async (action) => {
|
|
754
|
+
if (mutation.isPending) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (action.confirm) {
|
|
758
|
+
const confirmed = await prompt({
|
|
759
|
+
title: texts.actions.confirmTitle(action.label),
|
|
760
|
+
description: action.confirm,
|
|
761
|
+
confirmText: texts.actions.confirm,
|
|
762
|
+
cancelText: texts.actions.cancel,
|
|
763
|
+
variant: "confirmation"
|
|
764
|
+
});
|
|
765
|
+
if (!confirmed) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
mutation.mutate(action);
|
|
770
|
+
};
|
|
771
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
772
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: texts.sections.actions }) }),
|
|
773
|
+
plugin2.actions.map((action) => /* @__PURE__ */ jsx(
|
|
774
|
+
PluginActionRow,
|
|
775
|
+
{
|
|
776
|
+
pluginKey: plugin2.plugin_key,
|
|
777
|
+
action,
|
|
778
|
+
starting: startingKey === action.key,
|
|
779
|
+
blocked: mutation.isPending && startingKey !== action.key,
|
|
780
|
+
onRun: (entry) => void run(entry)
|
|
781
|
+
},
|
|
782
|
+
action.key
|
|
783
|
+
))
|
|
784
|
+
] });
|
|
785
|
+
};
|
|
113
786
|
const configuredFeatures = (plugin2) => Object.fromEntries(
|
|
114
787
|
plugin2.features.map((feature) => [feature.key, feature.enabled])
|
|
115
788
|
);
|
|
116
|
-
const PluginFeaturesCard = ({ plugin: plugin2 }) => {
|
|
789
|
+
const PluginFeaturesCard = ({ plugin: plugin2, title }) => {
|
|
117
790
|
const mutation = useUpdatePluginFeatures();
|
|
118
791
|
const pending = mutation.isPending;
|
|
119
792
|
const labels = new Map(
|
|
@@ -132,12 +805,12 @@ const PluginFeaturesCard = ({ plugin: plugin2 }) => {
|
|
|
132
805
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [
|
|
133
806
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [
|
|
134
807
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
135
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: plugin2.label }),
|
|
136
|
-
plugin2.description && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
|
|
808
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: title ?? plugin2.label }),
|
|
809
|
+
title === void 0 && plugin2.description && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
|
|
137
810
|
!plugin2.enabled && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.pluginDisabled }),
|
|
138
811
|
pending && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving })
|
|
139
812
|
] }),
|
|
140
|
-
/* @__PURE__ */ jsx(
|
|
813
|
+
plugin2.switchable && /* @__PURE__ */ jsx(
|
|
141
814
|
Switch,
|
|
142
815
|
{
|
|
143
816
|
id: pluginSwitchId,
|
|
@@ -197,14 +870,41 @@ const PluginFeaturesCard = ({ plugin: plugin2 }) => {
|
|
|
197
870
|
})
|
|
198
871
|
] });
|
|
199
872
|
};
|
|
200
|
-
const
|
|
201
|
-
const
|
|
873
|
+
const PluginFeatureSections = ({ pluginKey }) => {
|
|
874
|
+
const plugin2 = usePluginFeature(pluginKey).data?.plugin;
|
|
875
|
+
if (!plugin2) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
879
|
+
(plugin2.switchable || plugin2.features.length > 0) && /* @__PURE__ */ jsx(PluginFeaturesCard, { plugin: plugin2, title: texts.sections.features }),
|
|
880
|
+
plugin2.settings.length > 0 && /* @__PURE__ */ jsx(PluginSettingsForm, { plugin: plugin2 }),
|
|
881
|
+
plugin2.actions.length > 0 && /* @__PURE__ */ jsx(PluginActionsSection, { plugin: plugin2 })
|
|
882
|
+
] });
|
|
883
|
+
};
|
|
884
|
+
const isNotFound = (error) => typeof error === "object" && error !== null && error.status === 404;
|
|
885
|
+
const PluginPageShell = ({ pluginKey, children }) => {
|
|
886
|
+
const query = usePluginFeature(pluginKey);
|
|
887
|
+
const plugin2 = query.data?.plugin;
|
|
202
888
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
203
|
-
/* @__PURE__ */
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
889
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
|
|
890
|
+
Link,
|
|
891
|
+
{
|
|
892
|
+
to: PLUGIN_HUB_PATH,
|
|
893
|
+
className: "text-ui-fg-subtle hover:text-ui-fg-base txt-compact-small-plus",
|
|
894
|
+
children: [
|
|
895
|
+
"← ",
|
|
896
|
+
texts.detail.back
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
) }),
|
|
900
|
+
plugin2 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
901
|
+
/* @__PURE__ */ jsxs(Container, { className: "px-6 py-4", children: [
|
|
902
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: plugin2.label }),
|
|
903
|
+
plugin2.description && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: plugin2.description }),
|
|
904
|
+
/* @__PURE__ */ jsx(PluginStatusChips, { plugin: plugin2 })
|
|
905
|
+
] }),
|
|
906
|
+
children
|
|
907
|
+
] }) : query.isPending ? /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) }) : isNotFound(query.error) ? /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.notFound }) }) : /* @__PURE__ */ jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
208
908
|
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }),
|
|
209
909
|
/* @__PURE__ */ jsx(
|
|
210
910
|
Button,
|
|
@@ -216,12 +916,13 @@ const PluginFeaturesSettingsPage = () => {
|
|
|
216
916
|
children: texts.states.retry
|
|
217
917
|
}
|
|
218
918
|
)
|
|
219
|
-
] })
|
|
919
|
+
] })
|
|
220
920
|
] });
|
|
221
921
|
};
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
});
|
|
922
|
+
const PluginDetailPage = () => {
|
|
923
|
+
const { plugin_key: pluginKey = "" } = useParams();
|
|
924
|
+
return /* @__PURE__ */ jsx(PluginPageShell, { pluginKey, children: /* @__PURE__ */ jsx(PluginFeatureSections, { pluginKey }) });
|
|
925
|
+
};
|
|
225
926
|
const widgetModule = { widgets: [] };
|
|
226
927
|
const routeModule = {
|
|
227
928
|
routes: [
|
|
@@ -229,6 +930,10 @@ const routeModule = {
|
|
|
229
930
|
Component: PluginFeaturesSettingsPage,
|
|
230
931
|
path: "/settings/hartl-services",
|
|
231
932
|
handle: { label: config.label, translationNs: config.translationNs }
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
Component: PluginDetailPage,
|
|
936
|
+
path: "/settings/hartl-services/plugin/:plugin_key"
|
|
232
937
|
}
|
|
233
938
|
]
|
|
234
939
|
};
|