@rebasepro/studio 0.17.3 → 0.18.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 (81) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +12 -4
  3. package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
  4. package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
  5. package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
  6. package/dist/BackupsView-DVkKzaPR.js.map +1 -0
  7. package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
  8. package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
  9. package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
  10. package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
  11. package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
  12. package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
  13. package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
  14. package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
  15. package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
  16. package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
  17. package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
  18. package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
  19. package/dist/components/RLSEditor/saveRules.d.ts +38 -0
  20. package/dist/components/RebaseStudio.d.ts +1 -1
  21. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
  22. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
  23. package/dist/components/load-failure-view.d.ts +17 -0
  24. package/dist/components/load-failure.d.ts +47 -0
  25. package/dist/index.es.js +56 -36
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/load-failure-C6OMJSGC.js +31 -0
  28. package/dist/load-failure-C6OMJSGC.js.map +1 -0
  29. package/dist/load-failure-view-CBTB_h-h.js +48 -0
  30. package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
  31. package/package.json +36 -21
  32. package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
  33. package/dist/BackupsView-DNS6LdVg.js.map +0 -1
  34. package/dist/BranchesView-CM3B-ER3.js.map +0 -1
  35. package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
  36. package/dist/RLSEditor-CBX4uD66.js.map +0 -1
  37. package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
  38. package/dist/StorageView-BGDeC5GI.js.map +0 -1
  39. package/dist/components/StorageView/storage-failure.d.ts +0 -34
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
  41. package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
  42. package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
  43. package/src/components/ApiExplorer/parseSpec.ts +0 -104
  44. package/src/components/ApiExplorer/types.ts +0 -84
  45. package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
  46. package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
  47. package/src/components/ApiKeys/permissions.ts +0 -145
  48. package/src/components/AuthSimulationSelector.tsx +0 -72
  49. package/src/components/Backups/BackupsView.tsx +0 -185
  50. package/src/components/Branches/BranchesView.tsx +0 -386
  51. package/src/components/CronJobs/CronJobsView.tsx +0 -413
  52. package/src/components/JSEditor/JSEditor.tsx +0 -1100
  53. package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
  54. package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
  55. package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
  56. package/src/components/LogsExplorer/useLogTail.ts +0 -356
  57. package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
  58. package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
  59. package/src/components/RLSEditor/index.ts +0 -1
  60. package/src/components/RLSEditor/policy-presets.ts +0 -176
  61. package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
  62. package/src/components/RebaseStudio.tsx +0 -174
  63. package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
  64. package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
  65. package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
  66. package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
  67. package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
  68. package/src/components/SQLEditor/sql_editor_types.ts +0 -18
  69. package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
  70. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
  71. package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
  72. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
  73. package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
  74. package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
  75. package/src/components/StorageView/StorageView.tsx +0 -1588
  76. package/src/components/StorageView/storage-failure.ts +0 -63
  77. package/src/components/StudioHomePage.tsx +0 -430
  78. package/src/index.ts +0 -31
  79. package/src/utils/parseSpec.test.ts +0 -309
  80. package/src/utils/sql_utils.test.ts +0 -265
  81. package/src/utils/sql_utils.ts +0 -291
@@ -1,399 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from "react";
2
- import {
3
- Button,
4
- cls,
5
- defaultBorderMixin,
6
- Dialog,
7
- DialogActions,
8
- DialogContent,
9
- DialogTitle,
10
- HelpCircleIcon,
11
- IconButton,
12
- iconSize,
13
- MultiSelect,
14
- MultiSelectItem,
15
- Paper,
16
- Select,
17
- SelectItem,
18
- TextField,
19
- Typography
20
- } from "@rebasepro/ui";
21
- import { useTranslation } from "@rebasepro/app";
22
- import { RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL } from "@rebasepro/types";
23
- import {
24
- COMMAND_OPTIONS,
25
- POLICY_PRESETS,
26
- roleOptionsFor,
27
- type PolicyCommand
28
- } from "./policy-presets";
29
- import { MonacoEditor } from "../SQLEditor/MonacoEditor";
30
- import type { PostgresPolicy } from "./RLSEditor";
31
-
32
- export interface PolicyEditorProps {
33
- policy?: PostgresPolicy;
34
- schema: string;
35
- table: string;
36
- /**
37
- * Native PostgreSQL roles this database actually has, for the `TO` list.
38
- *
39
- * Supplied by the caller from `fetchAvailableRoles()`. Merged with the
40
- * always-offered defaults by `roleOptionsFor`, never used in place of them:
41
- * `public` is a keyword and never comes back from that query.
42
- */
43
- roleOptions?: readonly string[];
44
- onSave: (policyData: Partial<PostgresPolicy>) => void;
45
- onCancel: () => void;
46
- }
47
-
48
- export const PolicyEditor = ({
49
- policy,
50
- schema,
51
- table,
52
- roleOptions,
53
- onSave,
54
- onCancel
55
- }: PolicyEditorProps) => {
56
-
57
- const { t } = useTranslation();
58
- const [name, setName] = useState("");
59
- const [helpOpen, setHelpOpen] = useState(false);
60
- const [behavior, setBehavior] = useState<"PERMISSIVE" | "RESTRICTIVE">("PERMISSIVE");
61
- const [command, setCommand] = useState<PolicyCommand>("ALL");
62
- const [roles, setRoles] = useState<string[]>(["public"]);
63
- const [usingExpr, setUsingExpr] = useState<string>("");
64
- const [checkExpr, setCheckExpr] = useState<string>("");
65
-
66
- const [selectedPreset, setSelectedPreset] = useState<string>("");
67
-
68
- const availableRoles = useMemo(
69
- () => roleOptionsFor(roleOptions, policy?.roles),
70
- [roleOptions, policy]
71
- );
72
-
73
- useEffect(() => {
74
- if (policy) {
75
- setName(policy.policyname || "");
76
- setBehavior(policy.permissive || "PERMISSIVE");
77
- setCommand((policy.cmd as PolicyCommand) || "ALL");
78
-
79
- const initialRoles = policy.roles
80
- ? (Array.isArray(policy.roles) ? policy.roles : [policy.roles])
81
- : ["public"];
82
- setRoles(initialRoles as string[]);
83
-
84
- setUsingExpr(policy.qual || "");
85
- setCheckExpr(policy.with_check || "");
86
- } else {
87
- setName("");
88
- setBehavior("PERMISSIVE");
89
- setCommand("ALL");
90
- setRoles(["public"]);
91
- setUsingExpr("");
92
- setCheckExpr("");
93
- setSelectedPreset("");
94
- }
95
- }, [policy]);
96
-
97
- const handlePresetChange = (presetId: string) => {
98
- const preset = POLICY_PRESETS.find(p => p.id === presetId);
99
- if (!preset) return;
100
-
101
- setSelectedPreset(presetId);
102
- setName(preset.policyname);
103
- setBehavior(preset.permissive);
104
- setCommand(preset.cmd);
105
- setRoles(preset.roles);
106
- setUsingExpr(preset.qual);
107
- setCheckExpr(preset.with_check);
108
- };
109
-
110
- const showCheck = command === "ALL" || command === "INSERT" || command === "UPDATE";
111
-
112
- const handleSave = () => {
113
- onSave({
114
- policyname: name,
115
- permissive: behavior,
116
- cmd: command,
117
- roles: roles,
118
- qual: usingExpr,
119
- with_check: showCheck ? checkExpr : null
120
- });
121
- };
122
-
123
- return (
124
- <>
125
- {/* Inline header: this editor renders as a full pane, not inside a Dialog,
126
- so DialogTitle (Radix-bound) cannot be used here. */}
127
- <Typography variant="h6" gutterBottom className="mt-8 mx-8 flex justify-between items-center w-full">
128
- <div>
129
- <div>{policy ? t("studio_policy_edit") : t("studio_policy_create")}</div>
130
- <div className="text-sm font-normal text-text-secondary dark:text-text-secondary-dark tracking-wide mt-1">
131
- {t("studio_policy_defining_rules")} <span className="font-mono text-primary bg-primary-bg dark:bg-primary-bg-dark px-1 py-0.5 rounded">{schema}.{table}</span>
132
- </div>
133
- </div>
134
- <IconButton size="small" onClick={() => setHelpOpen(true)}>
135
- <HelpCircleIcon size={iconSize.smallest}/>
136
- </IconButton>
137
- </Typography>
138
-
139
- <DialogContent className="p-4 md:p-6 border-t dark:border-surface-950 bg-surface-50 dark:bg-surface-950" includeMargin={false}>
140
- <div className="max-w-4xl mx-auto">
141
- <Paper className={cls("p-4 md:p-6 flex flex-col gap-6 bg-white dark:bg-surface-900 border-none sm:border-solid rounded-none sm:rounded-xl", defaultBorderMixin)}>
142
-
143
- {/* Presets - only for new policies */}
144
- {!policy && (
145
- <div className="flex flex-col gap-1.5 bg-primary/5 dark:bg-primary-bg-dark/20 p-3 sm:p-4 rounded-lg border border-primary/10 dark:border-primary/20">
146
- <Typography variant="caption" className="text-primary dark:text-primary-light uppercase tracking-wider">{t("studio_policy_template")}</Typography>
147
- <Select
148
- size="small"
149
- value={selectedPreset}
150
- onValueChange={handlePresetChange}
151
- position="item-aligned"
152
- placeholder={t("studio_policy_select_template")}
153
- className="bg-white dark:bg-surface-950"
154
- >
155
- {POLICY_PRESETS.map((preset) => (
156
- <SelectItem key={preset.id} value={preset.id}>
157
- <div className="flex flex-col text-left">
158
- <span className="text-sm">{preset.label}</span>
159
- <span className="text-xs text-text-secondary dark:text-text-secondary-dark">{preset.description}</span>
160
- </div>
161
- </SelectItem>
162
- ))}
163
- </Select>
164
- </div>
165
- )}
166
-
167
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
168
- {/* Policy Name */}
169
- <div className="flex flex-col gap-1.5">
170
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_name")}</Typography>
171
- <TextField
172
- value={name}
173
- aria-label={t("studio_policy_name")}
174
- onChange={(e) => setName(e.target.value)}
175
- placeholder={t("studio_policy_name_placeholder")}
176
- className="w-full"
177
- />
178
- </div>
179
-
180
- {/* Behavior */}
181
- <div className="flex flex-col gap-1.5">
182
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
183
- {t("studio_policy_behavior")} <code className="text-[10px] bg-surface-200 dark:bg-surface-950 text-text-secondary dark:text-text-secondary-dark px-1 py-0.5 rounded ml-1">AS</code>
184
- </Typography>
185
- <Select
186
- value={behavior}
187
- onValueChange={(val) => setBehavior(val as "PERMISSIVE" | "RESTRICTIVE")}
188
- position="item-aligned"
189
- >
190
- <SelectItem value="PERMISSIVE">
191
- <div className="flex flex-col text-left">
192
- <span className="">{t("studio_policy_permissive")}</span>
193
- <span className="text-xs text-text-secondary dark:text-text-secondary-dark">{t("studio_policy_permissive_desc")}</span>
194
- </div>
195
- </SelectItem>
196
- <SelectItem value="RESTRICTIVE">
197
- <div className="flex flex-col text-left">
198
- <span className="">{t("studio_policy_restrictive")}</span>
199
- <span className="text-xs text-text-secondary dark:text-text-secondary-dark">{t("studio_policy_restrictive_desc")}</span>
200
- </div>
201
- </SelectItem>
202
- </Select>
203
- </div>
204
- </div>
205
-
206
- {/* Command */}
207
- <div className="flex flex-col gap-1.5">
208
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
209
- {t("studio_policy_command")} <code className="text-[10px] bg-surface-200 dark:bg-surface-950 text-text-secondary dark:text-text-secondary-dark px-1 py-0.5 rounded ml-1">FOR</code>
210
- </Typography>
211
- <div className="flex flex-wrap gap-1.5">
212
- {COMMAND_OPTIONS.map(cmd => (
213
- <Button
214
- key={cmd}
215
- size="small"
216
- variant={command === cmd ? "filled" : "text"}
217
- color="neutral"
218
- onClick={() => setCommand(cmd)}
219
- className="min-w-[80px]"
220
- >
221
- {cmd}
222
- </Button>
223
- ))}
224
- </div>
225
- </div>
226
-
227
- {/* Roles */}
228
- <div className="flex flex-col gap-1.5">
229
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
230
- {t("studio_policy_target_roles")} <code className="text-[10px] bg-surface-200 dark:bg-surface-950 text-text-secondary dark:text-text-secondary-dark px-1 py-0.5 rounded ml-1">TO</code>
231
- </Typography>
232
- <MultiSelect
233
- size="small"
234
- value={roles}
235
- onValueChange={setRoles}
236
- placeholder={t("studio_policy_roles_placeholder")}
237
- >
238
- {availableRoles.map(role => (
239
- <MultiSelectItem key={role} value={role}>
240
- {role}
241
- </MultiSelectItem>
242
- ))}
243
- </MultiSelect>
244
- </div>
245
-
246
- </Paper>
247
-
248
- <div className="mt-8 flex flex-col gap-4">
249
- {/* USING Expression */}
250
- {command !== "INSERT" && (
251
- <div className="flex flex-col gap-1.5">
252
- <div className="flex flex-col gap-0.5">
253
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_using_expr")}</Typography>
254
- <Typography variant="caption" className="text-text-secondary opacity-70">{t("studio_policy_using_expr_desc")}</Typography>
255
- </div>
256
- <div className={cls("h-32 border rounded-md overflow-hidden bg-white dark:bg-[#1e1e1e]", defaultBorderMixin)}>
257
- <MonacoEditor
258
- value={usingExpr}
259
- onChange={(v) => setUsingExpr(v || "")}
260
- readOnly={false}
261
- autoFocus={true}
262
- />
263
- </div>
264
- </div>
265
- )}
266
-
267
- {/* WITH CHECK Expression */}
268
- {showCheck && (
269
- <div className="flex flex-col gap-1.5 mt-2">
270
- <div className="flex flex-col gap-0.5">
271
- <Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_check_expr")}</Typography>
272
- <Typography variant="caption" className="text-text-secondary opacity-70">{t("studio_policy_check_expr_desc")}</Typography>
273
- </div>
274
- <div className={cls("h-32 border rounded-md overflow-hidden bg-white dark:bg-[#1e1e1e]", defaultBorderMixin)}>
275
- <MonacoEditor
276
- value={checkExpr}
277
- onChange={(v) => setCheckExpr(v || "")}
278
- readOnly={false}
279
- autoFocus={command === "INSERT"}
280
- />
281
- </div>
282
- </div>
283
- )}
284
- </div>
285
-
286
- </div>
287
- </DialogContent>
288
-
289
- <DialogActions>
290
- <Button size="small" variant="text" color="neutral" onClick={onCancel}>
291
- {t("studio_policy_cancel")}
292
- </Button>
293
- <Button size="small" variant="filled" color="primary" onClick={handleSave} disabled={!name}>
294
- {t("studio_policy_save")}
295
- </Button>
296
- </DialogActions>
297
-
298
- <Dialog open={helpOpen} onOpenChange={setHelpOpen} maxWidth="3xl">
299
- <DialogTitle hidden>Row-Level Security Help</DialogTitle>
300
- <DialogContent className="p-4 sm:p-6 lg:p-8 flex flex-col gap-6">
301
- <div>
302
- <Typography variant="h5" className="mb-2">{t("studio_policy_help_title")}</Typography>
303
- <Typography className="text-text-secondary dark:text-text-secondary-dark">
304
- {t("studio_policy_help_intro")}
305
- </Typography>
306
- </div>
307
-
308
- <div className="flex flex-col gap-4">
309
- <Paper className={cls("p-4 sm:p-5 flex flex-col gap-1", defaultBorderMixin)}>
310
- <Typography variant="subtitle2" className="text-primary dark:text-primary-light font-medium">{t("studio_policy_help_step1_title")}</Typography>
311
- <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark">
312
- {t("studio_policy_help_step1_desc")}
313
- </Typography>
314
- </Paper>
315
-
316
- <Paper className={cls("p-4 sm:p-5 flex flex-col gap-1", defaultBorderMixin)}>
317
- <Typography variant="subtitle2" className="text-primary dark:text-primary-light font-medium">{t("studio_policy_help_step2_title")}</Typography>
318
- <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark mb-1">
319
- {t("studio_policy_help_step2_desc")}
320
- </Typography>
321
- <ul className="list-disc pl-5 space-y-1 text-sm text-text-secondary dark:text-text-secondary-dark">
322
- <li><strong>public</strong>: {t("studio_policy_help_role_public")}</li>
323
- <li><strong>authenticated</strong>: {t("studio_policy_help_role_authenticated")}</li>
324
- <li><strong>anon</strong>: {t("studio_policy_help_role_anon")}</li>
325
- </ul>
326
- </Paper>
327
-
328
- <Paper className={cls("p-4 sm:p-5 flex flex-col gap-1", defaultBorderMixin)}>
329
- <Typography variant="subtitle2" className="text-primary dark:text-primary-light font-medium">{t("studio_policy_help_step3_title")}</Typography>
330
- <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark mb-1">
331
- {t("studio_policy_help_step3_desc")}
332
- </Typography>
333
- <div className={cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin)}>
334
- Example: {RLS_UID_SQL} = uid
335
- </div>
336
- <Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark">
337
- {t("studio_policy_help_step3_example")}
338
- </Typography>
339
- </Paper>
340
-
341
- <Paper className={cls("p-4 sm:p-5 flex flex-col gap-1", defaultBorderMixin)}>
342
- <Typography variant="subtitle2" className="text-primary dark:text-primary-light font-medium">{t("studio_policy_help_step4_title")}</Typography>
343
- <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark mb-1">
344
- {t("studio_policy_help_step4_desc")}
345
- </Typography>
346
- <div className={cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin)}>
347
- Example: {RLS_UID_SQL} = uid
348
- </div>
349
- <Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark">
350
- {t("studio_policy_help_step4_example")}
351
- </Typography>
352
- </Paper>
353
-
354
- <Paper className={cls("p-4 sm:p-5 flex flex-col gap-2 bg-primary/5 dark:bg-primary-bg-dark/10", defaultBorderMixin)}>
355
- <Typography variant="subtitle2" className="text-primary dark:text-primary-light font-medium">{t("studio_policy_help_auth_vars_title")}</Typography>
356
- <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark">
357
- {t("studio_policy_help_auth_vars_desc")}
358
- </Typography>
359
- <ul className="list-disc pl-5 space-y-2 text-sm text-text-secondary dark:text-text-secondary-dark font-normal">
360
- <li>
361
- <code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_UID_SQL}</code>
362
- <span className="block mt-0.5">Returns the current user&apos;s ID as text. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">{RLS_UID_SQL} = uid</code></span>
363
- </li>
364
- <li>
365
- <code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_JWT_SQL}</code>
366
- <span className="block mt-0.5">Returns the full JWT payload as JSONB so you can check custom claims. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">{RLS_JWT_SQL} -&gt;&gt; &apos;email&apos; = &apos;admin@example.com&apos;</code></span>
367
- </li>
368
- <li>
369
- <code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_ROLES_SQL}</code>
370
- <span className="block mt-0.5">Returns the user&apos;s role IDs as a comma-separated string. Best used with: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">string_to_array({RLS_ROLES_SQL}, &apos;,&apos;) @&gt; ARRAY[&apos;admin&apos;]</code></span>
371
- </li>
372
- </ul>
373
- </Paper>
374
- </div>
375
-
376
- <div className={cls("mt-2 flex flex-col sm:flex-row justify-between items-start sm:items-center bg-primary/5 dark:bg-primary-bg-dark/10 p-4 rounded-xl border border-primary/10 dark:border-primary/20", defaultBorderMixin)}>
377
- <Typography variant="body2" className="text-primary dark:text-primary-light mb-4 sm:mb-0 max-w-md">
378
- {t("studio_policy_help_docs_cta")}
379
- </Typography>
380
- <Button
381
- component="a"
382
- href="https://www.postgresql.org/docs/current/sql-createpolicy.html"
383
- target="_blank"
384
- variant="outlined"
385
- color="primary"
386
- size="small"
387
- className="whitespace-nowrap flex-shrink-0"
388
- >
389
- {t("studio_policy_help_read_docs")}
390
- </Button>
391
- </div>
392
- </DialogContent>
393
- <DialogActions className="p-4 sm:px-6 sm:pb-6 pt-0 border-t-0">
394
- <Button onClick={() => setHelpOpen(false)} variant="filled" color="primary">{t("studio_policy_help_got_it")}</Button>
395
- </DialogActions>
396
- </Dialog>
397
- </>
398
- );
399
- };