@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.
Files changed (128) hide show
  1. package/.env +2 -0
  2. package/.turbo/turbo-build.log +18 -0
  3. package/README.md +78 -0
  4. package/app/App.css +38 -0
  5. package/app/App.tsx +61 -0
  6. package/app/index.css +18 -0
  7. package/app/main.tsx +10 -0
  8. package/dist/components/StyleMe.d.ts +15 -0
  9. package/dist/components/StyleMe.d.ts.map +1 -0
  10. package/dist/components/error-boundary.d.ts +17 -0
  11. package/dist/components/error-boundary.d.ts.map +1 -0
  12. package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
  13. package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
  14. package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
  15. package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
  16. package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
  17. package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
  18. package/dist/components/explorer/explorer-layout.d.ts +8 -0
  19. package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
  20. package/dist/components/explorer/index.d.ts +44 -0
  21. package/dist/components/explorer/index.d.ts.map +1 -0
  22. package/dist/components/explorer/inner-explorer.d.ts +16 -0
  23. package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
  24. package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
  25. package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
  26. package/dist/components/explorer/query-inspector.d.ts +11 -0
  27. package/dist/components/explorer/query-inspector.d.ts.map +1 -0
  28. package/dist/components/explorer/recently-deleted.d.ts +36 -0
  29. package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
  30. package/dist/components/explorer/search-input.d.ts +9 -0
  31. package/dist/components/explorer/search-input.d.ts.map +1 -0
  32. package/dist/components/explorer/table-components.d.ts +16 -0
  33. package/dist/components/explorer/table-components.d.ts.map +1 -0
  34. package/dist/components/explorer/view-settings.d.ts +10 -0
  35. package/dist/components/explorer/view-settings.d.ts.map +1 -0
  36. package/dist/components/rosePineDawnTheme.d.ts +13 -0
  37. package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
  38. package/dist/components/select.d.ts +16 -0
  39. package/dist/components/select.d.ts.map +1 -0
  40. package/dist/components/toast.d.ts +4 -0
  41. package/dist/components/toast.d.ts.map +1 -0
  42. package/dist/components/ui.d.ts +336 -0
  43. package/dist/components/ui.d.ts.map +1 -0
  44. package/dist/config.d.ts +14 -0
  45. package/dist/config.d.ts.map +1 -0
  46. package/dist/hooks/explorer.d.ts +29 -0
  47. package/dist/hooks/explorer.d.ts.map +1 -0
  48. package/dist/hooks/useAttrNotes.d.ts +10 -0
  49. package/dist/hooks/useAttrNotes.d.ts.map +1 -0
  50. package/dist/hooks/useClickOutside.d.ts +3 -0
  51. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  52. package/dist/hooks/useColumnVisibility.d.ts +12 -0
  53. package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
  54. package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
  55. package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
  56. package/dist/hooks/useExplorerHistory.d.ts +1 -0
  57. package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
  58. package/dist/hooks/useIsOverflow.d.ts +6 -0
  59. package/dist/hooks/useIsOverflow.d.ts.map +1 -0
  60. package/dist/hooks/useLocalStorage.d.ts +2 -0
  61. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  62. package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
  63. package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
  64. package/dist/hooks/useStableDB.d.ts +7 -0
  65. package/dist/hooks/useStableDB.d.ts.map +1 -0
  66. package/dist/index.cjs +15 -0
  67. package/dist/index.d.ts +7 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +9270 -0
  70. package/dist/schema.d.ts +5 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/style.css +1 -0
  73. package/dist/types.d.ts +241 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/utils/format.d.ts +2 -0
  76. package/dist/utils/format.d.ts.map +1 -0
  77. package/dist/utils/indexingJobs.d.ts +24 -0
  78. package/dist/utils/indexingJobs.d.ts.map +1 -0
  79. package/dist/utils/parsePermsJSON.d.ts +11 -0
  80. package/dist/utils/parsePermsJSON.d.ts.map +1 -0
  81. package/dist/utils/renames.d.ts +3 -0
  82. package/dist/utils/renames.d.ts.map +1 -0
  83. package/dist/utils/tableWidthSize.d.ts +9 -0
  84. package/dist/utils/tableWidthSize.d.ts.map +1 -0
  85. package/index.html +13 -0
  86. package/package.json +109 -0
  87. package/src/components/StyleMe.tsx +97 -0
  88. package/src/components/error-boundary.tsx +76 -0
  89. package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
  90. package/src/components/explorer/edit-row-dialog.tsx +1151 -0
  91. package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
  92. package/src/components/explorer/explorer-layout.tsx +156 -0
  93. package/src/components/explorer/index.tsx +217 -0
  94. package/src/components/explorer/inner-explorer.tsx +1341 -0
  95. package/src/components/explorer/new-namespace-dialog.tsx +54 -0
  96. package/src/components/explorer/query-inspector.tsx +394 -0
  97. package/src/components/explorer/recently-deleted.tsx +344 -0
  98. package/src/components/explorer/search-input.tsx +358 -0
  99. package/src/components/explorer/table-components.tsx +341 -0
  100. package/src/components/explorer/view-settings.tsx +75 -0
  101. package/src/components/rosePineDawnTheme.ts +45 -0
  102. package/src/components/select.tsx +198 -0
  103. package/src/components/toast.tsx +18 -0
  104. package/src/components/ui.tsx +1561 -0
  105. package/src/config.ts +61 -0
  106. package/src/hooks/explorer.tsx +125 -0
  107. package/src/hooks/useAttrNotes.ts +27 -0
  108. package/src/hooks/useClickOutside.ts +23 -0
  109. package/src/hooks/useColumnVisibility.ts +39 -0
  110. package/src/hooks/useEditBlobConstraints.ts +185 -0
  111. package/src/hooks/useExplorerHistory.ts +0 -0
  112. package/src/hooks/useIsOverflow.ts +24 -0
  113. package/src/hooks/useLocalStorage.ts +51 -0
  114. package/src/hooks/useMonacoJSONSchema.ts +41 -0
  115. package/src/hooks/useStableDB.ts +30 -0
  116. package/src/index.tsx +8 -0
  117. package/src/schema.ts +285 -0
  118. package/src/style.css +5 -0
  119. package/src/types.ts +359 -0
  120. package/src/utils/format.ts +13 -0
  121. package/src/utils/indexingJobs.ts +126 -0
  122. package/src/utils/parsePermsJSON.ts +35 -0
  123. package/src/utils/renames.ts +42 -0
  124. package/src/utils/tableWidthSize.ts +62 -0
  125. package/tailwind.config.cjs +42 -0
  126. package/tsconfig.json +22 -0
  127. package/vite-env.d.ts +1 -0
  128. 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
+ };