@nsxbet/admin-sdk 0.5.1 → 0.6.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/CHECKLIST.md +40 -10
- package/README.md +337 -36
- package/dist/auth/client/gateway-token.d.ts +19 -0
- package/dist/auth/client/gateway-token.js +89 -0
- package/dist/auth/client/in-memory.d.ts +5 -1
- package/dist/auth/client/in-memory.js +75 -38
- package/dist/auth/client/index.d.ts +0 -1
- package/dist/auth/client/interface.d.ts +6 -3
- package/dist/auth/client/keycloak.d.ts +0 -1
- package/dist/auth/client/keycloak.js +6 -3
- package/dist/auth/components/UserSelector.d.ts +0 -1
- package/dist/auth/components/UserSelector.js +89 -7
- package/dist/auth/components/index.d.ts +0 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/components/AuthProvider.d.ts +0 -1
- package/dist/components/Timestamp.d.ts +7 -0
- package/dist/components/Timestamp.js +50 -0
- package/dist/hooks/useAuth.d.ts +0 -1
- package/dist/hooks/useAuth.js +1 -1
- package/dist/hooks/useFetch.d.ts +0 -1
- package/dist/hooks/useI18n.d.ts +0 -1
- package/dist/hooks/usePlatformAPI.d.ts +0 -1
- package/dist/hooks/useTelemetry.d.ts +0 -1
- package/dist/hooks/useTimestamp.d.ts +8 -0
- package/dist/hooks/useTimestamp.js +122 -0
- package/dist/i18n/config.d.ts +20 -2
- package/dist/i18n/config.js +48 -0
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +95 -18
- package/dist/i18n/locales/es.json +95 -18
- package/dist/i18n/locales/pt-BR.json +95 -18
- package/dist/i18n/locales/ro.json +95 -18
- package/dist/index.d.ts +11 -7
- package/dist/index.js +5 -1
- package/dist/registry/AdminShellRegistry.d.ts +1 -2
- package/dist/registry/cache/cached-catalog.d.ts +11 -0
- package/dist/registry/cache/cached-catalog.js +42 -0
- package/dist/registry/cache/catalog-cache.d.ts +10 -0
- package/dist/registry/cache/catalog-cache.js +58 -0
- package/dist/registry/cache/index.d.ts +5 -0
- package/dist/registry/cache/index.js +3 -0
- package/dist/registry/cache/types.d.ts +20 -0
- package/dist/registry/cache/types.js +3 -0
- package/dist/registry/client/http.d.ts +0 -1
- package/dist/registry/client/http.js +13 -0
- package/dist/registry/client/in-memory.d.ts +0 -1
- package/dist/registry/client/in-memory.js +117 -12
- package/dist/registry/client/index.d.ts +0 -1
- package/dist/registry/client/interface.d.ts +21 -6
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +4 -0
- package/dist/registry/types/index.d.ts +2 -3
- package/dist/registry/types/manifest.d.ts +20 -24
- package/dist/registry/types/manifest.js +17 -18
- package/dist/registry/types/module.d.ts +43 -14
- package/dist/registry/useRegistryPolling.d.ts +15 -0
- package/dist/registry/useRegistryPolling.js +66 -0
- package/dist/router/DynamicModule.d.ts +6 -22
- package/dist/router/DynamicModule.js +25 -48
- package/dist/router/ModuleErrorBoundary.d.ts +39 -0
- package/dist/router/ModuleErrorBoundary.js +101 -0
- package/dist/router/index.d.ts +1 -1
- package/dist/router/url-allowlist.d.ts +22 -0
- package/dist/router/url-allowlist.js +65 -0
- package/dist/shell/AdminShell.d.ts +0 -1
- package/dist/shell/AdminShell.js +178 -43
- package/dist/shell/BackofficeShell.d.ts +0 -1
- package/dist/shell/BackofficeShell.js +59 -25
- package/dist/shell/components/CommandPalette.d.ts +0 -1
- package/dist/shell/components/CommandPalette.js +26 -50
- package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
- package/dist/shell/components/DevtoolsPanel.js +145 -0
- package/dist/shell/components/HomePage.d.ts +0 -1
- package/dist/shell/components/HomePage.js +9 -4
- package/dist/shell/components/LeftNav.d.ts +0 -1
- package/dist/shell/components/LeftNav.js +91 -93
- package/dist/shell/components/MainContent.d.ts +3 -2
- package/dist/shell/components/MainContent.js +8 -23
- package/dist/shell/components/ModuleOverview.d.ts +0 -1
- package/dist/shell/components/ModuleOverview.js +4 -20
- package/dist/shell/components/ProfilePage.d.ts +0 -1
- package/dist/shell/components/ProfilePage.js +1 -1
- package/dist/shell/components/RegistryPage.d.ts +0 -1
- package/dist/shell/components/RegistryPage.js +154 -64
- package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
- package/dist/shell/components/RegistryStatusBanner.js +31 -0
- package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
- package/dist/shell/components/RegistryUnavailable.js +7 -0
- package/dist/shell/components/SettingsPage.d.ts +0 -1
- package/dist/shell/components/StackedPanel.d.ts +15 -0
- package/dist/shell/components/StackedPanel.js +45 -0
- package/dist/shell/components/TopBar.d.ts +4 -2
- package/dist/shell/components/TopBar.js +9 -3
- package/dist/shell/components/UpdateBanner.d.ts +5 -0
- package/dist/shell/components/UpdateBanner.js +8 -0
- package/dist/shell/components/index.d.ts +4 -1
- package/dist/shell/components/index.js +2 -0
- package/dist/shell/components/theme-provider.d.ts +0 -1
- package/dist/shell/components/theme-provider.js +8 -5
- package/dist/shell/hooks/useCspViolations.d.ts +12 -0
- package/dist/shell/hooks/useCspViolations.js +34 -0
- package/dist/shell/index.d.ts +1 -2
- package/dist/shell/polling-config.d.ts +10 -0
- package/dist/shell/polling-config.js +26 -0
- package/dist/shell/search/fuzzy.d.ts +0 -1
- package/dist/shell/search/index.d.ts +0 -1
- package/dist/shell/telemetry.d.ts +0 -1
- package/dist/shell/types.d.ts +34 -18
- package/dist/tailwind/index.d.ts +0 -1
- package/dist/types/keycloak.d.ts +0 -1
- package/dist/types/platform.d.ts +12 -1
- package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
- package/dist/vite/AdminShellSharedDeps.js +215 -0
- package/dist/vite/config.d.ts +2 -2
- package/dist/vite/config.js +5 -7
- package/dist/vite/i18n-plugin.d.ts +13 -0
- package/dist/vite/i18n-plugin.js +81 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +2 -0
- package/dist/vite/plugins.d.ts +0 -1
- package/package.json +6 -2
- package/dist/auth/client/in-memory.d.ts.map +0 -1
- package/dist/auth/client/index.d.ts.map +0 -1
- package/dist/auth/client/interface.d.ts.map +0 -1
- package/dist/auth/client/keycloak.d.ts.map +0 -1
- package/dist/auth/components/UserSelector.d.ts.map +0 -1
- package/dist/auth/components/index.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/components/AuthProvider.d.ts.map +0 -1
- package/dist/hooks/useAuth.d.ts.map +0 -1
- package/dist/hooks/useFetch.d.ts.map +0 -1
- package/dist/hooks/useI18n.d.ts.map +0 -1
- package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
- package/dist/hooks/useTelemetry.d.ts.map +0 -1
- package/dist/i18n/config.d.ts.map +0 -1
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
- package/dist/registry/client/http.d.ts.map +0 -1
- package/dist/registry/client/in-memory.d.ts.map +0 -1
- package/dist/registry/client/index.d.ts.map +0 -1
- package/dist/registry/client/interface.d.ts.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/types/index.d.ts.map +0 -1
- package/dist/registry/types/manifest.d.ts.map +0 -1
- package/dist/registry/types/module.d.ts.map +0 -1
- package/dist/router/DynamicModule.d.ts.map +0 -1
- package/dist/router/index.d.ts.map +0 -1
- package/dist/shell/AdminShell.d.ts.map +0 -1
- package/dist/shell/BackofficeShell.d.ts.map +0 -1
- package/dist/shell/components/CommandPalette.d.ts.map +0 -1
- package/dist/shell/components/HomePage.d.ts.map +0 -1
- package/dist/shell/components/LeftNav.d.ts.map +0 -1
- package/dist/shell/components/MainContent.d.ts.map +0 -1
- package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
- package/dist/shell/components/ProfilePage.d.ts.map +0 -1
- package/dist/shell/components/RegistryPage.d.ts.map +0 -1
- package/dist/shell/components/SettingsPage.d.ts.map +0 -1
- package/dist/shell/components/TopBar.d.ts.map +0 -1
- package/dist/shell/components/index.d.ts.map +0 -1
- package/dist/shell/components/theme-provider.d.ts.map +0 -1
- package/dist/shell/index.d.ts.map +0 -1
- package/dist/shell/search/fuzzy.d.ts.map +0 -1
- package/dist/shell/search/index.d.ts.map +0 -1
- package/dist/shell/telemetry.d.ts.map +0 -1
- package/dist/shell/types.d.ts.map +0 -1
- package/dist/tailwind/index.d.ts.map +0 -1
- package/dist/types/keycloak.d.ts.map +0 -1
- package/dist/types/platform.d.ts.map +0 -1
- package/dist/vite/config.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/plugins.d.ts.map +0 -1
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback } from "react";
|
|
3
|
-
import { Card, CardContent,
|
|
3
|
+
import { Card, CardContent, Switch, Label, Button, Icon, Badge, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@nsxbet/admin-ui";
|
|
4
|
+
import { useI18n } from "../../hooks/useI18n";
|
|
5
|
+
import { useAuth } from "../../hooks/useAuth";
|
|
6
|
+
import { resolveLocalizedString } from "../../i18n";
|
|
7
|
+
function formatRelativeDate(isoString) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const date = new Date(isoString).getTime();
|
|
10
|
+
const diffMs = now - date;
|
|
11
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
12
|
+
if (diffMins < 1)
|
|
13
|
+
return "just now";
|
|
14
|
+
if (diffMins < 60)
|
|
15
|
+
return `${diffMins}m ago`;
|
|
16
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
17
|
+
if (diffHours < 24)
|
|
18
|
+
return `${diffHours}h ago`;
|
|
19
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
20
|
+
if (diffDays < 30)
|
|
21
|
+
return `${diffDays}d ago`;
|
|
22
|
+
return new Date(isoString).toLocaleDateString();
|
|
23
|
+
}
|
|
4
24
|
export function RegistryPage({ apiUrl, registryClient }) {
|
|
25
|
+
const { t, i18n } = useI18n();
|
|
26
|
+
const auth = useAuth();
|
|
5
27
|
const [modules, setModules] = useState([]);
|
|
6
28
|
const [isLoading, setIsLoading] = useState(true);
|
|
7
29
|
const [error, setError] = useState(null);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
30
|
+
const canView = auth.hasPermission("admin.platform.view");
|
|
31
|
+
const canEdit = auth.hasPermission("admin.platform.edit");
|
|
32
|
+
const canDelete = auth.hasPermission("admin.platform.delete");
|
|
33
|
+
const [disableTarget, setDisableTarget] = useState(null);
|
|
34
|
+
const [historyTarget, setHistoryTarget] = useState(null);
|
|
35
|
+
const [versionHistory, setVersionHistory] = useState([]);
|
|
36
|
+
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
|
37
|
+
const [rollbackTarget, setRollbackTarget] = useState(null);
|
|
38
|
+
const [isRollingBack, setIsRollingBack] = useState(false);
|
|
39
|
+
const [isReorderMode, setIsReorderMode] = useState(false);
|
|
40
|
+
const [reorderList, setReorderList] = useState([]);
|
|
41
|
+
const [originalOrder, setOriginalOrder] = useState([]);
|
|
42
|
+
const [isSavingOrder, setIsSavingOrder] = useState(false);
|
|
43
|
+
const hasOrderChanged = isReorderMode &&
|
|
44
|
+
reorderList.some((m, i) => m.id !== originalOrder[i]);
|
|
12
45
|
const fetchModules = useCallback(async () => {
|
|
13
46
|
if (!registryClient) {
|
|
14
|
-
setError("
|
|
47
|
+
setError(t("registryPage.errorClientUnavailable"));
|
|
15
48
|
setIsLoading(false);
|
|
16
49
|
return;
|
|
17
50
|
}
|
|
@@ -22,108 +55,165 @@ export function RegistryPage({ apiUrl, registryClient }) {
|
|
|
22
55
|
setModules(data);
|
|
23
56
|
}
|
|
24
57
|
catch (err) {
|
|
25
|
-
setError(err instanceof Error ? err.message : "
|
|
58
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorLoadModules"));
|
|
26
59
|
}
|
|
27
60
|
finally {
|
|
28
61
|
setIsLoading(false);
|
|
29
62
|
}
|
|
30
|
-
}, [registryClient]);
|
|
63
|
+
}, [registryClient, t]);
|
|
31
64
|
useEffect(() => {
|
|
32
65
|
fetchModules();
|
|
33
66
|
}, [fetchModules]);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
const handleToggleEnabled = async (module) => {
|
|
68
|
+
if (!registryClient)
|
|
69
|
+
return;
|
|
70
|
+
if (module.enabled) {
|
|
71
|
+
setDisableTarget(module);
|
|
37
72
|
return;
|
|
38
73
|
}
|
|
39
74
|
try {
|
|
40
|
-
setIsAdding(true);
|
|
41
75
|
setError(null);
|
|
42
|
-
await registryClient.modules.
|
|
43
|
-
setNewModuleUrl("");
|
|
76
|
+
await registryClient.modules.update(module.id, { enabled: true });
|
|
44
77
|
await fetchModules();
|
|
45
78
|
}
|
|
46
79
|
catch (err) {
|
|
47
|
-
setError(err instanceof Error ? err.message : "
|
|
80
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorUpdateModule"));
|
|
48
81
|
}
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
};
|
|
83
|
+
const confirmDisable = async () => {
|
|
84
|
+
if (!registryClient || !disableTarget)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
setError(null);
|
|
88
|
+
await registryClient.modules.update(disableTarget.id, { enabled: false });
|
|
89
|
+
setDisableTarget(null);
|
|
90
|
+
await fetchModules();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorDisableModule"));
|
|
51
94
|
}
|
|
52
95
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
const handleDeleteModule = async (module) => {
|
|
97
|
+
if (!registryClient)
|
|
98
|
+
return;
|
|
99
|
+
const name = resolveLocalizedString(module.manifest.title, i18n.language);
|
|
100
|
+
const confirmed = window.confirm(t("registryPage.deleteConfirm", { name }));
|
|
101
|
+
if (!confirmed)
|
|
56
102
|
return;
|
|
57
103
|
try {
|
|
58
|
-
setIsAdding(true);
|
|
59
104
|
setError(null);
|
|
60
|
-
|
|
61
|
-
let manifest;
|
|
62
|
-
try {
|
|
63
|
-
manifest = JSON.parse(manifestJson.trim());
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
throw new Error("Invalid JSON format. Please check your manifest.");
|
|
67
|
-
}
|
|
68
|
-
await registryClient.modules.registerFromManifest(manifest, "");
|
|
69
|
-
setManifestJson("");
|
|
105
|
+
await registryClient.modules.delete(module.id);
|
|
70
106
|
await fetchModules();
|
|
71
107
|
}
|
|
72
108
|
catch (err) {
|
|
73
|
-
setError(err instanceof Error ? err.message : "
|
|
109
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorDeleteModule"));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const openVersionHistory = async (module) => {
|
|
113
|
+
if (!registryClient)
|
|
114
|
+
return;
|
|
115
|
+
setHistoryTarget(module);
|
|
116
|
+
setIsLoadingHistory(true);
|
|
117
|
+
setVersionHistory([]);
|
|
118
|
+
try {
|
|
119
|
+
const versions = await registryClient.modules.listVersions(module.id);
|
|
120
|
+
setVersionHistory(versions);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorLoadVersions"));
|
|
124
|
+
setHistoryTarget(null);
|
|
74
125
|
}
|
|
75
126
|
finally {
|
|
76
|
-
|
|
127
|
+
setIsLoadingHistory(false);
|
|
77
128
|
}
|
|
78
129
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!registryClient)
|
|
130
|
+
const confirmRollback = async () => {
|
|
131
|
+
if (!registryClient || !rollbackTarget)
|
|
82
132
|
return;
|
|
83
133
|
try {
|
|
134
|
+
setIsRollingBack(true);
|
|
84
135
|
setError(null);
|
|
85
|
-
await registryClient.modules.
|
|
86
|
-
|
|
87
|
-
});
|
|
136
|
+
await registryClient.modules.rollback(rollbackTarget.module.id, rollbackTarget.version.version);
|
|
137
|
+
setRollbackTarget(null);
|
|
88
138
|
await fetchModules();
|
|
139
|
+
if (historyTarget) {
|
|
140
|
+
const versions = await registryClient.modules.listVersions(historyTarget.id);
|
|
141
|
+
setVersionHistory(versions);
|
|
142
|
+
}
|
|
89
143
|
}
|
|
90
144
|
catch (err) {
|
|
91
|
-
setError(err instanceof Error ? err.message : "
|
|
145
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorRollback"));
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
setIsRollingBack(false);
|
|
92
149
|
}
|
|
93
150
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
151
|
+
const enterReorderMode = () => {
|
|
152
|
+
setReorderList([...modules]);
|
|
153
|
+
setOriginalOrder(modules.map((m) => m.id));
|
|
154
|
+
setIsReorderMode(true);
|
|
155
|
+
};
|
|
156
|
+
const cancelReorder = () => {
|
|
157
|
+
setIsReorderMode(false);
|
|
158
|
+
setReorderList([]);
|
|
159
|
+
};
|
|
160
|
+
const moveUp = (index) => {
|
|
161
|
+
if (index <= 0)
|
|
97
162
|
return;
|
|
98
|
-
const
|
|
99
|
-
|
|
163
|
+
const updated = [...reorderList];
|
|
164
|
+
[updated[index - 1], updated[index]] = [updated[index], updated[index - 1]];
|
|
165
|
+
setReorderList(updated);
|
|
166
|
+
};
|
|
167
|
+
const moveDown = (index) => {
|
|
168
|
+
if (index >= reorderList.length - 1)
|
|
169
|
+
return;
|
|
170
|
+
const updated = [...reorderList];
|
|
171
|
+
[updated[index], updated[index + 1]] = [updated[index + 1], updated[index]];
|
|
172
|
+
setReorderList(updated);
|
|
173
|
+
};
|
|
174
|
+
const saveOrder = async () => {
|
|
175
|
+
if (!registryClient)
|
|
100
176
|
return;
|
|
101
177
|
try {
|
|
178
|
+
setIsSavingOrder(true);
|
|
102
179
|
setError(null);
|
|
103
|
-
|
|
180
|
+
const order = reorderList.map((m, i) => ({
|
|
181
|
+
id: m.id,
|
|
182
|
+
navigationOrder: (i + 1) * 10,
|
|
183
|
+
}));
|
|
184
|
+
await registryClient.modules.reorder(order);
|
|
185
|
+
setIsReorderMode(false);
|
|
186
|
+
setReorderList([]);
|
|
104
187
|
await fetchModules();
|
|
105
188
|
}
|
|
106
189
|
catch (err) {
|
|
107
|
-
setError(err instanceof Error ? err.message : "
|
|
190
|
+
setError(err instanceof Error ? err.message : t("registryPage.errorSaveOrder"));
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
setIsSavingOrder(false);
|
|
108
194
|
}
|
|
109
195
|
};
|
|
196
|
+
if (!canView) {
|
|
197
|
+
return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsx("div", { children: _jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }) }), _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "shield-x", className: "h-5 w-5" }), _jsx("p", { children: t("errors.accessDeniedDescription") })] }) }) })] }));
|
|
198
|
+
}
|
|
110
199
|
if (!registryClient) {
|
|
111
|
-
return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: "
|
|
200
|
+
return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.description") })] }), _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: t("registryPage.errorClientUnavailable") })] }) }) })] }));
|
|
112
201
|
}
|
|
113
|
-
return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: "
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.description") })] }), _jsxs("div", { className: "flex items-center gap-2", children: [canEdit && !isReorderMode && (_jsxs(Button, { variant: "outline", size: "sm", onClick: enterReorderMode, children: [_jsx(Icon, { name: "arrow-up-down", className: "h-4 w-4 mr-2" }), t("registryPage.manageOrder")] })), _jsx(Badge, { variant: apiUrl ? "default" : "secondary", children: apiUrl ? t("registryPage.apiMode") : t("registryPage.inMemoryMode") })] })] }), error && (_jsx(Card, { className: "border-destructive bg-destructive/10", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: error })] }) }) })), isReorderMode && (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h2", { className: "text-lg font-semibold", children: t("registryPage.manageOrder") }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: cancelReorder, disabled: isSavingOrder, children: t("common.cancel") }), _jsx(Button, { size: "sm", onClick: saveOrder, disabled: isSavingOrder || !hasOrderChanged, children: isSavingOrder ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), t("registryPage.savingOrder")] })) : (t("registryPage.saveOrder")) })] })] }), _jsx("div", { className: "space-y-1", children: reorderList.map((module, index) => (_jsxs("div", { className: "flex items-center gap-3 p-2 rounded-lg border bg-background", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs shrink-0", children: t("registryPage.position", { position: index + 1 }) }), _jsx("div", { className: "flex-shrink-0 p-1.5 rounded bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-4 w-4" }) }), _jsx("span", { className: "flex-1 font-medium truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), _jsxs("div", { className: "flex gap-1 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveUp(index), disabled: index === 0, children: _jsx(Icon, { name: "chevron-up", className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveDown(index), disabled: index === reorderList.length - 1, children: _jsx(Icon, { name: "chevron-down", className: "h-4 w-4" }) })] })] }, module.id))) })] }) })), !isReorderMode && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "package", className: "h-5 w-5 text-muted-foreground" }), _jsxs("h2", { className: "text-lg font-semibold", children: [t("registryPage.registeredModules"), " ", !isLoading && (_jsxs("span", { className: "text-muted-foreground font-normal", children: ["(", modules.length, ")"] }))] })] }), isLoading ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-8 w-8 mx-auto mb-3 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.loadingModules") })] }) })) : modules.length === 0 ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-8 w-8 mx-auto mb-3 text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.noModules") }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t("registryPage.noModulesHint") })] }) })) : (_jsx("div", { className: "space-y-3", children: modules.map((module, index) => (_jsx(Card, { className: !module.enabled ? "opacity-60 border-dashed" : undefined, "data-testid": `module-card-${module.moduleId}`, children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-start gap-4 flex-1 min-w-0", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-lg bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-6 w-6" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1 flex-wrap", children: [_jsx("h3", { className: "font-semibold truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), canEdit && (_jsx(Badge, { variant: module.enabled ? "success" : "secondary", children: module.enabled
|
|
203
|
+
? t("registryPage.enabled")
|
|
204
|
+
: t("registryPage.disabled") })), module.version ? (_jsxs(Badge, { variant: "outline", "data-testid": `version-badge-${module.moduleId}`, children: ["v", module.version] })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: t("registryPage.unversioned") }))] }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: resolveLocalizedString(module.manifest.description, i18n.language) ||
|
|
205
|
+
t("registryPage.noDescription") }), _jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "hash", className: "h-3 w-3" }), module.moduleId] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "folder", className: "h-3 w-3" }), module.manifest.category || "General"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "navigation", className: "h-3 w-3" }), module.manifest.routeBase] }), module.baseUrl && (_jsxs("span", { className: "flex items-center gap-1", title: module.baseUrl, children: [_jsx(Icon, { name: "link", className: "h-3 w-3" }), _jsx("span", { className: "truncate max-w-[200px]", children: module.baseUrl })] }))] })] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs", children: t("registryPage.position", { position: index + 1 }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => openVersionHistory(module), title: t("registryPage.versionHistory"), "data-testid": `version-history-btn-${module.moduleId}`, children: _jsx(Icon, { name: "history", className: "h-4 w-4" }) }), canEdit && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-sm text-muted-foreground", children: t("registryPage.enabled") }), _jsx(Switch, { checked: module.enabled, onCheckedChange: () => handleToggleEnabled(module), "data-testid": `enable-switch-${module.moduleId}` })] })), canDelete && (_jsx(Button, { variant: "destructive", size: "sm", onClick: () => handleDeleteModule(module), children: _jsx(Icon, { name: "trash-2", className: "h-4 w-4" }) }))] })] }) }) }, module.id))) }))] })), _jsx(Dialog, { open: !!disableTarget, onOpenChange: (open) => !open && setDisableTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("registryPage.disableTitle", {
|
|
206
|
+
name: disableTarget
|
|
207
|
+
? resolveLocalizedString(disableTarget.manifest.title, i18n.language)
|
|
208
|
+
: "",
|
|
209
|
+
}) }), _jsx(DialogDescription, { children: t("registryPage.disableDescription") })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setDisableTarget(null), children: t("common.cancel") }), _jsx(Button, { variant: "destructive", onClick: confirmDisable, children: t("registryPage.disableConfirm") })] })] }) }), _jsx(Sheet, { open: !!historyTarget, onOpenChange: (open) => !open && setHistoryTarget(null), children: _jsxs(SheetContent, { className: "sm:max-w-md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t("registryPage.versionHistory") }), _jsx(SheetDescription, { children: historyTarget && resolveLocalizedString(historyTarget.manifest.title, i18n.language) })] }), _jsx("div", { className: "mt-6 space-y-3", "data-testid": "version-history-list", children: isLoadingHistory ? (_jsxs("div", { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-6 w-6 mx-auto mb-2 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("registryPage.loadingVersions") })] })) : versionHistory.length === 0 ? (_jsxs("div", { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-6 w-6 mx-auto mb-2 text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("registryPage.noVersionHistory") })] })) : (versionHistory.map((v) => (_jsxs("div", { className: "flex items-center justify-between gap-3 p-3 rounded-lg border", "data-testid": `version-entry-${v.version}`, children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-0.5", children: [_jsxs("span", { className: "font-mono text-sm font-medium", children: ["v", v.version] }), v.active && (_jsx(Badge, { variant: "success", className: "text-xs", children: t("registryPage.versionActive") }))] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatRelativeDate(v.publishedAt), typeof v.metadata?.rolledBackFrom === "string" && (_jsx("span", { className: "ml-2 text-amber-600", children: t("registryPage.rolledBackFrom", {
|
|
210
|
+
version: v.metadata.rolledBackFrom,
|
|
211
|
+
}) }))] })] }), !v.active && historyTarget && canEdit && (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setRollbackTarget({ module: historyTarget, version: v }), "data-testid": `rollback-btn-${v.version}`, children: [_jsx(Icon, { name: "undo-2", className: "h-3.5 w-3.5 mr-1.5" }), t("registryPage.rollback")] }))] }, v.id)))) })] }) }), _jsx(Dialog, { open: !!rollbackTarget, onOpenChange: (open) => !open && setRollbackTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("registryPage.rollbackTitle", {
|
|
212
|
+
version: rollbackTarget?.version.version ?? "",
|
|
213
|
+
}) }), _jsx(DialogDescription, { children: rollbackTarget &&
|
|
214
|
+
t("registryPage.rollbackDescription", {
|
|
215
|
+
name: resolveLocalizedString(rollbackTarget.module.manifest.title, i18n.language),
|
|
216
|
+
fromVersion: rollbackTarget.module.version ?? "",
|
|
217
|
+
toVersion: rollbackTarget.version.version,
|
|
218
|
+
}) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRollbackTarget(null), disabled: isRollingBack, children: t("common.cancel") }), _jsx(Button, { onClick: confirmRollback, disabled: isRollingBack, children: isRollingBack ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), t("registryPage.rollingBack")] })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "undo-2", className: "h-4 w-4 mr-2" }), t("registryPage.rollbackConfirm")] })) })] })] }) })] }));
|
|
129
219
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RegistryCacheStatus } from "../../registry/cache/types";
|
|
2
|
+
export interface RegistryStatusBannerProps {
|
|
3
|
+
status: RegistryCacheStatus;
|
|
4
|
+
onRetry: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function RegistryStatusBanner({ status, onRetry }: RegistryStatusBannerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, AlertDescription } from "@nsxbet/admin-ui";
|
|
3
|
+
import { useI18n } from "../../hooks/useI18n";
|
|
4
|
+
function formatRelativeTime(isoTimestamp) {
|
|
5
|
+
const diff = Date.now() - new Date(isoTimestamp).getTime();
|
|
6
|
+
const minutes = Math.floor(diff / 60000);
|
|
7
|
+
if (minutes < 1)
|
|
8
|
+
return "just now";
|
|
9
|
+
if (minutes < 60)
|
|
10
|
+
return `${minutes}m ago`;
|
|
11
|
+
const hours = Math.floor(minutes / 60);
|
|
12
|
+
if (hours < 24)
|
|
13
|
+
return `${hours}h ago`;
|
|
14
|
+
const days = Math.floor(hours / 24);
|
|
15
|
+
return `${days}d ago`;
|
|
16
|
+
}
|
|
17
|
+
export function RegistryStatusBanner({ status, onRetry }) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
if (status.state === "fresh" || status.state === "unavailable") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const isCached = status.state === "cached";
|
|
23
|
+
const className = isCached
|
|
24
|
+
? "border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
|
|
25
|
+
: "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200";
|
|
26
|
+
const message = isCached
|
|
27
|
+
? t("registryCache.cachedMessage")
|
|
28
|
+
: t("registryCache.staleMessage");
|
|
29
|
+
const relativeTime = formatRelativeTime(status.cachedAt);
|
|
30
|
+
return (_jsx(Alert, { variant: "default", className: className, "data-testid": "registry-status-banner", children: _jsxs(AlertDescription, { className: "flex items-center justify-between", children: [_jsxs("span", { "data-testid": "registry-banner-message", children: [message, " ", _jsxs("span", { className: "opacity-75", children: ["(", t("registryCache.lastUpdated", { time: relativeTime }), ")"] })] }), _jsx("button", { onClick: onRetry, className: "ml-4 shrink-0 underline underline-offset-2 hover:opacity-80", "data-testid": "registry-banner-retry", children: t("registryCache.retry") })] }) }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@nsxbet/admin-ui";
|
|
3
|
+
import { useI18n } from "../../hooks/useI18n";
|
|
4
|
+
export function RegistryUnavailable({ onRetry }) {
|
|
5
|
+
const { t } = useI18n();
|
|
6
|
+
return (_jsxs("div", { className: "flex h-screen flex-col items-center justify-center gap-4", "data-testid": "registry-unavailable", children: [_jsx("h1", { className: "text-2xl font-semibold text-foreground", children: t("registryCache.unavailableTitle") }), _jsx("p", { className: "max-w-md text-center text-muted-foreground", children: t("registryCache.unavailableDescription") }), _jsx(Button, { onClick: onRetry, "data-testid": "registry-unavailable-retry", children: t("registryCache.retry") })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Module } from "../types";
|
|
2
|
+
import type { LocalizedField } from "../../i18n/config.js";
|
|
3
|
+
interface StackedPanelProps {
|
|
4
|
+
module: Module;
|
|
5
|
+
showPinned: boolean;
|
|
6
|
+
isCommandPinned: (moduleId: string, commandId: string) => boolean;
|
|
7
|
+
togglePin: (module: Module, command: {
|
|
8
|
+
id: string;
|
|
9
|
+
title: LocalizedField;
|
|
10
|
+
route: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function StackedPanel({ module, showPinned, isCommandPinned, togglePin, }: StackedPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigate, useLocation } from "react-router-dom";
|
|
3
|
+
import { useI18n } from "../../hooks/useI18n";
|
|
4
|
+
import { resolveLocalizedString } from "../../i18n";
|
|
5
|
+
import { SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, useSidebar, Icon, ChevronLeft, Pin, } from "@nsxbet/admin-ui";
|
|
6
|
+
export function StackedPanel({ module, showPinned, isCommandPinned, togglePin, }) {
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const location = useLocation();
|
|
9
|
+
const { i18n } = useI18n();
|
|
10
|
+
const { state } = useSidebar();
|
|
11
|
+
const isCollapsed = state === "collapsed";
|
|
12
|
+
const moduleTitle = resolveLocalizedString(module.title, i18n.language);
|
|
13
|
+
const sections = module.navigation?.sections ?? [];
|
|
14
|
+
const commands = module.commands ?? [];
|
|
15
|
+
const isRouteActive = (route) => {
|
|
16
|
+
return location.pathname === route || location.pathname.startsWith(route + "/");
|
|
17
|
+
};
|
|
18
|
+
const commandsBySection = new Map();
|
|
19
|
+
for (const cmd of commands) {
|
|
20
|
+
const key = cmd.section;
|
|
21
|
+
if (!commandsBySection.has(key)) {
|
|
22
|
+
commandsBySection.set(key, []);
|
|
23
|
+
}
|
|
24
|
+
commandsBySection.get(key).push(cmd);
|
|
25
|
+
}
|
|
26
|
+
const renderCommand = (command) => {
|
|
27
|
+
const isPinned = isCommandPinned(module.id, command.id);
|
|
28
|
+
const commandTitle = resolveLocalizedString(command.title, i18n.language);
|
|
29
|
+
return (_jsx(SidebarMenuItem, { className: "group/command", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsxs(SidebarMenuButton, { onClick: () => navigate(command.route), isActive: isRouteActive(command.route), tooltip: commandTitle, "data-testid": `stacked-command-${command.id}`, className: "flex-1 min-w-0", children: [command.icon && _jsx(Icon, { name: command.icon, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: commandTitle })] }), showPinned && !isCollapsed && (_jsx("button", { onClick: (e) => {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
togglePin(module, command);
|
|
33
|
+
}, className: `shrink-0 p-0.5 rounded mr-2 transition-opacity ${isPinned
|
|
34
|
+
? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
|
|
35
|
+
: "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `stacked-pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
|
|
36
|
+
};
|
|
37
|
+
const unsectionedCommands = commandsBySection.get(undefined) ?? [];
|
|
38
|
+
return (_jsxs("div", { className: "flex flex-col h-full", "data-testid": "stacked-panel", children: [!isCollapsed && (_jsx("div", { className: "px-2 pt-2 pb-1", children: _jsxs("button", { onClick: () => navigate("/"), className: "flex items-center gap-1.5 w-full rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-foreground hover:bg-sidebar-accent transition-colors", "data-testid": "stacked-back", children: [_jsx(ChevronLeft, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: moduleTitle })] }) })), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [unsectionedCommands.length > 0 && (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: unsectionedCommands.map(renderCommand) }) }) })), sections.map((section) => {
|
|
39
|
+
const sectionCommands = commandsBySection.get(section.id) ?? [];
|
|
40
|
+
if (sectionCommands.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const sectionLabel = resolveLocalizedString(section.label, i18n.language);
|
|
43
|
+
return (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", "data-testid": `stacked-section-${section.id}`, children: sectionLabel }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: sectionCommands.map(renderCommand) }) })] }, section.id));
|
|
44
|
+
})] })] }));
|
|
45
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { TimezoneMode } from "../../types/platform";
|
|
1
2
|
export interface TopBarProps {
|
|
2
3
|
onSearchClick?: () => void;
|
|
3
4
|
environment?: string;
|
|
4
5
|
locale?: string;
|
|
5
6
|
onLocaleChange?: (locale: string) => void;
|
|
7
|
+
timezoneMode?: TimezoneMode;
|
|
8
|
+
onTimezoneToggle?: () => void;
|
|
6
9
|
}
|
|
7
|
-
export declare function TopBar({ onSearchClick, environment, locale: externalLocale, onLocaleChange, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
//# sourceMappingURL=TopBar.d.ts.map
|
|
10
|
+
export declare function TopBar({ onSearchClick, environment, locale: externalLocale, onLocaleChange, timezoneMode, onTimezoneToggle, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from "react";
|
|
3
|
-
import { Search, Moon, Sun, Check } from "lucide-react";
|
|
3
|
+
import { Search, Moon, Sun, Check, Clock, Globe } from "lucide-react";
|
|
4
4
|
import { useTheme } from "./theme-provider";
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Badge, Button, } from "@nsxbet/admin-ui";
|
|
6
6
|
import { SUPPORTED_LOCALES, LOCALE_FLAGS, LOCALE_NAMES } from "../../i18n";
|
|
@@ -27,7 +27,7 @@ const ENV_HEADER_COLORS_DARK = {
|
|
|
27
27
|
staging: "bg-yellow-950 border-yellow-800",
|
|
28
28
|
production: "bg-red-950 border-red-800",
|
|
29
29
|
};
|
|
30
|
-
export function TopBar({ onSearchClick, environment = "local", locale: externalLocale, onLocaleChange, }) {
|
|
30
|
+
export function TopBar({ onSearchClick, environment = "local", locale: externalLocale, onLocaleChange, timezoneMode = "local", onTimezoneToggle, }) {
|
|
31
31
|
const { theme, setTheme } = useTheme();
|
|
32
32
|
const { t } = useI18n();
|
|
33
33
|
const [locale, setLocale] = useState(externalLocale || "pt-BR");
|
|
@@ -57,5 +57,11 @@ export function TopBar({ onSearchClick, environment = "local", locale: externalL
|
|
|
57
57
|
: ENV_HEADER_COLORS_LIGHT[environment] || "bg-background border-b";
|
|
58
58
|
// Get flag for current locale
|
|
59
59
|
const currentFlag = LOCALE_FLAGS[locale] || "🌐";
|
|
60
|
-
return (_jsxs("header", { className: `flex h-14 items-center justify-between px-4 ${headerColorClasses}`, role: "banner", "data-environment": environment, children: [_jsx(Badge, { className: `${ENV_COLORS[environment]} uppercase text-white`, children: environment }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Button, { variant: "outline", onClick: onSearchClick, "data-testid": "search-button", className: "relative h-8 w-40 justify-start rounded-md bg-muted/50 px-3 text-sm font-normal text-muted-foreground shadow-none sm:w-48", children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: t('topBar.search') }), _jsxs("kbd", { className: "pointer-events-none absolute right-1.5 top-1 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { className: "flex items-center gap-1 text-xl transition-opacity hover:opacity-80 focus:outline-none", "data-testid": "language-selector", title: t('topBar.language'), children: _jsx("span", { children: currentFlag }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[180px]", children: SUPPORTED_LOCALES.map((localeOption) => (_jsxs(DropdownMenuItem, { onClick: () => handleLocaleChange(localeOption), className: "flex items-center justify-between", "data-testid": `language-option-${localeOption}`, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "mr-2 text-lg", children: LOCALE_FLAGS[localeOption] }), _jsx("span", { children: LOCALE_NAMES[localeOption] })] }), locale === localeOption && (_jsx(Check, { className: "h-4 w-4 text-primary" }))] }, localeOption))) })] }), _jsxs(
|
|
60
|
+
return (_jsxs("header", { className: `flex h-14 items-center justify-between px-4 ${headerColorClasses}`, role: "banner", "data-environment": environment, children: [_jsx(Badge, { "data-testid": "environment-badge", className: `${ENV_COLORS[environment]} uppercase text-white`, children: environment }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Button, { variant: "outline", onClick: onSearchClick, "data-testid": "search-button", className: "relative h-8 w-40 justify-start rounded-md bg-muted/50 px-3 text-sm font-normal text-muted-foreground shadow-none sm:w-48", children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: t('topBar.search') }), _jsxs("kbd", { className: "pointer-events-none absolute right-1.5 top-1 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { className: "flex items-center gap-1 text-xl transition-opacity hover:opacity-80 focus:outline-none", "data-testid": "language-selector", title: t('topBar.language'), children: _jsx("span", { children: currentFlag }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[180px]", children: SUPPORTED_LOCALES.map((localeOption) => (_jsxs(DropdownMenuItem, { onClick: () => handleLocaleChange(localeOption), className: "flex items-center justify-between", "data-testid": `language-option-${localeOption}`, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "mr-2 text-lg", children: LOCALE_FLAGS[localeOption] }), _jsx("span", { children: LOCALE_NAMES[localeOption] })] }), locale === localeOption && (_jsx(Check, { className: "h-4 w-4 text-primary" }))] }, localeOption))) })] }), _jsxs("div", { role: "radiogroup", "data-testid": "timezone-toggle", "aria-label": timezoneMode === "utc"
|
|
61
|
+
? t('topBar.timezoneToggleUtc')
|
|
62
|
+
: t('topBar.timezoneToggleLocal'), className: "relative inline-flex h-8 items-center gap-0.5 rounded-lg border bg-muted/50 p-1 text-muted-foreground", children: [_jsx("div", { className: `absolute top-1 bottom-1 w-[calc(50%-4px)] rounded-md bg-background shadow-sm transition-transform duration-200 ease-out ${timezoneMode === "local" ? "translate-x-[calc(100%+2px)]" : "translate-x-0"}` }), _jsxs("button", { type: "button", role: "radio", "aria-checked": timezoneMode === "utc", onClick: timezoneMode !== "utc" ? onTimezoneToggle : undefined, className: `relative z-10 inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors duration-200 ${timezoneMode === "utc"
|
|
63
|
+
? "text-foreground"
|
|
64
|
+
: "hover:text-foreground"}`, children: [_jsx(Globe, { className: "h-3.5 w-3.5" }), t('topBar.timezoneUtc')] }), _jsxs("button", { type: "button", role: "radio", "aria-checked": timezoneMode === "local", onClick: timezoneMode !== "local" ? onTimezoneToggle : undefined, className: `relative z-10 inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors duration-200 ${timezoneMode === "local"
|
|
65
|
+
? "text-foreground"
|
|
66
|
+
: "hover:text-foreground"}`, children: [_jsx(Clock, { className: "h-3.5 w-3.5" }), t('topBar.timezoneLocal')] })] }), _jsxs(Button, { variant: "ghost", size: "icon", onClick: toggleTheme, title: t('topBar.theme'), children: [_jsx(Sun, { className: "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" }), _jsx(Moon, { className: "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" }), _jsx("span", { className: "sr-only", children: t('topBar.theme') })] })] })] }));
|
|
61
67
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RefreshCw, X } from "lucide-react";
|
|
3
|
+
import { Button } from "@nsxbet/admin-ui";
|
|
4
|
+
import { useI18n } from "../../hooks/useI18n";
|
|
5
|
+
export function UpdateBanner({ onReload, onDismiss }) {
|
|
6
|
+
const { t } = useI18n();
|
|
7
|
+
return (_jsxs("div", { className: "relative flex items-center justify-center gap-3 bg-blue-600 px-4 py-2 text-sm text-white dark:bg-blue-700", children: [_jsx(RefreshCw, { className: "h-4 w-4 shrink-0 animate-spin-slow" }), _jsx("span", { children: t("shell.updateBanner.message", "Updates available — Reload to apply") }), _jsx(Button, { variant: "secondary", size: "sm", className: "h-7 bg-white/20 text-white hover:bg-white/30", onClick: onReload, children: t("shell.updateBanner.reload", "Reload") }), _jsx("button", { onClick: onDismiss, className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-white/70 hover:bg-white/20 hover:text-white", "aria-label": t("shell.updateBanner.dismiss", "Dismiss"), children: _jsx(X, { className: "h-4 w-4" }) })] }));
|
|
8
|
+
}
|
|
@@ -7,4 +7,7 @@ export type { CommandPaletteProps } from "./CommandPalette";
|
|
|
7
7
|
export { ThemeProvider, useTheme } from "./theme-provider";
|
|
8
8
|
export { RegistryPage } from "./RegistryPage";
|
|
9
9
|
export { HomePage } from "./HomePage";
|
|
10
|
-
|
|
10
|
+
export { RegistryStatusBanner } from "./RegistryStatusBanner";
|
|
11
|
+
export type { RegistryStatusBannerProps } from "./RegistryStatusBanner";
|
|
12
|
+
export { RegistryUnavailable } from "./RegistryUnavailable";
|
|
13
|
+
export type { RegistryUnavailableProps } from "./RegistryUnavailable";
|
|
@@ -5,3 +5,5 @@ export { CommandPalette } from "./CommandPalette";
|
|
|
5
5
|
export { ThemeProvider, useTheme } from "./theme-provider";
|
|
6
6
|
export { RegistryPage } from "./RegistryPage";
|
|
7
7
|
export { HomePage } from "./HomePage";
|
|
8
|
+
export { RegistryStatusBanner } from "./RegistryStatusBanner";
|
|
9
|
+
export { RegistryUnavailable } from "./RegistryUnavailable";
|
|
@@ -12,4 +12,3 @@ export declare function ThemeProvider({ children, defaultTheme, // Brasa Design
|
|
|
12
12
|
storageKey, ...props }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
13
13
|
export declare const useTheme: () => ThemeProviderState;
|
|
14
14
|
export {};
|
|
15
|
-
//# sourceMappingURL=theme-provider.d.ts.map
|
|
@@ -12,12 +12,15 @@ storageKey = "admin-ui-theme", ...props }) {
|
|
|
12
12
|
const root = window.document.documentElement;
|
|
13
13
|
root.classList.remove("light", "dark");
|
|
14
14
|
if (theme === "system") {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
? "dark"
|
|
18
|
-
: "light";
|
|
15
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
16
|
+
const systemTheme = mediaQuery.matches ? "dark" : "light";
|
|
19
17
|
root.classList.add(systemTheme);
|
|
20
|
-
|
|
18
|
+
const handleChange = (e) => {
|
|
19
|
+
root.classList.remove("light", "dark");
|
|
20
|
+
root.classList.add(e.matches ? "dark" : "light");
|
|
21
|
+
};
|
|
22
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
23
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
21
24
|
}
|
|
22
25
|
root.classList.add(theme);
|
|
23
26
|
}, [theme]);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CspViolation {
|
|
2
|
+
blockedURI: string;
|
|
3
|
+
violatedDirective: string;
|
|
4
|
+
effectiveDirective: string;
|
|
5
|
+
documentURI: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function useCspViolations(environment: string): {
|
|
9
|
+
violations: CspViolation[];
|
|
10
|
+
unseenCount: number;
|
|
11
|
+
resetUnseenCount: () => void;
|
|
12
|
+
};
|