@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,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowUturnLeftIcon,
|
|
3
|
+
ChevronDownIcon,
|
|
4
|
+
ChevronRightIcon,
|
|
5
|
+
} from '@heroicons/react/24/outline';
|
|
6
|
+
import { Button } from '@lib/components/ui';
|
|
7
|
+
import {
|
|
8
|
+
DBAttr,
|
|
9
|
+
relationshipConstraintsInverse,
|
|
10
|
+
RelationshipKinds,
|
|
11
|
+
} from '@lib/types';
|
|
12
|
+
import { add, formatDistanceToNow } from 'date-fns';
|
|
13
|
+
import React, { ReactNode } from 'react';
|
|
14
|
+
|
|
15
|
+
type SoftDeletedAttr = DBAttr & {
|
|
16
|
+
'deletion-marked-at': string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface ExpandableDeletedAttrProps {
|
|
20
|
+
attr: SoftDeletedAttr;
|
|
21
|
+
gracePeriodDays: number;
|
|
22
|
+
onRestore: (attrId: string) => void;
|
|
23
|
+
isExpanded: boolean;
|
|
24
|
+
setIsExpanded: (state: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ExpandableDeletedAttr: React.FC<ExpandableDeletedAttrProps> = ({
|
|
28
|
+
attr,
|
|
29
|
+
gracePeriodDays,
|
|
30
|
+
isExpanded,
|
|
31
|
+
setIsExpanded,
|
|
32
|
+
onRestore,
|
|
33
|
+
}) => {
|
|
34
|
+
const date = add(new Date(attr['deletion-marked-at']), {
|
|
35
|
+
days: gracePeriodDays,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const getForwardLabel = () => {
|
|
39
|
+
return attr['forward-identity'][2];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getReverseLabel = () => {
|
|
43
|
+
return attr['reverse-identity']?.[2];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getEtypes = () => {
|
|
47
|
+
const forwardEtype = attr['forward-identity'][1];
|
|
48
|
+
const reverseEtype = attr['reverse-identity']?.[1];
|
|
49
|
+
return { forwardEtype: forwardEtype, reverseEtype };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getRelationshipType = (): RelationshipKinds => {
|
|
53
|
+
const key = `${attr.cardinality}-${attr['unique?']}`;
|
|
54
|
+
return relationshipConstraintsInverse[key];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const toggleExpanded = () => {
|
|
58
|
+
setIsExpanded(!isExpanded);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const relationshipInfo = ((): Array<{ label: string; value: ReactNode }> => {
|
|
62
|
+
if (attr['value-type'] === 'blob') {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const relationshipType = getRelationshipType();
|
|
67
|
+
const { forwardEtype: forwardEtype, reverseEtype: reverseEtype } =
|
|
68
|
+
getEtypes();
|
|
69
|
+
const forwardLabel = getForwardLabel();
|
|
70
|
+
const reverseLabel = getReverseLabel();
|
|
71
|
+
|
|
72
|
+
const getCardinalityText = (
|
|
73
|
+
relType: RelationshipKinds,
|
|
74
|
+
isForward: boolean,
|
|
75
|
+
) => {
|
|
76
|
+
if (relType === 'one-one') return 'has one';
|
|
77
|
+
if (relType === 'many-many') return 'has many';
|
|
78
|
+
if (relType === 'one-many') return isForward ? 'has one' : 'has many';
|
|
79
|
+
if (relType === 'many-one') return isForward ? 'has many' : 'has one';
|
|
80
|
+
return 'has';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
label: 'forward',
|
|
86
|
+
value: (
|
|
87
|
+
<span>
|
|
88
|
+
<strong>{forwardEtype}</strong>{' '}
|
|
89
|
+
{getCardinalityText(relationshipType, true)}{' '}
|
|
90
|
+
<strong>{forwardLabel}</strong>
|
|
91
|
+
</span>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'reverse',
|
|
96
|
+
value: (
|
|
97
|
+
<span>
|
|
98
|
+
<strong>{reverseEtype}</strong>{' '}
|
|
99
|
+
{getCardinalityText(relationshipType, false)}{' '}
|
|
100
|
+
<strong>{reverseLabel}</strong>
|
|
101
|
+
</span>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
const tableRows: Array<{ label: string; value: ReactNode }> = [
|
|
108
|
+
{ label: 'type', value: attr['value-type'] },
|
|
109
|
+
...relationshipInfo,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
<div
|
|
115
|
+
className="flex cursor-pointer items-center justify-between"
|
|
116
|
+
onClick={toggleExpanded}
|
|
117
|
+
>
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
{isExpanded ? (
|
|
120
|
+
<ChevronDownIcon width={14} className="text-gray-400" />
|
|
121
|
+
) : (
|
|
122
|
+
<ChevronRightIcon width={14} className="text-gray-400" />
|
|
123
|
+
)}
|
|
124
|
+
<span className="font-mono text-sm font-medium">
|
|
125
|
+
{attr['forward-identity'][2]}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="font-mono text-xs text-gray-400">
|
|
128
|
+
expires {formatDistanceToNow(date, { includeSeconds: false })}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<Button
|
|
132
|
+
className="px-1"
|
|
133
|
+
size="mini"
|
|
134
|
+
variant="subtle"
|
|
135
|
+
onClick={(e) => {
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
onRestore(attr.id);
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<ArrowUturnLeftIcon width={14} />
|
|
141
|
+
Restore
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{isExpanded && (
|
|
146
|
+
<div className="pb-3 pl-6">
|
|
147
|
+
<table className="mt-2 w-full text-left font-mono text-xs text-gray-500">
|
|
148
|
+
<tbody>
|
|
149
|
+
{tableRows.map((row, index) => (
|
|
150
|
+
<tr
|
|
151
|
+
key={row.label}
|
|
152
|
+
className={
|
|
153
|
+
index === tableRows.length - 1
|
|
154
|
+
? 'pl-2'
|
|
155
|
+
: 'border-b border-gray-200'
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<td className="py-1 pr-4 pl-2 font-medium text-gray-700">
|
|
159
|
+
{row.label}
|
|
160
|
+
</td>
|
|
161
|
+
<td className="py-1 text-gray-600">{row.value}</td>
|
|
162
|
+
</tr>
|
|
163
|
+
))}
|
|
164
|
+
</tbody>
|
|
165
|
+
</table>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useExplorerProps } from '.';
|
|
3
|
+
import { SchemaNamespace } from '@lib/types';
|
|
4
|
+
import { Button, cn, Dialog, ToggleCollection, useDialog } from '../ui';
|
|
5
|
+
import {
|
|
6
|
+
RecentlyDeletedNamespaces,
|
|
7
|
+
useRecentlyDeletedNamespaces,
|
|
8
|
+
} from './recently-deleted';
|
|
9
|
+
import { useStableDB } from '@lib/hooks/useStableDB';
|
|
10
|
+
import {
|
|
11
|
+
Bars3Icon,
|
|
12
|
+
ChevronLeftIcon,
|
|
13
|
+
PlusIcon,
|
|
14
|
+
} from '@heroicons/react/24/solid';
|
|
15
|
+
import { NewNamespaceDialog } from './new-namespace-dialog';
|
|
16
|
+
import { InnerExplorer } from './inner-explorer';
|
|
17
|
+
import { useClickOutside } from '@lib/hooks/useClickOutside';
|
|
18
|
+
|
|
19
|
+
// Holds the explorer table itself and also the sidebar to select / create namespaces
|
|
20
|
+
export const ExplorerLayout = ({
|
|
21
|
+
namespaces,
|
|
22
|
+
db,
|
|
23
|
+
}: {
|
|
24
|
+
namespaces: SchemaNamespace[];
|
|
25
|
+
db: ReturnType<typeof useStableDB>;
|
|
26
|
+
}) => {
|
|
27
|
+
const props = useExplorerProps();
|
|
28
|
+
|
|
29
|
+
const recentlyDeletedNsDialog = useDialog();
|
|
30
|
+
const newNsDialog = useDialog();
|
|
31
|
+
|
|
32
|
+
const selectedNamespace = namespaces.find(
|
|
33
|
+
(ns) => ns.id === props.explorerState?.namespace,
|
|
34
|
+
);
|
|
35
|
+
const [isNsOpen, setIsNsOpen] = useState(false);
|
|
36
|
+
const nsRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
useClickOutside(nsRef, () => {
|
|
39
|
+
setIsNsOpen(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Auto-select first namespace if none selected
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!selectedNamespace && namespaces.length > 0) {
|
|
45
|
+
props.setExplorerState({ namespace: namespaces[0].id });
|
|
46
|
+
}
|
|
47
|
+
}, [selectedNamespace, namespaces, props]);
|
|
48
|
+
|
|
49
|
+
const deletedNamespaces = useRecentlyDeletedNamespaces(props.appId);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={cn(
|
|
54
|
+
'relative flex w-full flex-1 overflow-hidden border-solid dark:bg-neutral-800',
|
|
55
|
+
props.className,
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<Dialog {...recentlyDeletedNsDialog}>
|
|
59
|
+
<RecentlyDeletedNamespaces appId={props.appId} db={db} />
|
|
60
|
+
</Dialog>
|
|
61
|
+
<Dialog {...newNsDialog}>
|
|
62
|
+
<NewNamespaceDialog
|
|
63
|
+
db={db}
|
|
64
|
+
onClose={(p) => {
|
|
65
|
+
newNsDialog.onClose();
|
|
66
|
+
|
|
67
|
+
if (p?.name) {
|
|
68
|
+
props.setExplorerState({ namespace: p.name });
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</Dialog>
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
ref={nsRef}
|
|
76
|
+
className={cn(
|
|
77
|
+
'absolute top-0 bottom-0 left-0 z-40 flex min-w-[200px] flex-col gap-1 border-r border-solid border-r-gray-200 bg-white p-2 shadow-md md:static md:flex md:shadow-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-white',
|
|
78
|
+
{
|
|
79
|
+
hidden: !isNsOpen,
|
|
80
|
+
},
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex items-center gap-1 text-sm font-semibold dark:text-white">
|
|
84
|
+
<ChevronLeftIcon
|
|
85
|
+
height="1rem"
|
|
86
|
+
className="cursor-pointer md:hidden dark:text-white"
|
|
87
|
+
onClick={() => setIsNsOpen(false)}
|
|
88
|
+
/>
|
|
89
|
+
Namespaces
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex flex-col gap-2 border-r border-gray-300 bg-neutral-100 p-1 md:hidden dark:border-neutral-700 dark:bg-neutral-800">
|
|
92
|
+
<button
|
|
93
|
+
className="flex cursor-pointer items-center gap-1 rounded-sm px-1 py-0.5 select-none hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
|
94
|
+
onClick={(e) => {
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
setIsNsOpen(true);
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Bars3Icon height="1rem" className="dark:text-white" />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
{namespaces ? (
|
|
103
|
+
<>
|
|
104
|
+
<div className="overflow-x-hidden overflow-y-auto">
|
|
105
|
+
{namespaces.length ? (
|
|
106
|
+
<ToggleCollection
|
|
107
|
+
className="text-sm"
|
|
108
|
+
selectedId={props.explorerState?.namespace}
|
|
109
|
+
items={namespaces.map((ns) => ({
|
|
110
|
+
id: ns.id,
|
|
111
|
+
label: ns.name,
|
|
112
|
+
}))}
|
|
113
|
+
onChange={(ns) => {
|
|
114
|
+
props.setExplorerState({ namespace: ns.id });
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
<Button
|
|
120
|
+
variant="secondary"
|
|
121
|
+
size="mini"
|
|
122
|
+
className="justify-center"
|
|
123
|
+
onClick={newNsDialog.onOpen}
|
|
124
|
+
>
|
|
125
|
+
<PlusIcon height="1rem" /> Create
|
|
126
|
+
</Button>
|
|
127
|
+
{deletedNamespaces.length ? (
|
|
128
|
+
<Button
|
|
129
|
+
className="justify-start gap-2 rounded-sm p-2"
|
|
130
|
+
variant="subtle"
|
|
131
|
+
size="nano"
|
|
132
|
+
onClick={recentlyDeletedNsDialog.onOpen}
|
|
133
|
+
>
|
|
134
|
+
<span className="rounded-sm bg-gray-200 px-1">
|
|
135
|
+
{deletedNamespaces.length}
|
|
136
|
+
</span>
|
|
137
|
+
<span>Recently Deleted</span>
|
|
138
|
+
</Button>
|
|
139
|
+
) : null}
|
|
140
|
+
</>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="animate-slow-pulse flex w-full flex-col gap-2">
|
|
143
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
144
|
+
<div
|
|
145
|
+
key={i}
|
|
146
|
+
className="h-4 w-full rounded-md bg-neutral-300 dark:bg-neutral-700"
|
|
147
|
+
></div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{props.explorerState && <InnerExplorer namespaces={namespaces} db={db} />}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
import { StyleMe } from '@lib/components/StyleMe';
|
|
12
|
+
import type { HasDefault, WithDefaults, WithOptional } from '@lib/types';
|
|
13
|
+
import { config } from '@lib/config';
|
|
14
|
+
import { ExplorerLayout } from './explorer-layout';
|
|
15
|
+
import { useSchemaQuery } from '@lib/hooks/explorer';
|
|
16
|
+
import { useStableDB } from '@lib/hooks/useStableDB';
|
|
17
|
+
import ErrorBoundary from '@lib/components/error-boundary';
|
|
18
|
+
|
|
19
|
+
interface ExplorerProps {
|
|
20
|
+
appId: string;
|
|
21
|
+
adminToken: string;
|
|
22
|
+
apiURI: HasDefault<string>;
|
|
23
|
+
websocketURI: HasDefault<string>;
|
|
24
|
+
darkMode: HasDefault<boolean>;
|
|
25
|
+
|
|
26
|
+
className?: string;
|
|
27
|
+
|
|
28
|
+
// state management
|
|
29
|
+
// When undefined: uncontrolled mode (component manages its own state)
|
|
30
|
+
// When null: controlled mode with no selection
|
|
31
|
+
// When ExplorerNav: controlled mode with a selection
|
|
32
|
+
explorerState: HasDefault<ExplorerNav | null | undefined>;
|
|
33
|
+
setExplorerState: HasDefault<
|
|
34
|
+
React.Dispatch<React.SetStateAction<ExplorerNav | null>>
|
|
35
|
+
>;
|
|
36
|
+
useShadowDOM: HasDefault<boolean>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ExplorerPropsContext = createContext<{
|
|
40
|
+
props: WithDefaults<ExplorerProps> | null;
|
|
41
|
+
history: {
|
|
42
|
+
push: (
|
|
43
|
+
filter: React.SetStateAction<ExplorerNav>,
|
|
44
|
+
replace?: boolean,
|
|
45
|
+
) => void;
|
|
46
|
+
pop: () => void;
|
|
47
|
+
items: ExplorerNav[];
|
|
48
|
+
};
|
|
49
|
+
}>({ props: null, history: { push: () => {}, pop: () => {}, items: [] } });
|
|
50
|
+
|
|
51
|
+
export const useExplorerProps = (): WithDefaults<ExplorerProps> => {
|
|
52
|
+
const ctx = useContext(ExplorerPropsContext);
|
|
53
|
+
if (!ctx.props) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'useExplorerProps must be used within an Explorer component',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return ctx.props;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const useExplorerState = () => {
|
|
62
|
+
const ctx = useContext(ExplorerPropsContext);
|
|
63
|
+
if (!ctx.props || !ctx.props.explorerState) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'useExplorerProps must be used within an Explorer component',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return { explorerState: ctx.props.explorerState, history: ctx.history };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isControlled = (props: WithOptional<ExplorerProps>): boolean => {
|
|
72
|
+
// Component is controlled if explorerState prop is explicitly provided
|
|
73
|
+
// (even if null - that means "no selection" in controlled mode)
|
|
74
|
+
return (
|
|
75
|
+
props.explorerState !== undefined || props.setExplorerState !== undefined
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const fillPropsWithDefaults = (
|
|
80
|
+
input: WithOptional<ExplorerProps>,
|
|
81
|
+
_explorerState: ExplorerNav | null,
|
|
82
|
+
setExplorerState: React.Dispatch<React.SetStateAction<ExplorerNav | null>>,
|
|
83
|
+
): WithDefaults<ExplorerProps> => {
|
|
84
|
+
const controlled = isControlled(input);
|
|
85
|
+
return {
|
|
86
|
+
...input,
|
|
87
|
+
apiURI: input.apiURI || config.apiURI,
|
|
88
|
+
websocketURI: input.websocketURI || config.websocketURI,
|
|
89
|
+
darkMode: input.darkMode === undefined ? false : input.darkMode,
|
|
90
|
+
// In controlled mode, use the provided state (even if null)
|
|
91
|
+
// In uncontrolled mode, use the internal state
|
|
92
|
+
explorerState: controlled ? (input.explorerState ?? null) : _explorerState,
|
|
93
|
+
setExplorerState: input.setExplorerState || setExplorerState,
|
|
94
|
+
useShadowDOM: input.useShadowDOM || false,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type SearchFilterOp =
|
|
99
|
+
| '='
|
|
100
|
+
| '$ilike'
|
|
101
|
+
| '$like'
|
|
102
|
+
| '$gt'
|
|
103
|
+
| '$lt'
|
|
104
|
+
| '$isNull';
|
|
105
|
+
|
|
106
|
+
export type SearchFilter = [string, SearchFilterOp, any];
|
|
107
|
+
|
|
108
|
+
export interface ExplorerNav {
|
|
109
|
+
namespace: string;
|
|
110
|
+
where?: [string, any];
|
|
111
|
+
sortAttr?: string;
|
|
112
|
+
sortAsc?: boolean;
|
|
113
|
+
filters?: SearchFilter[];
|
|
114
|
+
limit?: number;
|
|
115
|
+
page?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const Explorer = (_props: WithOptional<ExplorerProps>) => {
|
|
119
|
+
// backup useState if explorer is uncontrolled component
|
|
120
|
+
const [_explorerState, _setExplorerState] = useState<ExplorerNav | null>(
|
|
121
|
+
null,
|
|
122
|
+
);
|
|
123
|
+
const props: WithDefaults<ExplorerProps> = fillPropsWithDefaults(
|
|
124
|
+
_props,
|
|
125
|
+
_explorerState,
|
|
126
|
+
_setExplorerState,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// inside the component avoid setting explorer state directly
|
|
130
|
+
// if change could be useful for history
|
|
131
|
+
const { explorerState, setExplorerState } = props;
|
|
132
|
+
|
|
133
|
+
const [explorerStateHistory, setExplorerStateHistory] = useState<
|
|
134
|
+
ExplorerNav[]
|
|
135
|
+
>([]);
|
|
136
|
+
|
|
137
|
+
const pushExplorerState = useCallback(
|
|
138
|
+
(filter: React.SetStateAction<ExplorerNav>, replace: boolean = false) => {
|
|
139
|
+
setExplorerStateHistory((prev) => {
|
|
140
|
+
if (!replace && explorerState) {
|
|
141
|
+
return [...prev, explorerState];
|
|
142
|
+
}
|
|
143
|
+
return prev;
|
|
144
|
+
});
|
|
145
|
+
setExplorerState(filter as any);
|
|
146
|
+
},
|
|
147
|
+
[explorerState, setExplorerState],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const popExplorerState = useCallback(() => {
|
|
151
|
+
setExplorerStateHistory((prev) => {
|
|
152
|
+
if (prev.length > 0) {
|
|
153
|
+
const [last, ...rest] = prev;
|
|
154
|
+
setExplorerState(last);
|
|
155
|
+
return rest;
|
|
156
|
+
}
|
|
157
|
+
return prev;
|
|
158
|
+
});
|
|
159
|
+
}, [setExplorerState]);
|
|
160
|
+
|
|
161
|
+
const db = useStableDB({
|
|
162
|
+
appId: props.appId,
|
|
163
|
+
apiURI: props.apiURI,
|
|
164
|
+
websocketURI: props.websocketURI,
|
|
165
|
+
adminToken: props.adminToken,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Track if this is the first render to avoid resetting on mount
|
|
169
|
+
const isFirstRender = useRef(true);
|
|
170
|
+
const prevAppId = useRef(props.appId);
|
|
171
|
+
|
|
172
|
+
// Reset explorer history when appId changes (but not on initial mount)
|
|
173
|
+
// Only reset internal state in uncontrolled mode - in controlled mode,
|
|
174
|
+
// the parent component manages state resets
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (isFirstRender.current) {
|
|
177
|
+
isFirstRender.current = false;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (prevAppId.current !== props.appId) {
|
|
182
|
+
prevAppId.current = props.appId;
|
|
183
|
+
// Always reset history when app changes
|
|
184
|
+
setExplorerStateHistory([]);
|
|
185
|
+
// Only reset state in uncontrolled mode
|
|
186
|
+
if (!isControlled(_props)) {
|
|
187
|
+
_setExplorerState(null);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, [props.appId, _props]);
|
|
191
|
+
|
|
192
|
+
const schemaData = useSchemaQuery(db);
|
|
193
|
+
|
|
194
|
+
const contextValue = useMemo(
|
|
195
|
+
() => ({
|
|
196
|
+
props,
|
|
197
|
+
history: {
|
|
198
|
+
push: pushExplorerState,
|
|
199
|
+
pop: popExplorerState,
|
|
200
|
+
items: explorerStateHistory,
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
[props, pushExplorerState, popExplorerState, explorerStateHistory],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const Wrapper = props.useShadowDOM ? StyleMe : React.Fragment;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<ExplorerPropsContext.Provider value={contextValue}>
|
|
210
|
+
<Wrapper>
|
|
211
|
+
<ErrorBoundary>
|
|
212
|
+
<ExplorerLayout db={db} namespaces={schemaData.namespaces || []} />
|
|
213
|
+
</ErrorBoundary>
|
|
214
|
+
</Wrapper>
|
|
215
|
+
</ExplorerPropsContext.Provider>
|
|
216
|
+
);
|
|
217
|
+
};
|