@rebasepro/app 0.25.1 → 0.26.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/RlsSimulatorPanel-DOfmVgM3.js +403 -0
  3. package/dist/RlsSimulatorPanel-DOfmVgM3.js.map +1 -0
  4. package/dist/collections/form-layout.d.ts +30 -27
  5. package/dist/components/LanguageToggle.d.ts +9 -0
  6. package/dist/components/LoginView/index.d.ts +7 -0
  7. package/dist/components/index.d.ts +1 -0
  8. package/dist/components/studio/RlsSimulatorBar.d.ts +39 -0
  9. package/dist/components/studio/RlsSimulatorPanel.d.ts +29 -0
  10. package/dist/contexts/RlsSimulationContext.d.ts +47 -0
  11. package/dist/contexts/index.d.ts +1 -0
  12. package/dist/core/effective_role_state.d.ts +9 -0
  13. package/dist/{de-DyzL3TA9.js → de-3yzzpuZj.js} +108 -1
  14. package/dist/de-3yzzpuZj.js.map +1 -0
  15. package/dist/debug.js +96 -46
  16. package/dist/{es-DJCi9Za4.js → es-mnyTaRkK.js} +108 -1
  17. package/dist/es-mnyTaRkK.js.map +1 -0
  18. package/dist/{fr-Ie-LqMS1.js → fr-nJHADzlI.js} +108 -1
  19. package/dist/fr-nJHADzlI.js.map +1 -0
  20. package/dist/{hi-D7t90uiv.js → hi-B9_iunnL.js} +108 -1
  21. package/dist/hi-B9_iunnL.js.map +1 -0
  22. package/dist/hooks/useBuildEffectiveRoleController.d.ts +9 -3
  23. package/dist/hooks/usePermissions.d.ts +4 -0
  24. package/dist/hooks/useUnsavedChangesDialog.d.ts +7 -1
  25. package/dist/i18n/RebaseI18nProvider.d.ts +11 -0
  26. package/dist/index.es.js +2747 -2555
  27. package/dist/index.es.js.map +1 -1
  28. package/dist/{it-DPvaMOt6.js → it-DoEtz-OK.js} +108 -1
  29. package/dist/it-DoEtz-OK.js.map +1 -0
  30. package/dist/{pt-DfaXMf_W.js → pt-DT26AmBU.js} +108 -1
  31. package/dist/pt-DT26AmBU.js.map +1 -0
  32. package/package.json +7 -7
  33. package/dist/de-DyzL3TA9.js.map +0 -1
  34. package/dist/es-DJCi9Za4.js.map +0 -1
  35. package/dist/fr-Ie-LqMS1.js.map +0 -1
  36. package/dist/hi-D7t90uiv.js.map +0 -1
  37. package/dist/it-DPvaMOt6.js.map +0 -1
  38. package/dist/pt-DfaXMf_W.js.map +0 -1
package/README.md CHANGED
@@ -58,7 +58,7 @@ This package is **framework-agnostic** in the sense that it doesn't depend on an
58
58
  | `useAuthSubscription` | Subscribe to auth state changes |
59
59
  | `usePermissions` | Check permissions for collections/actions |
60
60
  | `useAdminModeController` / `useBuildAdminModeController` | Admin mode toggle |
61
- | `useEffectiveRoleController` / `useBuildEffectiveRoleController` | Current user's effective role |
61
+ | `useEffectiveRoleController` | The role Studio's navigation is listed for (`useBuildEffectiveRoleController` is deprecated: use Simulate access to test permissions) |
62
62
 
63
63
  ### Hooks — UI & State
64
64
 
@@ -0,0 +1,403 @@
1
+ import { useApiBase, useApiConfig, useCanSimulateAccess, useRebaseClient, useRlsSimulation, useTranslation } from "./index.es.js";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { Alert, Button, CircularProgress, EyeIcon, IconButton, KeyRoundIcon, Popover, ShieldIcon, Tab, Tabs, TextField, Typography, UserIcon, cls, iconSize } from "@rebasepro/ui";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/studio/RlsSimulatorPanel.tsx
6
+ /**
7
+ * The "Simulate access" pill and banner themselves. Loaded on demand by the
8
+ * wrappers in `RlsSimulatorBar.tsx`: only an administrator ever sees them, so
9
+ * nobody else downloads them before the login screen paints.
10
+ */
11
+ /** How many users one search of the picker lists. */
12
+ var USER_PAGE_SIZE = 20;
13
+ /**
14
+ * A request to the simulator's admin surface with the administrator's own
15
+ * token — never through the simulated client, whose headers the admin routes
16
+ * refuse.
17
+ */
18
+ function useSimulatorRequest() {
19
+ const apiConfig = useApiConfig();
20
+ const apiBase = useApiBase();
21
+ return useCallback(async (path, init) => {
22
+ if (!apiBase) throw new Error("No API URL is configured.");
23
+ const token = await apiConfig?.getAuthToken?.();
24
+ const response = await fetch(`${apiBase}/admin/rls-simulator${path}`, {
25
+ method: init?.method ?? "GET",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ ...token ? { Authorization: `Bearer ${token}` } : {}
29
+ },
30
+ ...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
31
+ });
32
+ const json = await response.json().catch(() => void 0);
33
+ if (!response.ok) {
34
+ const message = typeof json === "object" && json !== null && "error" in json && typeof json.error === "object" && json.error !== null && "message" in json.error && typeof json.error.message === "string" ? json.error.message : response.statusText;
35
+ throw new Error(message);
36
+ }
37
+ return json;
38
+ }, [apiBase, apiConfig]);
39
+ }
40
+ /** One choosable row of the popover's lists. */
41
+ function ChoiceRow({ selected, onClick, icon, title, detail }) {
42
+ return /* @__PURE__ */ jsxs(Button, {
43
+ variant: "text",
44
+ size: "small",
45
+ fullWidth: true,
46
+ onClick,
47
+ className: cls("justify-start", { "bg-surface-active": selected }),
48
+ children: [
49
+ icon,
50
+ /* @__PURE__ */ jsx(Typography, {
51
+ variant: "body2",
52
+ noWrap: true,
53
+ className: "truncate",
54
+ children: title
55
+ }),
56
+ detail && /* @__PURE__ */ jsx(Typography, {
57
+ variant: "caption",
58
+ color: "secondary",
59
+ noWrap: true,
60
+ className: "truncate",
61
+ children: detail
62
+ })
63
+ ]
64
+ });
65
+ }
66
+ /**
67
+ * "Simulate access": the app bar pill that runs every data view as a role, a
68
+ * user or a service key, so an administrator sees exactly the rows and fields
69
+ * that identity's row-level security lets it read.
70
+ *
71
+ * Shown only to an administrator, and only when the data views ride a client
72
+ * that can derive a simulated one. The server checks the first again on every
73
+ * request: anyone else sending the simulation headers gets a 403.
74
+ *
75
+ * @group Components
76
+ */
77
+ function RlsSimulatorBarPanel({ className, compact, side, align = "end", portalContainer }) {
78
+ const { t } = useTranslation();
79
+ const controller = useRlsSimulation();
80
+ const canSimulate = useCanSimulateAccess();
81
+ const request = useSimulatorRequest();
82
+ const client = useRebaseClient();
83
+ const [open, setOpen] = useState(false);
84
+ const [tab, setTab] = useState("role");
85
+ const [options, setOptions] = useState(null);
86
+ const [optionsError, setOptionsError] = useState(null);
87
+ const [role, setRole] = useState(null);
88
+ const [roleUserId, setRoleUserId] = useState("");
89
+ const [userSearch, setUserSearch] = useState("");
90
+ const [users, setUsers] = useState([]);
91
+ const [usersLoading, setUsersLoading] = useState(false);
92
+ const [usersError, setUsersError] = useState(null);
93
+ useEffect(() => {
94
+ if (!open || options) return;
95
+ let cancelled = false;
96
+ setOptionsError(null);
97
+ request("/options").then((loaded) => {
98
+ if (!cancelled) setOptions(loaded);
99
+ }).catch((error) => {
100
+ if (!cancelled) setOptionsError(error instanceof Error ? error.message : String(error));
101
+ });
102
+ return () => {
103
+ cancelled = true;
104
+ };
105
+ }, [
106
+ open,
107
+ options,
108
+ request
109
+ ]);
110
+ const adminApi = client?.admin;
111
+ useEffect(() => {
112
+ if (!open || tab !== "user" || !adminApi) return;
113
+ let cancelled = false;
114
+ setUsersLoading(true);
115
+ const timer = setTimeout(() => {
116
+ adminApi.listUsersPaginated({
117
+ search: userSearch || void 0,
118
+ limit: USER_PAGE_SIZE
119
+ }).then(({ users: found }) => {
120
+ if (cancelled) return;
121
+ setUsersError(null);
122
+ setUsers(found.filter((user) => !user.disabled).map((user) => ({
123
+ uid: user.uid,
124
+ label: user.email ?? user.displayName ?? user.uid,
125
+ detail: user.roles?.length ? user.roles.join(", ") : void 0
126
+ })));
127
+ }).catch((error) => {
128
+ if (cancelled) return;
129
+ setUsers([]);
130
+ setUsersError(error instanceof Error ? error.message : String(error));
131
+ }).finally(() => {
132
+ if (!cancelled) setUsersLoading(false);
133
+ });
134
+ }, 250);
135
+ return () => {
136
+ cancelled = true;
137
+ clearTimeout(timer);
138
+ };
139
+ }, [
140
+ open,
141
+ tab,
142
+ userSearch,
143
+ adminApi
144
+ ]);
145
+ const start = (simulation, label) => {
146
+ controller.setSimulation(simulation, label);
147
+ setOpen(false);
148
+ };
149
+ if (!canSimulate) return null;
150
+ const active = controller.simulation !== null;
151
+ return /* @__PURE__ */ jsx(Popover, {
152
+ trigger: compact ? /* @__PURE__ */ jsxs(IconButton, {
153
+ "aria-label": t("rls_simulate_access"),
154
+ className: cls("relative", className),
155
+ children: [/* @__PURE__ */ jsx(EyeIcon, { size: iconSize.small }), active && /* @__PURE__ */ jsx("span", {
156
+ "aria-hidden": true,
157
+ className: "absolute top-1 right-1 w-2 h-2 rounded-full bg-amber-500"
158
+ })]
159
+ }) : /* @__PURE__ */ jsxs(Button, {
160
+ variant: "outlined",
161
+ size: "small",
162
+ "aria-label": t("rls_simulate_access"),
163
+ className: cls("rounded-full whitespace-nowrap", className),
164
+ children: [active ? /* @__PURE__ */ jsx("span", {
165
+ "aria-hidden": true,
166
+ className: "w-2 h-2 rounded-full bg-amber-500"
167
+ }) : /* @__PURE__ */ jsx(EyeIcon, { size: iconSize.smallest }), t("rls_simulate_access")]
168
+ }),
169
+ open,
170
+ onOpenChange: setOpen,
171
+ side,
172
+ align,
173
+ portalContainer,
174
+ children: /* @__PURE__ */ jsxs("div", {
175
+ className: "flex flex-col gap-3 p-3 w-[22rem]",
176
+ children: [
177
+ /* @__PURE__ */ jsxs("div", {
178
+ className: "flex flex-col gap-1",
179
+ children: [/* @__PURE__ */ jsx(Typography, {
180
+ variant: "label",
181
+ children: t("rls_simulate_access")
182
+ }), /* @__PURE__ */ jsx(Typography, {
183
+ variant: "caption",
184
+ color: "secondary",
185
+ children: t("rls_simulate_access_description")
186
+ })]
187
+ }),
188
+ /* @__PURE__ */ jsxs(Tabs, {
189
+ value: tab,
190
+ onValueChange: (value) => setTab(value === "user" || value === "apiKey" ? value : "role"),
191
+ children: [
192
+ /* @__PURE__ */ jsx(Tab, {
193
+ value: "role",
194
+ children: t("rls_simulate_tab_role")
195
+ }),
196
+ /* @__PURE__ */ jsx(Tab, {
197
+ value: "user",
198
+ children: t("rls_simulate_tab_user")
199
+ }),
200
+ /* @__PURE__ */ jsx(Tab, {
201
+ value: "apiKey",
202
+ children: t("rls_simulate_tab_api_key")
203
+ })
204
+ ]
205
+ }),
206
+ optionsError && /* @__PURE__ */ jsx(Alert, {
207
+ color: "error",
208
+ size: "small",
209
+ role: null,
210
+ children: t("rls_simulate_options_error", { message: optionsError })
211
+ }),
212
+ !options && !optionsError && tab !== "user" && /* @__PURE__ */ jsx("div", {
213
+ className: "flex justify-center p-2",
214
+ children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
215
+ }),
216
+ tab === "role" && options && /* @__PURE__ */ jsxs("div", {
217
+ className: "flex flex-col gap-2",
218
+ children: [
219
+ /* @__PURE__ */ jsx("div", {
220
+ className: "flex flex-col max-h-60 overflow-auto",
221
+ children: options.roles.map((entry) => /* @__PURE__ */ jsx(ChoiceRow, {
222
+ selected: role === entry.id,
223
+ onClick: () => setRole(entry.id),
224
+ icon: /* @__PURE__ */ jsx(ShieldIcon, { size: iconSize.smallest }),
225
+ title: entry.id,
226
+ detail: entry.name !== entry.id ? entry.name : void 0
227
+ }, entry.id))
228
+ }),
229
+ /* @__PURE__ */ jsx(TextField, {
230
+ size: "small",
231
+ value: roleUserId,
232
+ placeholder: t("rls_simulate_user_id_optional"),
233
+ onChange: (event) => setRoleUserId(event.target.value)
234
+ }),
235
+ /* @__PURE__ */ jsx(Button, {
236
+ color: "primary",
237
+ size: "small",
238
+ disabled: !role,
239
+ onClick: () => {
240
+ if (!role) return;
241
+ const userId = roleUserId.trim();
242
+ start({
243
+ role,
244
+ ...userId ? { userId } : {}
245
+ }, {
246
+ kind: "role",
247
+ role,
248
+ ...userId ? { userId } : {}
249
+ });
250
+ },
251
+ children: t("rls_simulate_apply")
252
+ })
253
+ ]
254
+ }),
255
+ tab === "user" && /* @__PURE__ */ jsxs("div", {
256
+ className: "flex flex-col gap-2",
257
+ children: [/* @__PURE__ */ jsx(TextField, {
258
+ size: "small",
259
+ value: userSearch,
260
+ placeholder: t("rls_simulate_search_users"),
261
+ onChange: (event) => setUserSearch(event.target.value)
262
+ }), /* @__PURE__ */ jsxs("div", {
263
+ className: "flex flex-col max-h-60 overflow-auto",
264
+ children: [
265
+ usersLoading && users.length === 0 && /* @__PURE__ */ jsx("div", {
266
+ className: "flex justify-center p-2",
267
+ children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
268
+ }),
269
+ !usersLoading && usersError && /* @__PURE__ */ jsx(Alert, {
270
+ color: "error",
271
+ size: "small",
272
+ role: null,
273
+ children: t("rls_simulate_users_error", { message: usersError })
274
+ }),
275
+ !usersLoading && !usersError && users.length === 0 && /* @__PURE__ */ jsx(Typography, {
276
+ variant: "caption",
277
+ color: "secondary",
278
+ className: "p-2",
279
+ children: t("rls_simulate_no_users")
280
+ }),
281
+ users.map((user) => /* @__PURE__ */ jsx(ChoiceRow, {
282
+ selected: controller.simulation?.userId === user.uid && !controller.simulation.role,
283
+ onClick: () => start({ userId: user.uid }, {
284
+ kind: "user",
285
+ user: user.label
286
+ }),
287
+ icon: /* @__PURE__ */ jsx(UserIcon, { size: iconSize.smallest }),
288
+ title: user.label,
289
+ detail: user.detail
290
+ }, user.uid))
291
+ ]
292
+ })]
293
+ }),
294
+ tab === "apiKey" && options && /* @__PURE__ */ jsxs("div", {
295
+ className: "flex flex-col max-h-60 overflow-auto",
296
+ children: [options.apiKeys.length === 0 && /* @__PURE__ */ jsx(Typography, {
297
+ variant: "caption",
298
+ color: "secondary",
299
+ className: "p-2",
300
+ children: t("rls_simulate_no_api_keys")
301
+ }), options.apiKeys.map((key) => /* @__PURE__ */ jsx(ChoiceRow, {
302
+ selected: controller.simulation?.apiKeyId === key.id,
303
+ onClick: () => start({ apiKeyId: key.id }, {
304
+ kind: "apiKey",
305
+ name: key.name,
306
+ keyPrefix: key.keyPrefix
307
+ }),
308
+ icon: /* @__PURE__ */ jsx(KeyRoundIcon, { size: iconSize.smallest }),
309
+ title: key.name,
310
+ detail: `${key.keyPrefix}…`
311
+ }, key.id))]
312
+ }),
313
+ active && /* @__PURE__ */ jsx(Button, {
314
+ variant: "text",
315
+ size: "small",
316
+ onClick: () => {
317
+ controller.setSimulation(null);
318
+ setOpen(false);
319
+ },
320
+ children: t("rls_simulate_stop")
321
+ })
322
+ ]
323
+ })
324
+ });
325
+ }
326
+ /**
327
+ * The banner every screen shows while access is simulated: who the data views
328
+ * run as, how many collections show that identity nothing, how many fields its
329
+ * roles cannot read — and the way out.
330
+ *
331
+ * @group Components
332
+ */
333
+ function RlsSimulationBannerPanel({ className }) {
334
+ const { t } = useTranslation();
335
+ const { simulation, label, setSimulation } = useRlsSimulation();
336
+ const request = useSimulatorRequest();
337
+ const [summary, setSummary] = useState(null);
338
+ const [summaryError, setSummaryError] = useState(null);
339
+ useEffect(() => {
340
+ setSummary(null);
341
+ setSummaryError(null);
342
+ if (!simulation) return;
343
+ let cancelled = false;
344
+ request("/preview", {
345
+ method: "POST",
346
+ body: simulation
347
+ }).then(({ summary: loaded }) => {
348
+ if (!cancelled) setSummary(loaded);
349
+ }).catch((error) => {
350
+ console.warn("Could not preview the simulated access", error);
351
+ if (!cancelled) setSummaryError(error instanceof Error ? error.message : String(error));
352
+ });
353
+ return () => {
354
+ cancelled = true;
355
+ };
356
+ }, [simulation, request]);
357
+ if (!simulation) return null;
358
+ const title = (() => {
359
+ if (label?.kind === "apiKey") return t("rls_simulation_viewing_as_api_key", {
360
+ name: label.name,
361
+ prefix: label.keyPrefix
362
+ });
363
+ if (label?.kind === "user") return t("rls_simulation_viewing_as_user", { user: label.user });
364
+ if (simulation.role && simulation.userId) return t("rls_simulation_viewing_as_role_user", {
365
+ role: simulation.role,
366
+ user: simulation.userId
367
+ });
368
+ if (simulation.role) return t("rls_simulation_viewing_as_role", { role: simulation.role });
369
+ return t("rls_simulation_viewing_as_user", { user: simulation.userId ?? simulation.apiKeyId ?? "" });
370
+ })();
371
+ const counts = summary ? [summary.hiddenCollections === 1 ? t("rls_simulation_hidden_collections_one") : t("rls_simulation_hidden_collections", { count: String(summary.hiddenCollections) }), summary.maskedColumns === 1 ? t("rls_simulation_masked_columns_one") : t("rls_simulation_masked_columns", { count: String(summary.maskedColumns) })].join(", ") : summaryError !== null ? t("rls_simulation_count_failed", { message: summaryError }) : t("rls_simulation_counting");
372
+ return /* @__PURE__ */ jsx("div", {
373
+ role: "status",
374
+ "aria-live": "polite",
375
+ className,
376
+ children: /* @__PURE__ */ jsx(Alert, {
377
+ color: "warning",
378
+ size: "small",
379
+ role: null,
380
+ action: /* @__PURE__ */ jsx(Button, {
381
+ variant: "text",
382
+ size: "small",
383
+ onClick: () => setSimulation(null),
384
+ children: t("rls_simulate_stop")
385
+ }),
386
+ children: /* @__PURE__ */ jsxs("div", {
387
+ className: "flex flex-col gap-0.5",
388
+ children: [/* @__PURE__ */ jsx(Typography, {
389
+ variant: "label",
390
+ children: `${title} — ${counts}`
391
+ }), /* @__PURE__ */ jsx(Typography, {
392
+ variant: "body2",
393
+ color: "secondary",
394
+ children: t("rls_simulation_note")
395
+ })]
396
+ })
397
+ })
398
+ });
399
+ }
400
+ //#endregion
401
+ export { RlsSimulationBannerPanel, RlsSimulatorBarPanel };
402
+
403
+ //# sourceMappingURL=RlsSimulatorPanel-DOfmVgM3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RlsSimulatorPanel-DOfmVgM3.js","names":[],"sources":["../src/components/studio/RlsSimulatorPanel.tsx"],"sourcesContent":["/**\n * The \"Simulate access\" pill and banner themselves. Loaded on demand by the\n * wrappers in `RlsSimulatorBar.tsx`: only an administrator ever sees them, so\n * nobody else downloads them before the login screen paints.\n */\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport {\n Alert,\n Button,\n CircularProgress,\n cls,\n EyeIcon,\n IconButton,\n iconSize,\n KeyRoundIcon,\n Popover,\n ShieldIcon,\n Tab,\n Tabs,\n TextField,\n Typography,\n UserIcon\n} from \"@rebasepro/ui\";\nimport type { AccessSimulation, RebaseClient } from \"@rebasepro/types\";\nimport { useRlsSimulation, type RlsSimulationLabel } from \"../../contexts/RlsSimulationContext\";\nimport { useApiBase, useApiConfig } from \"../../hooks/ApiConfigContext\";\nimport { useRebaseClient } from \"../../hooks/useRebaseClient\";\nimport { useTranslation } from \"../../hooks/useTranslation\";\nimport { useCanSimulateAccess, type RlsSimulatorBarProps } from \"./RlsSimulatorBar\";\n\n/** A role `GET /admin/rls-simulator/options` offers. */\ninterface SimulatorRole {\n id: string;\n name: string;\n}\n\n/** A live service key `GET /admin/rls-simulator/options` offers. */\ninterface SimulatorApiKey {\n id: string;\n name: string;\n keyPrefix: string;\n}\n\ninterface SimulatorOptions {\n roles: SimulatorRole[];\n apiKeys: SimulatorApiKey[];\n}\n\n/** What `POST /admin/rls-simulator/preview` sums up. */\ninterface SimulationSummary {\n hiddenCollections: number;\n maskedColumns: number;\n}\n\ntype SimulatorTab = \"role\" | \"user\" | \"apiKey\";\n\n/** How many users one search of the picker lists. */\nconst USER_PAGE_SIZE = 20;\n\n/**\n * A request to the simulator's admin surface with the administrator's own\n * token — never through the simulated client, whose headers the admin routes\n * refuse.\n */\nfunction useSimulatorRequest() {\n const apiConfig = useApiConfig();\n const apiBase = useApiBase();\n return useCallback(async <T, >(path: string, init?: { method?: string; body?: unknown }): Promise<T> => {\n if (!apiBase) throw new Error(\"No API URL is configured.\");\n const token = await apiConfig?.getAuthToken?.();\n const response = await fetch(`${apiBase}/admin/rls-simulator${path}`, {\n method: init?.method ?? \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { Authorization: `Bearer ${token}` } : {})\n },\n ...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {})\n });\n const json: unknown = await response.json().catch(() => undefined);\n if (!response.ok) {\n const message = typeof json === \"object\" && json !== null && \"error\" in json\n && typeof json.error === \"object\" && json.error !== null && \"message\" in json.error\n && typeof json.error.message === \"string\"\n ? json.error.message\n : response.statusText;\n throw new Error(message);\n }\n return json as T;\n }, [apiBase, apiConfig]);\n}\n\n/** One choosable row of the popover's lists. */\nfunction ChoiceRow({ selected, onClick, icon, title, detail }: {\n selected: boolean;\n onClick: () => void;\n icon: React.ReactNode;\n title: string;\n detail?: string;\n}) {\n return (\n <Button\n variant=\"text\"\n size=\"small\"\n fullWidth\n onClick={onClick}\n className={cls(\"justify-start\", { \"bg-surface-active\": selected })}>\n {icon}\n <Typography variant=\"body2\" noWrap className=\"truncate\">{title}</Typography>\n {detail && <Typography variant=\"caption\" color=\"secondary\" noWrap className=\"truncate\">{detail}</Typography>}\n </Button>\n );\n}\n\n/**\n * \"Simulate access\": the app bar pill that runs every data view as a role, a\n * user or a service key, so an administrator sees exactly the rows and fields\n * that identity's row-level security lets it read.\n *\n * Shown only to an administrator, and only when the data views ride a client\n * that can derive a simulated one. The server checks the first again on every\n * request: anyone else sending the simulation headers gets a 403.\n *\n * @group Components\n */\nexport function RlsSimulatorBarPanel({ className, compact, side, align = \"end\", portalContainer }: RlsSimulatorBarProps) {\n const { t } = useTranslation();\n const controller = useRlsSimulation();\n const canSimulate = useCanSimulateAccess();\n const request = useSimulatorRequest();\n const client = useRebaseClient<RebaseClient>();\n\n const [open, setOpen] = useState(false);\n const [tab, setTab] = useState<SimulatorTab>(\"role\");\n const [options, setOptions] = useState<SimulatorOptions | null>(null);\n const [optionsError, setOptionsError] = useState<string | null>(null);\n const [role, setRole] = useState<string | null>(null);\n const [roleUserId, setRoleUserId] = useState(\"\");\n const [userSearch, setUserSearch] = useState(\"\");\n const [users, setUsers] = useState<Array<{ uid: string; label: string; detail?: string }>>([]);\n const [usersLoading, setUsersLoading] = useState(false);\n // A failed search is not a search that found no one.\n const [usersError, setUsersError] = useState<string | null>(null);\n\n useEffect(() => {\n if (!open || options) return;\n let cancelled = false;\n // An error from an earlier open is about that request, not this one.\n setOptionsError(null);\n request<SimulatorOptions>(\"/options\")\n .then((loaded) => {\n if (!cancelled) setOptions(loaded);\n })\n .catch((error: unknown) => {\n if (!cancelled) setOptionsError(error instanceof Error ? error.message : String(error));\n });\n return () => {\n cancelled = true;\n };\n }, [open, options, request]);\n\n const adminApi = client?.admin;\n useEffect(() => {\n if (!open || tab !== \"user\" || !adminApi) return;\n let cancelled = false;\n setUsersLoading(true);\n const timer = setTimeout(() => {\n adminApi.listUsersPaginated({ search: userSearch || undefined, limit: USER_PAGE_SIZE })\n .then(({ users: found }) => {\n if (cancelled) return;\n setUsersError(null);\n setUsers(found.filter(user => !user.disabled).map(user => ({\n uid: user.uid,\n label: user.email ?? user.displayName ?? user.uid,\n detail: user.roles?.length ? user.roles.join(\", \") : undefined\n })));\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setUsers([]);\n setUsersError(error instanceof Error ? error.message : String(error));\n })\n .finally(() => {\n if (!cancelled) setUsersLoading(false);\n });\n }, 250);\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [open, tab, userSearch, adminApi]);\n\n const start = (simulation: AccessSimulation, label: RlsSimulationLabel) => {\n controller.setSimulation(simulation, label);\n setOpen(false);\n };\n\n if (!canSimulate) return null;\n\n const active = controller.simulation !== null;\n\n const trigger = compact ? (\n <IconButton aria-label={t(\"rls_simulate_access\")} className={cls(\"relative\", className)}>\n <EyeIcon size={iconSize.small}/>\n {active && <span aria-hidden className=\"absolute top-1 right-1 w-2 h-2 rounded-full bg-amber-500\"/>}\n </IconButton>\n ) : (\n <Button\n variant=\"outlined\"\n size=\"small\"\n aria-label={t(\"rls_simulate_access\")}\n className={cls(\"rounded-full whitespace-nowrap\", className)}>\n {active\n ? <span aria-hidden className=\"w-2 h-2 rounded-full bg-amber-500\"/>\n : <EyeIcon size={iconSize.smallest}/>}\n {t(\"rls_simulate_access\")}\n </Button>\n );\n\n return (\n <Popover trigger={trigger} open={open} onOpenChange={setOpen} side={side} align={align} portalContainer={portalContainer}>\n <div className=\"flex flex-col gap-3 p-3 w-[22rem]\">\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"label\">{t(\"rls_simulate_access\")}</Typography>\n <Typography variant=\"caption\" color=\"secondary\">{t(\"rls_simulate_access_description\")}</Typography>\n </div>\n\n <Tabs value={tab} onValueChange={(value) => setTab(value === \"user\" || value === \"apiKey\" ? value : \"role\")}>\n <Tab value=\"role\">{t(\"rls_simulate_tab_role\")}</Tab>\n <Tab value=\"user\">{t(\"rls_simulate_tab_user\")}</Tab>\n <Tab value=\"apiKey\">{t(\"rls_simulate_tab_api_key\")}</Tab>\n </Tabs>\n\n {optionsError && (\n <Alert color=\"error\" size=\"small\" role={null}>\n {t(\"rls_simulate_options_error\", { message: optionsError })}\n </Alert>\n )}\n\n {!options && !optionsError && tab !== \"user\" && (\n <div className=\"flex justify-center p-2\"><CircularProgress size=\"small\"/></div>\n )}\n\n {tab === \"role\" && options && (\n <div className=\"flex flex-col gap-2\">\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {options.roles.map(entry => (\n <ChoiceRow\n key={entry.id}\n selected={role === entry.id}\n onClick={() => setRole(entry.id)}\n icon={<ShieldIcon size={iconSize.smallest}/>}\n title={entry.id}\n detail={entry.name !== entry.id ? entry.name : undefined}/>\n ))}\n </div>\n <TextField\n size=\"small\"\n value={roleUserId}\n placeholder={t(\"rls_simulate_user_id_optional\")}\n onChange={(event) => setRoleUserId(event.target.value)}/>\n <Button\n color=\"primary\"\n size=\"small\"\n disabled={!role}\n onClick={() => {\n if (!role) return;\n const userId = roleUserId.trim();\n start(\n { role, ...(userId ? { userId } : {}) },\n { kind: \"role\", role, ...(userId ? { userId } : {}) }\n );\n }}>\n {t(\"rls_simulate_apply\")}\n </Button>\n </div>\n )}\n\n {tab === \"user\" && (\n <div className=\"flex flex-col gap-2\">\n <TextField\n size=\"small\"\n value={userSearch}\n placeholder={t(\"rls_simulate_search_users\")}\n onChange={(event) => setUserSearch(event.target.value)}/>\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {usersLoading && users.length === 0 && (\n <div className=\"flex justify-center p-2\"><CircularProgress size=\"small\"/></div>\n )}\n {!usersLoading && usersError && (\n <Alert color=\"error\" size=\"small\" role={null}>\n {t(\"rls_simulate_users_error\", { message: usersError })}\n </Alert>\n )}\n {!usersLoading && !usersError && users.length === 0 && (\n <Typography variant=\"caption\" color=\"secondary\" className=\"p-2\">{t(\"rls_simulate_no_users\")}</Typography>\n )}\n {users.map(user => (\n <ChoiceRow\n key={user.uid}\n selected={controller.simulation?.userId === user.uid && !controller.simulation.role}\n onClick={() => start({ userId: user.uid }, { kind: \"user\", user: user.label })}\n icon={<UserIcon size={iconSize.smallest}/>}\n title={user.label}\n detail={user.detail}/>\n ))}\n </div>\n </div>\n )}\n\n {tab === \"apiKey\" && options && (\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {options.apiKeys.length === 0 && (\n <Typography variant=\"caption\" color=\"secondary\" className=\"p-2\">{t(\"rls_simulate_no_api_keys\")}</Typography>\n )}\n {options.apiKeys.map(key => (\n <ChoiceRow\n key={key.id}\n selected={controller.simulation?.apiKeyId === key.id}\n onClick={() => start({ apiKeyId: key.id }, { kind: \"apiKey\", name: key.name, keyPrefix: key.keyPrefix })}\n icon={<KeyRoundIcon size={iconSize.smallest}/>}\n title={key.name}\n detail={`${key.keyPrefix}…`}/>\n ))}\n </div>\n )}\n\n {active && (\n <Button\n variant=\"text\"\n size=\"small\"\n onClick={() => {\n controller.setSimulation(null);\n setOpen(false);\n }}>\n {t(\"rls_simulate_stop\")}\n </Button>\n )}\n </div>\n </Popover>\n );\n}\n\n/**\n * The banner every screen shows while access is simulated: who the data views\n * run as, how many collections show that identity nothing, how many fields its\n * roles cannot read — and the way out.\n *\n * @group Components\n */\nexport function RlsSimulationBannerPanel({ className }: { className?: string }) {\n const { t } = useTranslation();\n const { simulation, label, setSimulation } = useRlsSimulation();\n const request = useSimulatorRequest();\n const [summary, setSummary] = useState<SimulationSummary | null>(null);\n // Why the count failed: without it the banner said \"Counting…\" for as long\n // as the simulation ran.\n const [summaryError, setSummaryError] = useState<string | null>(null);\n\n useEffect(() => {\n setSummary(null);\n setSummaryError(null);\n if (!simulation) return;\n let cancelled = false;\n request<{ summary: SimulationSummary }>(\"/preview\", { method: \"POST\", body: simulation })\n .then(({ summary: loaded }) => {\n if (!cancelled) setSummary(loaded);\n })\n .catch((error: unknown) => {\n console.warn(\"Could not preview the simulated access\", error);\n if (!cancelled) setSummaryError(error instanceof Error ? error.message : String(error));\n });\n return () => {\n cancelled = true;\n };\n }, [simulation, request]);\n\n if (!simulation) return null;\n\n const title = (() => {\n if (label?.kind === \"apiKey\") return t(\"rls_simulation_viewing_as_api_key\", { name: label.name, prefix: label.keyPrefix });\n if (label?.kind === \"user\") return t(\"rls_simulation_viewing_as_user\", { user: label.user });\n if (simulation.role && simulation.userId) {\n return t(\"rls_simulation_viewing_as_role_user\", { role: simulation.role, user: simulation.userId });\n }\n if (simulation.role) return t(\"rls_simulation_viewing_as_role\", { role: simulation.role });\n return t(\"rls_simulation_viewing_as_user\", { user: simulation.userId ?? simulation.apiKeyId ?? \"\" });\n })();\n\n const counts = summary\n ? [\n summary.hiddenCollections === 1\n ? t(\"rls_simulation_hidden_collections_one\")\n : t(\"rls_simulation_hidden_collections\", { count: String(summary.hiddenCollections) }),\n summary.maskedColumns === 1\n ? t(\"rls_simulation_masked_columns_one\")\n : t(\"rls_simulation_masked_columns\", { count: String(summary.maskedColumns) })\n ].join(\", \")\n : summaryError !== null\n ? t(\"rls_simulation_count_failed\", { message: summaryError })\n : t(\"rls_simulation_counting\");\n\n return (\n <div role=\"status\" aria-live=\"polite\" className={className}>\n <Alert\n color=\"warning\"\n size=\"small\"\n role={null}\n action={<Button variant=\"text\" size=\"small\" onClick={() => setSimulation(null)}>{t(\"rls_simulate_stop\")}</Button>}>\n <div className=\"flex flex-col gap-0.5\">\n <Typography variant=\"label\">{`${title} — ${counts}`}</Typography>\n <Typography variant=\"body2\" color=\"secondary\">{t(\"rls_simulation_note\")}</Typography>\n </div>\n </Alert>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -4,13 +4,14 @@
4
4
  * Turns a collection into the shape the entity form renders: an ordered list of
5
5
  * sections holding grid-spanned fields, plus the metadata rail beside them.
6
6
  *
7
- * The reason this is a pure function rather than logic inside the form is that
8
- * the *defaults* are the interesting part. A collection that never writes an
9
- * `admin.form` block still has to get a two-column layout out of this once it is
10
- * long enough to need one — the flat run of full-width fields it produced before
11
- * was the single biggest cost in the form, and no amount of config would have fixed it for collections nobody
12
- * hand-tunes. Deriving it here means it is testable in isolation, which matters
13
- * because "what span does a date get" is exactly the kind of rule that rots.
7
+ * A field takes the full row unless its property sets `admin.span`, with one
8
+ * exception: two neighbouring fields whose editors are both small and of the
9
+ * same kind — two numbers, two switches, two dates, two short selects — share a
10
+ * row. Nothing wider is ever guessed. Pairing by type alone put Namespace beside
11
+ * Custom Domain and Git Branch beside Git Repository URL, values that sat
12
+ * together only because both were strings; a string's editor says nothing about
13
+ * how long its value is, so a free-text field is never paired. Nothing here
14
+ * looks at which property is the record's title either.
14
15
  */
15
16
  import type { AdminCollection, PropertySpan } from "@rebasepro/cms-types";
16
17
  import type { Property } from "@rebasepro/types";
@@ -21,10 +22,7 @@ export interface ResolvedFormField {
21
22
  span: PropertySpan;
22
23
  /** True for an `additionalFields` entry rather than a property. */
23
24
  additional: boolean;
24
- /**
25
- * The span was written on the property rather than derived from its type.
26
- * Row filling leaves these alone: an author who wrote a width meant it.
27
- */
25
+ /** The span was written on the property. Pairing leaves these alone. */
28
26
  spanExplicit?: boolean;
29
27
  }
30
28
  export interface ResolvedFormSection {
@@ -65,14 +63,6 @@ export interface ResolveFormLayoutParams<M extends Record<string, unknown>> {
65
63
  */
66
64
  status: "new" | "existing" | "copy";
67
65
  }
68
- /**
69
- * The span a property gets when nothing is declared.
70
- *
71
- * Deliberately coarse: three buckets, chosen by how much room the *editor*
72
- * needs, not by how important the field is. Importance is what `admin.form`
73
- * sections are for.
74
- */
75
- export declare function deriveSpan(property: Property, isTitleProperty: boolean): PropertySpan;
76
66
  /**
77
67
  * Can the user still type this id?
78
68
  *
@@ -92,17 +82,29 @@ export declare function isIdPropertyEditable(property: Property, status: "new" |
92
82
  * screen.
93
83
  */
94
84
  export declare function isAuditTimestamp(property: Property | undefined): boolean;
85
+ /** What a field's editor is, for the purpose of sitting beside another one. */
86
+ type PairKind = "number" | "switch" | "date" | "date_time" | "select";
87
+ /**
88
+ * The kind of compact editor this property renders, or `undefined` when its
89
+ * editor needs the row — or when nothing about it says how long its value is.
90
+ *
91
+ * Deliberately narrow. A plain string is never compact: "Namespace" and "Git
92
+ * Branch" are both strings and nothing in the schema tells a 6-character branch
93
+ * from a 60-character URL. Neither is a relation, whose preview is the related
94
+ * record's own text. Only editors whose width is bounded by what they are —
95
+ * a number, a switch, a date picker, a select over short options — qualify.
96
+ */
97
+ export declare function pairKind(property: Property): PairKind | undefined;
95
98
  /**
96
- * Close the trailing gap in each row.
99
+ * Put neighbouring fields of the same compact kind two to a row.
97
100
  *
98
- * Without this a section of half-width fields with an odd count leaves a hole —
99
- * `name | sku` then `brand | ␣␣` — a field stranded beside half a row of
100
- * nothing, which reads as a mistake rather than a layout. The remainder is
101
- * spread across the row's derived fields rather than dumped on the last one, so
102
- * a lone half-width field becomes full width and a pair grows evenly. Fields
103
- * with an explicit span are never resized.
101
+ * Greedy and in declaration order: a run of three numbers is a pair and a full
102
+ * row, never a pair with the third pulled out of order. A pair only starts at
103
+ * the beginning of a row — the grid flows, so a pair placed after an author's
104
+ * `span: 2` would split across two rows. Fields with a span the author wrote are
105
+ * left as written, and nothing is widened to close a gap they leave.
104
106
  */
105
- export declare function fillRows(fields: ResolvedFormField[]): ResolvedFormField[];
107
+ export declare function pairCompactFields(fields: ResolvedFormField[], propertyOf: (key: string) => Property | undefined): ResolvedFormField[];
106
108
  /**
107
109
  * Resolve the layout for a collection's generated form.
108
110
  *
@@ -111,3 +113,4 @@ export declare function fillRows(fields: ResolvedFormField[]): ResolvedFormField
111
113
  * in the panel.
112
114
  */
113
115
  export declare function resolveFormLayout<M extends Record<string, unknown>>({ collection, fieldKeys, status }: ResolveFormLayoutParams<M>): ResolvedFormLayout;
116
+ export {};
@@ -1,2 +1,11 @@
1
1
  import React from "react";
2
+ /**
3
+ * A language's name in that language. One Rebase does not ship is named by
4
+ * the browser (`nl` → "Nederlands"), and by its code when the browser cannot.
5
+ */
6
+ export declare function languageName(locale: string): string;
7
+ /**
8
+ * The language menu: every language the admin can be shown in, which is the
9
+ * shipped ones plus any the app adds through `<Rebase translations>`.
10
+ */
2
11
  export declare function LanguageToggle(): React.JSX.Element;
@@ -2,3 +2,10 @@ export { LoginView } from "./LoginView.js";
2
2
  export type { LoginViewProps } from "./LoginView.js";
3
3
  export { readEmailLinkAction } from "./email-link.js";
4
4
  export type { EmailLinkAction } from "./email-link.js";
5
+ export { startOAuthRedirect, consumeOAuthCallback } from "./oauth-redirect-flow.js";
6
+ export type { OAuthRedirectRequest, OAuthCallbackResult } from "./oauth-redirect-flow.js";
7
+ export { readMfaRequired, canAnswerMfa, classifyMfaRefusal } from "./mfa-required.js";
8
+ export type { PendingMfaSignIn } from "./mfa-required.js";
9
+ export { authErrorMessage } from "./auth-error-message.js";
10
+ export { appAddressOfEmailLink } from "./email-link.js";
11
+ export { emailChangeErrorMessage } from "../email-change-message.js";
@@ -14,3 +14,4 @@ export * from "./LoginView/index.js";
14
14
  export * from "./RebaseAuth.js";
15
15
  export * from "./SchemaDriftBanner.js";
16
16
  export * from "./ConnectionLostBanner.js";
17
+ export * from "./studio/RlsSimulatorBar.js";
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ export interface RlsSimulatorBarProps {
3
+ className?: string;
4
+ /** An icon button instead of the pill — for a collapsed drawer rail. */
5
+ compact?: boolean;
6
+ side?: "top" | "right" | "bottom" | "left";
7
+ align?: "start" | "center" | "end";
8
+ /** Where the popover mounts — inside a floating drawer, so hovering it keeps the drawer open. */
9
+ portalContainer?: HTMLElement | null;
10
+ }
11
+ /**
12
+ * Whether the signed-in person may simulate access here: an administrator,
13
+ * with data views that ride a client able to derive a simulated one.
14
+ *
15
+ * @group Hooks and utilities
16
+ */
17
+ export declare function useCanSimulateAccess(): boolean;
18
+ /**
19
+ * "Simulate access": the app bar pill that runs every data view as a role, a
20
+ * user or a service key, so an administrator sees exactly the rows and fields
21
+ * that identity's row-level security lets it read.
22
+ *
23
+ * Shown only to an administrator, and only when the data views ride a client
24
+ * that can derive a simulated one. The server checks the first again on every
25
+ * request: anyone else sending the simulation headers gets a 403.
26
+ *
27
+ * @group Components
28
+ */
29
+ export declare function RlsSimulatorBar(props: RlsSimulatorBarProps): React.JSX.Element | null;
30
+ /**
31
+ * The banner every screen shows while access is simulated: who the data views
32
+ * run as, how many collections show that identity nothing, how many fields its
33
+ * roles cannot read — and the way out.
34
+ *
35
+ * @group Components
36
+ */
37
+ export declare function RlsSimulationBanner({ className }: {
38
+ className?: string;
39
+ }): React.JSX.Element | null;