@rebasepro/admin 0.12.0 → 0.12.1-canary.g06f263c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog--VwQl4pk.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog--VwQl4pk.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-QaEYldcT.js} +12 -9
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-QaEYldcT.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-DvRSUk9o.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-DvRSUk9o.js.map} +1 -1
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-BZZfQkT5.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-BZZfQkT5.js.map} +1 -1
- package/dist/{history-g9688IaK.js → history-Ao1HBAK3.js} +2 -2
- package/dist/{history-g9688IaK.js.map → history-Ao1HBAK3.js.map} +1 -1
- package/dist/{import-BjUG-cIY.js → import-AFJRUo99.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-AFJRUo99.js.map} +1 -1
- package/dist/index.js +105 -34
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/{util-B5fJm1FA.js → util-ByPlsapJ.js} +103 -61
- package/dist/util-ByPlsapJ.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +1 -1
- package/src/components/CollectionViewBinding/SplitListView.tsx +1 -1
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +1 -1
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditViewBinding.tsx +1 -1
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SidePanelBinding.tsx +1 -1
- package/src/components/UserSelector.tsx +6 -5
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +1 -1
- package/src/hooks/useHistory.ts +6 -5
- package/src/hooks/useResolvedUsers.ts +8 -5
- package/src/routes/RebaseRoute.tsx +2 -2
- package/dist/util-B5fJm1FA.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.1-canary.g06f263c",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"source": "src/index.ts",
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=22.22.0"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"rebase",
|
|
@@ -54,66 +54,64 @@
|
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
56
|
"@dnd-kit/utilities": "^3.2.2",
|
|
57
|
-
"@floating-ui/dom": "^1.
|
|
57
|
+
"@floating-ui/dom": "^1.8.0",
|
|
58
58
|
"cmdk": "^1.1.1",
|
|
59
59
|
"date-fns": "^4.4.0",
|
|
60
60
|
"exceljs": "^4.4.0",
|
|
61
|
-
"fast-equals": "6.0.
|
|
62
|
-
"fractional-indexing": "
|
|
63
|
-
"fuse.js": "^7.
|
|
61
|
+
"fast-equals": "6.0.2",
|
|
62
|
+
"fractional-indexing": "4.0.0",
|
|
63
|
+
"fuse.js": "^7.5.0",
|
|
64
64
|
"json5": "^2.2.3",
|
|
65
|
-
"markdown-it": "^14.
|
|
65
|
+
"markdown-it": "^14.3.0",
|
|
66
66
|
"markdown-it-ins": "^4.0.0",
|
|
67
67
|
"markdown-it-mark": "^4.0.0",
|
|
68
68
|
"markdown-it-task-lists": "^2.1.1",
|
|
69
69
|
"prism-react-renderer": "^2.4.1",
|
|
70
70
|
"prosemirror-commands": "^1.7.1",
|
|
71
|
-
"prosemirror-dropcursor": "^1.8.
|
|
71
|
+
"prosemirror-dropcursor": "^1.8.3",
|
|
72
72
|
"prosemirror-gapcursor": "^1.4.1",
|
|
73
73
|
"prosemirror-history": "^1.5.0",
|
|
74
74
|
"prosemirror-inputrules": "^1.5.1",
|
|
75
75
|
"prosemirror-keymap": "^1.2.3",
|
|
76
|
-
"prosemirror-markdown": "^1.13.
|
|
77
|
-
"prosemirror-model": "^1.25.
|
|
76
|
+
"prosemirror-markdown": "^1.13.5",
|
|
77
|
+
"prosemirror-model": "^1.25.11",
|
|
78
78
|
"prosemirror-schema-list": "^1.5.1",
|
|
79
79
|
"prosemirror-state": "^1.4.4",
|
|
80
80
|
"prosemirror-tables": "^1.8.5",
|
|
81
81
|
"prosemirror-transform": "^1.12.0",
|
|
82
|
-
"prosemirror-view": "^1.
|
|
83
|
-
"react-dropzone": "^
|
|
82
|
+
"prosemirror-view": "^1.42.2",
|
|
83
|
+
"react-dropzone": "^19.1.1",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/ui": "0.12.
|
|
93
|
-
"@rebasepro/utils": "0.12.
|
|
86
|
+
"@rebasepro/app": "0.12.1-canary.g06f263c",
|
|
87
|
+
"@rebasepro/admin-types": "0.12.1-canary.g06f263c",
|
|
88
|
+
"@rebasepro/forms": "0.12.1-canary.g06f263c",
|
|
89
|
+
"@rebasepro/common": "0.12.1-canary.g06f263c",
|
|
90
|
+
"@rebasepro/inference": "0.12.1-canary.g06f263c",
|
|
91
|
+
"@rebasepro/types": "0.12.1-canary.g06f263c",
|
|
92
|
+
"@rebasepro/ui": "0.12.1-canary.g06f263c",
|
|
93
|
+
"@rebasepro/utils": "0.12.1-canary.g06f263c"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"react": ">=19.
|
|
97
|
-
"react-dom": ">=19.
|
|
98
|
-
"react-router": "^
|
|
99
|
-
"react-router-dom": "^7.0.0"
|
|
96
|
+
"react": ">=19.2.7",
|
|
97
|
+
"react-dom": ">=19.2.7",
|
|
98
|
+
"react-router": "^8.3.0"
|
|
100
99
|
},
|
|
101
100
|
"devDependencies": {
|
|
102
101
|
"@jest/globals": "^30.4.1",
|
|
103
102
|
"@testing-library/react": "^16.3.2",
|
|
104
103
|
"@types/jest": "^30.0.0",
|
|
105
104
|
"@types/markdown-it": "^14.1.2",
|
|
106
|
-
"@types/node": "^
|
|
105
|
+
"@types/node": "^26.1.2",
|
|
107
106
|
"@types/react": "^19.2.17",
|
|
108
107
|
"@types/react-dom": "^19.2.3",
|
|
109
108
|
"@types/react-measure": "^2.0.12",
|
|
110
|
-
"@vitejs/plugin-react": "^6.0.
|
|
109
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
111
110
|
"jest": "^30.4.2",
|
|
112
|
-
"react-router": "^
|
|
113
|
-
"
|
|
114
|
-
"ts-jest": "^29.4.11",
|
|
111
|
+
"react-router": "^8.3.0",
|
|
112
|
+
"ts-jest": "^29.4.12",
|
|
115
113
|
"typescript": "^6.0.3",
|
|
116
|
-
"vite": "^8.
|
|
114
|
+
"vite": "^8.1.5"
|
|
117
115
|
},
|
|
118
116
|
"files": [
|
|
119
117
|
"dist",
|
|
@@ -131,13 +129,15 @@
|
|
|
131
129
|
},
|
|
132
130
|
"jest": {
|
|
133
131
|
"transform": {
|
|
134
|
-
"^.+\\.
|
|
132
|
+
"^.+\\.tsx?$": "ts-jest",
|
|
133
|
+
"^.+\\.m?jsx?$": "<rootDir>/../../scripts/jest/react-router-esm-transform.cjs"
|
|
135
134
|
},
|
|
136
135
|
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
|
137
136
|
"moduleFileExtensions": [
|
|
138
137
|
"ts",
|
|
139
138
|
"tsx",
|
|
140
139
|
"js",
|
|
140
|
+
"mjs",
|
|
141
141
|
"jsx",
|
|
142
142
|
"json",
|
|
143
143
|
"node"
|
|
@@ -146,9 +146,7 @@
|
|
|
146
146
|
"testPathIgnorePatterns": [
|
|
147
147
|
"/node_modules/"
|
|
148
148
|
],
|
|
149
|
-
"transformIgnorePatterns": [
|
|
150
|
-
"node_modules/(?!.*fractional-indexing)/"
|
|
151
|
-
],
|
|
149
|
+
"transformIgnorePatterns": [],
|
|
152
150
|
"setupFilesAfterEnv": [
|
|
153
151
|
"<rootDir>/test/setupTests.ts"
|
|
154
152
|
],
|
|
@@ -6,7 +6,7 @@ import { deepEqual as equal } from "fast-equals";
|
|
|
6
6
|
import { CollectionsConfigController } from "./types/config_controller";
|
|
7
7
|
import { useCustomizationController, useRebaseContext, useAuthController, useSnackbarController } from "@rebasepro/app";
|
|
8
8
|
import { getTableName } from "@rebasepro/common";
|
|
9
|
-
import { useNavigate } from "react-router
|
|
9
|
+
import { useNavigate } from "react-router";
|
|
10
10
|
import { CollectionEditorController } from "./types/collection_editor_controller";
|
|
11
11
|
import { CollectionInference } from "./types/collection_inference";
|
|
12
12
|
import { CollectionGenerationCallback } from "./api/generateCollectionApi";
|
|
@@ -30,7 +30,8 @@ import {
|
|
|
30
30
|
IconButton,
|
|
31
31
|
LoadingButton,
|
|
32
32
|
Tab,
|
|
33
|
-
Tabs
|
|
33
|
+
Tabs,
|
|
34
|
+
Tooltip
|
|
34
35
|
} from "@rebasepro/ui";
|
|
35
36
|
import { EngineProperties, Entity, getDataSourceCapabilities, MapProperty, Properties, Property, TableMetadata, User } from "@rebasepro/types";
|
|
36
37
|
import { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -254,11 +255,38 @@ export function CollectionEditor(props: CollectionEditorDialogProps & {
|
|
|
254
255
|
? applyPropertyConfigs(initialCollection, propertyConfigs)
|
|
255
256
|
: copyFromProp
|
|
256
257
|
? (() => {
|
|
257
|
-
// When duplicating, copy all properties but clear identifiers
|
|
258
|
-
|
|
258
|
+
// When duplicating, copy all properties but clear identifiers.
|
|
259
|
+
//
|
|
260
|
+
// "Identifiers" has to include the ones this dialog cannot
|
|
261
|
+
// show. `schema` has no field anywhere in the editor, so a
|
|
262
|
+
// duplicate of the Users collection — the one collection
|
|
263
|
+
// scaffolded with `schema: "rebase"` — silently produced a
|
|
264
|
+
// new collection whose table is created in `rebase` rather
|
|
265
|
+
// than `public`, with nothing on screen to explain why.
|
|
266
|
+
// Panel-authored collections are `public` collections.
|
|
267
|
+
//
|
|
268
|
+
// `slug`/`table` are cleared rather than carried: both are
|
|
269
|
+
// re-derived from the name the user types (see
|
|
270
|
+
// `updateName` in GeneralSettingsForm, which fills untouched
|
|
271
|
+
// fields while `isNewCollection`), and carrying them meant a
|
|
272
|
+
// duplicate pointed at the source's table until then.
|
|
273
|
+
//
|
|
274
|
+
// `auth` goes too — a duplicate must not become a second
|
|
275
|
+
// auth collection wired to the same user store.
|
|
276
|
+
const {
|
|
277
|
+
subcollections: _sub,
|
|
278
|
+
schema: _schema,
|
|
279
|
+
auth: _auth,
|
|
280
|
+
...rest
|
|
281
|
+
} = copyFromProp as unknown as Record<string, unknown>;
|
|
259
282
|
return {
|
|
260
283
|
...rest,
|
|
261
284
|
name: "",
|
|
285
|
+
// Same shape the "new collection" branch below starts
|
|
286
|
+
// from, so validation and the registry lookups never see
|
|
287
|
+
// an undefined slug mid-edit.
|
|
288
|
+
slug: fallbackSlug,
|
|
289
|
+
table: "",
|
|
262
290
|
ownerId: authController.user?.uid ?? ""
|
|
263
291
|
} as AdminCollection<any>;
|
|
264
292
|
})()
|
|
@@ -341,6 +369,10 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
341
369
|
}
|
|
342
370
|
) {
|
|
343
371
|
|
|
372
|
+
/** Why the submit buttons below are dead, straight from the backend. */
|
|
373
|
+
const readOnlyTitle = configController?.readOnlyReason
|
|
374
|
+
|| "This backend does not accept collection edits.";
|
|
375
|
+
|
|
344
376
|
const importConfigRaw = useImportConfig();
|
|
345
377
|
const urlControllerRaw = useUrlController();
|
|
346
378
|
const collectionRegistryRaw = useCollectionRegistryController();
|
|
@@ -720,16 +752,23 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
720
752
|
)}
|
|
721
753
|
{fullScreen && !isNewCollection && (
|
|
722
754
|
<div className="flex items-center gap-2">
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
755
|
+
{/* "(Read-only)" says the button will not work but not why.
|
|
756
|
+
The controller now carries the backend's own reason —
|
|
757
|
+
production, `baas` mode, no `collectionsDir` — so show it. */}
|
|
758
|
+
<Tooltip title={configController?.readOnly ? readOnlyTitle : undefined}>
|
|
759
|
+
<div>
|
|
760
|
+
<LoadingButton
|
|
761
|
+
variant="filled"
|
|
762
|
+
color="primary"
|
|
763
|
+
type="submit"
|
|
764
|
+
onClick={() => formController.handleSubmit()}
|
|
765
|
+
disabled={!dirty || isSubmitting || configController?.readOnly}
|
|
766
|
+
loading={isSubmitting}
|
|
767
|
+
>
|
|
768
|
+
{configController?.readOnly ? "Update (Read-only)" : "Update"}
|
|
769
|
+
</LoadingButton>
|
|
770
|
+
</div>
|
|
771
|
+
</Tooltip>
|
|
733
772
|
</div>
|
|
734
773
|
)}
|
|
735
774
|
</div>
|
|
@@ -935,15 +974,19 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
|
|
|
935
974
|
{currentView === "properties" && "Create collection"}
|
|
936
975
|
</LoadingButton>}
|
|
937
976
|
|
|
938
|
-
{!isNewCollection && !fullScreen && <
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
977
|
+
{!isNewCollection && !fullScreen && <Tooltip title={configController?.readOnly ? readOnlyTitle : undefined}>
|
|
978
|
+
<div>
|
|
979
|
+
<LoadingButton
|
|
980
|
+
variant="filled"
|
|
981
|
+
color="primary"
|
|
982
|
+
type="submit"
|
|
983
|
+
disabled={isSubmitting || configController?.readOnly}
|
|
984
|
+
loading={isSubmitting}
|
|
985
|
+
>
|
|
986
|
+
{configController?.readOnly ? "Update collection (Read-only)" : "Update collection"}
|
|
987
|
+
</LoadingButton>
|
|
988
|
+
</div>
|
|
989
|
+
</Tooltip>}
|
|
947
990
|
|
|
948
991
|
</div>
|
|
949
992
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { getPolicyNamesForRule, getPolicyNamesForRules } from "@rebasepro/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { getGeneratedPolicyNames } from "@rebasepro/common";
|
|
4
4
|
import React, { useState, useEffect, useMemo } from "react";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
Typography
|
|
40
40
|
} from "@rebasepro/ui";
|
|
41
41
|
import { useFormex } from "@rebasepro/forms";
|
|
42
|
+
import { useCollectionsConfigController } from "../../useCollectionsConfigController";
|
|
42
43
|
import { useRebaseContext } from "@rebasepro/app";
|
|
43
44
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
44
45
|
import type { PostgresPolicy, SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
@@ -114,6 +115,14 @@ export function CollectionRLSTab() {
|
|
|
114
115
|
const { values, setFieldValue } = useFormex<CollectionWithSecurity>();
|
|
115
116
|
const [editingPolicy, setEditingPolicy] = useState<PostgresPolicy | "new" | null>(null);
|
|
116
117
|
|
|
118
|
+
// Every other tab in this dialog disables its inputs when the backend will
|
|
119
|
+
// not accept an edit — the general and display forms behind a `fieldset
|
|
120
|
+
// disabled`, the properties editor per control. This one did not, so a
|
|
121
|
+
// read-only panel still invited you to write a policy, delete one, and
|
|
122
|
+
// reorder the lot, and only said no at the submit button on the way out.
|
|
123
|
+
const { readOnly, readOnlyReason } = useCollectionsConfigController();
|
|
124
|
+
const readOnlyTitle = readOnlyReason || "This backend does not accept collection edits.";
|
|
125
|
+
|
|
117
126
|
const rules: SecurityRule[] = values.securityRules || [];
|
|
118
127
|
|
|
119
128
|
const { databaseAdmin } = useRebaseContext();
|
|
@@ -209,26 +218,14 @@ export function CollectionRLSTab() {
|
|
|
209
218
|
|
|
210
219
|
const tableName = values.id || values.table || values.alias;
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
* - a rule without an explicit `name` becomes `<table>_<op>_<hash>`, so
|
|
217
|
-
* comparing `rule.name` to `policyname` never matches for it;
|
|
218
|
-
* - the generator also injects the safe-by-default baseline
|
|
219
|
-
* (`<table>_default_admin_*`), which is in no collection's `securityRules`.
|
|
220
|
-
*
|
|
221
|
-
* Missing either made Rebase's own policies look hand-written, and offered to
|
|
222
|
-
* import them back into the codebase that produced them.
|
|
223
|
-
*/
|
|
221
|
+
// Every policy name `rebase db push` would write for this collection — see
|
|
222
|
+
// `getGeneratedPolicyNames`. This used to be derived here by hand, and the
|
|
223
|
+
// Studio's RLS editor derived a different subset of it by hand, which is
|
|
224
|
+
// exactly how the two came to disagree about which policies were drift.
|
|
224
225
|
const generatedPolicyNames = useMemo(() => {
|
|
225
226
|
if (!tableName) return new Set<string>();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
[...rules, ...effectiveRules],
|
|
229
|
-
tableName
|
|
230
|
-
);
|
|
231
|
-
}, [rules, tableName, values]);
|
|
227
|
+
return getGeneratedPolicyNames(values as AdminCollection);
|
|
228
|
+
}, [tableName, values]);
|
|
232
229
|
|
|
233
230
|
const unmappedPolicies = dbPolicies.filter(dp => !generatedPolicyNames.has(dp.policyname));
|
|
234
231
|
|
|
@@ -251,9 +248,13 @@ export function CollectionRLSTab() {
|
|
|
251
248
|
<div className="w-full flex flex-col">
|
|
252
249
|
<div className="flex items-center justify-between mb-8">
|
|
253
250
|
<Typography variant="h5">Row Level Security</Typography>
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
<Tooltip title={readOnly ? readOnlyTitle : undefined}>
|
|
252
|
+
<div>
|
|
253
|
+
<Button variant="filled" color="neutral" disabled={readOnly} onClick={() => setEditingPolicy("new")}>
|
|
254
|
+
CREATE POLICY
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</Tooltip>
|
|
257
258
|
</div>
|
|
258
259
|
|
|
259
260
|
{rules.length === 0 ? (
|
|
@@ -282,7 +283,7 @@ export function CollectionRLSTab() {
|
|
|
282
283
|
</div>
|
|
283
284
|
</div>
|
|
284
285
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
285
|
-
<Button size="small" variant="text" onClick={() => setEditingPolicy({
|
|
286
|
+
<Button size="small" variant="text" disabled={readOnly} onClick={() => setEditingPolicy({
|
|
286
287
|
policyname: rule.name ?? "",
|
|
287
288
|
tablename: values.id || values.table || values.alias || "your_table",
|
|
288
289
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
@@ -293,7 +294,7 @@ export function CollectionRLSTab() {
|
|
|
293
294
|
})}>
|
|
294
295
|
EDIT
|
|
295
296
|
</Button>
|
|
296
|
-
<IconButton size="small" onClick={() => {
|
|
297
|
+
<IconButton size="small" disabled={readOnly} onClick={() => {
|
|
297
298
|
setFieldValue("securityRules", rules.filter((r: SecurityRule) => r.name !== rule.name));
|
|
298
299
|
}}>
|
|
299
300
|
<Trash2Icon size={iconSize.smallest} className="text-text-secondary dark:text-text-secondary-dark hover:text-red-500 dark:hover:text-red-500 transition-colors"/>
|
|
@@ -336,12 +337,19 @@ export function CollectionRLSTab() {
|
|
|
336
337
|
</div>
|
|
337
338
|
</div>
|
|
338
339
|
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
{/* "Import to codebase" is the one action here that
|
|
341
|
+
names its destination, and it is the one the
|
|
342
|
+
backend refuses when it has no codebase to write. */}
|
|
343
|
+
<Tooltip title={readOnly ? readOnlyTitle : undefined}>
|
|
344
|
+
<div>
|
|
345
|
+
<Button size="small" variant="outlined" color="primary" disabled={readOnly} onClick={() => {
|
|
346
|
+
const rule: SecurityRule = toSecurityRule(dp);
|
|
347
|
+
setFieldValue("securityRules", [...rules, rule]);
|
|
348
|
+
}}>
|
|
349
|
+
Import to codebase
|
|
350
|
+
</Button>
|
|
351
|
+
</div>
|
|
352
|
+
</Tooltip>
|
|
345
353
|
</div>
|
|
346
354
|
</Paper>
|
|
347
355
|
))}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (which has no react-router dependency).
|
|
11
11
|
*/
|
|
12
12
|
import React from "react";
|
|
13
|
-
import { useNavigate } from "react-router
|
|
13
|
+
import { useNavigate } from "react-router";
|
|
14
14
|
import { useUrlController } from "../../_cms_internals";
|
|
15
15
|
import { useSnackbarController } from "@rebasepro/app";
|
|
16
16
|
import { CollectionStudioView, CollectionStudioViewProps } from "./CollectionStudioView";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (which has no react-router dependency).
|
|
10
10
|
*/
|
|
11
11
|
import React from "react";
|
|
12
|
-
import { useLocation, useNavigate } from "react-router
|
|
12
|
+
import { useLocation, useNavigate } from "react-router";
|
|
13
13
|
import { useUrlController } from "../../_cms_internals";
|
|
14
14
|
import { CollectionsStudioView, CollectionsStudioViewProps } from "./CollectionsStudioView";
|
|
15
15
|
|
|
@@ -2,42 +2,87 @@ import { CollectionsConfigController, SaveCollectionParams, UpdateCollectionPara
|
|
|
2
2
|
import { Properties } from "@rebasepro/types";
|
|
3
3
|
import { getSubcollections } from "@rebasepro/common";
|
|
4
4
|
|
|
5
|
-
import React, { useMemo, useRef } from "react";
|
|
5
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
7
|
+
import { DEFAULT_API_PATH } from "@rebasepro/app";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What the backend said about its schema editor, and how sure we are.
|
|
11
|
+
*
|
|
12
|
+
* `unknown` covers both "still asking" and "could not ask" — a backend too old
|
|
13
|
+
* to have the endpoint, or one that is unreachable. Those fall back to the
|
|
14
|
+
* build-mode guess below rather than locking the editor for someone whose
|
|
15
|
+
* editor used to work.
|
|
16
|
+
*/
|
|
17
|
+
type SchemaEditorAvailability =
|
|
18
|
+
| { state: "unknown" }
|
|
19
|
+
| { state: "known", enabled: boolean, reason?: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The guess this hook used to make on its own.
|
|
23
|
+
*
|
|
24
|
+
* `process.env.NODE_ENV` here is the *frontend bundle's* build mode. Whether
|
|
25
|
+
* the schema editor exists is decided by a different process entirely — the
|
|
26
|
+
* server's own `NODE_ENV`, its `collectionsDir`, whether its collections were
|
|
27
|
+
* introspected, whether `ts-morph` is installed. The two disagree in every
|
|
28
|
+
* ordinary setup: a dev frontend against a deployed API, a `baas` project, a
|
|
29
|
+
* hosted console. When they did, the editor offered itself and every save came
|
|
30
|
+
* back `404 Not Found`. It is kept only as the answer for a backend too old to
|
|
31
|
+
* be asked.
|
|
32
|
+
*/
|
|
33
|
+
const buildModeGuess = (): boolean => process.env.NODE_ENV === "production";
|
|
34
|
+
|
|
7
35
|
export function useLocalCollectionsConfigController(
|
|
8
36
|
clientOrUrl: any,
|
|
9
37
|
baseCollections: AdminCollection[] = [],
|
|
10
38
|
options?: {
|
|
11
39
|
readOnly?: boolean;
|
|
12
40
|
getAuthToken?: () => Promise<string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Identity of the signed-in user, if any.
|
|
43
|
+
*
|
|
44
|
+
* Only used to re-ask the backend when it changes: whether the editor
|
|
45
|
+
* will accept a write depends on who is asking, and the answer to an
|
|
46
|
+
* anonymous probe does not survive a sign-in.
|
|
47
|
+
*/
|
|
48
|
+
authKey?: string | null;
|
|
13
49
|
}
|
|
14
50
|
): CollectionsConfigController {
|
|
15
51
|
|
|
16
52
|
const parsedCollections = baseCollections;
|
|
17
|
-
|
|
53
|
+
|
|
18
54
|
// Store latest options in a ref to prevent stale closures in the `request` function
|
|
19
55
|
// due to useMemo caching the saveCollection function.
|
|
20
56
|
const optionsRef = useRef(options);
|
|
21
57
|
optionsRef.current = options;
|
|
22
58
|
|
|
59
|
+
// `/api` only by default — a backend configured with another `basePath`
|
|
60
|
+
// mounts the schema editor under that instead.
|
|
61
|
+
const clientBaseUrl = typeof clientOrUrl === "string"
|
|
62
|
+
? clientOrUrl
|
|
63
|
+
: (clientOrUrl?.baseUrl ?? "");
|
|
64
|
+
const clientApiPath = (typeof clientOrUrl === "object" && clientOrUrl !== null && clientOrUrl.apiPath)
|
|
65
|
+
|| DEFAULT_API_PATH;
|
|
66
|
+
const editorUrl = `${String(clientBaseUrl).replace(/\/$/, "")}${clientApiPath}/schema-editor`;
|
|
67
|
+
|
|
68
|
+
const resolveToken = async (): Promise<string | null> => {
|
|
69
|
+
let token = optionsRef.current?.getAuthToken ? await optionsRef.current.getAuthToken() : null;
|
|
70
|
+
if (!token && typeof clientOrUrl === "object" && clientOrUrl !== null && clientOrUrl.resolveToken) {
|
|
71
|
+
token = await clientOrUrl.resolveToken();
|
|
72
|
+
}
|
|
73
|
+
return token ?? null;
|
|
74
|
+
};
|
|
75
|
+
|
|
23
76
|
const request = async (endpoint: string, payload: Record<string, unknown>) => {
|
|
24
77
|
try {
|
|
25
|
-
|
|
26
|
-
let baseUrl = typeof clientOrUrl === "string" ? clientOrUrl : "";
|
|
27
|
-
|
|
28
|
-
if (typeof clientOrUrl === "object" && clientOrUrl !== null) {
|
|
29
|
-
baseUrl = clientOrUrl.baseUrl || baseUrl;
|
|
30
|
-
if (!token && clientOrUrl.resolveToken) {
|
|
31
|
-
token = await clientOrUrl.resolveToken();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
78
|
+
const token = await resolveToken();
|
|
34
79
|
|
|
35
80
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
36
81
|
if (token) {
|
|
37
82
|
headers["Authorization"] = `Bearer ${token}`;
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
const response = await fetch(`${
|
|
85
|
+
const response = await fetch(`${editorUrl}${endpoint}`, {
|
|
41
86
|
method: "POST",
|
|
42
87
|
headers,
|
|
43
88
|
body: JSON.stringify(payload)
|
|
@@ -67,10 +112,77 @@ export function useLocalCollectionsConfigController(
|
|
|
67
112
|
}
|
|
68
113
|
};
|
|
69
114
|
|
|
115
|
+
// ── Ask the backend whether it will accept an edit ────────────────
|
|
116
|
+
const forcedReadOnly = options?.readOnly;
|
|
117
|
+
const authKey = options?.authKey ?? null;
|
|
118
|
+
const [availability, setAvailability] = useState<SchemaEditorAvailability>({ state: "unknown" });
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// Nothing to ask about: the caller has already decided.
|
|
122
|
+
if (forcedReadOnly === true) return;
|
|
123
|
+
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const token = await resolveToken();
|
|
128
|
+
const response = await fetch(`${editorUrl}/status`, {
|
|
129
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
130
|
+
});
|
|
131
|
+
if (cancelled) return;
|
|
132
|
+
|
|
133
|
+
// A backend from before the endpoint existed. Guessing from the
|
|
134
|
+
// bundle's build mode is wrong, but it is what that backend's
|
|
135
|
+
// admin panel has always done, and locking the editor for
|
|
136
|
+
// someone whose editor works would be worse.
|
|
137
|
+
if (response.status === 404) {
|
|
138
|
+
setAvailability({ state: "unknown" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const body = await response.json().catch(() => ({})) as {
|
|
143
|
+
enabled?: boolean,
|
|
144
|
+
reason?: string,
|
|
145
|
+
error?: { message?: string }
|
|
146
|
+
};
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
|
|
149
|
+
if (response.ok) {
|
|
150
|
+
setAvailability({
|
|
151
|
+
state: "known",
|
|
152
|
+
enabled: body.enabled === true,
|
|
153
|
+
reason: body.reason
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
// 401/403 (not an admin) or 501 (no auth configured at all)
|
|
157
|
+
// — all of them mean this session cannot edit collections,
|
|
158
|
+
// and the body says why better than we could.
|
|
159
|
+
setAvailability({
|
|
160
|
+
state: "known",
|
|
161
|
+
enabled: false,
|
|
162
|
+
reason: body.error?.message
|
|
163
|
+
?? `The backend refused to say whether collections are editable (HTTP ${response.status}).`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
if (!cancelled) setAvailability({ state: "unknown" });
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
return () => { cancelled = true; };
|
|
172
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
|
+
}, [editorUrl, forcedReadOnly, authKey]);
|
|
174
|
+
|
|
175
|
+
const readOnly = forcedReadOnly
|
|
176
|
+
?? (availability.state === "known" ? !availability.enabled : buildModeGuess());
|
|
177
|
+
|
|
178
|
+
const readOnlyReason = availability.state === "known" && availability.reason
|
|
179
|
+
? availability.reason
|
|
180
|
+
: "Collections can only be edited against a backend running the schema editor, which is off in production.";
|
|
181
|
+
|
|
70
182
|
return useMemo(() => ({
|
|
71
183
|
loading: false,
|
|
72
|
-
readOnly
|
|
73
|
-
readOnlyReason
|
|
184
|
+
readOnly,
|
|
185
|
+
readOnlyReason,
|
|
74
186
|
collections: parsedCollections,
|
|
75
187
|
getCollection: (id: string) => {
|
|
76
188
|
const found = parsedCollections.find(c => (c as AdminCollection & { id?: string }).id === id || c.slug === id);
|
|
@@ -119,5 +231,5 @@ collectionData: { propertiesOrder: newPropertiesOrder } });
|
|
|
119
231
|
|
|
120
232
|
navigationEntries: [],
|
|
121
233
|
saveNavigationEntries: async () => { }
|
|
122
|
-
}), [clientOrUrl, parsedCollections,
|
|
234
|
+
}), [clientOrUrl, parsedCollections, readOnly, readOnlyReason, options?.getAuthToken]);
|
|
123
235
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppView } from "@rebasepro/admin-types";
|
|
2
2
|
import React, { useEffect } from "react";
|
|
3
|
-
import { useLocation } from "react-router
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
4
|
;
|
|
5
5
|
import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
6
6
|
import { useAdminModeController } from "@rebasepro/app";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
ViewMode
|
|
14
14
|
} from "@rebasepro/admin-types";
|
|
15
15
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
16
|
-
import { useSearchParams } from "react-router
|
|
16
|
+
import { useSearchParams } from "react-router";
|
|
17
17
|
|
|
18
18
|
import { deepEqual as equal } from "fast-equals"
|
|
19
19
|
import { CollectionRowActions, CollectionTableBinding } from "../CollectionTableBinding";
|
|
@@ -9,7 +9,7 @@ import { ClearFilterSortButton } from "../ClearFilterSortButton";
|
|
|
9
9
|
import { FiltersDialog } from "./FiltersDialog";
|
|
10
10
|
import { FilterPresetsButton } from "./FilterPresetsButton";
|
|
11
11
|
import { toArray } from "@rebasepro/utils";
|
|
12
|
-
import { useNavigate } from "react-router
|
|
12
|
+
import { useNavigate } from "react-router";
|
|
13
13
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
14
14
|
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
15
15
|
import { withViewMode } from "../../util/view_mode";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "@rebasepro/ui";
|
|
12
12
|
import { useLargeLayout, UnsavedChangesDialog, useNavigationBlocker } from "@rebasepro/app";
|
|
13
13
|
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
14
|
-
import { useNavigate, useLocation } from "react-router
|
|
14
|
+
import { useNavigate, useLocation } from "react-router";
|
|
15
15
|
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
16
16
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
17
17
|
import { withViewMode } from "../../util/view_mode";
|