@instantdb/components 0.0.1
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/.env +2 -0
- package/.turbo/turbo-build.log +18 -0
- package/README.md +78 -0
- package/app/App.css +38 -0
- package/app/App.tsx +61 -0
- package/app/index.css +18 -0
- package/app/main.tsx +10 -0
- package/dist/components/StyleMe.d.ts +15 -0
- package/dist/components/StyleMe.d.ts.map +1 -0
- package/dist/components/error-boundary.d.ts +17 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
- package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
- package/dist/components/explorer/explorer-layout.d.ts +8 -0
- package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
- package/dist/components/explorer/index.d.ts +44 -0
- package/dist/components/explorer/index.d.ts.map +1 -0
- package/dist/components/explorer/inner-explorer.d.ts +16 -0
- package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/query-inspector.d.ts +11 -0
- package/dist/components/explorer/query-inspector.d.ts.map +1 -0
- package/dist/components/explorer/recently-deleted.d.ts +36 -0
- package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
- package/dist/components/explorer/search-input.d.ts +9 -0
- package/dist/components/explorer/search-input.d.ts.map +1 -0
- package/dist/components/explorer/table-components.d.ts +16 -0
- package/dist/components/explorer/table-components.d.ts.map +1 -0
- package/dist/components/explorer/view-settings.d.ts +10 -0
- package/dist/components/explorer/view-settings.d.ts.map +1 -0
- package/dist/components/rosePineDawnTheme.d.ts +13 -0
- package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/toast.d.ts +4 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/ui.d.ts +336 -0
- package/dist/components/ui.d.ts.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/hooks/explorer.d.ts +29 -0
- package/dist/hooks/explorer.d.ts.map +1 -0
- package/dist/hooks/useAttrNotes.d.ts +10 -0
- package/dist/hooks/useAttrNotes.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.d.ts +3 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useColumnVisibility.d.ts +12 -0
- package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
- package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
- package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
- package/dist/hooks/useExplorerHistory.d.ts +1 -0
- package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
- package/dist/hooks/useIsOverflow.d.ts +6 -0
- package/dist/hooks/useIsOverflow.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.d.ts +2 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
- package/dist/hooks/useStableDB.d.ts +7 -0
- package/dist/hooks/useStableDB.d.ts.map +1 -0
- package/dist/index.cjs +15 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9270 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/indexingJobs.d.ts +24 -0
- package/dist/utils/indexingJobs.d.ts.map +1 -0
- package/dist/utils/parsePermsJSON.d.ts +11 -0
- package/dist/utils/parsePermsJSON.d.ts.map +1 -0
- package/dist/utils/renames.d.ts +3 -0
- package/dist/utils/renames.d.ts.map +1 -0
- package/dist/utils/tableWidthSize.d.ts +9 -0
- package/dist/utils/tableWidthSize.d.ts.map +1 -0
- package/index.html +13 -0
- package/package.json +109 -0
- package/src/components/StyleMe.tsx +97 -0
- package/src/components/error-boundary.tsx +76 -0
- package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
- package/src/components/explorer/edit-row-dialog.tsx +1151 -0
- package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
- package/src/components/explorer/explorer-layout.tsx +156 -0
- package/src/components/explorer/index.tsx +217 -0
- package/src/components/explorer/inner-explorer.tsx +1341 -0
- package/src/components/explorer/new-namespace-dialog.tsx +54 -0
- package/src/components/explorer/query-inspector.tsx +394 -0
- package/src/components/explorer/recently-deleted.tsx +344 -0
- package/src/components/explorer/search-input.tsx +358 -0
- package/src/components/explorer/table-components.tsx +341 -0
- package/src/components/explorer/view-settings.tsx +75 -0
- package/src/components/rosePineDawnTheme.ts +45 -0
- package/src/components/select.tsx +198 -0
- package/src/components/toast.tsx +18 -0
- package/src/components/ui.tsx +1561 -0
- package/src/config.ts +61 -0
- package/src/hooks/explorer.tsx +125 -0
- package/src/hooks/useAttrNotes.ts +27 -0
- package/src/hooks/useClickOutside.ts +23 -0
- package/src/hooks/useColumnVisibility.ts +39 -0
- package/src/hooks/useEditBlobConstraints.ts +185 -0
- package/src/hooks/useExplorerHistory.ts +0 -0
- package/src/hooks/useIsOverflow.ts +24 -0
- package/src/hooks/useLocalStorage.ts +51 -0
- package/src/hooks/useMonacoJSONSchema.ts +41 -0
- package/src/hooks/useStableDB.ts +30 -0
- package/src/index.tsx +8 -0
- package/src/schema.ts +285 -0
- package/src/style.css +5 -0
- package/src/types.ts +359 -0
- package/src/utils/format.ts +13 -0
- package/src/utils/indexingJobs.ts +126 -0
- package/src/utils/parsePermsJSON.ts +35 -0
- package/src/utils/renames.ts +42 -0
- package/src/utils/tableWidthSize.ts +62 -0
- package/tailwind.config.cjs +42 -0
- package/tsconfig.json +22 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.ts +49 -0
|
@@ -0,0 +1,1886 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { id } from '@instantdb/core';
|
|
3
|
+
import { InstantReactWebDatabase } from '@instantdb/react';
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
ReactNode,
|
|
10
|
+
MutableRefObject,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
ArrowLeftIcon,
|
|
14
|
+
PlusIcon,
|
|
15
|
+
TrashIcon,
|
|
16
|
+
ArrowUturnLeftIcon,
|
|
17
|
+
PencilSquareIcon,
|
|
18
|
+
} from '@heroicons/react/24/solid';
|
|
19
|
+
import { errorToast, successToast } from '@lib/components/toast';
|
|
20
|
+
import {
|
|
21
|
+
ActionButton,
|
|
22
|
+
ActionForm,
|
|
23
|
+
Button,
|
|
24
|
+
Checkbox,
|
|
25
|
+
cn,
|
|
26
|
+
Content,
|
|
27
|
+
Divider,
|
|
28
|
+
IconButton,
|
|
29
|
+
InfoTip,
|
|
30
|
+
Label,
|
|
31
|
+
ProgressButton,
|
|
32
|
+
Select,
|
|
33
|
+
TextInput,
|
|
34
|
+
ToggleGroup,
|
|
35
|
+
} from '@lib/components/ui';
|
|
36
|
+
import {
|
|
37
|
+
RelationshipKinds,
|
|
38
|
+
relationshipConstraints,
|
|
39
|
+
relationshipConstraintsInverse,
|
|
40
|
+
} from '@lib/types';
|
|
41
|
+
import {
|
|
42
|
+
CheckedDataType,
|
|
43
|
+
DBAttr,
|
|
44
|
+
InstantIndexingJob,
|
|
45
|
+
InstantIndexingJobInvalidTriple,
|
|
46
|
+
SchemaAttr,
|
|
47
|
+
SchemaNamespace,
|
|
48
|
+
} from '@lib/types';
|
|
49
|
+
import {
|
|
50
|
+
createJob,
|
|
51
|
+
jobFetchLoop,
|
|
52
|
+
jobIsCompleted,
|
|
53
|
+
jobIsErrored,
|
|
54
|
+
} from '@lib/utils/indexingJobs';
|
|
55
|
+
import { useExplorerProps, useExplorerState } from './index';
|
|
56
|
+
import { useClose } from '@headlessui/react';
|
|
57
|
+
import {
|
|
58
|
+
PendingJob,
|
|
59
|
+
useEditBlobConstraints,
|
|
60
|
+
} from '@lib/hooks/useEditBlobConstraints';
|
|
61
|
+
import { RecentlyDeletedAttrs } from './recently-deleted';
|
|
62
|
+
import { useAttrNotes } from '@lib/hooks/useAttrNotes';
|
|
63
|
+
import { createRenameNamespaceOps } from '@lib/utils/renames';
|
|
64
|
+
import { useSWRConfig } from 'swr';
|
|
65
|
+
|
|
66
|
+
export function EditNamespaceDialog({
|
|
67
|
+
db,
|
|
68
|
+
namespace,
|
|
69
|
+
namespaces,
|
|
70
|
+
onClose,
|
|
71
|
+
isSystemCatalogNs,
|
|
72
|
+
}: {
|
|
73
|
+
db: InstantReactWebDatabase<any>;
|
|
74
|
+
namespace: SchemaNamespace;
|
|
75
|
+
namespaces: SchemaNamespace[];
|
|
76
|
+
onClose: (p?: { ok: boolean }) => void;
|
|
77
|
+
readOnly: boolean;
|
|
78
|
+
isSystemCatalogNs: boolean;
|
|
79
|
+
}) {
|
|
80
|
+
const props = useExplorerProps();
|
|
81
|
+
const appId = props.appId;
|
|
82
|
+
const { history, explorerState } = useExplorerState();
|
|
83
|
+
const { mutate } = useSWRConfig();
|
|
84
|
+
const [screen, setScreen] = useState<
|
|
85
|
+
| { type: 'main' }
|
|
86
|
+
| { type: 'delete' }
|
|
87
|
+
| { type: 'rename' }
|
|
88
|
+
| { type: 'add' }
|
|
89
|
+
| { type: 'edit'; attrId: string; isForward: boolean }
|
|
90
|
+
>({ type: 'main' });
|
|
91
|
+
|
|
92
|
+
const [renameNsInput, setRenameNsInput] = useState(namespace.name);
|
|
93
|
+
const [renameNsErrorText, setRenameNsErrorText] = useState<string | null>(
|
|
94
|
+
null,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
async function deleteNs() {
|
|
98
|
+
const ops = namespace.attrs.map((attr) => ['delete-attr', attr.id]);
|
|
99
|
+
await db.core._reactor.pushOps(ops);
|
|
100
|
+
// update the recently deleted attr cache
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
mutate(['recently-deleted', appId]);
|
|
103
|
+
}, 500);
|
|
104
|
+
onClose({ ok: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function renameNs(newName: string) {
|
|
108
|
+
if (newName.startsWith('$')) {
|
|
109
|
+
setRenameNsErrorText('Namespace name cannot start with $');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ops = createRenameNamespaceOps(newName, namespace, namespaces);
|
|
114
|
+
|
|
115
|
+
await db.core._reactor.pushOps(ops);
|
|
116
|
+
history.push(
|
|
117
|
+
{
|
|
118
|
+
namespace: newName,
|
|
119
|
+
},
|
|
120
|
+
true,
|
|
121
|
+
);
|
|
122
|
+
successToast('Renamed namespace to ' + newName);
|
|
123
|
+
setRenameNsInput('');
|
|
124
|
+
setScreen({ type: 'main' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const notes = useAttrNotes();
|
|
128
|
+
|
|
129
|
+
const screenAttr = useMemo(() => {
|
|
130
|
+
return (
|
|
131
|
+
screen.type === 'edit' &&
|
|
132
|
+
namespace.attrs.find(
|
|
133
|
+
(a) => a.id === screen.attrId && a.isForward === screen.isForward,
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}, [
|
|
137
|
+
screen.type === 'edit' ? screen.attrId : null,
|
|
138
|
+
screen.type === 'edit' ? screen.isForward : null,
|
|
139
|
+
namespace.attrs,
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
{screen.type === 'rename' && (
|
|
145
|
+
<div className="px-2">
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => {
|
|
148
|
+
setScreen({
|
|
149
|
+
type: 'main',
|
|
150
|
+
});
|
|
151
|
+
}}
|
|
152
|
+
className="mb-3"
|
|
153
|
+
>
|
|
154
|
+
<ArrowLeftIcon className="h-4 w-4 cursor-pointer" />
|
|
155
|
+
</button>
|
|
156
|
+
<h6 className="text-md pb-2 font-bold">Rename {namespace.name}</h6>
|
|
157
|
+
<form
|
|
158
|
+
onSubmit={(e) => {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
renameNs(renameNsInput);
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<Content className="pb-2 text-sm">
|
|
164
|
+
This will immediately rename the namespace. You'll need to{' '}
|
|
165
|
+
<strong className="dark:text-white">update your code</strong> to
|
|
166
|
+
the new name.
|
|
167
|
+
</Content>
|
|
168
|
+
<TextInput
|
|
169
|
+
disabled={isSystemCatalogNs}
|
|
170
|
+
value={renameNsInput}
|
|
171
|
+
onChange={(n) => setRenameNsInput(n)}
|
|
172
|
+
/>
|
|
173
|
+
<div className="flex flex-col gap-2 rounded-sm py-2">
|
|
174
|
+
<Button
|
|
175
|
+
type="submit"
|
|
176
|
+
disabled={
|
|
177
|
+
renameNsInput.startsWith('$') || renameNsInput.length === 0
|
|
178
|
+
}
|
|
179
|
+
>
|
|
180
|
+
Rename {namespace.name} → {renameNsInput}
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</form>{' '}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{screen.type === 'main' ? (
|
|
188
|
+
<div className="flex flex-col gap-4 px-2">
|
|
189
|
+
<div className="mr-8 flex gap-1">
|
|
190
|
+
<h5 className="flex items-center text-lg font-bold">
|
|
191
|
+
{namespace.name}
|
|
192
|
+
</h5>
|
|
193
|
+
<IconButton
|
|
194
|
+
variant="subtle"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
setScreen({ type: 'rename' });
|
|
197
|
+
}}
|
|
198
|
+
icon={
|
|
199
|
+
<PencilSquareIcon className="h-4 w-4 opacity-50"></PencilSquareIcon>
|
|
200
|
+
}
|
|
201
|
+
label="Rename"
|
|
202
|
+
></IconButton>
|
|
203
|
+
|
|
204
|
+
<Button
|
|
205
|
+
className="ml-4"
|
|
206
|
+
disabled={isSystemCatalogNs}
|
|
207
|
+
title={
|
|
208
|
+
isSystemCatalogNs
|
|
209
|
+
? `The ${namespace.name} namespace can't be deleted.`
|
|
210
|
+
: undefined
|
|
211
|
+
}
|
|
212
|
+
size="mini"
|
|
213
|
+
variant="secondary"
|
|
214
|
+
onClick={() => setScreen({ type: 'delete' })}
|
|
215
|
+
>
|
|
216
|
+
<TrashIcon className="inline" height="1rem" />
|
|
217
|
+
Delete
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div className="flex flex-col gap-2">
|
|
222
|
+
{namespace.attrs.map((attr) => (
|
|
223
|
+
<div
|
|
224
|
+
key={attr.id + '-' + attr.name}
|
|
225
|
+
className="flex justify-between"
|
|
226
|
+
>
|
|
227
|
+
<div className="flex items-center gap-3">
|
|
228
|
+
<span className="py-0.5 font-bold">{attr.name}</span>
|
|
229
|
+
{notes.notes[attr.id]?.message && (
|
|
230
|
+
<InfoTip>
|
|
231
|
+
<div className="px-2 text-xs text-gray-500 dark:text-neutral-400">
|
|
232
|
+
{notes.notes[attr.id].message}
|
|
233
|
+
</div>
|
|
234
|
+
</InfoTip>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
{attr.name !== 'id' ? (
|
|
238
|
+
<Button
|
|
239
|
+
className="px-2"
|
|
240
|
+
size="mini"
|
|
241
|
+
variant="subtle"
|
|
242
|
+
onClick={() => {
|
|
243
|
+
notes.removeNote(attr.id);
|
|
244
|
+
setScreen({
|
|
245
|
+
type: 'edit',
|
|
246
|
+
attrId: attr.id,
|
|
247
|
+
isForward: attr.isForward,
|
|
248
|
+
});
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
Edit
|
|
252
|
+
</Button>
|
|
253
|
+
) : null}
|
|
254
|
+
</div>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div>
|
|
259
|
+
<Button
|
|
260
|
+
size="mini"
|
|
261
|
+
variant="secondary"
|
|
262
|
+
onClick={() => setScreen({ type: 'add' })}
|
|
263
|
+
>
|
|
264
|
+
<PlusIcon className="inline" height="12px" />
|
|
265
|
+
New attribute
|
|
266
|
+
</Button>
|
|
267
|
+
</div>
|
|
268
|
+
<RecentlyDeletedAttrs
|
|
269
|
+
notes={notes}
|
|
270
|
+
db={db}
|
|
271
|
+
appId={appId}
|
|
272
|
+
namespace={namespace}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
) : screen.type === 'add' ? (
|
|
276
|
+
<AddAttrForm
|
|
277
|
+
db={db}
|
|
278
|
+
namespace={namespace}
|
|
279
|
+
namespaces={namespaces}
|
|
280
|
+
onClose={() => setScreen({ type: 'main' })}
|
|
281
|
+
constraints={getSystemConstraints({
|
|
282
|
+
namespaceName: namespace.name,
|
|
283
|
+
isSystemCatalogNs,
|
|
284
|
+
})}
|
|
285
|
+
/>
|
|
286
|
+
) : screen.type === 'delete' ? (
|
|
287
|
+
<DeleteForm
|
|
288
|
+
name={namespace.name}
|
|
289
|
+
type="namespace"
|
|
290
|
+
onClose={onClose}
|
|
291
|
+
onConfirm={deleteNs}
|
|
292
|
+
/>
|
|
293
|
+
) : screen.type === 'edit' && screenAttr ? (
|
|
294
|
+
<EditAttrForm
|
|
295
|
+
db={db}
|
|
296
|
+
attr={screenAttr}
|
|
297
|
+
onClose={() => setScreen({ type: 'main' })}
|
|
298
|
+
constraints={getSystemConstraints({
|
|
299
|
+
namespaceName: namespace.name,
|
|
300
|
+
isSystemCatalogNs: isSystemCatalogNs,
|
|
301
|
+
attr: screenAttr,
|
|
302
|
+
})}
|
|
303
|
+
/>
|
|
304
|
+
) : null}
|
|
305
|
+
</>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function DeleteForm({
|
|
310
|
+
name,
|
|
311
|
+
type,
|
|
312
|
+
onClose,
|
|
313
|
+
onConfirm,
|
|
314
|
+
}: {
|
|
315
|
+
name: string;
|
|
316
|
+
type: 'namespace' | 'attribute';
|
|
317
|
+
onClose: () => void;
|
|
318
|
+
onConfirm: () => void;
|
|
319
|
+
}) {
|
|
320
|
+
return (
|
|
321
|
+
<ActionForm className="min flex flex-col gap-4">
|
|
322
|
+
<h5 className="flex items-center gap-2 text-lg font-bold">
|
|
323
|
+
<ArrowLeftIcon
|
|
324
|
+
height="1rem"
|
|
325
|
+
className="cursor-pointer"
|
|
326
|
+
onClick={() => onClose()}
|
|
327
|
+
/>
|
|
328
|
+
Delete {name}
|
|
329
|
+
</h5>
|
|
330
|
+
|
|
331
|
+
<div className="flex flex-col gap-2">
|
|
332
|
+
<p className="pb-2">
|
|
333
|
+
Are you sure you want to delete the <strong>{name}</strong> {type}?
|
|
334
|
+
</p>
|
|
335
|
+
<ActionButton
|
|
336
|
+
variant="destructive"
|
|
337
|
+
label={`Delete ${name}`}
|
|
338
|
+
submitLabel="Deleting..."
|
|
339
|
+
errorMessage="Failed to delete"
|
|
340
|
+
onClick={onConfirm}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
</ActionForm>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function AddAttrForm({
|
|
348
|
+
db,
|
|
349
|
+
namespace,
|
|
350
|
+
namespaces,
|
|
351
|
+
onClose,
|
|
352
|
+
constraints,
|
|
353
|
+
}: {
|
|
354
|
+
db: InstantReactWebDatabase<any>;
|
|
355
|
+
namespace: SchemaNamespace;
|
|
356
|
+
namespaces: SchemaNamespace[];
|
|
357
|
+
onClose: () => void;
|
|
358
|
+
constraints: SystemConstraints;
|
|
359
|
+
}) {
|
|
360
|
+
const [isRequired, setIsRequired] = useState(false);
|
|
361
|
+
const [isIndex, setIsIndex] = useState(false);
|
|
362
|
+
const [isUniq, setIsUniq] = useState(false);
|
|
363
|
+
const [isCascade, setIsCascade] = useState(false);
|
|
364
|
+
const [isCascadeReverse, setIsCascadeReverse] = useState(false);
|
|
365
|
+
const [checkedDataType, setCheckedDataType] =
|
|
366
|
+
useState<CheckedDataType | null>(null);
|
|
367
|
+
const [attrType, setAttrType] = useState<'blob' | 'ref'>('blob');
|
|
368
|
+
const [relationship, setRelationship] =
|
|
369
|
+
useState<RelationshipKinds>('many-many');
|
|
370
|
+
|
|
371
|
+
const [reverseNamespace, setReverseNamespace] = useState<
|
|
372
|
+
SchemaNamespace | undefined
|
|
373
|
+
>(() => namespaces.find((n) => n.name !== namespace.name) ?? namespaces[0]);
|
|
374
|
+
const [attrName, setAttrName] = useState('');
|
|
375
|
+
const [reverseAttrName, setReverseAttrName] = useState(namespace.name);
|
|
376
|
+
|
|
377
|
+
const isCascadeAllowed =
|
|
378
|
+
relationship === 'one-one' || relationship === 'one-many';
|
|
379
|
+
const isCascadeReverseAllowed =
|
|
380
|
+
relationship === 'one-one' || relationship === 'many-one';
|
|
381
|
+
|
|
382
|
+
const linkValidation = validateLink({
|
|
383
|
+
attrName,
|
|
384
|
+
reverseAttrName,
|
|
385
|
+
namespaceName: namespace.name,
|
|
386
|
+
reverseNamespaceName: reverseNamespace?.name,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const canSubmit = attrType === 'blob' ? attrName : linkValidation.isValidLink;
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (attrType !== 'ref') return;
|
|
393
|
+
if (!reverseNamespace) return;
|
|
394
|
+
|
|
395
|
+
const isSelfLink = reverseNamespace.name === namespace.name;
|
|
396
|
+
setAttrName(isSelfLink ? 'parent' : reverseNamespace.name);
|
|
397
|
+
setReverseAttrName(isSelfLink ? 'children' : namespace.name);
|
|
398
|
+
if (isSelfLink) {
|
|
399
|
+
setRelationship('one-many');
|
|
400
|
+
}
|
|
401
|
+
}, [attrType, reverseNamespace]);
|
|
402
|
+
|
|
403
|
+
async function addAttr() {
|
|
404
|
+
if (attrType === 'blob') {
|
|
405
|
+
const attr: DBAttr = {
|
|
406
|
+
id: id(),
|
|
407
|
+
'forward-identity': [id(), namespace.name, attrName],
|
|
408
|
+
'value-type': 'blob',
|
|
409
|
+
cardinality: 'one',
|
|
410
|
+
'unique?': isUniq,
|
|
411
|
+
'index?': isIndex,
|
|
412
|
+
'required?': isRequired,
|
|
413
|
+
'checked-data-type': checkedDataType ?? undefined,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const ops = [['add-attr', attr]];
|
|
417
|
+
await db._core._reactor.pushOps(ops);
|
|
418
|
+
onClose();
|
|
419
|
+
} else {
|
|
420
|
+
// invariants
|
|
421
|
+
if (!reverseNamespace) throw new Error('No reverse namespace');
|
|
422
|
+
|
|
423
|
+
const attr: DBAttr = {
|
|
424
|
+
id: id(),
|
|
425
|
+
...relationshipConstraints[relationship],
|
|
426
|
+
'forward-identity': [id(), namespace.name, attrName],
|
|
427
|
+
'reverse-identity': [id(), reverseNamespace.name, reverseAttrName],
|
|
428
|
+
'value-type': 'ref',
|
|
429
|
+
'index?': false,
|
|
430
|
+
'required?': isRequired,
|
|
431
|
+
'on-delete': isCascadeAllowed && isCascade ? 'cascade' : undefined,
|
|
432
|
+
'on-delete-reverse':
|
|
433
|
+
isCascadeReverseAllowed && isCascadeReverse ? 'cascade' : undefined,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const ops = [['add-attr', attr]];
|
|
437
|
+
await db._core._reactor.pushOps(ops);
|
|
438
|
+
onClose();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<ActionForm className="min flex flex-col gap-4">
|
|
444
|
+
<h5 className="flex items-center gap-2 text-lg font-bold">
|
|
445
|
+
<ArrowLeftIcon className="h-4 w-4 cursor-pointer" onClick={onClose} />
|
|
446
|
+
Add an attribute
|
|
447
|
+
</h5>
|
|
448
|
+
|
|
449
|
+
<div className="flex flex-col gap-1">
|
|
450
|
+
<h6 className="text-md font-bold">Type</h6>
|
|
451
|
+
<ToggleGroup
|
|
452
|
+
ariaLabel="Text alignment"
|
|
453
|
+
selectedId={attrType}
|
|
454
|
+
items={[
|
|
455
|
+
{ id: 'blob', label: 'Data' },
|
|
456
|
+
{ id: 'ref', label: 'Link' },
|
|
457
|
+
]}
|
|
458
|
+
onChange={(item) => setAttrType(item.id as 'blob' | 'ref')}
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
{attrType === 'blob' ? (
|
|
462
|
+
<>
|
|
463
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
464
|
+
<h6 className="text-md font-bold">Name</h6>
|
|
465
|
+
<TextInput value={attrName} onChange={(n) => setAttrName(n)} />
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex flex-col gap-2">
|
|
468
|
+
<h6 className="text-md font-bold">Constraints</h6>
|
|
469
|
+
<div className="flex gap-2">
|
|
470
|
+
<Checkbox
|
|
471
|
+
disabled={constraints.require.disabled}
|
|
472
|
+
checked={isRequired}
|
|
473
|
+
onChange={(enabled) => setIsRequired(enabled)}
|
|
474
|
+
label={
|
|
475
|
+
<span>
|
|
476
|
+
<strong>Require this attribute</strong> so all entities will
|
|
477
|
+
be guaranteed to have it
|
|
478
|
+
</span>
|
|
479
|
+
}
|
|
480
|
+
title={constraints.require.message}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="flex gap-2">
|
|
484
|
+
<Checkbox
|
|
485
|
+
checked={isIndex}
|
|
486
|
+
onChange={(enabled) => setIsIndex(enabled)}
|
|
487
|
+
label={
|
|
488
|
+
<span>
|
|
489
|
+
<strong>Index this attribute</strong> to improve lookup
|
|
490
|
+
performance of values
|
|
491
|
+
</span>
|
|
492
|
+
}
|
|
493
|
+
/>
|
|
494
|
+
</div>
|
|
495
|
+
<div className="flex gap-2">
|
|
496
|
+
<Checkbox
|
|
497
|
+
checked={isUniq}
|
|
498
|
+
onChange={(enabled) => setIsUniq(enabled)}
|
|
499
|
+
label={
|
|
500
|
+
<span>
|
|
501
|
+
<strong>Enforce uniqueness</strong> so no two entities can
|
|
502
|
+
have the same value for this attribute
|
|
503
|
+
</span>
|
|
504
|
+
}
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div className="flex flex-col gap-2">
|
|
509
|
+
<h6 className="text-md font-bold">Enforce type</h6>
|
|
510
|
+
<div className="flex gap-2">
|
|
511
|
+
<Select<CheckedDataType | 'none'>
|
|
512
|
+
value={checkedDataType || 'none'}
|
|
513
|
+
onChange={(v) => {
|
|
514
|
+
if (!v) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const { value } = v;
|
|
518
|
+
if (value === 'none') {
|
|
519
|
+
setCheckedDataType(null);
|
|
520
|
+
}
|
|
521
|
+
setCheckedDataType(value as CheckedDataType);
|
|
522
|
+
}}
|
|
523
|
+
options={[
|
|
524
|
+
{
|
|
525
|
+
label: 'Any (not enforced)',
|
|
526
|
+
value: 'none',
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
label: 'String',
|
|
530
|
+
value: 'string',
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
label: 'Number',
|
|
534
|
+
value: 'number',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
label: 'Boolean',
|
|
538
|
+
value: 'boolean',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
label: 'Date',
|
|
542
|
+
value: 'date',
|
|
543
|
+
},
|
|
544
|
+
]}
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</>
|
|
549
|
+
) : attrType === 'ref' ? (
|
|
550
|
+
<>
|
|
551
|
+
<div className="flex flex-col gap-1">
|
|
552
|
+
<h6 className="text-md font-bold">Link to namespace</h6>
|
|
553
|
+
<Select
|
|
554
|
+
value={reverseNamespace?.id ?? undefined}
|
|
555
|
+
options={namespaces.map((ns) => {
|
|
556
|
+
const label =
|
|
557
|
+
ns.name + (ns.name === namespace.name ? ' (self-link)' : '');
|
|
558
|
+
return {
|
|
559
|
+
label,
|
|
560
|
+
value: ns.id,
|
|
561
|
+
};
|
|
562
|
+
})}
|
|
563
|
+
onChange={(item) => {
|
|
564
|
+
if (!item) return;
|
|
565
|
+
const ns = namespaces.find((n) => n.id === item.value);
|
|
566
|
+
|
|
567
|
+
setReverseNamespace(ns);
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<RelationshipConfigurator
|
|
573
|
+
relationship={relationship}
|
|
574
|
+
attrName={attrName}
|
|
575
|
+
reverseAttrName={reverseAttrName}
|
|
576
|
+
namespaceName={namespace.name}
|
|
577
|
+
reverseNamespaceName={reverseNamespace?.name}
|
|
578
|
+
setAttrName={setAttrName}
|
|
579
|
+
setReverseAttrName={setReverseAttrName}
|
|
580
|
+
setRelationship={setRelationship}
|
|
581
|
+
isCascadeAllowed={isCascadeAllowed}
|
|
582
|
+
isCascade={isCascade}
|
|
583
|
+
setIsCascade={setIsCascade}
|
|
584
|
+
isCascadeReverseAllowed={isCascadeReverseAllowed}
|
|
585
|
+
isCascadeReverse={isCascadeReverse}
|
|
586
|
+
setIsCascadeReverse={setIsCascadeReverse}
|
|
587
|
+
isRequired={isRequired}
|
|
588
|
+
setIsRequired={setIsRequired}
|
|
589
|
+
constraints={constraints}
|
|
590
|
+
/>
|
|
591
|
+
</>
|
|
592
|
+
) : null}
|
|
593
|
+
|
|
594
|
+
<div className="flex flex-col gap-2">
|
|
595
|
+
<ActionButton
|
|
596
|
+
type="submit"
|
|
597
|
+
label="Create attribute"
|
|
598
|
+
submitLabel="Creating attribute..."
|
|
599
|
+
errorMessage="Failed to create attribute"
|
|
600
|
+
disabled={!canSubmit}
|
|
601
|
+
className="border-gray-500 disabled:opacity-20"
|
|
602
|
+
onClick={addAttr}
|
|
603
|
+
/>
|
|
604
|
+
{linkValidation.shouldShowSelfLinkNameError ? (
|
|
605
|
+
<span className="text-red-500">
|
|
606
|
+
Self-links must have different attribute names.
|
|
607
|
+
</span>
|
|
608
|
+
) : null}
|
|
609
|
+
</div>
|
|
610
|
+
</ActionForm>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function jobWorkingStatus(job: InstantIndexingJob | null) {
|
|
615
|
+
if (
|
|
616
|
+
!job ||
|
|
617
|
+
(job.job_status !== 'processing' && job.job_status !== 'waiting')
|
|
618
|
+
) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (job.job_status === 'waiting') {
|
|
623
|
+
return 'Waiting for worker...';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!job.work_estimate) {
|
|
627
|
+
return 'Estimating work...';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const completed = Math.min(job.work_estimate, job.work_completed || 0);
|
|
631
|
+
|
|
632
|
+
const percent = Math.floor((completed / job.work_estimate) * 100);
|
|
633
|
+
|
|
634
|
+
return `${percent}% complete...`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function InvalidTriplesSample({
|
|
638
|
+
indexingJob,
|
|
639
|
+
attr,
|
|
640
|
+
onClickSample,
|
|
641
|
+
}: {
|
|
642
|
+
indexingJob: InstantIndexingJob | null;
|
|
643
|
+
attr: SchemaAttr;
|
|
644
|
+
onClickSample: (triple: InstantIndexingJobInvalidTriple) => void;
|
|
645
|
+
}) {
|
|
646
|
+
if (!indexingJob?.invalid_triples_sample?.length) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
return (
|
|
650
|
+
<div>
|
|
651
|
+
Here are the first few invalid entities we found:
|
|
652
|
+
<table className="dark:text-netural-500 mx-2 my-2 flex-1 text-left font-mono text-xs text-gray-500">
|
|
653
|
+
<thead className="bg-white text-gray-700 dark:bg-neutral-800 dark:text-white">
|
|
654
|
+
<tr>
|
|
655
|
+
<th className="pr-2">id</th>
|
|
656
|
+
<th className="max-w-fit pr-2">{attr.name}</th>
|
|
657
|
+
<th className="pr-2">type</th>
|
|
658
|
+
</tr>
|
|
659
|
+
</thead>
|
|
660
|
+
<tbody>
|
|
661
|
+
{indexingJob.invalid_triples_sample.slice(0, 3).map((t, i) => (
|
|
662
|
+
<tr
|
|
663
|
+
key={i}
|
|
664
|
+
className="cursor-pointer rounded-md px-2 whitespace-nowrap hover:bg-gray-200"
|
|
665
|
+
onClick={() => onClickSample(t)}
|
|
666
|
+
>
|
|
667
|
+
<td className="pr-2">
|
|
668
|
+
<pre>{t.entity_id}</pre>
|
|
669
|
+
</td>
|
|
670
|
+
<td className="truncate pr-2" style={{ maxWidth: '12rem' }}>
|
|
671
|
+
{JSON.stringify(t.value)}
|
|
672
|
+
</td>
|
|
673
|
+
<td className="pr-2">{t.json_type}</td>
|
|
674
|
+
</tr>
|
|
675
|
+
))}
|
|
676
|
+
</tbody>
|
|
677
|
+
</table>
|
|
678
|
+
</div>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function IndexingJobError({
|
|
683
|
+
indexingJob,
|
|
684
|
+
attr,
|
|
685
|
+
onClose,
|
|
686
|
+
}: {
|
|
687
|
+
indexingJob?: InstantIndexingJob | null;
|
|
688
|
+
attr: SchemaAttr;
|
|
689
|
+
onClose: () => void;
|
|
690
|
+
}) {
|
|
691
|
+
const { history } = useExplorerState();
|
|
692
|
+
if (!indexingJob) return;
|
|
693
|
+
if (indexingJob.error === 'missing-required-error') {
|
|
694
|
+
return (
|
|
695
|
+
<div className="mt-2 mb-2 border-l-2 border-l-red-500 pl-2">
|
|
696
|
+
<div>
|
|
697
|
+
{indexingJob.error_data?.count} <code>{attr.namespace}</code>{' '}
|
|
698
|
+
{indexingJob.error_data?.count === 1 ? 'entity does' : 'entities do'}{' '}
|
|
699
|
+
not have <code>{attr.name}</code> set.
|
|
700
|
+
</div>
|
|
701
|
+
<InvalidTriplesSample
|
|
702
|
+
indexingJob={{
|
|
703
|
+
...indexingJob,
|
|
704
|
+
invalid_triples_sample:
|
|
705
|
+
indexingJob.error_data &&
|
|
706
|
+
indexingJob.error_data['entity-ids']?.map((id) => ({
|
|
707
|
+
entity_id: String(id),
|
|
708
|
+
value: null,
|
|
709
|
+
json_type: attr.checkedDataType || 'null',
|
|
710
|
+
})),
|
|
711
|
+
}}
|
|
712
|
+
attr={attr}
|
|
713
|
+
onClickSample={(t) => {
|
|
714
|
+
history.push({
|
|
715
|
+
namespace: attr.namespace,
|
|
716
|
+
where: ['id', t.entity_id],
|
|
717
|
+
});
|
|
718
|
+
// It would be nice to have a way to minimize the dialog so you could go back
|
|
719
|
+
onClose();
|
|
720
|
+
}}
|
|
721
|
+
/>
|
|
722
|
+
</div>
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (indexingJob.error === 'triple-too-large-error') {
|
|
727
|
+
return (
|
|
728
|
+
<div className="mt-2 mb-2 border-l-2 border-l-red-500 pl-2">
|
|
729
|
+
<div>Some of the existing data is too large to index. </div>
|
|
730
|
+
<InvalidTriplesSample
|
|
731
|
+
indexingJob={indexingJob}
|
|
732
|
+
attr={attr}
|
|
733
|
+
onClickSample={(t) => {
|
|
734
|
+
history.push({
|
|
735
|
+
namespace: attr.namespace,
|
|
736
|
+
where: ['id', t.entity_id],
|
|
737
|
+
});
|
|
738
|
+
// It would be nice to have a way to minimize the dialog so you could go back
|
|
739
|
+
onClose();
|
|
740
|
+
}}
|
|
741
|
+
/>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (indexingJob.error === 'invalid-triple-error') {
|
|
747
|
+
return (
|
|
748
|
+
<div className="mt-2 mb-2 border-l-2 border-l-red-500 pl-2">
|
|
749
|
+
<div>
|
|
750
|
+
The type can't be set to {indexingJob?.checked_data_type} because some
|
|
751
|
+
data is the wrong type.
|
|
752
|
+
</div>
|
|
753
|
+
<InvalidTriplesSample
|
|
754
|
+
indexingJob={indexingJob}
|
|
755
|
+
attr={attr}
|
|
756
|
+
onClickSample={(t) => {
|
|
757
|
+
history.push({
|
|
758
|
+
namespace: attr.namespace,
|
|
759
|
+
where: ['id', t.entity_id],
|
|
760
|
+
});
|
|
761
|
+
// It would be nice to have a way to minimize the dialog so you could go back
|
|
762
|
+
onClose();
|
|
763
|
+
}}
|
|
764
|
+
/>
|
|
765
|
+
</div>
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (indexingJob.error === 'triple-not-unique-error') {
|
|
770
|
+
return (
|
|
771
|
+
<div className="mt-2 mb-2 border-l-2 border-l-red-500 pl-2">
|
|
772
|
+
<div>Some of the existing data is not unique. </div>
|
|
773
|
+
{indexingJob.invalid_unique_value != null ? (
|
|
774
|
+
<div>
|
|
775
|
+
Found{' '}
|
|
776
|
+
<span
|
|
777
|
+
className={
|
|
778
|
+
typeof indexingJob.invalid_unique_value === 'object'
|
|
779
|
+
? ''
|
|
780
|
+
: 'cursor-pointer underline'
|
|
781
|
+
}
|
|
782
|
+
onClick={
|
|
783
|
+
typeof indexingJob.invalid_unique_value === 'object'
|
|
784
|
+
? undefined
|
|
785
|
+
: () => {
|
|
786
|
+
history.push({
|
|
787
|
+
namespace: attr.namespace,
|
|
788
|
+
where: [attr.name, indexingJob.invalid_unique_value],
|
|
789
|
+
});
|
|
790
|
+
onClose();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
>
|
|
794
|
+
multiple entities with value{' '}
|
|
795
|
+
<code>{JSON.stringify(indexingJob.invalid_unique_value)}</code>
|
|
796
|
+
</span>
|
|
797
|
+
.
|
|
798
|
+
</div>
|
|
799
|
+
) : null}
|
|
800
|
+
<InvalidTriplesSample
|
|
801
|
+
indexingJob={indexingJob}
|
|
802
|
+
attr={attr}
|
|
803
|
+
onClickSample={(t) => {
|
|
804
|
+
history.push({
|
|
805
|
+
namespace: attr.namespace,
|
|
806
|
+
where: ['id', t.entity_id],
|
|
807
|
+
});
|
|
808
|
+
onClose();
|
|
809
|
+
}}
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
// Catchall for unexpected errors
|
|
815
|
+
if (indexingJob.error) {
|
|
816
|
+
return (
|
|
817
|
+
<div className="mt-2 mb-2 space-y-2 border-l-2 border-l-red-500 pl-2">
|
|
818
|
+
<div>
|
|
819
|
+
An unexpected error occured while changing constraints. Please share
|
|
820
|
+
these details with the Instant team:
|
|
821
|
+
</div>
|
|
822
|
+
<pre>id: "{indexingJob.id}"</pre>
|
|
823
|
+
</div>
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function RelationshipConfigurator({
|
|
829
|
+
attrName,
|
|
830
|
+
reverseAttrName,
|
|
831
|
+
namespaceName,
|
|
832
|
+
reverseNamespaceName,
|
|
833
|
+
relationship,
|
|
834
|
+
setAttrName,
|
|
835
|
+
setReverseAttrName,
|
|
836
|
+
setRelationship,
|
|
837
|
+
isCascade,
|
|
838
|
+
setIsCascade,
|
|
839
|
+
isCascadeAllowed,
|
|
840
|
+
isCascadeReverse,
|
|
841
|
+
setIsCascadeReverse,
|
|
842
|
+
isCascadeReverseAllowed,
|
|
843
|
+
isRequired,
|
|
844
|
+
setIsRequired,
|
|
845
|
+
constraints,
|
|
846
|
+
}: {
|
|
847
|
+
relationship: RelationshipKinds;
|
|
848
|
+
reverseNamespaceName: string | undefined;
|
|
849
|
+
attrName: string;
|
|
850
|
+
reverseAttrName: string;
|
|
851
|
+
namespaceName: string;
|
|
852
|
+
|
|
853
|
+
setAttrName: (n: string) => void;
|
|
854
|
+
setReverseAttrName: (n: string) => void;
|
|
855
|
+
setRelationship: (n: RelationshipKinds) => void;
|
|
856
|
+
|
|
857
|
+
isCascadeAllowed: boolean;
|
|
858
|
+
isCascade: boolean;
|
|
859
|
+
setIsCascade: (n: boolean) => void;
|
|
860
|
+
|
|
861
|
+
isCascadeReverseAllowed: boolean;
|
|
862
|
+
isCascadeReverse: boolean;
|
|
863
|
+
setIsCascadeReverse: (n: boolean) => void;
|
|
864
|
+
|
|
865
|
+
isRequired: boolean;
|
|
866
|
+
setIsRequired: (n: boolean) => void;
|
|
867
|
+
constraints: SystemConstraints;
|
|
868
|
+
}) {
|
|
869
|
+
const isFullLink = attrName && reverseNamespaceName && reverseAttrName;
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<>
|
|
873
|
+
<div className="flex flex-col gap-4 md:flex-row md:gap-2">
|
|
874
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
875
|
+
<h6 className="text-md font-bold">Forward attribute name</h6>
|
|
876
|
+
<TextInput
|
|
877
|
+
disabled={constraints.attr.disabled}
|
|
878
|
+
title={constraints.attr.message}
|
|
879
|
+
value={attrName}
|
|
880
|
+
onChange={(n) => setAttrName(n)}
|
|
881
|
+
/>
|
|
882
|
+
<div className="rounded-xs py-0.5 text-xs text-gray-500 dark:text-neutral-400">
|
|
883
|
+
{isFullLink ? (
|
|
884
|
+
<>
|
|
885
|
+
<strong>
|
|
886
|
+
{namespaceName}.{attrName}
|
|
887
|
+
</strong>{' '}
|
|
888
|
+
will link to <strong>{reverseNamespaceName}</strong>
|
|
889
|
+
</>
|
|
890
|
+
) : (
|
|
891
|
+
<> </>
|
|
892
|
+
)}
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
|
|
896
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
897
|
+
<h6 className="text-md font-bold">Reverse attribute name</h6>
|
|
898
|
+
<TextInput
|
|
899
|
+
disabled={constraints.attr.disabled}
|
|
900
|
+
title={constraints.attr.message}
|
|
901
|
+
value={reverseAttrName}
|
|
902
|
+
onChange={(n) => setReverseAttrName(n)}
|
|
903
|
+
/>
|
|
904
|
+
<div className="rounded-xs py-0.5 text-xs text-gray-500 dark:text-neutral-400">
|
|
905
|
+
{isFullLink ? (
|
|
906
|
+
<>
|
|
907
|
+
<strong>
|
|
908
|
+
{reverseNamespaceName}.{reverseAttrName}
|
|
909
|
+
</strong>{' '}
|
|
910
|
+
will link to <strong>{namespaceName}</strong>
|
|
911
|
+
</>
|
|
912
|
+
) : (
|
|
913
|
+
<> </>
|
|
914
|
+
)}
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<div className="flex flex-col gap-1">
|
|
920
|
+
<h6 className="text-md font-bold">Relationship</h6>
|
|
921
|
+
<RelationshipSelect
|
|
922
|
+
disabled={!isFullLink || constraints.attr.disabled}
|
|
923
|
+
value={relationship}
|
|
924
|
+
onChange={(v) => {
|
|
925
|
+
setRelationship(v.value);
|
|
926
|
+
}}
|
|
927
|
+
namespace={namespaceName}
|
|
928
|
+
reverseNamespace={reverseNamespaceName ?? ''}
|
|
929
|
+
attr={attrName}
|
|
930
|
+
reverseAttr={reverseAttrName}
|
|
931
|
+
title={
|
|
932
|
+
constraints.attr.disabled ? constraints.attr.message : undefined
|
|
933
|
+
}
|
|
934
|
+
/>
|
|
935
|
+
<div
|
|
936
|
+
className={
|
|
937
|
+
'text-xs wrap-break-word text-gray-500 dark:text-neutral-400'
|
|
938
|
+
}
|
|
939
|
+
>
|
|
940
|
+
{isFullLink ? (
|
|
941
|
+
relationshipDescriptions[relationship](
|
|
942
|
+
namespaceName,
|
|
943
|
+
reverseNamespaceName,
|
|
944
|
+
attrName,
|
|
945
|
+
reverseAttrName,
|
|
946
|
+
)
|
|
947
|
+
) : (
|
|
948
|
+
<> </>
|
|
949
|
+
)}
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<div className="flex gap-2">
|
|
954
|
+
<Checkbox
|
|
955
|
+
checked={isCascadeAllowed && isCascade}
|
|
956
|
+
disabled={!isCascadeAllowed || constraints.attr.disabled}
|
|
957
|
+
onChange={setIsCascade}
|
|
958
|
+
title={constraints.attr.message}
|
|
959
|
+
label={
|
|
960
|
+
<span className="dark:text-neutral-200">
|
|
961
|
+
<div>
|
|
962
|
+
<strong>
|
|
963
|
+
Cascade Delete {reverseNamespaceName} → {namespaceName}
|
|
964
|
+
</strong>
|
|
965
|
+
</div>
|
|
966
|
+
When a <strong>{reverseNamespaceName}</strong> entity is deleted,
|
|
967
|
+
all linked <strong>{namespaceName}</strong> will be deleted
|
|
968
|
+
automatically
|
|
969
|
+
</span>
|
|
970
|
+
}
|
|
971
|
+
/>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<div className="flex gap-2">
|
|
975
|
+
<Checkbox
|
|
976
|
+
checked={isCascadeReverseAllowed && isCascadeReverse}
|
|
977
|
+
disabled={!isCascadeReverseAllowed || constraints.attr.disabled}
|
|
978
|
+
onChange={setIsCascadeReverse}
|
|
979
|
+
title={constraints.attr.message}
|
|
980
|
+
label={
|
|
981
|
+
<span className="dark:text-neutral-200">
|
|
982
|
+
<div>
|
|
983
|
+
<strong>
|
|
984
|
+
Cascade Delete {namespaceName} → {reverseNamespaceName}
|
|
985
|
+
</strong>
|
|
986
|
+
</div>
|
|
987
|
+
When a <strong>{namespaceName}</strong> entity is deleted, all
|
|
988
|
+
linked <strong>{reverseNamespaceName}</strong> will be deleted
|
|
989
|
+
automatically
|
|
990
|
+
</span>
|
|
991
|
+
}
|
|
992
|
+
/>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<div className="flex flex-col gap-1">
|
|
996
|
+
<h6 className="text-md font-bold">Constraints</h6>
|
|
997
|
+
<div className="flex gap-2">
|
|
998
|
+
<Checkbox
|
|
999
|
+
disabled={constraints.require.disabled}
|
|
1000
|
+
title={constraints.require.message}
|
|
1001
|
+
checked={isRequired}
|
|
1002
|
+
onChange={(enabled) => setIsRequired(enabled)}
|
|
1003
|
+
label={
|
|
1004
|
+
<span>
|
|
1005
|
+
<strong>Require this attribute</strong> so all entities will be
|
|
1006
|
+
guaranteed to have it
|
|
1007
|
+
</span>
|
|
1008
|
+
}
|
|
1009
|
+
/>
|
|
1010
|
+
</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
</>
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function RelationshipSelect({
|
|
1017
|
+
value,
|
|
1018
|
+
disabled,
|
|
1019
|
+
onChange,
|
|
1020
|
+
namespace,
|
|
1021
|
+
attr,
|
|
1022
|
+
reverseNamespace,
|
|
1023
|
+
reverseAttr,
|
|
1024
|
+
title,
|
|
1025
|
+
}: {
|
|
1026
|
+
disabled?: boolean;
|
|
1027
|
+
value: RelationshipKinds;
|
|
1028
|
+
onChange: (v: { value: RelationshipKinds; label: string }) => void;
|
|
1029
|
+
namespace: string;
|
|
1030
|
+
attr: string;
|
|
1031
|
+
reverseNamespace: string;
|
|
1032
|
+
reverseAttr: string;
|
|
1033
|
+
title?: string;
|
|
1034
|
+
}) {
|
|
1035
|
+
return (
|
|
1036
|
+
<Select
|
|
1037
|
+
disabled={disabled}
|
|
1038
|
+
value={value}
|
|
1039
|
+
onChange={(v) => {
|
|
1040
|
+
if (!v) return;
|
|
1041
|
+
|
|
1042
|
+
onChange(v as { value: RelationshipKinds; label: string });
|
|
1043
|
+
}}
|
|
1044
|
+
options={[
|
|
1045
|
+
{
|
|
1046
|
+
label: 'Many-to-many',
|
|
1047
|
+
value: 'many-many',
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
label: 'One-to-one',
|
|
1051
|
+
value: 'one-one',
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
label: `${namespace} has-many ${
|
|
1055
|
+
attr || '---'
|
|
1056
|
+
} / ${reverseNamespace} has-one ${reverseAttr || '---'}`,
|
|
1057
|
+
value: 'many-one',
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
label: `${namespace} has-one ${
|
|
1061
|
+
attr || '---'
|
|
1062
|
+
} / ${reverseNamespace} has-many ${reverseAttr || '---'}`,
|
|
1063
|
+
value: 'one-many',
|
|
1064
|
+
},
|
|
1065
|
+
]}
|
|
1066
|
+
title={title}
|
|
1067
|
+
/>
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const relationshipDescriptions: Record<
|
|
1072
|
+
RelationshipKinds,
|
|
1073
|
+
(f: string, r: string, fa: string, ra: string) => ReactNode
|
|
1074
|
+
> = {
|
|
1075
|
+
'many-many': (fn, rn, fa, ra) => (
|
|
1076
|
+
<>
|
|
1077
|
+
<strong>{fn}</strong> can have many <strong>{fa}</strong>, and{' '}
|
|
1078
|
+
<strong>{rn}</strong> can be associated with more than one{' '}
|
|
1079
|
+
<strong>{ra}</strong>
|
|
1080
|
+
</>
|
|
1081
|
+
),
|
|
1082
|
+
'one-one': (fn, rn, fa, ra) => (
|
|
1083
|
+
<>
|
|
1084
|
+
<strong>{fn}</strong> can have only one <strong>{fa}</strong>, and a{' '}
|
|
1085
|
+
<strong>{rn}</strong> can only have one <strong>{ra}</strong>
|
|
1086
|
+
</>
|
|
1087
|
+
),
|
|
1088
|
+
'many-one': (fn, rn, fa, ra) => (
|
|
1089
|
+
<>
|
|
1090
|
+
<strong>{fn}</strong> can have many <strong>{fa}</strong>, but{' '}
|
|
1091
|
+
<strong>{rn}</strong> can only have one <strong>{ra}</strong>
|
|
1092
|
+
</>
|
|
1093
|
+
),
|
|
1094
|
+
'one-many': (fn, rn, fa, ra) => (
|
|
1095
|
+
<>
|
|
1096
|
+
<strong>{fn}</strong> can have only one <strong>{fa}</strong>, but{' '}
|
|
1097
|
+
<strong>{rn}</strong> can be associated with more than one{' '}
|
|
1098
|
+
<strong>{ra}</strong>
|
|
1099
|
+
</>
|
|
1100
|
+
),
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
async function updateRequired({
|
|
1104
|
+
appId,
|
|
1105
|
+
attr,
|
|
1106
|
+
isRequired,
|
|
1107
|
+
authToken,
|
|
1108
|
+
setIndexingJob,
|
|
1109
|
+
stopFetchLoop,
|
|
1110
|
+
apiURI,
|
|
1111
|
+
}: {
|
|
1112
|
+
appId: string;
|
|
1113
|
+
attr: SchemaAttr;
|
|
1114
|
+
isRequired: boolean;
|
|
1115
|
+
authToken: string | undefined;
|
|
1116
|
+
setIndexingJob: (job: InstantIndexingJob) => void;
|
|
1117
|
+
apiURI: string;
|
|
1118
|
+
stopFetchLoop: MutableRefObject<null | (() => void)>;
|
|
1119
|
+
}) {
|
|
1120
|
+
if (!authToken || isRequired === attr.isRequired) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
stopFetchLoop.current?.();
|
|
1124
|
+
const friendlyName = `${attr.namespace}.${attr.name}`;
|
|
1125
|
+
try {
|
|
1126
|
+
const job = await createJob(
|
|
1127
|
+
{
|
|
1128
|
+
appId,
|
|
1129
|
+
attrId: attr.id,
|
|
1130
|
+
jobType: isRequired ? 'required' : 'remove-required',
|
|
1131
|
+
apiURI,
|
|
1132
|
+
},
|
|
1133
|
+
authToken,
|
|
1134
|
+
);
|
|
1135
|
+
setIndexingJob(job);
|
|
1136
|
+
const fetchLoop = jobFetchLoop(appId, job.id, authToken, apiURI);
|
|
1137
|
+
stopFetchLoop.current = fetchLoop.stop;
|
|
1138
|
+
const finishedJob = await fetchLoop.start((data, error) => {
|
|
1139
|
+
if (error) {
|
|
1140
|
+
errorToast(`Error while marking ${friendlyName} as required.`);
|
|
1141
|
+
}
|
|
1142
|
+
if (data) {
|
|
1143
|
+
setIndexingJob(data);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
if (finishedJob) {
|
|
1147
|
+
if (finishedJob.job_status === 'completed') {
|
|
1148
|
+
successToast(
|
|
1149
|
+
isRequired
|
|
1150
|
+
? `Marked ${friendlyName} as required.`
|
|
1151
|
+
: `Marked ${friendlyName} as optional.`,
|
|
1152
|
+
);
|
|
1153
|
+
return 'completed';
|
|
1154
|
+
}
|
|
1155
|
+
if (finishedJob.job_status === 'canceled') {
|
|
1156
|
+
errorToast('Marking required was canceled.');
|
|
1157
|
+
return 'canceled';
|
|
1158
|
+
}
|
|
1159
|
+
if (finishedJob.job_status === 'errored') {
|
|
1160
|
+
if (finishedJob.error === 'invalid-triple-error') {
|
|
1161
|
+
errorToast(`Found invalid data while updating ${friendlyName}.`);
|
|
1162
|
+
} else {
|
|
1163
|
+
errorToast(`Encountered an error while updating ${friendlyName}.`);
|
|
1164
|
+
}
|
|
1165
|
+
return 'errored';
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
console.error(e);
|
|
1170
|
+
errorToast(`Unexpected error while updating ${friendlyName}`);
|
|
1171
|
+
return 'errored';
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
type BlobConstraintControlComponent<V> = (props: {
|
|
1176
|
+
pendingJob?: PendingJob;
|
|
1177
|
+
runningJob?: InstantIndexingJob;
|
|
1178
|
+
value: V;
|
|
1179
|
+
setValue: (v: V) => void;
|
|
1180
|
+
disabled: boolean;
|
|
1181
|
+
disabledReason?: string;
|
|
1182
|
+
attr: SchemaAttr;
|
|
1183
|
+
}) => JSX.Element;
|
|
1184
|
+
|
|
1185
|
+
const EditCheckedDataTypeControl: BlobConstraintControlComponent<
|
|
1186
|
+
CheckedDataType | 'any'
|
|
1187
|
+
> = ({
|
|
1188
|
+
pendingJob,
|
|
1189
|
+
runningJob,
|
|
1190
|
+
value,
|
|
1191
|
+
setValue,
|
|
1192
|
+
disabled,
|
|
1193
|
+
disabledReason,
|
|
1194
|
+
attr,
|
|
1195
|
+
}) => {
|
|
1196
|
+
const notRunning = !runningJob || jobIsCompleted(runningJob);
|
|
1197
|
+
const closeDialog = useClose();
|
|
1198
|
+
|
|
1199
|
+
// Revert to previous value if job errored
|
|
1200
|
+
useEffect(() => {
|
|
1201
|
+
if (runningJob && jobIsErrored(runningJob)) {
|
|
1202
|
+
setValue(attr.checkedDataType || 'any');
|
|
1203
|
+
}
|
|
1204
|
+
}, [runningJob]);
|
|
1205
|
+
|
|
1206
|
+
return (
|
|
1207
|
+
<>
|
|
1208
|
+
<div className="flex flex-col gap-2">
|
|
1209
|
+
<h6 className="text-md font-bold">
|
|
1210
|
+
Enforce type{' '}
|
|
1211
|
+
<InfoTip>
|
|
1212
|
+
<div className="w-48 text-sm">
|
|
1213
|
+
Checks the type on all existing entities and enforces the type
|
|
1214
|
+
when entities are created or updated.
|
|
1215
|
+
</div>
|
|
1216
|
+
</InfoTip>
|
|
1217
|
+
</h6>
|
|
1218
|
+
</div>
|
|
1219
|
+
<div className="flex items-center gap-2">
|
|
1220
|
+
<Select
|
|
1221
|
+
className={cn(
|
|
1222
|
+
pendingJob &&
|
|
1223
|
+
'border-[#606AF4] ring-1 ring-[#606AF4] ring-inset focus:ring-[#606AF4]',
|
|
1224
|
+
)}
|
|
1225
|
+
disabled={disabled || (runningJob && !jobIsCompleted(runningJob))}
|
|
1226
|
+
title={disabled ? disabledReason : undefined}
|
|
1227
|
+
value={value}
|
|
1228
|
+
onChange={(v) => {
|
|
1229
|
+
if (!v) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
setValue(v.value as CheckedDataType | 'any');
|
|
1233
|
+
}}
|
|
1234
|
+
options={[
|
|
1235
|
+
{
|
|
1236
|
+
label: 'Any (not enforced)',
|
|
1237
|
+
value: 'any',
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
label: 'String',
|
|
1241
|
+
value: 'string',
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
label: 'Number',
|
|
1245
|
+
value: 'number',
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
label: 'Boolean',
|
|
1249
|
+
value: 'boolean',
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
label: 'Date',
|
|
1253
|
+
value: 'date',
|
|
1254
|
+
},
|
|
1255
|
+
]}
|
|
1256
|
+
/>
|
|
1257
|
+
{pendingJob && notRunning && (
|
|
1258
|
+
<ArrowUturnLeftIcon
|
|
1259
|
+
onClick={() => {
|
|
1260
|
+
setValue(attr.checkedDataType || 'any');
|
|
1261
|
+
}}
|
|
1262
|
+
height="1.2rem"
|
|
1263
|
+
className="cursor-pointer pr-2 text-[#606AF4]"
|
|
1264
|
+
/>
|
|
1265
|
+
)}
|
|
1266
|
+
</div>
|
|
1267
|
+
{runningJob && jobIsErrored(runningJob) && (
|
|
1268
|
+
<IndexingJobError
|
|
1269
|
+
indexingJob={runningJob}
|
|
1270
|
+
attr={attr}
|
|
1271
|
+
onClose={closeDialog}
|
|
1272
|
+
/>
|
|
1273
|
+
)}
|
|
1274
|
+
</>
|
|
1275
|
+
);
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const EditRequiredControl: BlobConstraintControlComponent<boolean> = ({
|
|
1279
|
+
pendingJob,
|
|
1280
|
+
runningJob,
|
|
1281
|
+
value,
|
|
1282
|
+
setValue,
|
|
1283
|
+
disabled,
|
|
1284
|
+
disabledReason,
|
|
1285
|
+
attr,
|
|
1286
|
+
}) => {
|
|
1287
|
+
const closeDialog = useClose();
|
|
1288
|
+
|
|
1289
|
+
// If job is errored, revert the value
|
|
1290
|
+
useEffect(() => {
|
|
1291
|
+
if (runningJob && jobIsErrored(runningJob)) {
|
|
1292
|
+
setValue(attr.isRequired || false);
|
|
1293
|
+
}
|
|
1294
|
+
}, [runningJob]);
|
|
1295
|
+
|
|
1296
|
+
return (
|
|
1297
|
+
<>
|
|
1298
|
+
<div className="flex justify-between">
|
|
1299
|
+
<Checkbox
|
|
1300
|
+
disabled={disabled || (runningJob && !jobIsCompleted(runningJob))}
|
|
1301
|
+
title={disabled ? disabledReason : undefined}
|
|
1302
|
+
checked={value}
|
|
1303
|
+
onChange={(enabled) => setValue(enabled)}
|
|
1304
|
+
label={
|
|
1305
|
+
<span
|
|
1306
|
+
className={cn(
|
|
1307
|
+
disabled || (runningJob && !jobIsCompleted(runningJob))
|
|
1308
|
+
? 'cursor-default'
|
|
1309
|
+
: 'cursor-pointer',
|
|
1310
|
+
pendingJob && 'text-[#606AF4]',
|
|
1311
|
+
)}
|
|
1312
|
+
>
|
|
1313
|
+
<strong>Require this attribute</strong> so all entities will be
|
|
1314
|
+
guaranteed to have it
|
|
1315
|
+
</span>
|
|
1316
|
+
}
|
|
1317
|
+
/>
|
|
1318
|
+
{pendingJob && (
|
|
1319
|
+
<ArrowUturnLeftIcon
|
|
1320
|
+
onClick={() => {
|
|
1321
|
+
setValue(!value);
|
|
1322
|
+
}}
|
|
1323
|
+
height="1.2rem"
|
|
1324
|
+
className="cursor-pointer pr-2 text-[#606AF4]"
|
|
1325
|
+
/>
|
|
1326
|
+
)}
|
|
1327
|
+
</div>
|
|
1328
|
+
{runningJob && jobIsErrored(runningJob) && (
|
|
1329
|
+
<IndexingJobError
|
|
1330
|
+
indexingJob={runningJob}
|
|
1331
|
+
attr={attr}
|
|
1332
|
+
onClose={closeDialog}
|
|
1333
|
+
/>
|
|
1334
|
+
)}
|
|
1335
|
+
</>
|
|
1336
|
+
);
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const EditIndexedControl: BlobConstraintControlComponent<boolean> = ({
|
|
1340
|
+
pendingJob,
|
|
1341
|
+
runningJob,
|
|
1342
|
+
value,
|
|
1343
|
+
setValue,
|
|
1344
|
+
disabled,
|
|
1345
|
+
disabledReason,
|
|
1346
|
+
attr,
|
|
1347
|
+
}) => {
|
|
1348
|
+
const closeDialog = useClose();
|
|
1349
|
+
|
|
1350
|
+
// If job is errored, revert the value
|
|
1351
|
+
useEffect(() => {
|
|
1352
|
+
if (runningJob && jobIsErrored(runningJob)) {
|
|
1353
|
+
setValue(attr.isIndex);
|
|
1354
|
+
}
|
|
1355
|
+
}, [runningJob]);
|
|
1356
|
+
|
|
1357
|
+
return (
|
|
1358
|
+
<>
|
|
1359
|
+
<div className="flex justify-between">
|
|
1360
|
+
<Checkbox
|
|
1361
|
+
disabled={disabled || (runningJob && !jobIsCompleted(runningJob))}
|
|
1362
|
+
title={disabled ? disabledReason : undefined}
|
|
1363
|
+
checked={value}
|
|
1364
|
+
onChange={(enabled) => setValue(enabled)}
|
|
1365
|
+
label={
|
|
1366
|
+
<span
|
|
1367
|
+
className={cn(
|
|
1368
|
+
disabled || (runningJob && !jobIsCompleted(runningJob))
|
|
1369
|
+
? 'cursor-default'
|
|
1370
|
+
: 'cursor-pointer',
|
|
1371
|
+
pendingJob && 'text-[#606AF4]',
|
|
1372
|
+
)}
|
|
1373
|
+
>
|
|
1374
|
+
<strong>Index this attribute</strong> to improve lookup
|
|
1375
|
+
performance of values
|
|
1376
|
+
</span>
|
|
1377
|
+
}
|
|
1378
|
+
/>
|
|
1379
|
+
{pendingJob && (
|
|
1380
|
+
<ArrowUturnLeftIcon
|
|
1381
|
+
onClick={() => {
|
|
1382
|
+
setValue(!value);
|
|
1383
|
+
}}
|
|
1384
|
+
height="1.2rem"
|
|
1385
|
+
className="cursor-pointer pr-2 text-[#606AF4]"
|
|
1386
|
+
/>
|
|
1387
|
+
)}
|
|
1388
|
+
</div>
|
|
1389
|
+
{runningJob && jobIsErrored(runningJob) && (
|
|
1390
|
+
<IndexingJobError
|
|
1391
|
+
indexingJob={runningJob}
|
|
1392
|
+
attr={attr}
|
|
1393
|
+
onClose={closeDialog}
|
|
1394
|
+
/>
|
|
1395
|
+
)}
|
|
1396
|
+
</>
|
|
1397
|
+
);
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
const EditUniqueControl: BlobConstraintControlComponent<boolean> = ({
|
|
1401
|
+
pendingJob,
|
|
1402
|
+
runningJob,
|
|
1403
|
+
value,
|
|
1404
|
+
setValue,
|
|
1405
|
+
disabled,
|
|
1406
|
+
disabledReason,
|
|
1407
|
+
attr,
|
|
1408
|
+
}) => {
|
|
1409
|
+
const closeDialog = useClose();
|
|
1410
|
+
|
|
1411
|
+
// If job is errored, revert the value
|
|
1412
|
+
useEffect(() => {
|
|
1413
|
+
if (runningJob && jobIsErrored(runningJob)) {
|
|
1414
|
+
setValue(attr.isUniq);
|
|
1415
|
+
}
|
|
1416
|
+
}, [runningJob]);
|
|
1417
|
+
|
|
1418
|
+
return (
|
|
1419
|
+
<>
|
|
1420
|
+
<div className="flex justify-between">
|
|
1421
|
+
<Checkbox
|
|
1422
|
+
disabled={disabled || (runningJob && !jobIsCompleted(runningJob))}
|
|
1423
|
+
title={disabled ? disabledReason : undefined}
|
|
1424
|
+
checked={value}
|
|
1425
|
+
onChange={(enabled) => setValue(enabled)}
|
|
1426
|
+
label={
|
|
1427
|
+
<span
|
|
1428
|
+
className={cn(
|
|
1429
|
+
disabled || (runningJob && !jobIsCompleted(runningJob))
|
|
1430
|
+
? 'cursor-default'
|
|
1431
|
+
: 'cursor-pointer',
|
|
1432
|
+
pendingJob && 'text-[#606AF4]',
|
|
1433
|
+
)}
|
|
1434
|
+
>
|
|
1435
|
+
<strong>Enforce uniqueness</strong> so no two entities can have
|
|
1436
|
+
the same value for this attribute
|
|
1437
|
+
</span>
|
|
1438
|
+
}
|
|
1439
|
+
/>
|
|
1440
|
+
{pendingJob && (
|
|
1441
|
+
<ArrowUturnLeftIcon
|
|
1442
|
+
onClick={() => {
|
|
1443
|
+
setValue(!value);
|
|
1444
|
+
}}
|
|
1445
|
+
height="1.2rem"
|
|
1446
|
+
className="cursor-pointer pr-2 text-[#606AF4]"
|
|
1447
|
+
/>
|
|
1448
|
+
)}
|
|
1449
|
+
</div>
|
|
1450
|
+
{runningJob && jobIsErrored(runningJob) && (
|
|
1451
|
+
<IndexingJobError
|
|
1452
|
+
indexingJob={runningJob}
|
|
1453
|
+
attr={attr}
|
|
1454
|
+
onClose={closeDialog}
|
|
1455
|
+
/>
|
|
1456
|
+
)}
|
|
1457
|
+
</>
|
|
1458
|
+
);
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
const EditBlobConstraints = ({
|
|
1462
|
+
appId,
|
|
1463
|
+
attr,
|
|
1464
|
+
constraints,
|
|
1465
|
+
}: {
|
|
1466
|
+
appId: string;
|
|
1467
|
+
attr: SchemaAttr;
|
|
1468
|
+
constraints: SystemConstraints;
|
|
1469
|
+
}) => {
|
|
1470
|
+
const [requiredChecked, setRequiredChecked] = useState(
|
|
1471
|
+
attr.isRequired || false,
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
const [indexedChecked, setIndexedChecked] = useState(attr.isIndex);
|
|
1475
|
+
|
|
1476
|
+
const [uniqueChecked, setUniqueChecked] = useState(attr.isUniq);
|
|
1477
|
+
|
|
1478
|
+
const [checkedDataType, setCheckedDataType] = useState<
|
|
1479
|
+
CheckedDataType | 'any'
|
|
1480
|
+
>(attr.checkedDataType || 'any');
|
|
1481
|
+
|
|
1482
|
+
const explorerProps = useExplorerProps();
|
|
1483
|
+
|
|
1484
|
+
const { isPending, pending, apply, isRunning, running, progress } =
|
|
1485
|
+
useEditBlobConstraints({
|
|
1486
|
+
attr,
|
|
1487
|
+
appId,
|
|
1488
|
+
token: explorerProps.adminToken,
|
|
1489
|
+
isRequired: requiredChecked,
|
|
1490
|
+
isIndexed: indexedChecked,
|
|
1491
|
+
isUnique: uniqueChecked,
|
|
1492
|
+
checkedDataType,
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
return (
|
|
1496
|
+
<div>
|
|
1497
|
+
<div className="flex flex-col gap-2">
|
|
1498
|
+
<h6 className="text-md font-bold">Constraints</h6>
|
|
1499
|
+
<EditRequiredControl
|
|
1500
|
+
pendingJob={pending.require}
|
|
1501
|
+
runningJob={running.require}
|
|
1502
|
+
value={requiredChecked}
|
|
1503
|
+
setValue={setRequiredChecked}
|
|
1504
|
+
disabled={constraints.require.disabled}
|
|
1505
|
+
disabledReason={constraints.require.message}
|
|
1506
|
+
attr={attr}
|
|
1507
|
+
/>
|
|
1508
|
+
<EditIndexedControl
|
|
1509
|
+
pendingJob={pending.index}
|
|
1510
|
+
runningJob={running.index}
|
|
1511
|
+
value={indexedChecked}
|
|
1512
|
+
setValue={setIndexedChecked}
|
|
1513
|
+
disabled={constraints.attr.disabled}
|
|
1514
|
+
disabledReason={constraints.attr.message}
|
|
1515
|
+
attr={attr}
|
|
1516
|
+
/>
|
|
1517
|
+
<EditUniqueControl
|
|
1518
|
+
pendingJob={pending.unique}
|
|
1519
|
+
runningJob={running.unique}
|
|
1520
|
+
value={uniqueChecked}
|
|
1521
|
+
setValue={setUniqueChecked}
|
|
1522
|
+
disabled={constraints.attr.disabled}
|
|
1523
|
+
disabledReason={constraints.attr.message}
|
|
1524
|
+
attr={attr}
|
|
1525
|
+
/>
|
|
1526
|
+
<EditCheckedDataTypeControl
|
|
1527
|
+
pendingJob={pending.type}
|
|
1528
|
+
runningJob={running.type}
|
|
1529
|
+
value={checkedDataType}
|
|
1530
|
+
setValue={setCheckedDataType}
|
|
1531
|
+
disabled={constraints.attr.disabled}
|
|
1532
|
+
disabledReason={constraints.attr.message}
|
|
1533
|
+
attr={attr}
|
|
1534
|
+
/>
|
|
1535
|
+
<ProgressButton
|
|
1536
|
+
loading={!!progress}
|
|
1537
|
+
percentage={progress || 0}
|
|
1538
|
+
variant={isPending || isRunning ? 'primary' : 'secondary'}
|
|
1539
|
+
// Switching from primary <-> secondary changes height without this
|
|
1540
|
+
className="border"
|
|
1541
|
+
onClick={() => apply()}
|
|
1542
|
+
disabled={!isPending && !progress}
|
|
1543
|
+
>
|
|
1544
|
+
{isRunning ? 'Updating Constraints...' : 'Update Constraints'}
|
|
1545
|
+
</ProgressButton>
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
);
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
function EditAttrForm({
|
|
1552
|
+
db,
|
|
1553
|
+
attr,
|
|
1554
|
+
onClose,
|
|
1555
|
+
constraints,
|
|
1556
|
+
}: {
|
|
1557
|
+
db: InstantReactWebDatabase<any>;
|
|
1558
|
+
attr: SchemaAttr;
|
|
1559
|
+
onClose: () => void;
|
|
1560
|
+
constraints: SystemConstraints;
|
|
1561
|
+
}) {
|
|
1562
|
+
const props = useExplorerProps();
|
|
1563
|
+
const appId = props.appId;
|
|
1564
|
+
const { mutate } = useSWRConfig();
|
|
1565
|
+
const [screen, setScreen] = useState<{ type: 'main' } | { type: 'delete' }>({
|
|
1566
|
+
type: 'main',
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
const [attrName, setAttrName] = useState(attr.linkConfig.forward.attr);
|
|
1570
|
+
const [reverseAttrName, setReverseAttrName] = useState(
|
|
1571
|
+
attr.linkConfig.reverse?.attr,
|
|
1572
|
+
);
|
|
1573
|
+
const [relationship, setRelationship] = useState<RelationshipKinds>(() => {
|
|
1574
|
+
const relKey = `${attr.cardinality}-${attr.isUniq}`;
|
|
1575
|
+
const relKind = relationshipConstraintsInverse[relKey];
|
|
1576
|
+
return relKind;
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const explorerProps = useExplorerProps();
|
|
1580
|
+
|
|
1581
|
+
const [isCascade, setIsCascade] = useState(() => attr.onDelete === 'cascade');
|
|
1582
|
+
|
|
1583
|
+
const [isCascadeReverse, setIsCascadeReverse] = useState(
|
|
1584
|
+
() => attr.onDeleteReverse === 'cascade',
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
const [isRequired, setIsRequired] = useState(attr.isRequired || false);
|
|
1588
|
+
const [wasRequired, _] = useState(isRequired);
|
|
1589
|
+
|
|
1590
|
+
const [indexingJob, setIndexingJob] = useState<InstantIndexingJob | null>(
|
|
1591
|
+
null,
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
const stopFetchLoop = useRef<null | (() => void)>(null);
|
|
1595
|
+
const closeDialog = useClose();
|
|
1596
|
+
|
|
1597
|
+
useEffect(() => {
|
|
1598
|
+
return () => stopFetchLoop.current?.();
|
|
1599
|
+
}, [stopFetchLoop]);
|
|
1600
|
+
|
|
1601
|
+
const isCascadeAllowed =
|
|
1602
|
+
relationship === 'one-one' || relationship === 'one-many';
|
|
1603
|
+
const isCascadeReverseAllowed =
|
|
1604
|
+
relationship === 'one-one' || relationship === 'many-one';
|
|
1605
|
+
|
|
1606
|
+
const linkValidation = validateLink({
|
|
1607
|
+
attrName,
|
|
1608
|
+
reverseAttrName,
|
|
1609
|
+
namespaceName: attr.linkConfig.forward.namespace,
|
|
1610
|
+
reverseNamespaceName: attr.linkConfig.reverse?.namespace,
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
async function updateRef() {
|
|
1614
|
+
if (!attr.linkConfig.reverse) {
|
|
1615
|
+
throw new Error('No reverse link config');
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if (isRequired !== wasRequired) {
|
|
1619
|
+
const res = await updateRequired({
|
|
1620
|
+
appId,
|
|
1621
|
+
attr,
|
|
1622
|
+
isRequired,
|
|
1623
|
+
authToken: explorerProps.adminToken,
|
|
1624
|
+
setIndexingJob,
|
|
1625
|
+
stopFetchLoop,
|
|
1626
|
+
apiURI: explorerProps.apiURI,
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
if (res != 'completed') return;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const ops = [
|
|
1633
|
+
[
|
|
1634
|
+
'update-attr',
|
|
1635
|
+
{
|
|
1636
|
+
id: attr.id,
|
|
1637
|
+
...relationshipConstraints[relationship],
|
|
1638
|
+
'forward-identity': [
|
|
1639
|
+
attr.linkConfig.forward.id,
|
|
1640
|
+
attr.linkConfig.forward.namespace,
|
|
1641
|
+
attrName,
|
|
1642
|
+
],
|
|
1643
|
+
'reverse-identity': [
|
|
1644
|
+
attr.linkConfig.reverse.id,
|
|
1645
|
+
attr.linkConfig.reverse.namespace,
|
|
1646
|
+
reverseAttrName,
|
|
1647
|
+
],
|
|
1648
|
+
'on-delete': isCascadeAllowed && isCascade ? 'cascade' : null,
|
|
1649
|
+
'on-delete-reverse':
|
|
1650
|
+
isCascadeReverseAllowed && isCascadeReverse ? 'cascade' : null,
|
|
1651
|
+
},
|
|
1652
|
+
],
|
|
1653
|
+
];
|
|
1654
|
+
|
|
1655
|
+
await db.core._reactor.pushOps(ops);
|
|
1656
|
+
|
|
1657
|
+
successToast('Updated attribute');
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async function renameBlobAttr() {
|
|
1661
|
+
const ops = [
|
|
1662
|
+
[
|
|
1663
|
+
'update-attr',
|
|
1664
|
+
{
|
|
1665
|
+
id: attr.id,
|
|
1666
|
+
'forward-identity': [
|
|
1667
|
+
attr.linkConfig.forward.id,
|
|
1668
|
+
attr.linkConfig.forward.namespace,
|
|
1669
|
+
attrName,
|
|
1670
|
+
],
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
];
|
|
1674
|
+
|
|
1675
|
+
await db.core._reactor.pushOps(ops);
|
|
1676
|
+
|
|
1677
|
+
successToast('Renamed attribute');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async function deleteAttr() {
|
|
1681
|
+
await db.core._reactor.pushOps([['delete-attr', attr.id]]);
|
|
1682
|
+
// update the recently deleted attr cache
|
|
1683
|
+
setTimeout(() => {
|
|
1684
|
+
mutate(['recently-deleted', appId]);
|
|
1685
|
+
}, 500);
|
|
1686
|
+
onClose();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (screen.type === 'delete') {
|
|
1690
|
+
return (
|
|
1691
|
+
<DeleteForm
|
|
1692
|
+
onConfirm={deleteAttr}
|
|
1693
|
+
onClose={onClose}
|
|
1694
|
+
name={attr.name}
|
|
1695
|
+
type="attribute"
|
|
1696
|
+
/>
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
return (
|
|
1700
|
+
<div className="flex flex-col gap-4">
|
|
1701
|
+
<div className="mr-8 flex gap-4">
|
|
1702
|
+
<div className="flex items-center gap-2">
|
|
1703
|
+
<ArrowLeftIcon className="h-4 w-4 cursor-pointer" onClick={onClose} />
|
|
1704
|
+
<h5 className="flex items-center text-lg font-bold">
|
|
1705
|
+
Edit {attr.namespace}.{attr.name}
|
|
1706
|
+
</h5>
|
|
1707
|
+
</div>
|
|
1708
|
+
|
|
1709
|
+
<Button
|
|
1710
|
+
disabled={constraints.attr.disabled}
|
|
1711
|
+
title={constraints.attr.message}
|
|
1712
|
+
variant="secondary"
|
|
1713
|
+
size="mini"
|
|
1714
|
+
onClick={() => setScreen({ type: 'delete' })}
|
|
1715
|
+
>
|
|
1716
|
+
<TrashIcon className="inline" height="1rem" />
|
|
1717
|
+
Delete
|
|
1718
|
+
</Button>
|
|
1719
|
+
</div>
|
|
1720
|
+
|
|
1721
|
+
{attr.type === 'blob' ? (
|
|
1722
|
+
<>
|
|
1723
|
+
<EditBlobConstraints
|
|
1724
|
+
appId={appId}
|
|
1725
|
+
attr={attr}
|
|
1726
|
+
constraints={constraints}
|
|
1727
|
+
/>
|
|
1728
|
+
|
|
1729
|
+
<Divider />
|
|
1730
|
+
|
|
1731
|
+
<ActionForm className="flex flex-col gap-1">
|
|
1732
|
+
<h6 className="text-md font-bold">Rename</h6>
|
|
1733
|
+
<Content className="text-sm">
|
|
1734
|
+
This will immediately rename the attribute. You'll need to{' '}
|
|
1735
|
+
<strong className="dark:text-white">update your code</strong> to
|
|
1736
|
+
the new name.
|
|
1737
|
+
</Content>
|
|
1738
|
+
<TextInput
|
|
1739
|
+
disabled={constraints.attr.disabled}
|
|
1740
|
+
title={constraints.attr.message}
|
|
1741
|
+
value={attrName}
|
|
1742
|
+
onChange={(n) => setAttrName(n)}
|
|
1743
|
+
/>
|
|
1744
|
+
<div className="flex flex-col gap-2 rounded-sm py-2">
|
|
1745
|
+
<ActionButton
|
|
1746
|
+
type="submit"
|
|
1747
|
+
label={`Rename ${attr.name} → ${attrName}`}
|
|
1748
|
+
submitLabel="Renaming attribute..."
|
|
1749
|
+
errorMessage="Failed to rename attribute"
|
|
1750
|
+
disabled={
|
|
1751
|
+
constraints.attr.disabled ||
|
|
1752
|
+
!attrName ||
|
|
1753
|
+
attrName === attr.name
|
|
1754
|
+
}
|
|
1755
|
+
title={constraints.attr.message}
|
|
1756
|
+
onClick={renameBlobAttr}
|
|
1757
|
+
/>
|
|
1758
|
+
</div>
|
|
1759
|
+
</ActionForm>
|
|
1760
|
+
</>
|
|
1761
|
+
) : (
|
|
1762
|
+
<ActionForm className="flex flex-col gap-6">
|
|
1763
|
+
<RelationshipConfigurator
|
|
1764
|
+
relationship={relationship}
|
|
1765
|
+
attrName={attrName}
|
|
1766
|
+
reverseAttrName={reverseAttrName ?? ''}
|
|
1767
|
+
namespaceName={attr.linkConfig.forward.namespace}
|
|
1768
|
+
reverseNamespaceName={attr.linkConfig.reverse!.namespace}
|
|
1769
|
+
setAttrName={setAttrName}
|
|
1770
|
+
setReverseAttrName={setReverseAttrName}
|
|
1771
|
+
setRelationship={setRelationship}
|
|
1772
|
+
isCascadeAllowed={isCascadeAllowed}
|
|
1773
|
+
isCascade={isCascade}
|
|
1774
|
+
setIsCascade={setIsCascade}
|
|
1775
|
+
isCascadeReverseAllowed={isCascadeReverseAllowed}
|
|
1776
|
+
isCascadeReverse={isCascadeReverse}
|
|
1777
|
+
setIsCascadeReverse={setIsCascadeReverse}
|
|
1778
|
+
isRequired={isRequired}
|
|
1779
|
+
setIsRequired={setIsRequired}
|
|
1780
|
+
constraints={constraints}
|
|
1781
|
+
/>
|
|
1782
|
+
|
|
1783
|
+
<IndexingJobError
|
|
1784
|
+
indexingJob={indexingJob}
|
|
1785
|
+
attr={attr}
|
|
1786
|
+
onClose={() => {
|
|
1787
|
+
closeDialog();
|
|
1788
|
+
onClose();
|
|
1789
|
+
}}
|
|
1790
|
+
/>
|
|
1791
|
+
|
|
1792
|
+
<div className="flex flex-col gap-6">
|
|
1793
|
+
<ActionButton
|
|
1794
|
+
disabled={
|
|
1795
|
+
constraints.attr.disabled || !linkValidation.isValidLink
|
|
1796
|
+
}
|
|
1797
|
+
type="submit"
|
|
1798
|
+
label="Update relationship"
|
|
1799
|
+
submitLabel="Updating relationship..."
|
|
1800
|
+
errorMessage="Failed to update relationship"
|
|
1801
|
+
onClick={updateRef}
|
|
1802
|
+
title={constraints.attr.message}
|
|
1803
|
+
/>
|
|
1804
|
+
{linkValidation.shouldShowSelfLinkNameError ? (
|
|
1805
|
+
<span className="text-red-500">
|
|
1806
|
+
Self-links must have different attribute names.
|
|
1807
|
+
</span>
|
|
1808
|
+
) : null}
|
|
1809
|
+
</div>
|
|
1810
|
+
</ActionForm>
|
|
1811
|
+
)}
|
|
1812
|
+
</div>
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
function validateLink({
|
|
1816
|
+
reverseNamespaceName,
|
|
1817
|
+
namespaceName,
|
|
1818
|
+
attrName,
|
|
1819
|
+
reverseAttrName,
|
|
1820
|
+
}: {
|
|
1821
|
+
reverseNamespaceName: string | undefined;
|
|
1822
|
+
reverseAttrName: string | undefined;
|
|
1823
|
+
namespaceName: string;
|
|
1824
|
+
attrName: string;
|
|
1825
|
+
}) {
|
|
1826
|
+
const isSelfLink =
|
|
1827
|
+
reverseNamespaceName && reverseNamespaceName === namespaceName;
|
|
1828
|
+
const isNonEmptyLink =
|
|
1829
|
+
attrName && namespaceName && reverseNamespaceName && reverseAttrName;
|
|
1830
|
+
const isValidSelfLinkNames = attrName !== reverseAttrName;
|
|
1831
|
+
const isValidLink =
|
|
1832
|
+
isNonEmptyLink && (isSelfLink ? isValidSelfLinkNames : true);
|
|
1833
|
+
const shouldShowSelfLinkNameError =
|
|
1834
|
+
isNonEmptyLink && isSelfLink && !isValidSelfLinkNames;
|
|
1835
|
+
|
|
1836
|
+
return {
|
|
1837
|
+
isSelfLink,
|
|
1838
|
+
isNonEmptyLink,
|
|
1839
|
+
isValidLink,
|
|
1840
|
+
shouldShowSelfLinkNameError,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
type SystemConstraints = {
|
|
1845
|
+
attr: {
|
|
1846
|
+
disabled: boolean;
|
|
1847
|
+
message?: string;
|
|
1848
|
+
};
|
|
1849
|
+
require: {
|
|
1850
|
+
disabled: boolean;
|
|
1851
|
+
message?: string;
|
|
1852
|
+
};
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
function getSystemConstraints({
|
|
1856
|
+
namespaceName,
|
|
1857
|
+
isSystemCatalogNs: isSystemCatalogNs,
|
|
1858
|
+
attr: isSystemCatalogAttr,
|
|
1859
|
+
}: {
|
|
1860
|
+
namespaceName: string;
|
|
1861
|
+
isSystemCatalogNs: boolean;
|
|
1862
|
+
attr?: SchemaAttr;
|
|
1863
|
+
}): SystemConstraints {
|
|
1864
|
+
const isSystemAttr = isSystemCatalogAttr?.catalog === 'system';
|
|
1865
|
+
|
|
1866
|
+
const attrMessage = isSystemCatalogAttr
|
|
1867
|
+
? `${isSystemCatalogAttr.namespace}.${isSystemCatalogAttr.name} is managed by the system and can't be edited`
|
|
1868
|
+
: undefined;
|
|
1869
|
+
|
|
1870
|
+
const requireMessage = isSystemAttr
|
|
1871
|
+
? attrMessage
|
|
1872
|
+
: isSystemCatalogNs
|
|
1873
|
+
? `The ${namespaceName} namespace is managed by the system and can't modify required constraints yet.`
|
|
1874
|
+
: undefined;
|
|
1875
|
+
|
|
1876
|
+
return {
|
|
1877
|
+
attr: {
|
|
1878
|
+
disabled: isSystemAttr || false,
|
|
1879
|
+
message: attrMessage,
|
|
1880
|
+
},
|
|
1881
|
+
require: {
|
|
1882
|
+
disabled: isSystemAttr || isSystemCatalogNs,
|
|
1883
|
+
message: requireMessage,
|
|
1884
|
+
},
|
|
1885
|
+
};
|
|
1886
|
+
}
|