apiblaze 0.17.21 → 0.17.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/index.js +451 -217
- package/dist/react/index.d.mts +67 -7
- package/dist/react/index.d.ts +67 -7
- package/dist/react/index.js +651 -69
- package/dist/react/index.mjs +651 -69
- package/dist/server/index.d.mts +165 -25
- package/dist/server/index.d.ts +165 -25
- package/dist/server/index.js +207 -35
- package/dist/server/index.mjs +204 -36
- package/package.json +28 -16
package/dist/react/index.mjs
CHANGED
|
@@ -1,146 +1,728 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// src/react/index.tsx
|
|
4
|
+
import * as React2 from "react";
|
|
5
|
+
|
|
6
|
+
// src/react/groups.tsx
|
|
2
7
|
import * as React from "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var SEALED = "apiblaze_admins";
|
|
4
10
|
var DEFAULTS = {
|
|
5
11
|
accent: "#4f46e5",
|
|
12
|
+
accentText: "#ffffff",
|
|
6
13
|
background: "transparent",
|
|
7
14
|
surface: "#ffffff",
|
|
15
|
+
headerBackground: "",
|
|
8
16
|
text: "#111827",
|
|
9
17
|
muted: "#6b7280",
|
|
10
18
|
border: "#e5e7eb",
|
|
11
|
-
|
|
19
|
+
danger: "#dc2626",
|
|
20
|
+
success: "#16a34a",
|
|
21
|
+
radius: "12px",
|
|
22
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
23
|
+
monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
12
24
|
};
|
|
13
|
-
function
|
|
25
|
+
function userLabel(u) {
|
|
26
|
+
return u.display_name || u.email || u.abz_sub;
|
|
27
|
+
}
|
|
28
|
+
function relTime(iso) {
|
|
29
|
+
if (!iso) return null;
|
|
30
|
+
const t = new Date(iso).getTime();
|
|
31
|
+
if (Number.isNaN(t)) return null;
|
|
32
|
+
const s = Math.floor((Date.now() - t) / 1e3);
|
|
33
|
+
if (s < 60) return "just now";
|
|
34
|
+
const m = Math.floor(s / 60);
|
|
35
|
+
if (m < 60) return `${m}m ago`;
|
|
36
|
+
const h = Math.floor(m / 60);
|
|
37
|
+
if (h < 24) return `${h}h ago`;
|
|
38
|
+
const d = Math.floor(h / 24);
|
|
39
|
+
if (d < 30) return `${d}d ago`;
|
|
40
|
+
return new Date(iso).toLocaleDateString();
|
|
41
|
+
}
|
|
42
|
+
var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint = "/api/apiblaze/groups", theme, title = "Users & groups", className }, ref) {
|
|
14
43
|
const t = { ...DEFAULTS, ...theme ?? {} };
|
|
15
|
-
const
|
|
16
|
-
const [
|
|
17
|
-
const [
|
|
44
|
+
const headerBg = t.headerBackground || t.surface;
|
|
45
|
+
const [access, setAccess] = React.useState("ok");
|
|
46
|
+
const [groups, setGroups] = React.useState(null);
|
|
47
|
+
const [users, setUsers] = React.useState([]);
|
|
48
|
+
const [observed, setObserved] = React.useState([]);
|
|
49
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
50
|
+
const [open, setOpen] = React.useState(null);
|
|
51
|
+
const [detail, setDetail] = React.useState(null);
|
|
52
|
+
const [detailLoading, setDetailLoading] = React.useState(false);
|
|
53
|
+
const [creating, setCreating] = React.useState(false);
|
|
54
|
+
const [newName, setNewName] = React.useState("");
|
|
55
|
+
const [showCreate, setShowCreate] = React.useState(false);
|
|
56
|
+
const [pending, setPending] = React.useState(null);
|
|
18
57
|
const [err, setErr] = React.useState(null);
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
58
|
+
const [confirmDelete, setConfirmDelete] = React.useState(null);
|
|
59
|
+
const [showTraffic, setShowTraffic] = React.useState(false);
|
|
60
|
+
const [addSel, setAddSel] = React.useState("");
|
|
61
|
+
const [addRole, setAddRole] = React.useState("member");
|
|
62
|
+
const [nestSel, setNestSel] = React.useState("");
|
|
21
63
|
const call = React.useCallback(async (body) => {
|
|
22
|
-
setErr(null);
|
|
23
64
|
const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
|
|
24
65
|
const data = await res.json().catch(() => ({}));
|
|
25
66
|
if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
|
|
26
67
|
return data;
|
|
27
68
|
}, [endpoint]);
|
|
28
69
|
const load = React.useCallback(async () => {
|
|
70
|
+
setLoadError(null);
|
|
29
71
|
try {
|
|
30
72
|
const d = await call();
|
|
31
|
-
|
|
73
|
+
setAccess(d.access === "pending" ? "pending" : "ok");
|
|
74
|
+
setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
|
|
75
|
+
setUsers(d.users ?? []);
|
|
76
|
+
setObserved(d.observed ?? []);
|
|
32
77
|
} catch (e) {
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
setLoadError(e instanceof Error ? e.message : "Could not load groups.");
|
|
79
|
+
setGroups(null);
|
|
35
80
|
}
|
|
36
81
|
}, [call]);
|
|
37
82
|
React.useEffect(() => {
|
|
38
83
|
load();
|
|
39
84
|
}, [load]);
|
|
85
|
+
React.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
|
|
86
|
+
const loadDetail = React.useCallback(async (groupId) => {
|
|
87
|
+
setDetailLoading(true);
|
|
88
|
+
setDetail(null);
|
|
89
|
+
setErr(null);
|
|
90
|
+
setAddSel("");
|
|
91
|
+
setNestSel("");
|
|
92
|
+
try {
|
|
93
|
+
setDetail(await call({ action: "group-detail", groupId }));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
setErr(e instanceof Error ? e.message : "Could not load this group.");
|
|
96
|
+
} finally {
|
|
97
|
+
setDetailLoading(false);
|
|
98
|
+
}
|
|
99
|
+
}, [call]);
|
|
100
|
+
function toggle(g) {
|
|
101
|
+
if (open === g.id) {
|
|
102
|
+
setOpen(null);
|
|
103
|
+
setDetail(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setOpen(g.id);
|
|
107
|
+
loadDetail(g.id);
|
|
108
|
+
}
|
|
109
|
+
async function act(label, body, after) {
|
|
110
|
+
setPending(label);
|
|
111
|
+
setErr(null);
|
|
112
|
+
try {
|
|
113
|
+
await call(body);
|
|
114
|
+
await load();
|
|
115
|
+
if (after) await after();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
setErr(e instanceof Error ? e.message : "The action failed.");
|
|
118
|
+
} finally {
|
|
119
|
+
setPending(null);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function createGroup() {
|
|
123
|
+
const name = newName.trim();
|
|
124
|
+
if (!name) return;
|
|
125
|
+
setCreating(true);
|
|
126
|
+
setErr(null);
|
|
127
|
+
try {
|
|
128
|
+
await call({ action: "create-group", name });
|
|
129
|
+
setNewName("");
|
|
130
|
+
setShowCreate(false);
|
|
131
|
+
await load();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
setErr(e instanceof Error ? e.message : "Could not create the group.");
|
|
134
|
+
} finally {
|
|
135
|
+
setCreating(false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const vars = {
|
|
139
|
+
// @ts-expect-error CSS custom properties
|
|
140
|
+
"--abz-accent": t.accent,
|
|
141
|
+
background: t.background,
|
|
142
|
+
color: t.text,
|
|
143
|
+
fontFamily: t.fontFamily,
|
|
144
|
+
fontSize: 14
|
|
145
|
+
};
|
|
146
|
+
const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
|
|
147
|
+
const btn = (kind = "ghost", small = false) => ({
|
|
148
|
+
cursor: "pointer",
|
|
149
|
+
borderRadius: 8,
|
|
150
|
+
fontWeight: 600,
|
|
151
|
+
fontSize: small ? 12 : 13,
|
|
152
|
+
padding: small ? "5px 10px" : "8px 14px",
|
|
153
|
+
fontFamily: "inherit",
|
|
154
|
+
border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
|
|
155
|
+
background: kind === "primary" ? t.accent : "transparent",
|
|
156
|
+
color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
|
|
157
|
+
whiteSpace: "nowrap"
|
|
158
|
+
});
|
|
159
|
+
const inputStyle = {
|
|
160
|
+
fontFamily: "inherit",
|
|
161
|
+
fontSize: 13,
|
|
162
|
+
padding: "6px 10px",
|
|
163
|
+
borderRadius: 8,
|
|
164
|
+
border: `1px solid ${t.border}`,
|
|
165
|
+
background: t.surface,
|
|
166
|
+
color: t.text,
|
|
167
|
+
minWidth: 0
|
|
168
|
+
};
|
|
169
|
+
const selectStyle = { ...inputStyle, fontWeight: 600, fontSize: 12, cursor: "pointer" };
|
|
170
|
+
const chip = { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted };
|
|
171
|
+
const busy = pending !== null || creating;
|
|
172
|
+
return /* @__PURE__ */ jsxs("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
|
|
173
|
+
/* @__PURE__ */ jsx("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible,.abz-widget input:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
|
|
174
|
+
/* @__PURE__ */ jsxs("div", { style: card, children: [
|
|
175
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "14px 16px", borderBottom: `1px solid ${t.border}`, background: headerBg }, children: [
|
|
176
|
+
/* @__PURE__ */ jsx("strong", { style: { fontSize: 15 }, children: title }),
|
|
177
|
+
access === "ok" && /* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: busy, onClick: () => setShowCreate((v) => !v), "aria-label": "Create a new group", children: "+ New group" })
|
|
178
|
+
] }),
|
|
179
|
+
access === "pending" && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: [
|
|
180
|
+
/* @__PURE__ */ jsx("div", { style: { fontWeight: 600, color: t.text, marginBottom: 6 }, children: "Admin access pending" }),
|
|
181
|
+
"You don\u2019t have group-management access for this workspace yet. If you were just added as an admin, it activates on your first sign-in \u2014 try refreshing. Otherwise ask your API provider to add your email as a workspace admin.",
|
|
182
|
+
/* @__PURE__ */ jsx("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: load, children: "Refresh" }) })
|
|
183
|
+
] }),
|
|
184
|
+
access === "ok" && showCreate && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, padding: "12px 16px", borderBottom: `1px solid ${t.border}` }, children: [
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
"input",
|
|
187
|
+
{
|
|
188
|
+
style: { ...inputStyle, flex: 1 },
|
|
189
|
+
placeholder: "Group name (e.g. admin, reservationists)",
|
|
190
|
+
value: newName,
|
|
191
|
+
onChange: (e) => setNewName(e.target.value),
|
|
192
|
+
onKeyDown: (e) => e.key === "Enter" && createGroup(),
|
|
193
|
+
"aria-label": "New group name"
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
/* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: creating || !newName.trim(), onClick: createGroup, children: creating ? "Creating\u2026" : "Create" }),
|
|
197
|
+
/* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: () => {
|
|
198
|
+
setShowCreate(false);
|
|
199
|
+
setNewName("");
|
|
200
|
+
}, children: "Cancel" })
|
|
201
|
+
] }),
|
|
202
|
+
access === "ok" && groups === null && !loadError && /* @__PURE__ */ jsx("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading groups\u2026" }),
|
|
203
|
+
access === "ok" && loadError && /* @__PURE__ */ jsxs("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
|
|
204
|
+
loadError,
|
|
205
|
+
" ",
|
|
206
|
+
/* @__PURE__ */ jsx("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
|
|
207
|
+
] }),
|
|
208
|
+
access === "ok" && groups !== null && groups.length === 0 && /* @__PURE__ */ jsx("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "No groups yet. Create one to start organizing your users." }),
|
|
209
|
+
access === "ok" && groups !== null && groups.length > 0 && /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: groups.map((g) => {
|
|
210
|
+
const sealed = g.name === SEALED;
|
|
211
|
+
const isOpen = open === g.id;
|
|
212
|
+
return /* @__PURE__ */ jsxs("li", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
213
|
+
/* @__PURE__ */ jsxs(
|
|
214
|
+
"button",
|
|
215
|
+
{
|
|
216
|
+
onClick: () => toggle(g),
|
|
217
|
+
"aria-expanded": isOpen,
|
|
218
|
+
style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 14, textAlign: "left" },
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: isOpen ? "\u25BE" : "\u25B8" }),
|
|
221
|
+
/* @__PURE__ */ jsx("strong", { style: { flex: 1 }, children: g.name }),
|
|
222
|
+
sealed && /* @__PURE__ */ jsx("span", { style: chip, children: "admins \xB7 managed by allowlist" }),
|
|
223
|
+
/* @__PURE__ */ jsxs("span", { style: { ...chip }, children: [
|
|
224
|
+
g.member_count ?? 0,
|
|
225
|
+
" member",
|
|
226
|
+
(g.member_count ?? 0) === 1 ? "" : "s"
|
|
227
|
+
] })
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
isOpen && /* @__PURE__ */ jsxs("div", { style: { padding: "0 16px 14px 34px" }, children: [
|
|
232
|
+
detailLoading && /* @__PURE__ */ jsx("div", { style: { color: t.muted, fontSize: 13 }, children: "Loading\u2026" }),
|
|
233
|
+
detail && detail.id === g.id && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
234
|
+
/* @__PURE__ */ jsxs("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
|
|
235
|
+
detail.members.map((m) => /* @__PURE__ */ jsxs("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
|
|
236
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1, minWidth: 140 }, children: userLabel(m) }),
|
|
237
|
+
m.source_label && /* @__PURE__ */ jsx("span", { style: chip, children: m.source_label }),
|
|
238
|
+
sealed ? /* @__PURE__ */ jsx("span", { style: { ...chip, fontWeight: 600 }, children: "admin" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
239
|
+
/* @__PURE__ */ jsxs(
|
|
240
|
+
"select",
|
|
241
|
+
{
|
|
242
|
+
style: selectStyle,
|
|
243
|
+
value: m.role,
|
|
244
|
+
disabled: busy,
|
|
245
|
+
"aria-label": `Role of ${userLabel(m)}`,
|
|
246
|
+
onChange: (e) => act(`role:${m.abz_sub}`, { action: "change-role", groupId: g.id, abzSub: m.abz_sub, role: e.target.value }, () => loadDetail(g.id)),
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsx("option", { value: "member", children: "member" }),
|
|
249
|
+
/* @__PURE__ */ jsx("option", { value: "admin", children: "group admin" })
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsx(
|
|
254
|
+
"button",
|
|
255
|
+
{
|
|
256
|
+
style: btn("danger", true),
|
|
257
|
+
disabled: busy,
|
|
258
|
+
"aria-label": `Remove ${userLabel(m)} from ${g.name}`,
|
|
259
|
+
onClick: () => act(`rm:${m.abz_sub}`, { action: "remove-member", groupId: g.id, abzSub: m.abz_sub }, () => loadDetail(g.id)),
|
|
260
|
+
children: "Remove"
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
] })
|
|
264
|
+
] }, m.abz_sub)),
|
|
265
|
+
detail.members.length === 0 && /* @__PURE__ */ jsx("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No members yet." })
|
|
266
|
+
] }),
|
|
267
|
+
sealed ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "Workspace admins are granted by email on the provider\u2019s admin list \u2014 membership can\u2019t be edited here." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
268
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }, children: [
|
|
269
|
+
/* @__PURE__ */ jsxs(
|
|
270
|
+
"select",
|
|
271
|
+
{
|
|
272
|
+
style: { ...selectStyle, flex: 1, minWidth: 160 },
|
|
273
|
+
value: addSel,
|
|
274
|
+
disabled: busy,
|
|
275
|
+
"aria-label": "User to add",
|
|
276
|
+
onChange: (e) => setAddSel(e.target.value),
|
|
277
|
+
children: [
|
|
278
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Add a user\u2026" }),
|
|
279
|
+
users.filter((u) => !detail.members.some((m) => m.abz_sub === u.abz_sub)).map((u) => /* @__PURE__ */ jsx("option", { value: u.abz_sub, children: userLabel(u) }, u.abz_sub))
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
),
|
|
283
|
+
/* @__PURE__ */ jsxs(
|
|
284
|
+
"select",
|
|
285
|
+
{
|
|
286
|
+
style: selectStyle,
|
|
287
|
+
value: addRole,
|
|
288
|
+
disabled: busy,
|
|
289
|
+
"aria-label": "Role for the new member",
|
|
290
|
+
onChange: (e) => setAddRole(e.target.value),
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ jsx("option", { value: "member", children: "member" }),
|
|
293
|
+
/* @__PURE__ */ jsx("option", { value: "admin", children: "group admin" })
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
/* @__PURE__ */ jsx(
|
|
298
|
+
"button",
|
|
299
|
+
{
|
|
300
|
+
style: btn("primary", true),
|
|
301
|
+
disabled: busy || !addSel,
|
|
302
|
+
onClick: () => act("add-member", { action: "add-member", groupId: g.id, abzSub: addSel, role: addRole }, () => {
|
|
303
|
+
setAddSel("");
|
|
304
|
+
return loadDetail(g.id);
|
|
305
|
+
}),
|
|
306
|
+
children: "Add"
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] }),
|
|
310
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 12, flexWrap: "wrap" }, children: [
|
|
311
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: t.muted }, children: "Subgroups:" }),
|
|
312
|
+
detail.subgroups.map((s) => /* @__PURE__ */ jsxs("span", { style: { ...chip, display: "inline-flex", gap: 6, alignItems: "center" }, children: [
|
|
313
|
+
s.name,
|
|
314
|
+
/* @__PURE__ */ jsx(
|
|
315
|
+
"button",
|
|
316
|
+
{
|
|
317
|
+
style: { background: "none", border: "none", cursor: "pointer", color: t.danger, fontSize: 12, padding: 0 },
|
|
318
|
+
disabled: busy,
|
|
319
|
+
"aria-label": `Un-nest ${s.name} from ${g.name}`,
|
|
320
|
+
onClick: () => act("unnest", { action: "remove-subgroup", groupId: g.id, childGroupId: s.id }, () => loadDetail(g.id)),
|
|
321
|
+
children: "\u2715"
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
] }, s.id)),
|
|
325
|
+
detail.subgroups.length === 0 && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: t.muted }, children: "none" }),
|
|
326
|
+
/* @__PURE__ */ jsxs(
|
|
327
|
+
"select",
|
|
328
|
+
{
|
|
329
|
+
style: selectStyle,
|
|
330
|
+
value: nestSel,
|
|
331
|
+
disabled: busy,
|
|
332
|
+
"aria-label": "Group to nest",
|
|
333
|
+
onChange: (e) => setNestSel(e.target.value),
|
|
334
|
+
children: [
|
|
335
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Nest a group\u2026" }),
|
|
336
|
+
groups.filter((x) => x.id !== g.id && x.name !== SEALED && !detail.subgroups.some((s) => s.id === x.id)).map((x) => /* @__PURE__ */ jsx("option", { value: x.id, children: x.name }, x.id))
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
),
|
|
340
|
+
/* @__PURE__ */ jsx(
|
|
341
|
+
"button",
|
|
342
|
+
{
|
|
343
|
+
style: btn("ghost", true),
|
|
344
|
+
disabled: busy || !nestSel,
|
|
345
|
+
onClick: () => act("nest", { action: "add-subgroup", groupId: g.id, childGroupId: nestSel }, () => {
|
|
346
|
+
setNestSel("");
|
|
347
|
+
return loadDetail(g.id);
|
|
348
|
+
}),
|
|
349
|
+
children: "Nest"
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
] }),
|
|
353
|
+
/* @__PURE__ */ jsx("div", { style: { marginTop: 12, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: confirmDelete === g.id ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: t.text }, children: [
|
|
355
|
+
"Delete \u201C",
|
|
356
|
+
g.name,
|
|
357
|
+
"\u201D? Members lose this group everywhere it\u2019s used. This can\u2019t be undone."
|
|
358
|
+
] }),
|
|
359
|
+
/* @__PURE__ */ jsx(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
style: btn("danger", true),
|
|
363
|
+
disabled: busy,
|
|
364
|
+
onClick: () => {
|
|
365
|
+
setConfirmDelete(null);
|
|
366
|
+
act("del", { action: "delete-group", groupId: g.id }, () => {
|
|
367
|
+
setOpen(null);
|
|
368
|
+
setDetail(null);
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
children: "Delete group"
|
|
372
|
+
}
|
|
373
|
+
),
|
|
374
|
+
/* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: () => setConfirmDelete(null), children: "Cancel" })
|
|
375
|
+
] }) : /* @__PURE__ */ jsx("button", { style: { ...btn("ghost", true), color: t.danger }, disabled: busy, onClick: () => setConfirmDelete(g.id), children: "Delete group" }) })
|
|
376
|
+
] })
|
|
377
|
+
] })
|
|
378
|
+
] })
|
|
379
|
+
] }, g.id);
|
|
380
|
+
}) }),
|
|
381
|
+
access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
382
|
+
/* @__PURE__ */ jsxs(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
onClick: () => setShowTraffic((v) => !v),
|
|
386
|
+
"aria-expanded": showTraffic,
|
|
387
|
+
style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 13, textAlign: "left" },
|
|
388
|
+
children: [
|
|
389
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showTraffic ? "\u25BE" : "\u25B8" }),
|
|
390
|
+
/* @__PURE__ */ jsx("strong", { style: { flex: 1 }, children: "Seen in traffic" }),
|
|
391
|
+
/* @__PURE__ */ jsxs("span", { style: chip, children: [
|
|
392
|
+
observed.filter((o) => !o.provisioned).length,
|
|
393
|
+
" new"
|
|
394
|
+
] })
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
showTraffic && /* @__PURE__ */ jsxs("ul", { style: { listStyle: "none", margin: 0, padding: "0 16px 14px 34px" }, children: [
|
|
399
|
+
observed.length === 0 && /* @__PURE__ */ jsx("li", { style: { color: t.muted, fontSize: 13 }, children: "No identities observed yet \u2014 they appear here after their first API call." }),
|
|
400
|
+
observed.map((o) => /* @__PURE__ */ jsxs("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
|
|
401
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1, minWidth: 140, fontFamily: t.monoFontFamily, fontSize: 13 }, children: o.email || o.abz_sub }),
|
|
402
|
+
o.source_label && /* @__PURE__ */ jsx("span", { style: chip, children: o.source_label }),
|
|
403
|
+
o.last_seen && /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, color: t.muted }, children: [
|
|
404
|
+
"seen ",
|
|
405
|
+
relTime(o.last_seen),
|
|
406
|
+
o.seen_count ? ` \xB7 ${o.seen_count}\xD7` : ""
|
|
407
|
+
] }),
|
|
408
|
+
o.provisioned ? /* @__PURE__ */ jsxs("span", { style: { ...chip, color: t.success, borderColor: t.success }, children: [
|
|
409
|
+
"user",
|
|
410
|
+
o.groups?.length ? ` \xB7 ${o.groups.join(", ")}` : ""
|
|
411
|
+
] }) : /* @__PURE__ */ jsx(
|
|
412
|
+
"button",
|
|
413
|
+
{
|
|
414
|
+
style: btn("primary", true),
|
|
415
|
+
disabled: busy,
|
|
416
|
+
"aria-label": `Add ${o.email || o.abz_sub} as a user`,
|
|
417
|
+
onClick: () => act("provision", { action: "provision-observed", consumerUserId: o.abz_sub, email: o.email ?? void 0 }),
|
|
418
|
+
children: "Add as user"
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
] }, o.abz_sub))
|
|
422
|
+
] })
|
|
423
|
+
] }),
|
|
424
|
+
err && /* @__PURE__ */ jsx("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: err })
|
|
425
|
+
] })
|
|
426
|
+
] });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// src/react/index.tsx
|
|
430
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
431
|
+
var DEFAULTS2 = {
|
|
432
|
+
accent: "#4f46e5",
|
|
433
|
+
accentText: "#ffffff",
|
|
434
|
+
background: "transparent",
|
|
435
|
+
surface: "#ffffff",
|
|
436
|
+
headerBackground: "",
|
|
437
|
+
text: "#111827",
|
|
438
|
+
muted: "#6b7280",
|
|
439
|
+
border: "#e5e7eb",
|
|
440
|
+
danger: "#dc2626",
|
|
441
|
+
success: "#16a34a",
|
|
442
|
+
radius: "12px",
|
|
443
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
444
|
+
monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
445
|
+
};
|
|
446
|
+
function relTime2(iso) {
|
|
447
|
+
if (!iso) return null;
|
|
448
|
+
const t = new Date(iso).getTime();
|
|
449
|
+
if (Number.isNaN(t)) return null;
|
|
450
|
+
const s = Math.floor((Date.now() - t) / 1e3);
|
|
451
|
+
if (s < 60) return "just now";
|
|
452
|
+
const m = Math.floor(s / 60);
|
|
453
|
+
if (m < 60) return `${m}m ago`;
|
|
454
|
+
const h = Math.floor(m / 60);
|
|
455
|
+
if (h < 24) return `${h}h ago`;
|
|
456
|
+
const d = Math.floor(h / 24);
|
|
457
|
+
if (d < 30) return `${d}d ago`;
|
|
458
|
+
return new Date(iso).toLocaleDateString();
|
|
459
|
+
}
|
|
460
|
+
var ApiKeyWidget = React2.forwardRef(function ApiKeyWidget2({ endpoint = "/api/apiblaze/keys", theme, title = "Your API keys", className }, ref) {
|
|
461
|
+
const t = { ...DEFAULTS2, ...theme ?? {} };
|
|
462
|
+
const headerBg = t.headerBackground || t.surface;
|
|
463
|
+
const [keys, setKeys] = React2.useState(null);
|
|
464
|
+
const [eligible, setEligible] = React2.useState([]);
|
|
465
|
+
const [access, setAccess] = React2.useState("ok");
|
|
466
|
+
const [chosenType, setChosenType] = React2.useState("");
|
|
467
|
+
const [loadError, setLoadError] = React2.useState(null);
|
|
468
|
+
const [secret, setSecret] = React2.useState(null);
|
|
469
|
+
const [creating, setCreating] = React2.useState(false);
|
|
470
|
+
const [pending, setPending] = React2.useState(null);
|
|
471
|
+
const [confirming, setConfirming] = React2.useState(null);
|
|
472
|
+
const [rowError, setRowError] = React2.useState(null);
|
|
473
|
+
const [copied, setCopied] = React2.useState(false);
|
|
474
|
+
const call = React2.useCallback(async (body) => {
|
|
475
|
+
const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
|
|
476
|
+
const data = await res.json().catch(() => ({}));
|
|
477
|
+
if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
|
|
478
|
+
return data;
|
|
479
|
+
}, [endpoint]);
|
|
480
|
+
const load = React2.useCallback(async () => {
|
|
481
|
+
setLoadError(null);
|
|
482
|
+
try {
|
|
483
|
+
const d = await call();
|
|
484
|
+
setKeys((d.keys ?? []).slice().sort(byNewest));
|
|
485
|
+
setEligible(d.keyTypes ?? []);
|
|
486
|
+
setAccess(d.access === "denied" ? "denied" : "ok");
|
|
487
|
+
} catch (e) {
|
|
488
|
+
setLoadError(e instanceof Error ? e.message : "Could not load your keys.");
|
|
489
|
+
setKeys(null);
|
|
490
|
+
}
|
|
491
|
+
}, [call]);
|
|
492
|
+
React2.useEffect(() => {
|
|
493
|
+
load();
|
|
494
|
+
}, [load]);
|
|
495
|
+
React2.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
|
|
496
|
+
React2.useEffect(() => {
|
|
497
|
+
if (eligible.length && !eligible.includes(chosenType)) {
|
|
498
|
+
setChosenType(eligible.includes("call-only") ? "call-only" : eligible[0]);
|
|
499
|
+
}
|
|
500
|
+
}, [eligible, chosenType]);
|
|
40
501
|
async function create() {
|
|
41
|
-
|
|
502
|
+
setCreating(true);
|
|
503
|
+
setRowError(null);
|
|
42
504
|
try {
|
|
43
|
-
const d = await call({ action: "create" });
|
|
44
|
-
if (d.key) setSecret(d.key);
|
|
505
|
+
const d = await call({ action: "create", keyType: chosenType || void 0 });
|
|
506
|
+
if (d.key) setSecret({ key: d.key, keyId: d.key_id });
|
|
45
507
|
await load();
|
|
46
508
|
} catch (e) {
|
|
47
|
-
|
|
509
|
+
setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
|
|
48
510
|
} finally {
|
|
49
|
-
|
|
511
|
+
setCreating(false);
|
|
50
512
|
}
|
|
51
513
|
}
|
|
52
514
|
async function reveal(keyId) {
|
|
53
|
-
|
|
515
|
+
setPending(`reveal:${keyId}`);
|
|
516
|
+
setRowError(null);
|
|
54
517
|
try {
|
|
55
518
|
const d = await call({ action: "reveal", keyId });
|
|
56
|
-
if (d.key) setSecret(d.key);
|
|
57
|
-
else
|
|
519
|
+
if (d.key) setSecret({ key: d.key, keyId });
|
|
520
|
+
else setRowError({ keyId, msg: "For security, this key can only be shown once \u2014 right after it was created." });
|
|
58
521
|
} catch (e) {
|
|
59
|
-
|
|
522
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
|
|
60
523
|
} finally {
|
|
61
|
-
|
|
524
|
+
setPending(null);
|
|
62
525
|
}
|
|
63
526
|
}
|
|
64
527
|
async function rotate(keyId) {
|
|
65
|
-
|
|
528
|
+
setConfirming(null);
|
|
529
|
+
setPending(`rotate:${keyId}`);
|
|
530
|
+
setRowError(null);
|
|
66
531
|
try {
|
|
67
532
|
const d = await call({ action: "rotate", keyId });
|
|
68
|
-
if (d.key) setSecret(d.key);
|
|
533
|
+
if (d.key) setSecret({ key: d.key, keyId: d.key_id });
|
|
69
534
|
await load();
|
|
70
535
|
} catch (e) {
|
|
71
|
-
|
|
536
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
|
|
72
537
|
} finally {
|
|
73
|
-
|
|
538
|
+
setPending(null);
|
|
74
539
|
}
|
|
75
540
|
}
|
|
76
541
|
async function revoke(keyId) {
|
|
77
|
-
|
|
542
|
+
setConfirming(null);
|
|
543
|
+
setPending(`revoke:${keyId}`);
|
|
544
|
+
setRowError(null);
|
|
78
545
|
try {
|
|
79
546
|
await call({ action: "revoke", keyId });
|
|
80
547
|
await load();
|
|
81
548
|
} catch (e) {
|
|
82
|
-
|
|
549
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
|
|
83
550
|
} finally {
|
|
84
|
-
|
|
551
|
+
setPending(null);
|
|
85
552
|
}
|
|
86
553
|
}
|
|
87
|
-
function copy(v) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
554
|
+
async function copy(v) {
|
|
555
|
+
try {
|
|
556
|
+
await navigator.clipboard.writeText(v);
|
|
557
|
+
setCopied(true);
|
|
558
|
+
setTimeout(() => setCopied(false), 1600);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
91
561
|
}
|
|
92
562
|
const vars = {
|
|
93
|
-
// @ts-expect-error CSS custom
|
|
563
|
+
// @ts-expect-error CSS custom properties
|
|
94
564
|
"--abz-accent": t.accent,
|
|
95
565
|
"--abz-surface": t.surface,
|
|
96
566
|
"--abz-text": t.text,
|
|
97
567
|
"--abz-muted": t.muted,
|
|
98
568
|
"--abz-border": t.border,
|
|
569
|
+
"--abz-danger": t.danger,
|
|
99
570
|
"--abz-radius": t.radius,
|
|
100
571
|
background: t.background,
|
|
101
572
|
color: t.text,
|
|
102
|
-
fontFamily:
|
|
573
|
+
fontFamily: t.fontFamily,
|
|
103
574
|
fontSize: 14
|
|
104
575
|
};
|
|
105
|
-
const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius,
|
|
106
|
-
const btn = (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
576
|
+
const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
|
|
577
|
+
const btn = (kind = "ghost", small = false) => ({
|
|
578
|
+
cursor: "pointer",
|
|
579
|
+
borderRadius: 8,
|
|
580
|
+
fontWeight: 600,
|
|
581
|
+
fontSize: small ? 12 : 13,
|
|
582
|
+
padding: small ? "5px 10px" : "8px 14px",
|
|
583
|
+
fontFamily: "inherit",
|
|
584
|
+
border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
|
|
585
|
+
background: kind === "primary" ? t.accent : "transparent",
|
|
586
|
+
color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
|
|
587
|
+
whiteSpace: "nowrap"
|
|
588
|
+
});
|
|
589
|
+
const selectStyle = {
|
|
590
|
+
fontFamily: "inherit",
|
|
591
|
+
fontSize: 12,
|
|
592
|
+
fontWeight: 600,
|
|
593
|
+
padding: "5px 8px",
|
|
594
|
+
borderRadius: 8,
|
|
595
|
+
border: `1px solid ${t.border}`,
|
|
596
|
+
background: t.surface,
|
|
597
|
+
color: t.text,
|
|
598
|
+
cursor: "pointer"
|
|
599
|
+
};
|
|
600
|
+
const disabledStyle = (on) => on ? { opacity: 0.5, cursor: "not-allowed" } : {};
|
|
601
|
+
const secretBg = `color-mix(in srgb, ${t.accent} 8%, transparent)`;
|
|
602
|
+
const busyAny = creating || pending !== null;
|
|
603
|
+
const denied = access === "denied";
|
|
604
|
+
const showPicker = eligible.length > 1;
|
|
605
|
+
const Picker = showPicker ? /* @__PURE__ */ jsx2("select", { value: chosenType, onChange: (e) => setChosenType(e.target.value), style: selectStyle, "aria-label": "Key type", disabled: busyAny, children: eligible.map((k) => /* @__PURE__ */ jsx2("option", { value: k, children: k }, k)) }) : null;
|
|
606
|
+
return /* @__PURE__ */ jsxs2("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
|
|
607
|
+
/* @__PURE__ */ jsx2("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
|
|
608
|
+
/* @__PURE__ */ jsxs2("div", { style: card, children: [
|
|
609
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "14px 16px", borderBottom: `1px solid ${t.border}`, background: headerBg }, children: [
|
|
610
|
+
/* @__PURE__ */ jsx2("strong", { style: { fontSize: 15 }, children: title }),
|
|
611
|
+
!denied && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
612
|
+
Picker,
|
|
613
|
+
/* @__PURE__ */ jsx2(
|
|
614
|
+
"button",
|
|
615
|
+
{
|
|
616
|
+
style: { ...btn("primary", true), ...disabledStyle(busyAny) },
|
|
617
|
+
disabled: busyAny,
|
|
618
|
+
onClick: create,
|
|
619
|
+
"aria-label": "Create a new API key",
|
|
620
|
+
children: creating ? "Creating\u2026" : "+ Create key"
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
] })
|
|
624
|
+
] }),
|
|
625
|
+
denied && /* @__PURE__ */ jsx2("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "Your account doesn\u2019t have API access." }),
|
|
626
|
+
!denied && secret && /* @__PURE__ */ jsxs2("div", { role: "status", "aria-live": "polite", style: { margin: 16, padding: 14, borderRadius: 10, border: `1px solid ${t.accent}`, background: secretBg }, children: [
|
|
627
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "New API key" }),
|
|
628
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: t.muted, marginBottom: 8 }, children: "This is the only time you\u2019ll see this key. Copy it and store it somewhere safe." }),
|
|
629
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
630
|
+
/* @__PURE__ */ jsx2(
|
|
631
|
+
"input",
|
|
632
|
+
{
|
|
633
|
+
readOnly: true,
|
|
634
|
+
value: secret.key,
|
|
635
|
+
onFocus: (e) => e.currentTarget.select(),
|
|
636
|
+
"aria-label": "New API key value",
|
|
637
|
+
style: { flex: 1, fontFamily: t.monoFontFamily, fontSize: 13, padding: "8px 10px", borderRadius: 8, border: `1px solid ${t.border}`, background: t.surface, color: t.text, minWidth: 0 }
|
|
638
|
+
}
|
|
639
|
+
),
|
|
640
|
+
/* @__PURE__ */ jsx2("button", { style: { ...btn("primary"), ...copied ? { background: t.success, borderColor: t.success } : {} }, onClick: () => copy(secret.key), children: copied ? "Copied" : "Copy" })
|
|
641
|
+
] }),
|
|
642
|
+
/* @__PURE__ */ jsx2("div", { style: { marginTop: 10, textAlign: "right" }, children: /* @__PURE__ */ jsx2("button", { style: btn("ghost", true), onClick: () => setSecret(null), children: "I\u2019ve saved it" }) })
|
|
643
|
+
] }),
|
|
644
|
+
!denied && keys === null && !loadError && /* @__PURE__ */ jsx2("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading your keys\u2026" }),
|
|
645
|
+
!denied && loadError && /* @__PURE__ */ jsxs2("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
|
|
646
|
+
loadError,
|
|
647
|
+
" ",
|
|
648
|
+
/* @__PURE__ */ jsx2("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
|
|
649
|
+
] }),
|
|
650
|
+
!denied && keys !== null && keys.length === 0 && /* @__PURE__ */ jsxs2("div", { style: { padding: "28px 20px", textAlign: "center" }, children: [
|
|
651
|
+
/* @__PURE__ */ jsx2("div", { style: { color: t.muted, marginBottom: 14 }, children: "You don\u2019t have an API key yet. Create one to start calling the API." }),
|
|
652
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "inline-flex", gap: 8, alignItems: "center" }, children: [
|
|
653
|
+
Picker,
|
|
654
|
+
/* @__PURE__ */ jsx2("button", { style: { ...btn("primary"), ...disabledStyle(creating) }, disabled: creating, onClick: create, children: creating ? "Creating\u2026" : "Create API key" })
|
|
655
|
+
] })
|
|
131
656
|
] }),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
657
|
+
!denied && keys !== null && keys.length > 0 && /* @__PURE__ */ jsx2("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: keys.map((k) => {
|
|
658
|
+
const anyBusy = busyAny;
|
|
659
|
+
const used = relTime2(k.last_used);
|
|
660
|
+
const created = k.created_at ? new Date(k.created_at).toLocaleDateString() : null;
|
|
661
|
+
const isConfirm = confirming?.keyId === k.key_id;
|
|
662
|
+
const showable = !!k.expires_at;
|
|
663
|
+
return /* @__PURE__ */ jsxs2("li", { style: { padding: "12px 16px", borderTop: `1px solid ${t.border}`, opacity: k.disabled ? 0.6 : 1 }, children: [
|
|
664
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
|
|
665
|
+
/* @__PURE__ */ jsxs2("code", { "aria-label": `API key ending in ${k.key_suffix ?? ""}`, style: { flex: 1, minWidth: 150, fontFamily: t.monoFontFamily, color: t.muted }, children: [
|
|
666
|
+
k.key_prefix ?? "\u2022\u2022\u2022",
|
|
667
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "\u2022\u2022\u2022\u2022\u2022\u2022" }),
|
|
668
|
+
k.key_suffix ?? "\u2022\u2022\u2022"
|
|
669
|
+
] }),
|
|
670
|
+
k.environment && /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted }, children: k.environment }),
|
|
671
|
+
k.disabled ? /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, fontWeight: 600, color: t.danger }, children: "Revoked" }) : isConfirm ? /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", flexBasis: "100%", flexWrap: "wrap", marginTop: 6 }, children: [
|
|
672
|
+
/* @__PURE__ */ jsx2("span", { style: { fontSize: 12, color: t.text, flex: 1, minWidth: 200 }, children: confirming.action === "rotate" ? "Rotate this key? A new secret is generated now. The current key keeps working for a short grace period, then stops \u2014 anything still using it will break." : "Revoke this key? It stops working immediately and can\u2019t be undone." }),
|
|
673
|
+
/* @__PURE__ */ jsx2("button", { style: btn(confirming.action === "revoke" ? "danger" : "primary", true), onClick: () => confirming.action === "rotate" ? rotate(k.key_id) : revoke(k.key_id), children: confirming.action === "rotate" ? "Rotate key" : "Revoke key" }),
|
|
674
|
+
/* @__PURE__ */ jsx2("button", { style: btn("ghost", true), onClick: () => setConfirming(null), children: "Cancel" })
|
|
675
|
+
] }) : /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
|
|
676
|
+
showable && /* @__PURE__ */ jsx2(
|
|
677
|
+
"button",
|
|
678
|
+
{
|
|
679
|
+
style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
|
|
680
|
+
disabled: anyBusy,
|
|
681
|
+
"aria-label": `Show key ending in ${k.key_suffix ?? ""}`,
|
|
682
|
+
onClick: () => reveal(k.key_id),
|
|
683
|
+
children: pending === `reveal:${k.key_id}` ? "Showing\u2026" : "Show"
|
|
684
|
+
}
|
|
685
|
+
),
|
|
686
|
+
/* @__PURE__ */ jsx2(
|
|
687
|
+
"button",
|
|
688
|
+
{
|
|
689
|
+
style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
|
|
690
|
+
disabled: anyBusy,
|
|
691
|
+
"aria-label": `Rotate key ending in ${k.key_suffix ?? ""}`,
|
|
692
|
+
onClick: () => setConfirming({ keyId: k.key_id, action: "rotate" }),
|
|
693
|
+
children: pending === `rotate:${k.key_id}` ? "Rotating\u2026" : "Rotate"
|
|
694
|
+
}
|
|
695
|
+
),
|
|
696
|
+
/* @__PURE__ */ jsx2(
|
|
697
|
+
"button",
|
|
698
|
+
{
|
|
699
|
+
style: { ...btn("danger", true), ...disabledStyle(anyBusy) },
|
|
700
|
+
disabled: anyBusy,
|
|
701
|
+
"aria-label": `Revoke key ending in ${k.key_suffix ?? ""}`,
|
|
702
|
+
onClick: () => setConfirming({ keyId: k.key_id, action: "revoke" }),
|
|
703
|
+
children: "Revoke"
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
] })
|
|
707
|
+
] }),
|
|
708
|
+
(created || used) && !isConfirm && /* @__PURE__ */ jsxs2("div", { style: { fontSize: 11, color: t.muted, marginTop: 5 }, children: [
|
|
709
|
+
created ? `Created ${created}` : "",
|
|
710
|
+
created && used ? " \xB7 " : "",
|
|
711
|
+
used ? `Last used ${used}` : created ? " \xB7 Never used" : "Never used"
|
|
712
|
+
] }),
|
|
713
|
+
rowError?.keyId === k.key_id && /* @__PURE__ */ jsx2("div", { role: "alert", style: { fontSize: 12, color: t.danger, marginTop: 6 }, children: rowError.msg })
|
|
714
|
+
] }, k.key_id);
|
|
715
|
+
}) }),
|
|
716
|
+
!denied && rowError && rowError.keyId === "" && /* @__PURE__ */ jsx2("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: rowError.msg })
|
|
139
717
|
] })
|
|
140
|
-
] })
|
|
718
|
+
] });
|
|
719
|
+
});
|
|
720
|
+
function byNewest(a, b) {
|
|
721
|
+
return new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime();
|
|
141
722
|
}
|
|
142
723
|
var react_default = ApiKeyWidget;
|
|
143
724
|
export {
|
|
144
725
|
ApiKeyWidget,
|
|
726
|
+
UsersGroupsWidget,
|
|
145
727
|
react_default as default
|
|
146
728
|
};
|