@rebasepro/studio 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
- package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
- package/dist/ApiKeysView-B66TdzhH.js +1738 -0
- package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
- package/dist/BackupsView-DmSMzAbT.js +368 -0
- package/dist/BackupsView-DmSMzAbT.js.map +1 -0
- package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
- package/dist/BranchesView-DP1a87E-.js.map +1 -0
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
- package/dist/CronJobsView-TDErHTuK.js.map +1 -0
- package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
- package/dist/JSEditor-B59pwg4c.js.map +1 -0
- package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
- package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
- package/dist/SQLEditor-1An7w0j6.js.map +1 -0
- package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
- package/dist/StorageView-CbsHuCBf.js.map +1 -0
- package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
- package/dist/components/ApiKeys/scope-words.d.ts +37 -0
- package/dist/components/ApiKeys/scopes.d.ts +113 -0
- package/dist/components/AuthSimulationSelector.d.ts +5 -1
- package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
- package/dist/components/RLSEditor/policyListing.d.ts +44 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
- package/dist/components/load-failure.d.ts +1 -1
- package/dist/components/useRunAsUsers.d.ts +25 -0
- package/dist/index.es.js +11 -11
- package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
- package/dist/load-failure-Bd8lFIO7.js.map +1 -0
- package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
- package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +126 -36
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
- package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
- package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
- package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
- package/dist/BackupsView-DYTAD6aW.js +0 -253
- package/dist/BackupsView-DYTAD6aW.js.map +0 -1
- package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
- package/dist/components/ApiKeys/permissions.d.ts +0 -66
- package/dist/load-failure-C6OMJSGC.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MonacoEditor-CUxy5_k5.js","names":[],"sources":["../src/components/SQLEditor/MonacoEditor.tsx"],"sourcesContent":["import React, { useImperativeHandle, useRef } from \"react\";\nimport Editor, { Monaco, OnMount } from \"@monaco-editor/react\";\nimport type { editor, Position, IRange } from \"monaco-editor\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport { useModeController } from \"@rebasepro/app\";\n\n/**\n * What a host can ask of the editor outside its own key bindings.\n *\n * The toolbar's buttons need the selection that Cmd+Enter already reads: a\n * button that runs the whole buffer while the shortcut beside it runs only the\n * highlighted statement is two answers to one question.\n */\nexport interface MonacoEditorHandle {\n /** The selected text, trimmed — `undefined` when nothing is selected. */\n getSelectedText: () => string | undefined;\n}\n\n/** The editor's selection, trimmed — `undefined` when there is none. */\nfunction selectedTextOf(instance: editor.IStandaloneCodeEditor): string | undefined {\n const selection = instance.getSelection();\n if (!selection || selection.isEmpty()) return undefined;\n return instance.getModel()?.getValueInRange(selection)?.trim() || undefined;\n}\n\nexport type MonacoEditorProps = {\n ref?: React.Ref<MonacoEditorHandle>;\n value: string;\n onChange: (value: string | undefined) => void;\n onRun?: (selectedText?: string) => void;\n className?: string;\n readOnly?: boolean;\n autoFocus?: boolean;\n schemas?: Record<string, { schemaName: string, tableName: string, columns: { name: string, dataType: string }[] }[]>;\n};\n\nexport const MonacoEditor = ({\n ref,\n value,\n onChange,\n onRun,\n className,\n readOnly = false,\n autoFocus = true,\n schemas\n}: MonacoEditorProps) => {\n const { mode } = useModeController();\n const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);\n const monacoRef = useRef<Monaco | null>(null);\n const onRunRef = useRef(onRun);\n onRunRef.current = onRun;\n\n const schemasRef = useRef(schemas);\n schemasRef.current = schemas;\n\n useImperativeHandle(ref, () => ({\n getSelectedText: () => (editorRef.current ? selectedTextOf(editorRef.current) : undefined)\n }), []);\n\n const handleEditorOnMount: OnMount = (editor, monaco) => {\n editorRef.current = editor;\n monacoRef.current = monaco;\n\n editor.addAction({\n id: \"run-query\",\n label: \"Run Query\",\n keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],\n contextMenuGroupId: \"operation\",\n contextMenuOrder: 0,\n run: () => {\n onRunRef.current?.(selectedTextOf(editor));\n }\n });\n\n // Register custom autocomplete for tables, columns, and SQL keywords\n const sqlKeywords = [\n \"SELECT\", \"FROM\", \"WHERE\", \"INSERT INTO\", \"VALUES\", \"UPDATE\", \"SET\",\n \"DELETE FROM\", \"JOIN\", \"INNER JOIN\", \"LEFT JOIN\", \"RIGHT JOIN\", \"FULL JOIN\",\n \"ON\", \"GROUP BY\", \"HAVING\", \"ORDER BY\", \"ASC\", \"DESC\", \"LIMIT\", \"OFFSET\",\n \"AS\", \"AND\", \"OR\", \"NOT\", \"IN\", \"BETWEEN\", \"LIKE\", \"IS NULL\", \"IS NOT NULL\",\n \"CREATE TABLE\", \"ALTER TABLE\", \"DROP TABLE\", \"PRIMARY KEY\", \"FOREIGN KEY\",\n \"COUNT\", \"SUM\", \"AVG\", \"MIN\", \"MAX\", \"DISTINCT\", \"UNION\", \"ALL\", \"EXISTS\",\n \"CASE\", \"WHEN\", \"THEN\", \"ELSE\", \"END\", \"CAST\", \"COALESCE\"\n ];\n\n monaco.languages.registerCompletionItemProvider(\"pgsql\", {\n triggerCharacters: [\".\", \" \", \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\", \"P\", \"Q\", \"R\", \"S\", \"T\", \"U\", \"V\", \"W\", \"X\", \"Y\", \"Z\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\", \"o\", \"p\", \"q\", \"r\", \"s\", \"t\", \"u\", \"v\", \"w\", \"x\", \"y\", \"z\"],\n provideCompletionItems: (model: editor.ITextModel, position: Position) => {\n const word = model.getWordUntilPosition(position);\n const range = {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: word.startColumn,\n endColumn: word.endColumn\n };\n\n const textUntilPosition = model.getValueInRange({\n startLineNumber: position.lineNumber,\n startColumn: 1,\n endLineNumber: position.lineNumber,\n endColumn: position.column\n });\n\n // Check if user is typing `tableName.`\n const match = textUntilPosition.match(/([a-zA-Z0-9_]+)\\.$/);\n const tableNameMatch = match ? match[1] : null;\n\n const suggestions: { label: string; kind: number; insertText: string; range: IRange; detail: string; sortText: string }[] = [];\n const allSchemas = schemasRef.current || {};\n\n if (tableNameMatch) {\n // Provide columns for the matched table\n for (const [, tables] of Object.entries(allSchemas)) {\n const table = tables.find(t => t.tableName === tableNameMatch);\n if (table) {\n table.columns.forEach(col => {\n suggestions.push({\n label: col.name,\n kind: monaco.languages.CompletionItemKind.Field,\n insertText: col.name,\n range,\n detail: \"Column\",\n sortText: \"1\"\n });\n });\n }\n }\n } else {\n // Provide table names\n for (const [sName, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n suggestions.push({\n label: table.tableName,\n kind: monaco.languages.CompletionItemKind.Class,\n insertText: table.tableName,\n range,\n detail: `Table (${sName})`,\n sortText: \"2\"\n });\n });\n }\n\n // Provide SQL keywords\n sqlKeywords.forEach(keyword => {\n suggestions.push({\n label: keyword,\n kind: monaco.languages.CompletionItemKind.Keyword,\n insertText: keyword + \" \",\n range,\n detail: \"SQL Keyword\",\n sortText: \"3\"\n });\n });\n\n // Heuristic: Suggest columns for tables that are mentioned in the current editor text\n const fullText = model.getValue();\n const activeTables = new Set<string>();\n for (const [, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n const regex = new RegExp(`\\\\b${table.tableName}\\\\b`, \"i\");\n if (regex.test(fullText)) {\n activeTables.add(table.tableName);\n }\n });\n }\n\n const addedColumns = new Set<string>();\n for (const [, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n if (activeTables.has(table.tableName)) {\n table.columns.forEach(col => {\n if (!addedColumns.has(col.name)) {\n addedColumns.add(col.name);\n suggestions.push({\n label: col.name,\n kind: monaco.languages.CompletionItemKind.Field,\n insertText: col.name,\n range,\n detail: `Column (${table.tableName})`,\n sortText: \"1\" // Prioritize columns from active tables\n });\n }\n });\n }\n });\n }\n }\n\n return { suggestions };\n\n }\n });\n\n if (autoFocus) {\n editor.focus();\n }\n };\n\n return (\n <div className={cls(\"relative w-full h-full overflow-hidden\", className)}>\n <Editor\n height=\"100%\"\n defaultLanguage=\"pgsql\"\n value={value}\n onChange={onChange}\n onMount={handleEditorOnMount}\n theme={mode === \"dark\" ? \"vs-dark\" : \"vs\"}\n options={{\n minimap: { enabled: false },\n fontSize: 14,\n lineNumbers: \"on\",\n scrollBeyondLastLine: false,\n automaticLayout: true,\n readOnly,\n tabSize: 2,\n wordWrap: \"on\"\n }}\n />\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { t as MonacoEditor } from "./MonacoEditor-
|
|
1
|
+
import { t as MonacoEditor } from "./MonacoEditor-CUxy5_k5.js";
|
|
2
2
|
import { ConfirmationDialog, ErrorView, useApiBase, useApiConfig, useRebaseContext, useSnackbarController, useStudioCapabilities, useStudioCollectionRegistry, useStudioSchemaEditing, useTranslation } from "@rebasepro/app";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { Alert, AlertTriangleIcon, Button, Chip, CircularProgress, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, HelpCircleIcon, IconButton, KeyIcon, Link2Icon, LockIcon, MultiSelect, MultiSelectItem, Paper, RefreshCwIcon, ResizablePanels, Select, SelectItem, ShieldIcon, Tab, Tabs, TextField, Tooltip, Trash2Icon, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, isPostgresCollectionConfig, policy } from "@rebasepro/types";
|
|
7
7
|
import { getPolicyNamesForRule, getPolicyNamesForRules, getPolicyOperations } from "@rebasepro/utils";
|
|
8
|
-
import { JUNCTION_TABLES_SQL, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_USER_ROLE, getEffectiveSecurityRules, getJunctionSecurityRules, policyToPostgres, resolveJunctionSpecs } from "@rebasepro/common";
|
|
8
|
+
import { JUNCTION_TABLES_SQL, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_USER_ROLE, compileRulePolicies, getEffectiveSecurityRules, getInjectedSecurityRules, getJunctionSecurityRules, getTableName, policyToPostgres, policyToSecurityRule, resolveJunctionSpecs } from "@rebasepro/common";
|
|
9
9
|
//#region src/components/RLSEditor/policy-presets.ts
|
|
10
10
|
/**
|
|
11
11
|
* What the RLS editor offers before the user types anything: the roles a `TO`
|
|
@@ -506,23 +506,20 @@ var PolicyEditor = ({ policy, schema, table, roleOptions, onSave, onCancel }) =>
|
|
|
506
506
|
}),
|
|
507
507
|
/* @__PURE__ */ jsxs("ul", {
|
|
508
508
|
className: "list-disc pl-5 space-y-1 text-sm text-text-secondary dark:text-text-secondary-dark",
|
|
509
|
-
children: [
|
|
510
|
-
/* @__PURE__ */
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
t("studio_policy_help_role_anon")
|
|
524
|
-
] })
|
|
525
|
-
]
|
|
509
|
+
children: [/* @__PURE__ */ jsxs("li", { children: [
|
|
510
|
+
/* @__PURE__ */ jsx("strong", { children: "public" }),
|
|
511
|
+
": ",
|
|
512
|
+
t("studio_policy_help_role_public")
|
|
513
|
+
] }), /* @__PURE__ */ jsxs("li", { children: [
|
|
514
|
+
/* @__PURE__ */ jsx("strong", { children: REBASE_USER_ROLE }),
|
|
515
|
+
": ",
|
|
516
|
+
t("studio_policy_help_role_rebase_user")
|
|
517
|
+
] })]
|
|
518
|
+
}),
|
|
519
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
520
|
+
variant: "body2",
|
|
521
|
+
className: "text-text-secondary dark:text-text-secondary-dark mt-1",
|
|
522
|
+
children: t("studio_policy_help_roles_not_users")
|
|
526
523
|
})
|
|
527
524
|
]
|
|
528
525
|
}),
|
|
@@ -760,7 +757,215 @@ function isCancellation(err) {
|
|
|
760
757
|
return err instanceof Error && err.name === "SchemaChangeCancelled";
|
|
761
758
|
}
|
|
762
759
|
//#endregion
|
|
760
|
+
//#region src/components/RLSEditor/policyRules.ts
|
|
761
|
+
/**
|
|
762
|
+
* Write an edit of one policy back to the declared rule it compiles from.
|
|
763
|
+
*
|
|
764
|
+
* The rule is found by the policy names it compiles to, not by `rule.name`:
|
|
765
|
+
* an unnamed rule's policy is `<table>_<op>_<hash>`, and a rule over several
|
|
766
|
+
* operations names each `<name>_<op>`. Matching on `name` missed both, saved
|
|
767
|
+
* the rules unchanged and reported success.
|
|
768
|
+
*
|
|
769
|
+
* A rule over several operations is refused rather than split: taking one
|
|
770
|
+
* operation out renames every policy the rule compiles to.
|
|
771
|
+
*
|
|
772
|
+
* @param declared The collection's `securityRules`, as written.
|
|
773
|
+
* @param effective Every rule the generator compiles for the table — the
|
|
774
|
+
* declared ones plus what Rebase adds — to tell a generated
|
|
775
|
+
* policy from one that exists only in the database.
|
|
776
|
+
*/
|
|
777
|
+
function applyPolicyEdit(declared, effective, tableName, original, edited) {
|
|
778
|
+
const compilesTo = (rule) => getPolicyNamesForRule(rule, tableName).includes(original.policyname);
|
|
779
|
+
const index = declared.findIndex(compilesTo);
|
|
780
|
+
if (index === -1) return {
|
|
781
|
+
ok: false,
|
|
782
|
+
reason: effective.some(compilesTo) ? "generated" : "not_declared"
|
|
783
|
+
};
|
|
784
|
+
const rule = declared[index];
|
|
785
|
+
if (getPolicyOperations(rule).length > 1) return {
|
|
786
|
+
ok: false,
|
|
787
|
+
reason: "several_operations"
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
ok: true,
|
|
791
|
+
rules: declared.map((r, i) => i === index ? editedRule(r, original, edited) : r)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The rule with what the editor changed, and nothing else.
|
|
796
|
+
*
|
|
797
|
+
* The editor shows a rule as a policy — name, command, mode, `TO` list and raw
|
|
798
|
+
* USING / WITH CHECK — and an `ownerField` or `access` rule has no raw clause
|
|
799
|
+
* to show. So a field is only written when it differs from what the editor was
|
|
800
|
+
* opened with: renaming an `access: "public"` rule keeps it public, and an
|
|
801
|
+
* unnamed rule stays unnamed unless the name was changed. A condition typed in
|
|
802
|
+
* replaces the rule's condition whole — the forms are exclusive.
|
|
803
|
+
*/
|
|
804
|
+
function editedRule(rule, original, edited) {
|
|
805
|
+
const next = policyToSecurityRule(edited);
|
|
806
|
+
const result = { ...rule };
|
|
807
|
+
if ((edited.policyname ?? "") !== original.policyname) if (next.name) result.name = next.name;
|
|
808
|
+
else delete result.name;
|
|
809
|
+
if (edited.cmd !== original.cmd) {
|
|
810
|
+
delete result.operations;
|
|
811
|
+
result.operation = next.operation;
|
|
812
|
+
}
|
|
813
|
+
if (edited.permissive !== original.permissive) result.mode = next.mode;
|
|
814
|
+
if (!sameRoles$1(edited.roles, original.roles)) if (next.pgRoles) result.pgRoles = next.pgRoles;
|
|
815
|
+
else delete result.pgRoles;
|
|
816
|
+
if ((edited.qual || null) !== (original.qual || null) || (edited.with_check || null) !== (original.with_check || null)) {
|
|
817
|
+
delete result.ownerField;
|
|
818
|
+
delete result.access;
|
|
819
|
+
delete result.condition;
|
|
820
|
+
delete result.check;
|
|
821
|
+
delete result.using;
|
|
822
|
+
delete result.withCheck;
|
|
823
|
+
if (next.using) result.using = next.using;
|
|
824
|
+
if (next.withCheck) result.withCheck = next.withCheck;
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
function sameRoles$1(a, b) {
|
|
829
|
+
const left = [...a ?? []].sort();
|
|
830
|
+
const right = [...b ?? []].sort();
|
|
831
|
+
return left.length === right.length && left.every((role, i) => role === right[i]);
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/components/RLSEditor/policyListing.ts
|
|
835
|
+
var COMMANDS = {
|
|
836
|
+
select: "SELECT",
|
|
837
|
+
insert: "INSERT",
|
|
838
|
+
update: "UPDATE",
|
|
839
|
+
delete: "DELETE",
|
|
840
|
+
all: "ALL"
|
|
841
|
+
};
|
|
842
|
+
/** A compiled policy, in the shape `pg_policies` describes one. */
|
|
843
|
+
function asPostgresPolicy(compiled, tableName) {
|
|
844
|
+
return {
|
|
845
|
+
policyname: compiled.name,
|
|
846
|
+
tablename: tableName,
|
|
847
|
+
permissive: compiled.mode === "restrictive" ? "RESTRICTIVE" : "PERMISSIVE",
|
|
848
|
+
cmd: COMMANDS[compiled.operation],
|
|
849
|
+
roles: compiled.roles,
|
|
850
|
+
qual: compiled.using,
|
|
851
|
+
with_check: compiled.withCheck
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The rule's own clauses, for a rule the compiler could not compile — an
|
|
856
|
+
* `existsIn` naming a collection the registry does not hold, say. Only ever a
|
|
857
|
+
* `code_only` row's content: nothing is compared against it.
|
|
858
|
+
*/
|
|
859
|
+
function uncompiledPolicy(rule, operation, name, tableName) {
|
|
860
|
+
return {
|
|
861
|
+
policyname: name,
|
|
862
|
+
tablename: tableName,
|
|
863
|
+
permissive: rule.mode === "restrictive" ? "RESTRICTIVE" : "PERMISSIVE",
|
|
864
|
+
cmd: COMMANDS[operation],
|
|
865
|
+
roles: [...rule.pgRoles ?? ["public"]],
|
|
866
|
+
qual: rule.using || null,
|
|
867
|
+
with_check: rule.withCheck || null
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function sameRoles(a, b) {
|
|
871
|
+
const left = [...a].sort();
|
|
872
|
+
const right = [...b].sort();
|
|
873
|
+
return left.length === right.length && left.every((role, i) => role === right[i]);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Where a live policy differs from the one the code compiles to.
|
|
877
|
+
*
|
|
878
|
+
* The same comparison `rebase doctor` makes: the `TO` list, the command, the
|
|
879
|
+
* mode, and whether each clause is there at all. Never the clause text —
|
|
880
|
+
* Postgres rewrites an expression when it stores it, so the text of a policy
|
|
881
|
+
* that matches its declaration exactly still differs from the declaration's.
|
|
882
|
+
* A clause that is missing is not a rewrite, though: a `USING` that is gone
|
|
883
|
+
* changes who the policy admits.
|
|
884
|
+
*/
|
|
885
|
+
function policyDrift(declared, live) {
|
|
886
|
+
const drift = [];
|
|
887
|
+
if (!sameRoles(declared.roles, live.roles)) drift.push("roles");
|
|
888
|
+
if (declared.cmd !== live.cmd) drift.push("command");
|
|
889
|
+
if (declared.permissive !== live.permissive) drift.push("mode");
|
|
890
|
+
if (declared.qual != null !== (live.qual != null)) drift.push("USING");
|
|
891
|
+
if (declared.with_check != null !== (live.with_check != null)) drift.push("WITH CHECK");
|
|
892
|
+
return drift;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* The policies of one table: every policy the database has, as the database
|
|
896
|
+
* has it, and every policy the code declares for the table that it does not.
|
|
897
|
+
*
|
|
898
|
+
* A policy in both keeps the database's fields — this list is what Postgres
|
|
899
|
+
* enforces — and carries the declaration beside them, with where the two
|
|
900
|
+
* differ.
|
|
901
|
+
*
|
|
902
|
+
* @param live The table's `pg_policies` rows.
|
|
903
|
+
* @param collection The collection the table belongs to, if any.
|
|
904
|
+
* @param collections Every collection, to resolve the ones a rule refers to.
|
|
905
|
+
*/
|
|
906
|
+
function listPolicies(live, collection, tableName, collections) {
|
|
907
|
+
const listed = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const policy of live) listed.set(policy.policyname, {
|
|
909
|
+
...policy,
|
|
910
|
+
status: "live"
|
|
911
|
+
});
|
|
912
|
+
if (!collection || !isPostgresCollectionConfig(collection)) return [...listed.values()];
|
|
913
|
+
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
914
|
+
const injectedNames = getPolicyNamesForRules(getInjectedSecurityRules(collection), tableName);
|
|
915
|
+
const livePolicies = new Map(live.map((policy) => [policy.policyname, policy]));
|
|
916
|
+
for (const rule of getEffectiveSecurityRules(collection)) {
|
|
917
|
+
const operations = getPolicyOperations(rule);
|
|
918
|
+
let compiled;
|
|
919
|
+
try {
|
|
920
|
+
compiled = compileRulePolicies(collection, rule, { resolveCollection });
|
|
921
|
+
} catch {
|
|
922
|
+
compiled = void 0;
|
|
923
|
+
}
|
|
924
|
+
getPolicyNamesForRule(rule, tableName).forEach((name, index) => {
|
|
925
|
+
const declared = compiled ? asPostgresPolicy(compiled[index], tableName) : void 0;
|
|
926
|
+
const generated = injectedNames.has(name);
|
|
927
|
+
const existing = livePolicies.get(name);
|
|
928
|
+
if (existing) listed.set(name, {
|
|
929
|
+
...existing,
|
|
930
|
+
status: "both",
|
|
931
|
+
declared,
|
|
932
|
+
drift: declared ? policyDrift(declared, existing) : void 0,
|
|
933
|
+
generated
|
|
934
|
+
});
|
|
935
|
+
else listed.set(name, {
|
|
936
|
+
...declared ?? uncompiledPolicy(rule, operations[index] ?? "all", name, tableName),
|
|
937
|
+
status: "code_only",
|
|
938
|
+
generated
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return [...listed.values()];
|
|
943
|
+
}
|
|
944
|
+
//#endregion
|
|
763
945
|
//#region src/components/RLSEditor/RLSEditor.tsx
|
|
946
|
+
/**
|
|
947
|
+
* Whether `collection` is the one `db push` writes `table` for: the table
|
|
948
|
+
* `getTableName` derives, in the schema the collection declares or `public` —
|
|
949
|
+
* the planner's own resolution.
|
|
950
|
+
*
|
|
951
|
+
* This matched a table to any collection whose `id`, `path`, `table`, `slug`
|
|
952
|
+
* or `collectionId` equalled its name, in any schema. So an unmapped table
|
|
953
|
+
* named like another collection's slug, or a same-named table in another
|
|
954
|
+
* schema, was taken for that collection: it listed the collection's policies
|
|
955
|
+
* as missing from the table, and offered to import the table's own policies
|
|
956
|
+
* into the collection. And a collection that leaves `table` to a slug that is
|
|
957
|
+
* not already snake case mapped nothing.
|
|
958
|
+
*/
|
|
959
|
+
function mapsTable(collection, table) {
|
|
960
|
+
if (!isPostgresCollectionConfig(collection)) return false;
|
|
961
|
+
return getTableName(collection) === table.tableName && (collection.schema || "public") === table.schemaName;
|
|
962
|
+
}
|
|
963
|
+
/** What the editor says when an edit has no declared rule to go back to. */
|
|
964
|
+
var POLICY_EDIT_REFUSALS = {
|
|
965
|
+
generated: "studio_rls_edit_generated",
|
|
966
|
+
not_declared: "studio_rls_edit_not_declared",
|
|
967
|
+
several_operations: "studio_rls_edit_several_operations"
|
|
968
|
+
};
|
|
764
969
|
function classifyTableClient(tableName, schemaName, junctionTableNames, isMappedToCollection) {
|
|
765
970
|
if (isMappedToCollection) return "collection";
|
|
766
971
|
if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "internal";
|
|
@@ -1092,7 +1297,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1092
1297
|
other: []
|
|
1093
1298
|
};
|
|
1094
1299
|
tables.forEach((table) => {
|
|
1095
|
-
const isMapped = !!collectionRegistry.collections?.
|
|
1300
|
+
const isMapped = !!collectionRegistry.collections?.some((c) => mapsTable(c, table));
|
|
1096
1301
|
const cat = classifyTableClient(table.tableName, table.schemaName, junctionTableNames, isMapped);
|
|
1097
1302
|
groups[cat].push(table);
|
|
1098
1303
|
});
|
|
@@ -1104,7 +1309,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1104
1309
|
]);
|
|
1105
1310
|
const activeCollection = useMemo(() => {
|
|
1106
1311
|
if (!activeTableData) return null;
|
|
1107
|
-
return collectionRegistry.collections?.find((c) => c
|
|
1312
|
+
return collectionRegistry.collections?.find((c) => mapsTable(c, activeTableData)) || null;
|
|
1108
1313
|
}, [activeTableData, collectionRegistry.collections]);
|
|
1109
1314
|
/** The category of the currently selected table. */
|
|
1110
1315
|
const activeTableCategory = useMemo(() => {
|
|
@@ -1116,41 +1321,55 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1116
1321
|
activeCollection,
|
|
1117
1322
|
junctionTableNames
|
|
1118
1323
|
]);
|
|
1324
|
+
/**
|
|
1325
|
+
* The junction a relation derives this table as, when it is one. Its
|
|
1326
|
+
* policies are Rebase's — derived from the endpoints' relations — and the
|
|
1327
|
+
* server re-creates them on every start, like a collection's. `null` when
|
|
1328
|
+
* the registry's collections carry no resolvable relations (serialized
|
|
1329
|
+
* configs without relation closures).
|
|
1330
|
+
*/
|
|
1331
|
+
const activeJunctionSpec = useMemo(() => {
|
|
1332
|
+
if (!activeTableData || activeCollection) return null;
|
|
1333
|
+
try {
|
|
1334
|
+
return resolveJunctionSpecs(collectionRegistry.collections ?? []).get(activeTableData.tableName) ?? null;
|
|
1335
|
+
} catch {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
}, [
|
|
1339
|
+
activeTableData,
|
|
1340
|
+
activeCollection,
|
|
1341
|
+
collectionRegistry.collections
|
|
1342
|
+
]);
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether the server re-asserts this table's RLS on every start: it switches
|
|
1345
|
+
* RLS back on and re-creates every policy the project declares for it.
|
|
1346
|
+
*/
|
|
1347
|
+
const activeTableManaged = Boolean(activeCollection || activeJunctionSpec);
|
|
1348
|
+
/**
|
|
1349
|
+
* What Postgres enforces on the selected table, and what the code declares
|
|
1350
|
+
* that it does not have yet.
|
|
1351
|
+
*
|
|
1352
|
+
* A policy in both is listed with the *database's* fields, and the
|
|
1353
|
+
* declaration rides beside it with where the two differ (`listPolicies`).
|
|
1354
|
+
* The declaration's fields in their place would make a policy changed in
|
|
1355
|
+
* SQL look exactly like its declaration, and would show every rule written
|
|
1356
|
+
* as a structured condition — all of Rebase's own policies — with no USING.
|
|
1357
|
+
*/
|
|
1119
1358
|
const mergedPolicies = useMemo(() => {
|
|
1120
1359
|
if (!activeTableData) return [];
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
status
|
|
1126
|
-
|
|
1127
|
-
});
|
|
1128
|
-
if (activeCollection && isPostgresCollectionConfig(activeCollection)) getEffectiveSecurityRules(activeCollection).forEach((rule) => {
|
|
1129
|
-
const ops = getPolicyOperations(rule);
|
|
1130
|
-
getPolicyNamesForRule(rule, activeTableData.tableName).forEach((policyName, opIdx) => {
|
|
1131
|
-
policiesMap[policyName] = {
|
|
1132
|
-
policyname: policyName,
|
|
1133
|
-
tablename: activeTableData.tableName,
|
|
1134
|
-
permissive: (rule.mode || "permissive").toUpperCase(),
|
|
1135
|
-
cmd: (ops[opIdx] ?? rule.operation ?? "ALL").toUpperCase(),
|
|
1136
|
-
roles: [...rule.pgRoles ?? ["public"]],
|
|
1137
|
-
qual: rule.using || null,
|
|
1138
|
-
with_check: rule.withCheck || null,
|
|
1139
|
-
status: policiesMap[policyName] ? "both" : "code_only"
|
|
1140
|
-
};
|
|
1141
|
-
});
|
|
1142
|
-
});
|
|
1143
|
-
if (!activeCollection) try {
|
|
1144
|
-
const spec = resolveJunctionSpecs(collectionRegistry.collections ?? []).get(activeTableData.tableName);
|
|
1145
|
-
if (spec) {
|
|
1146
|
-
const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(spec), spec.table);
|
|
1147
|
-
for (const p of Object.values(policiesMap)) if (generatedNames.has(p.policyname)) p.status = "both";
|
|
1360
|
+
const listed = listPolicies(activeTableData.policies || [], activeCollection, activeTableData.tableName, collectionRegistry.collections ?? []);
|
|
1361
|
+
if (activeJunctionSpec) {
|
|
1362
|
+
const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(activeJunctionSpec), activeJunctionSpec.table);
|
|
1363
|
+
for (const p of listed) if (generatedNames.has(p.policyname)) {
|
|
1364
|
+
p.status = "both";
|
|
1365
|
+
p.generated = true;
|
|
1148
1366
|
}
|
|
1149
|
-
}
|
|
1150
|
-
return
|
|
1367
|
+
}
|
|
1368
|
+
return listed.sort((a, b) => a.policyname.localeCompare(b.policyname));
|
|
1151
1369
|
}, [
|
|
1152
1370
|
activeTableData,
|
|
1153
1371
|
activeCollection,
|
|
1372
|
+
activeJunctionSpec,
|
|
1154
1373
|
collectionRegistry.collections
|
|
1155
1374
|
]);
|
|
1156
1375
|
const rlsStats = useMemo(() => {
|
|
@@ -1451,7 +1670,8 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1451
1670
|
onClick: () => setConfirmToggleRls({
|
|
1452
1671
|
table: activeTableData.tableName,
|
|
1453
1672
|
schema: activeTableData.schemaName,
|
|
1454
|
-
enabled: activeTableData.rlsEnabled
|
|
1673
|
+
enabled: activeTableData.rlsEnabled,
|
|
1674
|
+
managed: activeTableManaged
|
|
1455
1675
|
}),
|
|
1456
1676
|
children: activeTableData.rlsEnabled ? t("studio_rls_disable_rls") : t("studio_rls_enable_rls")
|
|
1457
1677
|
}),
|
|
@@ -1509,20 +1729,29 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1509
1729
|
roleOptions: pgRoleOptions,
|
|
1510
1730
|
onSave: async (newPolicy) => {
|
|
1511
1731
|
if (activeCollection && hasCodebase) {
|
|
1512
|
-
const rule = {
|
|
1513
|
-
name: newPolicy.policyname,
|
|
1514
|
-
operation: newPolicy.cmd?.toLowerCase(),
|
|
1515
|
-
mode: newPolicy.permissive?.toLowerCase(),
|
|
1516
|
-
using: newPolicy.qual || void 0,
|
|
1517
|
-
withCheck: newPolicy.with_check || void 0,
|
|
1518
|
-
...newPolicy.roles && !(newPolicy.roles.length === 1 && newPolicy.roles[0] === "public") ? { pgRoles: newPolicy.roles } : {}
|
|
1519
|
-
};
|
|
1520
1732
|
const existingRules = (isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : void 0) || [];
|
|
1521
1733
|
let newRules;
|
|
1522
|
-
if (editingPolicy === "new") newRules = [...existingRules,
|
|
1523
|
-
else
|
|
1734
|
+
if (editingPolicy === "new") newRules = [...existingRules, policyToSecurityRule(newPolicy)];
|
|
1735
|
+
else {
|
|
1736
|
+
const edit = applyPolicyEdit(existingRules, getEffectiveSecurityRules(activeCollection), activeTableData.tableName, editingPolicy, newPolicy);
|
|
1737
|
+
if (!edit.ok) {
|
|
1738
|
+
snackbarController.open({
|
|
1739
|
+
type: "error",
|
|
1740
|
+
message: t(POLICY_EDIT_REFUSALS[edit.reason], { policy: editingPolicy.policyname })
|
|
1741
|
+
});
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
newRules = edit.rules;
|
|
1745
|
+
if (JSON.stringify(newRules) === JSON.stringify(existingRules)) {
|
|
1746
|
+
snackbarController.open({
|
|
1747
|
+
type: "info",
|
|
1748
|
+
message: t("studio_rls_nothing_changed")
|
|
1749
|
+
});
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1524
1753
|
try {
|
|
1525
|
-
await saveSecurityRules(activeCollection.
|
|
1754
|
+
await saveSecurityRules(activeCollection.slug, newRules);
|
|
1526
1755
|
snackbarController.open({
|
|
1527
1756
|
type: "success",
|
|
1528
1757
|
message: "Policy saved successfully"
|
|
@@ -1532,31 +1761,40 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1532
1761
|
} catch (e) {
|
|
1533
1762
|
reportSaveFailure(e);
|
|
1534
1763
|
}
|
|
1535
|
-
} else
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1764
|
+
} else {
|
|
1765
|
+
if (editingPolicy !== "new" && editingPolicy.generated) {
|
|
1766
|
+
snackbarController.open({
|
|
1767
|
+
type: "error",
|
|
1768
|
+
message: t(POLICY_EDIT_REFUSALS.generated, { policy: editingPolicy.policyname })
|
|
1769
|
+
});
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
const qualifiedTable = `${sanitizeSqlIdentifier(activeTableData.schemaName)}.${sanitizeSqlIdentifier(activeTableData.tableName)}`;
|
|
1774
|
+
const policyName = sanitizeSqlIdentifier(newPolicy.policyname || "unnamed_policy");
|
|
1775
|
+
const cmd = newPolicy.cmd || "ALL";
|
|
1776
|
+
const permissive = (newPolicy.permissive || "PERMISSIVE") === "PERMISSIVE" ? "PERMISSIVE" : "RESTRICTIVE";
|
|
1777
|
+
const roles = newPolicy.roles && newPolicy.roles.length > 0 ? newPolicy.roles.map((r) => sanitizeSqlIdentifier(r)).join(", ") : "public";
|
|
1778
|
+
let sql = `CREATE POLICY ${policyName} ON ${qualifiedTable}`;
|
|
1779
|
+
sql += ` AS ${permissive}`;
|
|
1780
|
+
sql += ` FOR ${cmd}`;
|
|
1781
|
+
sql += ` TO ${roles}`;
|
|
1782
|
+
if (newPolicy.qual) sql += ` USING (${newPolicy.qual})`;
|
|
1783
|
+
if (newPolicy.with_check) sql += ` WITH CHECK (${newPolicy.with_check})`;
|
|
1784
|
+
if (editingPolicy !== "new") sql = `DROP POLICY IF EXISTS ${sanitizeSqlIdentifier(editingPolicy.policyname)} ON ${qualifiedTable}; ${sql}`;
|
|
1785
|
+
await databaseAdmin.executeSql(sql);
|
|
1786
|
+
snackbarController.open({
|
|
1787
|
+
type: "success",
|
|
1788
|
+
message: `Policy "${newPolicy.policyname}" applied to the database`
|
|
1789
|
+
});
|
|
1790
|
+
setEditingPolicy(null);
|
|
1791
|
+
fetchRLSData();
|
|
1792
|
+
} catch (e) {
|
|
1793
|
+
snackbarController.open({
|
|
1794
|
+
type: "error",
|
|
1795
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1560
1798
|
}
|
|
1561
1799
|
},
|
|
1562
1800
|
onCancel: () => setEditingPolicy(null)
|
|
@@ -1567,6 +1805,19 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1567
1805
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1568
1806
|
className: "max-w-4xl mx-auto flex flex-col gap-6",
|
|
1569
1807
|
children: [
|
|
1808
|
+
activeTableData && activeTableManaged && !(activeCollection && hasCodebase) && /* @__PURE__ */ jsx(Alert, {
|
|
1809
|
+
color: "info",
|
|
1810
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1811
|
+
className: "flex items-start gap-2",
|
|
1812
|
+
children: [/* @__PURE__ */ jsx(RefreshCwIcon, {
|
|
1813
|
+
size: 16,
|
|
1814
|
+
className: "shrink-0 mt-0.5"
|
|
1815
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1816
|
+
variant: "caption",
|
|
1817
|
+
children: t("studio_rls_reapplied_on_start", { table: activeTableData.tableName })
|
|
1818
|
+
})]
|
|
1819
|
+
})
|
|
1820
|
+
}),
|
|
1570
1821
|
activeTableData && activeTableCategory === "internal" && /* @__PURE__ */ jsx(Alert, {
|
|
1571
1822
|
color: "info",
|
|
1572
1823
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -1676,6 +1927,13 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1676
1927
|
children: t("studio_rls_unapplied")
|
|
1677
1928
|
})
|
|
1678
1929
|
}),
|
|
1930
|
+
policy.drift && policy.drift.length > 0 && /* @__PURE__ */ jsx(Tooltip, {
|
|
1931
|
+
title: t("studio_rls_drift_tooltip", { fields: policy.drift.join(", ") }),
|
|
1932
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1933
|
+
className: "px-1.5 py-0.5 rounded text-[10px] bg-orange-500/10 text-orange-600 border border-orange-500/20 shrink-0",
|
|
1934
|
+
children: `${t("studio_rls_drift")}: ${policy.drift.join(", ")}`
|
|
1935
|
+
})
|
|
1936
|
+
}),
|
|
1679
1937
|
policy.status === "live" && hasCodebase && /* @__PURE__ */ jsx(Tooltip, {
|
|
1680
1938
|
title: "This policy is live in the database but missing from your codebase schema.",
|
|
1681
1939
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -1696,17 +1954,10 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1696
1954
|
variant: "outlined",
|
|
1697
1955
|
color: "primary",
|
|
1698
1956
|
onClick: async () => {
|
|
1699
|
-
const rule =
|
|
1700
|
-
name: policy.policyname,
|
|
1701
|
-
operation: policy.cmd?.toLowerCase(),
|
|
1702
|
-
mode: policy.permissive?.toLowerCase(),
|
|
1703
|
-
using: policy.qual || void 0,
|
|
1704
|
-
withCheck: policy.with_check || void 0,
|
|
1705
|
-
roles: policy.roles
|
|
1706
|
-
};
|
|
1957
|
+
const rule = policyToSecurityRule(policy);
|
|
1707
1958
|
const newRules = [...(isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : void 0) || [], rule];
|
|
1708
1959
|
try {
|
|
1709
|
-
await saveSecurityRules(activeCollection.
|
|
1960
|
+
await saveSecurityRules(activeCollection.slug, newRules);
|
|
1710
1961
|
snackbarController.open({
|
|
1711
1962
|
type: "success",
|
|
1712
1963
|
message: "Policy imported successfully"
|
|
@@ -1729,6 +1980,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1729
1980
|
title: t("studio_rls_delete"),
|
|
1730
1981
|
asChild: true,
|
|
1731
1982
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1983
|
+
"aria-label": t("studio_rls_delete"),
|
|
1732
1984
|
size: "small",
|
|
1733
1985
|
onClick: () => setConfirmDropPolicy({
|
|
1734
1986
|
policyName: policy.policyname,
|
|
@@ -1799,7 +2051,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1799
2051
|
onAccept: applyToggleRls,
|
|
1800
2052
|
onCancel: () => setConfirmToggleRls(null),
|
|
1801
2053
|
title: confirmToggleRls?.enabled ? t("studio_rls_disable_confirm_title") : t("studio_rls_enable_confirm_title"),
|
|
1802
|
-
body: confirmToggleRls?.enabled ? t("studio_rls_disable_confirm_body", { table: confirmToggleRls
|
|
2054
|
+
body: confirmToggleRls?.enabled ? [t("studio_rls_disable_confirm_body", { table: confirmToggleRls.table }), confirmToggleRls.managed ? t("studio_rls_disable_reapplied", { table: confirmToggleRls.table }) : ""].filter(Boolean).join(" ") : t("studio_rls_enable_confirm_body", { table: confirmToggleRls?.table ?? "" })
|
|
1803
2055
|
}),
|
|
1804
2056
|
/* @__PURE__ */ jsx(ConfirmationDialog, {
|
|
1805
2057
|
open: confirmDropPolicy !== null,
|
|
@@ -1818,4 +2070,4 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1818
2070
|
//#endregion
|
|
1819
2071
|
export { RLSEditor };
|
|
1820
2072
|
|
|
1821
|
-
//# sourceMappingURL=RLSEditor-
|
|
2073
|
+
//# sourceMappingURL=RLSEditor-BlLLV6xs.js.map
|