@marimo-team/islands 0.23.10-dev9 → 0.23.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
- package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
- package/dist/{code-visibility-v6ucEVUg.js → code-visibility-DrFozVlm.js} +1943 -1223
- package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
- package/dist/files/wasm-intro.py +11 -8
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
- package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
- package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +7630 -7558
- package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
- package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
- package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
- package/dist/{reveal-component-gRcjQbNL.js → reveal-component-3YQBHWuD.js} +698 -620
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +4 -3
- package/src/__tests__/setup.ts +16 -0
- package/src/components/chat/chat-display.tsx +2 -2
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/code/cell-editor.tsx +70 -32
- package/src/components/editor/cell/code/code-placeholder.css +18 -0
- package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
- package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
- package/src/components/editor/connections/storage/as-code.ts +20 -6
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/notebook-cell.tsx +3 -3
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
- package/src/components/markdown/markdown-renderer.tsx +24 -1
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
- package/src/components/storage/storage-inspector.tsx +535 -81
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/number-field.tsx +5 -1
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/toast.tsx +1 -0
- package/src/core/cells/__tests__/document-changes.test.ts +2 -2
- package/src/core/cells/__tests__/focus.test.ts +45 -2
- package/src/core/cells/focus.ts +1 -1
- package/src/core/cells/scrollCellIntoView.ts +4 -0
- package/src/core/cells/utils.ts +1 -1
- package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
- package/src/core/codemirror/copilot/getCodes.ts +3 -1
- package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/errors/errors.ts +1 -1
- package/src/core/storage/__tests__/state.test.ts +49 -0
- package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
- package/src/core/storage/state.ts +124 -13
- package/src/core/storage/types.ts +6 -0
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
- package/src/plugins/core/registerReactComponent.tsx +42 -10
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/SliderPlugin.tsx +132 -18
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
- package/src/utils/__tests__/numbers.test.ts +20 -0
- package/src/utils/errors.ts +9 -0
- package/src/utils/lazy.ts +6 -1
- package/src/utils/numbers.ts +27 -0
- package/src/utils/schedule-task.ts +71 -0
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-CCuYCd3R.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -43,10 +43,12 @@ import {
|
|
|
43
43
|
useStorage,
|
|
44
44
|
useStorageActions,
|
|
45
45
|
useStorageEntries,
|
|
46
|
+
useStoragePageFetcher,
|
|
46
47
|
} from "@/core/storage/state";
|
|
47
48
|
import type {
|
|
48
49
|
StorageEntry,
|
|
49
50
|
StorageNamespace,
|
|
51
|
+
StoragePageMetadata,
|
|
50
52
|
StoragePathKey,
|
|
51
53
|
} from "@/core/storage/types";
|
|
52
54
|
import { storagePathKey } from "@/core/storage/types";
|
|
@@ -94,28 +96,52 @@ function displayName(path: string): string {
|
|
|
94
96
|
return parts[parts.length - 1] || trimmed;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
function directoryPrefix(path: string): string {
|
|
100
|
+
return path.endsWith("/") ? path : `${path}/`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stable, unique identity for an entry row. Prefer the
|
|
105
|
+
* backend's stable id when present and fall back to the list index
|
|
106
|
+
*/
|
|
107
|
+
export function storageEntryKey(entry: StorageEntry, index: number): string {
|
|
108
|
+
const id = entry.metadata?.id;
|
|
109
|
+
if (typeof id === "string" && id.length > 0) {
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
return `${entry.path}::${index}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface SearchContext {
|
|
116
|
+
namespace: string;
|
|
117
|
+
searchValue: string;
|
|
118
|
+
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>;
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
/**
|
|
98
122
|
* Recursively check whether an entry (or any of its loaded descendants)
|
|
99
123
|
* matches the search query.
|
|
100
124
|
*/
|
|
101
125
|
function entryMatchesSearch(
|
|
102
126
|
entry: StorageEntry,
|
|
103
|
-
namespace:
|
|
104
|
-
searchValue: string,
|
|
105
|
-
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>,
|
|
127
|
+
{ namespace, searchValue, entriesByPath }: SearchContext,
|
|
106
128
|
): boolean {
|
|
107
|
-
const query = searchValue.toLowerCase();
|
|
129
|
+
const query = searchValue.trim().toLowerCase();
|
|
130
|
+
const path = entry.path.toLowerCase();
|
|
131
|
+
const name = displayName(entry.path).toLowerCase();
|
|
108
132
|
|
|
109
|
-
if (
|
|
133
|
+
if (name.includes(query) || path.includes(query)) {
|
|
110
134
|
return true;
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
// For directories, check loaded children recursively
|
|
114
138
|
if (entry.kind === "directory") {
|
|
115
|
-
const children = entriesByPath.get(
|
|
139
|
+
const children = entriesByPath.get(
|
|
140
|
+
storagePathKey(namespace, directoryPrefix(entry.path)),
|
|
141
|
+
);
|
|
116
142
|
if (children) {
|
|
117
143
|
return children.some((child) =>
|
|
118
|
-
entryMatchesSearch(child, namespace, searchValue, entriesByPath),
|
|
144
|
+
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
119
145
|
);
|
|
120
146
|
}
|
|
121
147
|
}
|
|
@@ -127,20 +153,142 @@ function entryMatchesSearch(
|
|
|
127
153
|
* Filter entries to those matching the search (or having loaded descendants
|
|
128
154
|
* that match). Returns all entries when there is no active search.
|
|
129
155
|
*/
|
|
130
|
-
function filterEntries(
|
|
156
|
+
export function filterEntries(
|
|
131
157
|
entries: StorageEntry[],
|
|
132
|
-
|
|
133
|
-
searchValue: string,
|
|
134
|
-
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>,
|
|
158
|
+
context: SearchContext,
|
|
135
159
|
): StorageEntry[] {
|
|
136
|
-
if (!searchValue.trim()) {
|
|
160
|
+
if (!context.searchValue.trim()) {
|
|
137
161
|
return entries;
|
|
138
162
|
}
|
|
139
|
-
return entries.filter((entry) =>
|
|
140
|
-
|
|
163
|
+
return entries.filter((entry) => entryMatchesSearch(entry, context));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const MAX_REMOTE_SEARCH_PAGES = 5;
|
|
167
|
+
|
|
168
|
+
type RemoteSearchState =
|
|
169
|
+
| { query: string; status: "idle" }
|
|
170
|
+
| { query: string; status: "searching" }
|
|
171
|
+
| { query: string; status: "found" }
|
|
172
|
+
| { query: string; status: "exhausted" }
|
|
173
|
+
| { query: string; status: "capped" }
|
|
174
|
+
| { query: string; status: "error"; error: Error };
|
|
175
|
+
|
|
176
|
+
type RemoteSearchByNamespace = Record<string, RemoteSearchState>;
|
|
177
|
+
|
|
178
|
+
function idleRemoteSearch(query: string): RemoteSearchState {
|
|
179
|
+
return { query, status: "idle" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function canRetryRemoteSearch(remoteSearch: RemoteSearchState): boolean {
|
|
183
|
+
return (
|
|
184
|
+
remoteSearch.status === "idle" ||
|
|
185
|
+
remoteSearch.status === "error" ||
|
|
186
|
+
remoteSearch.status === "capped"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether the backend may still have an unfetched page for a prefix: either we
|
|
192
|
+
* have never listed it, or its last listing returned a next-page token.
|
|
193
|
+
*/
|
|
194
|
+
function hasUnfetchedPrefixPage(
|
|
195
|
+
searchKey: StoragePathKey,
|
|
196
|
+
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>,
|
|
197
|
+
pageMetadataByPath: ReadonlyMap<StoragePathKey, StoragePageMetadata>,
|
|
198
|
+
): boolean {
|
|
199
|
+
return (
|
|
200
|
+
entriesByPath.get(searchKey) === undefined ||
|
|
201
|
+
pageMetadataByPath.get(searchKey)?.nextPageToken != null
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function canSearchMoreRemoteEntries({
|
|
206
|
+
hasSearch,
|
|
207
|
+
hasLoadedMatches,
|
|
208
|
+
isPending,
|
|
209
|
+
remoteSearch,
|
|
210
|
+
searchKey,
|
|
211
|
+
entriesByPath,
|
|
212
|
+
pageMetadataByPath,
|
|
213
|
+
}: {
|
|
214
|
+
hasSearch: boolean;
|
|
215
|
+
hasLoadedMatches: boolean;
|
|
216
|
+
isPending: boolean;
|
|
217
|
+
remoteSearch: RemoteSearchState;
|
|
218
|
+
searchKey: StoragePathKey;
|
|
219
|
+
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>;
|
|
220
|
+
pageMetadataByPath: ReadonlyMap<StoragePathKey, StoragePageMetadata>;
|
|
221
|
+
}): boolean {
|
|
222
|
+
if (!hasSearch || hasLoadedMatches || isPending) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (!canRetryRemoteSearch(remoteSearch)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return hasUnfetchedPrefixPage(searchKey, entriesByPath, pageMetadataByPath);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns the directory prefix to query the backend with for a given search.
|
|
234
|
+
*
|
|
235
|
+
* Object stores like obstore evaluate prefixes on a path-segment basis
|
|
236
|
+
* (`folder/x` would only match `folder/x/...`, never `folder/xsomething`), so
|
|
237
|
+
* for substring searches we list the parent directory and filter on the
|
|
238
|
+
* client. Returns `""` when the search has no directory component.
|
|
239
|
+
*/
|
|
240
|
+
export function remoteSearchPrefix(searchValue: string): string {
|
|
241
|
+
const trimmed = searchValue.trim();
|
|
242
|
+
const lastSlash = trimmed.lastIndexOf("/");
|
|
243
|
+
return lastSlash === -1 ? "" : trimmed.slice(0, lastSlash + 1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Shallow check (no recursion into loaded children) used inside the
|
|
248
|
+
* remote-search pagination loop to decide whether a fetched page has
|
|
249
|
+
* any candidates worth surfacing to the user.
|
|
250
|
+
*/
|
|
251
|
+
function entryMatchesQueryShallow(
|
|
252
|
+
entry: StorageEntry,
|
|
253
|
+
searchValue: string,
|
|
254
|
+
): boolean {
|
|
255
|
+
const query = searchValue.trim().toLowerCase();
|
|
256
|
+
if (!query) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return (
|
|
260
|
+
entry.path.toLowerCase().includes(query) ||
|
|
261
|
+
displayName(entry.path).toLowerCase().includes(query)
|
|
141
262
|
);
|
|
142
263
|
}
|
|
143
264
|
|
|
265
|
+
const LoadMoreStorageEntries: React.FC<{
|
|
266
|
+
depth: number;
|
|
267
|
+
isLoading: boolean;
|
|
268
|
+
error?: Error;
|
|
269
|
+
onLoadMore: () => void;
|
|
270
|
+
}> = ({ depth, isLoading, error, onLoadMore }) => {
|
|
271
|
+
return (
|
|
272
|
+
<div className="py-px text-xs" style={indentStyle(depth)}>
|
|
273
|
+
<Button
|
|
274
|
+
variant="text"
|
|
275
|
+
size="xs"
|
|
276
|
+
className="h-6 px-0 hover:text-blue-600"
|
|
277
|
+
disabled={isLoading}
|
|
278
|
+
onClick={onLoadMore}
|
|
279
|
+
>
|
|
280
|
+
{isLoading && <LoaderCircle className="h-3 w-3 mr-1 animate-spin" />}
|
|
281
|
+
{isLoading ? "Loading..." : "Load more"}
|
|
282
|
+
</Button>
|
|
283
|
+
{error && (
|
|
284
|
+
<span className="ml-2 text-destructive">
|
|
285
|
+
Failed to load: {error.message}
|
|
286
|
+
</span>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
144
292
|
/**
|
|
145
293
|
* Lazily loaded children of a directory entry.
|
|
146
294
|
* Caches fetched entries in the Jotai store so re-expanding doesn't re-fetch.
|
|
@@ -171,6 +319,10 @@ const StorageEntryChildren: React.FC<{
|
|
|
171
319
|
entries: children,
|
|
172
320
|
isPending,
|
|
173
321
|
error,
|
|
322
|
+
hasMore,
|
|
323
|
+
loadMore,
|
|
324
|
+
isLoadingMore,
|
|
325
|
+
loadMoreError,
|
|
174
326
|
} = useStorageEntries(namespace, prefix);
|
|
175
327
|
|
|
176
328
|
if (isPending) {
|
|
@@ -204,35 +356,47 @@ const StorageEntryChildren: React.FC<{
|
|
|
204
356
|
);
|
|
205
357
|
}
|
|
206
358
|
|
|
207
|
-
const filtered = filterEntries(
|
|
208
|
-
children,
|
|
359
|
+
const filtered = filterEntries(children, {
|
|
209
360
|
namespace,
|
|
210
361
|
searchValue,
|
|
211
362
|
entriesByPath,
|
|
212
|
-
);
|
|
363
|
+
});
|
|
213
364
|
|
|
214
365
|
return (
|
|
215
366
|
<>
|
|
216
|
-
{filtered.map((child) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
367
|
+
{filtered.map((child) => {
|
|
368
|
+
const rowKey = storageEntryKey(child, children.indexOf(child));
|
|
369
|
+
return (
|
|
370
|
+
<StorageEntryRow
|
|
371
|
+
key={rowKey}
|
|
372
|
+
rowKey={rowKey}
|
|
373
|
+
entry={child}
|
|
374
|
+
namespace={namespace}
|
|
375
|
+
protocol={protocol}
|
|
376
|
+
rootPath={rootPath}
|
|
377
|
+
backendType={backendType}
|
|
378
|
+
depth={depth}
|
|
379
|
+
locale={locale}
|
|
380
|
+
searchValue={searchValue}
|
|
381
|
+
onOpenFile={onOpenFile}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
})}
|
|
385
|
+
{hasMore && (
|
|
386
|
+
<LoadMoreStorageEntries
|
|
224
387
|
depth={depth}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
388
|
+
isLoading={isLoadingMore}
|
|
389
|
+
error={loadMoreError}
|
|
390
|
+
onLoadMore={loadMore}
|
|
228
391
|
/>
|
|
229
|
-
)
|
|
392
|
+
)}
|
|
230
393
|
</>
|
|
231
394
|
);
|
|
232
395
|
};
|
|
233
396
|
|
|
234
397
|
const StorageEntryRow: React.FC<{
|
|
235
398
|
entry: StorageEntry;
|
|
399
|
+
rowKey: string;
|
|
236
400
|
namespace: string;
|
|
237
401
|
protocol: string;
|
|
238
402
|
rootPath: string;
|
|
@@ -243,6 +407,7 @@ const StorageEntryRow: React.FC<{
|
|
|
243
407
|
onOpenFile: (info: OpenFileInfo) => void;
|
|
244
408
|
}> = ({
|
|
245
409
|
entry,
|
|
410
|
+
rowKey,
|
|
246
411
|
namespace,
|
|
247
412
|
protocol,
|
|
248
413
|
rootPath,
|
|
@@ -269,9 +434,9 @@ const StorageEntryRow: React.FC<{
|
|
|
269
434
|
isDir &&
|
|
270
435
|
hasSearch &&
|
|
271
436
|
!!entriesByPath
|
|
272
|
-
.get(storagePathKey(namespace, entry.path))
|
|
437
|
+
.get(storagePathKey(namespace, directoryPrefix(entry.path)))
|
|
273
438
|
?.some((child) =>
|
|
274
|
-
entryMatchesSearch(child, namespace, searchValue, entriesByPath),
|
|
439
|
+
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
275
440
|
);
|
|
276
441
|
|
|
277
442
|
// Folder is shown expanded by manual toggle OR by search auto-expand
|
|
@@ -312,7 +477,7 @@ const StorageEntryRow: React.FC<{
|
|
|
312
477
|
isDir && "font-medium",
|
|
313
478
|
)}
|
|
314
479
|
style={indentStyle(depth)}
|
|
315
|
-
value={`${namespace}:${
|
|
480
|
+
value={`${namespace}:${rowKey}`}
|
|
316
481
|
onSelect={() => {
|
|
317
482
|
if (isDir) {
|
|
318
483
|
setIsExpanded(!effectiveExpanded);
|
|
@@ -418,7 +583,7 @@ const StorageEntryRow: React.FC<{
|
|
|
418
583
|
protocol={protocol}
|
|
419
584
|
rootPath={rootPath}
|
|
420
585
|
backendType={backendType}
|
|
421
|
-
prefix={entry.path}
|
|
586
|
+
prefix={directoryPrefix(entry.path)}
|
|
422
587
|
depth={depth + 1}
|
|
423
588
|
locale={locale}
|
|
424
589
|
searchValue={selfMatches ? "" : searchValue} // When a parent directory matches the search, we don't need to filter the children.
|
|
@@ -433,10 +598,19 @@ const StorageNamespaceSection: React.FC<{
|
|
|
433
598
|
namespace: StorageNamespace;
|
|
434
599
|
locale: string;
|
|
435
600
|
searchValue: string;
|
|
601
|
+
remoteSearch: RemoteSearchState;
|
|
602
|
+
onContinueRemoteSearch: () => void;
|
|
436
603
|
onOpenFile: (info: OpenFileInfo) => void;
|
|
437
|
-
}> = ({
|
|
604
|
+
}> = ({
|
|
605
|
+
namespace,
|
|
606
|
+
locale,
|
|
607
|
+
searchValue,
|
|
608
|
+
remoteSearch,
|
|
609
|
+
onContinueRemoteSearch,
|
|
610
|
+
onOpenFile,
|
|
611
|
+
}) => {
|
|
438
612
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
439
|
-
const { entriesByPath } = useStorage();
|
|
613
|
+
const { entriesByPath, pageMetadataByPath } = useStorage();
|
|
440
614
|
const { clearNamespaceCache } = useStorageActions();
|
|
441
615
|
const namespaceName = namespace.name ?? namespace.displayName;
|
|
442
616
|
|
|
@@ -444,6 +618,10 @@ const StorageNamespaceSection: React.FC<{
|
|
|
444
618
|
entries: fetchedEntries,
|
|
445
619
|
isPending,
|
|
446
620
|
error,
|
|
621
|
+
hasMore,
|
|
622
|
+
loadMore,
|
|
623
|
+
isLoadingMore,
|
|
624
|
+
loadMoreError,
|
|
447
625
|
refetch,
|
|
448
626
|
} = useStorageEntries(namespaceName);
|
|
449
627
|
|
|
@@ -458,12 +636,105 @@ const StorageNamespaceSection: React.FC<{
|
|
|
458
636
|
|
|
459
637
|
// While loading, fall back to initial entries from the namespace notification
|
|
460
638
|
const entries = isPending ? namespace.storageEntries : fetchedEntries;
|
|
461
|
-
const filtered = filterEntries(
|
|
462
|
-
|
|
463
|
-
namespaceName,
|
|
639
|
+
const filtered = filterEntries(entries, {
|
|
640
|
+
namespace: namespaceName,
|
|
464
641
|
searchValue,
|
|
465
642
|
entriesByPath,
|
|
466
|
-
);
|
|
643
|
+
});
|
|
644
|
+
const searchPrefix = remoteSearchPrefix(searchValue);
|
|
645
|
+
const searchKey = storagePathKey(namespaceName, searchPrefix);
|
|
646
|
+
const remoteEntries =
|
|
647
|
+
searchPrefix === "" ? [] : (entriesByPath.get(searchKey) ?? []);
|
|
648
|
+
// The fetched page is the whole parent directory; we still need to filter
|
|
649
|
+
// it by the full search query before showing entries to the user.
|
|
650
|
+
const filteredRemoteEntries = filterEntries(remoteEntries, {
|
|
651
|
+
namespace: namespaceName,
|
|
652
|
+
searchValue,
|
|
653
|
+
entriesByPath,
|
|
654
|
+
});
|
|
655
|
+
const hasSearch = !!searchValue.trim();
|
|
656
|
+
const hasLoadedMatches =
|
|
657
|
+
filtered.length > 0 || filteredRemoteEntries.length > 0;
|
|
658
|
+
const canSearchMore =
|
|
659
|
+
searchPrefix !== "" &&
|
|
660
|
+
canSearchMoreRemoteEntries({
|
|
661
|
+
hasSearch,
|
|
662
|
+
hasLoadedMatches,
|
|
663
|
+
isPending,
|
|
664
|
+
remoteSearch,
|
|
665
|
+
searchKey,
|
|
666
|
+
entriesByPath,
|
|
667
|
+
pageMetadataByPath,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const showRemoteResults = hasSearch && filtered.length === 0;
|
|
671
|
+
const statusRow = (() => {
|
|
672
|
+
if (isPending && entries.length === 0) {
|
|
673
|
+
return (
|
|
674
|
+
<span className="flex items-center gap-1.5">
|
|
675
|
+
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
676
|
+
Loading...
|
|
677
|
+
</span>
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (remoteSearch.status === "searching") {
|
|
681
|
+
return (
|
|
682
|
+
<span className="flex items-center gap-1.5">
|
|
683
|
+
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
684
|
+
Searching more entries...
|
|
685
|
+
</span>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (remoteSearch.status === "error") {
|
|
689
|
+
return (
|
|
690
|
+
<span className="text-destructive">
|
|
691
|
+
Search failed: {remoteSearch.error.message}
|
|
692
|
+
</span>
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (remoteSearch.status === "capped") {
|
|
696
|
+
return (
|
|
697
|
+
<span className="flex items-center gap-1.5">
|
|
698
|
+
Searched more entries.
|
|
699
|
+
<Button
|
|
700
|
+
variant="text"
|
|
701
|
+
size="xs"
|
|
702
|
+
className="h-5 px-0 text-xs hover:text-blue-600"
|
|
703
|
+
onClick={onContinueRemoteSearch}
|
|
704
|
+
>
|
|
705
|
+
Continue searching
|
|
706
|
+
</Button>
|
|
707
|
+
<span className="text-[10px]">(or press Enter)</span>
|
|
708
|
+
</span>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
if (remoteSearch.status === "exhausted" && !hasLoadedMatches) {
|
|
712
|
+
return "No matches";
|
|
713
|
+
}
|
|
714
|
+
if (!hasSearch && !isPending && entries.length === 0) {
|
|
715
|
+
return "No entries";
|
|
716
|
+
}
|
|
717
|
+
if (canSearchMore) {
|
|
718
|
+
return (
|
|
719
|
+
<span className="flex items-center gap-1.5">
|
|
720
|
+
No loaded matches.
|
|
721
|
+
<Button
|
|
722
|
+
variant="text"
|
|
723
|
+
size="xs"
|
|
724
|
+
className="h-5 px-0 text-xs hover:text-blue-600"
|
|
725
|
+
onClick={onContinueRemoteSearch}
|
|
726
|
+
>
|
|
727
|
+
Search more entries
|
|
728
|
+
</Button>
|
|
729
|
+
<span className="text-[10px]">(or press Enter)</span>
|
|
730
|
+
</span>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
if (hasSearch && !hasLoadedMatches && entries.length > 0) {
|
|
734
|
+
return "No matches";
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
})();
|
|
467
738
|
|
|
468
739
|
return (
|
|
469
740
|
<>
|
|
@@ -492,15 +763,6 @@ const StorageNamespaceSection: React.FC<{
|
|
|
492
763
|
</CommandItem>
|
|
493
764
|
{isExpanded && (
|
|
494
765
|
<>
|
|
495
|
-
{isPending && entries.length === 0 && (
|
|
496
|
-
<div
|
|
497
|
-
className="flex items-center gap-1.5 py-1 text-xs text-muted-foreground"
|
|
498
|
-
style={indentStyle(1)}
|
|
499
|
-
>
|
|
500
|
-
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
501
|
-
Loading...
|
|
502
|
-
</div>
|
|
503
|
-
)}
|
|
504
766
|
{error && entries.length === 0 && (
|
|
505
767
|
<ErrorState
|
|
506
768
|
error={error}
|
|
@@ -509,36 +771,62 @@ const StorageNamespaceSection: React.FC<{
|
|
|
509
771
|
showIcon={false}
|
|
510
772
|
/>
|
|
511
773
|
)}
|
|
512
|
-
{!
|
|
774
|
+
{!error && statusRow && (
|
|
513
775
|
<div
|
|
514
776
|
className="py-1 text-xs text-muted-foreground italic"
|
|
515
777
|
style={indentStyle(1)}
|
|
516
778
|
>
|
|
517
|
-
|
|
779
|
+
{statusRow}
|
|
518
780
|
</div>
|
|
519
781
|
)}
|
|
520
|
-
{
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
782
|
+
{filtered.map((entry) => {
|
|
783
|
+
const rowKey = storageEntryKey(entry, entries.indexOf(entry));
|
|
784
|
+
return (
|
|
785
|
+
<StorageEntryRow
|
|
786
|
+
key={rowKey}
|
|
787
|
+
rowKey={rowKey}
|
|
788
|
+
entry={entry}
|
|
789
|
+
namespace={namespaceName}
|
|
790
|
+
protocol={namespace.protocol}
|
|
791
|
+
rootPath={namespace.rootPath}
|
|
792
|
+
backendType={namespace.backendType}
|
|
793
|
+
depth={1}
|
|
794
|
+
locale={locale}
|
|
795
|
+
searchValue={searchValue}
|
|
796
|
+
onOpenFile={onOpenFile}
|
|
797
|
+
/>
|
|
798
|
+
);
|
|
799
|
+
})}
|
|
800
|
+
{showRemoteResults &&
|
|
801
|
+
filteredRemoteEntries.map((entry) => {
|
|
802
|
+
const rowKey = storageEntryKey(
|
|
803
|
+
entry,
|
|
804
|
+
remoteEntries.indexOf(entry),
|
|
805
|
+
);
|
|
806
|
+
return (
|
|
807
|
+
<StorageEntryRow
|
|
808
|
+
key={`remote-search:${rowKey}`}
|
|
809
|
+
rowKey={`remote-search:${rowKey}`}
|
|
810
|
+
entry={entry}
|
|
811
|
+
namespace={namespaceName}
|
|
812
|
+
protocol={namespace.protocol}
|
|
813
|
+
rootPath={namespace.rootPath}
|
|
814
|
+
backendType={namespace.backendType}
|
|
815
|
+
depth={1}
|
|
816
|
+
locale={locale}
|
|
817
|
+
searchValue={searchValue}
|
|
818
|
+
onOpenFile={onOpenFile}
|
|
819
|
+
/>
|
|
820
|
+
);
|
|
821
|
+
})}
|
|
822
|
+
{hasMore && !canSearchMore && (
|
|
823
|
+
<LoadMoreStorageEntries
|
|
536
824
|
depth={1}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
825
|
+
isLoading={isLoadingMore}
|
|
826
|
+
error={loadMoreError}
|
|
827
|
+
onLoadMore={loadMore}
|
|
540
828
|
/>
|
|
541
|
-
)
|
|
829
|
+
)}
|
|
542
830
|
</>
|
|
543
831
|
)}
|
|
544
832
|
</>
|
|
@@ -546,11 +834,163 @@ const StorageNamespaceSection: React.FC<{
|
|
|
546
834
|
};
|
|
547
835
|
|
|
548
836
|
export const StorageInspector: React.FC = () => {
|
|
549
|
-
const { namespaces } = useStorage();
|
|
837
|
+
const { namespaces, entriesByPath, pageMetadataByPath } = useStorage();
|
|
550
838
|
const { locale } = useLocale();
|
|
551
839
|
const [searchValue, setSearchValue] = useState("");
|
|
840
|
+
const [remoteSearchByNamespace, setRemoteSearchByNamespace] =
|
|
841
|
+
useState<RemoteSearchByNamespace>({});
|
|
552
842
|
const [openFile, setOpenFile] = useState<OpenFileInfo | null>(null);
|
|
843
|
+
const fetchStoragePage = useStoragePageFetcher();
|
|
553
844
|
const hasSearch = !!searchValue.trim();
|
|
845
|
+
const currentQuery = searchValue.trim();
|
|
846
|
+
|
|
847
|
+
const remoteSearchForNamespace = useCallback(
|
|
848
|
+
(namespaceName: string): RemoteSearchState => {
|
|
849
|
+
const remoteSearch = remoteSearchByNamespace[namespaceName];
|
|
850
|
+
if (remoteSearch?.query === currentQuery) {
|
|
851
|
+
return remoteSearch;
|
|
852
|
+
}
|
|
853
|
+
return idleRemoteSearch(currentQuery);
|
|
854
|
+
},
|
|
855
|
+
[currentQuery, remoteSearchByNamespace],
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const setRemoteSearch = useCallback(
|
|
859
|
+
(namespaceName: string, remoteSearch: RemoteSearchState) => {
|
|
860
|
+
setRemoteSearchByNamespace((state) => ({
|
|
861
|
+
...state,
|
|
862
|
+
[namespaceName]: remoteSearch,
|
|
863
|
+
}));
|
|
864
|
+
},
|
|
865
|
+
[],
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const canContinueRemoteSearch = useCallback(
|
|
869
|
+
(namespace: StorageNamespace): boolean => {
|
|
870
|
+
if (!currentQuery) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const namespaceName = namespace.name ?? namespace.displayName;
|
|
875
|
+
const searchPrefix = remoteSearchPrefix(currentQuery);
|
|
876
|
+
// No directory component in the query - the user is doing a fuzzy
|
|
877
|
+
// search and the backend can't help; rely on local filtering instead.
|
|
878
|
+
if (searchPrefix === "") {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const remoteSearch = remoteSearchForNamespace(namespaceName);
|
|
883
|
+
if (!canRetryRemoteSearch(remoteSearch)) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Already surfacing matches from loaded entries?
|
|
888
|
+
const rootEntries =
|
|
889
|
+
entriesByPath.get(storagePathKey(namespaceName, "")) ??
|
|
890
|
+
namespace.storageEntries;
|
|
891
|
+
const rootMatches = filterEntries(rootEntries, {
|
|
892
|
+
namespace: namespaceName,
|
|
893
|
+
searchValue: currentQuery,
|
|
894
|
+
entriesByPath,
|
|
895
|
+
});
|
|
896
|
+
if (rootMatches.length > 0) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const searchKey = storagePathKey(namespaceName, searchPrefix);
|
|
901
|
+
const prefixEntries = entriesByPath.get(searchKey) ?? [];
|
|
902
|
+
const prefixMatches = filterEntries(prefixEntries, {
|
|
903
|
+
namespace: namespaceName,
|
|
904
|
+
searchValue: currentQuery,
|
|
905
|
+
entriesByPath,
|
|
906
|
+
});
|
|
907
|
+
if (prefixMatches.length > 0) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return hasUnfetchedPrefixPage(
|
|
912
|
+
searchKey,
|
|
913
|
+
entriesByPath,
|
|
914
|
+
pageMetadataByPath,
|
|
915
|
+
);
|
|
916
|
+
},
|
|
917
|
+
[currentQuery, entriesByPath, pageMetadataByPath, remoteSearchForNamespace],
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const continueRemoteSearch = useCallback(
|
|
921
|
+
async (namespace: StorageNamespace) => {
|
|
922
|
+
if (!canContinueRemoteSearch(namespace)) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const query = currentQuery;
|
|
927
|
+
const namespaceName = namespace.name ?? namespace.displayName;
|
|
928
|
+
const prefix = remoteSearchPrefix(query);
|
|
929
|
+
const key = storagePathKey(namespaceName, prefix);
|
|
930
|
+
const cachedEntries = entriesByPath.get(key);
|
|
931
|
+
let nextPageToken = pageMetadataByPath.get(key)?.nextPageToken ?? null;
|
|
932
|
+
let hasFetchedAny = cachedEntries !== undefined;
|
|
933
|
+
|
|
934
|
+
setRemoteSearch(namespaceName, { query, status: "searching" });
|
|
935
|
+
try {
|
|
936
|
+
for (let page = 0; page < MAX_REMOTE_SEARCH_PAGES; page++) {
|
|
937
|
+
// First iteration with a stale cache hit needs no fetch; just check
|
|
938
|
+
// the cached page before paginating.
|
|
939
|
+
const shouldFetch = !hasFetchedAny || nextPageToken !== null;
|
|
940
|
+
let newEntries: StorageEntry[] = [];
|
|
941
|
+
if (shouldFetch) {
|
|
942
|
+
const result = await fetchStoragePage({
|
|
943
|
+
namespace: namespaceName,
|
|
944
|
+
prefix,
|
|
945
|
+
pageToken: nextPageToken,
|
|
946
|
+
append: hasFetchedAny,
|
|
947
|
+
});
|
|
948
|
+
newEntries = result.entries;
|
|
949
|
+
nextPageToken = result.next_page_token ?? null;
|
|
950
|
+
hasFetchedAny = true;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const entriesToCheck = shouldFetch
|
|
954
|
+
? newEntries
|
|
955
|
+
: (cachedEntries ?? []);
|
|
956
|
+
const hasMatches = entriesToCheck.some((entry) =>
|
|
957
|
+
entryMatchesQueryShallow(entry, query),
|
|
958
|
+
);
|
|
959
|
+
if (hasMatches) {
|
|
960
|
+
setRemoteSearch(namespaceName, { query, status: "found" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (nextPageToken === null) {
|
|
965
|
+
setRemoteSearch(namespaceName, { query, status: "exhausted" });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
setRemoteSearch(namespaceName, { query, status: "capped" });
|
|
970
|
+
} catch (error) {
|
|
971
|
+
setRemoteSearch(namespaceName, {
|
|
972
|
+
query,
|
|
973
|
+
status: "error",
|
|
974
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
[
|
|
979
|
+
canContinueRemoteSearch,
|
|
980
|
+
currentQuery,
|
|
981
|
+
entriesByPath,
|
|
982
|
+
fetchStoragePage,
|
|
983
|
+
pageMetadataByPath,
|
|
984
|
+
setRemoteSearch,
|
|
985
|
+
],
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
const continueRemoteSearches = useCallback(() => {
|
|
989
|
+
const searchableNamespaces = namespaces.filter(canContinueRemoteSearch);
|
|
990
|
+
for (const namespace of searchableNamespaces) {
|
|
991
|
+
void continueRemoteSearch(namespace);
|
|
992
|
+
}
|
|
993
|
+
}, [canContinueRemoteSearch, continueRemoteSearch, namespaces]);
|
|
554
994
|
|
|
555
995
|
if (namespaces.length === 0) {
|
|
556
996
|
return (
|
|
@@ -609,6 +1049,15 @@ export const StorageInspector: React.FC = () => {
|
|
|
609
1049
|
className="h-6 m-1"
|
|
610
1050
|
value={searchValue}
|
|
611
1051
|
onValueChange={setSearchValue}
|
|
1052
|
+
onKeyDown={(event) => {
|
|
1053
|
+
if (
|
|
1054
|
+
event.key === "Enter" &&
|
|
1055
|
+
namespaces.some(canContinueRemoteSearch)
|
|
1056
|
+
) {
|
|
1057
|
+
event.preventDefault();
|
|
1058
|
+
continueRemoteSearches();
|
|
1059
|
+
}
|
|
1060
|
+
}}
|
|
612
1061
|
rootClassName="flex-1 border-b-0"
|
|
613
1062
|
/>
|
|
614
1063
|
{hasSearch && (
|
|
@@ -622,7 +1071,7 @@ export const StorageInspector: React.FC = () => {
|
|
|
622
1071
|
</Button>
|
|
623
1072
|
)}
|
|
624
1073
|
<Tooltip
|
|
625
|
-
content="
|
|
1074
|
+
content="Search by file name within loaded entries, or by prefix (e.g. 'folder/x') for backend search. Press Enter to fetch more results."
|
|
626
1075
|
delayDuration={200}
|
|
627
1076
|
>
|
|
628
1077
|
<HelpCircleIcon className="h-3.5 w-3.5 shrink-0 cursor-help text-muted-foreground hover:text-foreground mr-2" />
|
|
@@ -638,15 +1087,20 @@ export const StorageInspector: React.FC = () => {
|
|
|
638
1087
|
</AddConnectionDialog>
|
|
639
1088
|
</div>
|
|
640
1089
|
<CommandList className="flex flex-col">
|
|
641
|
-
{namespaces.map((ns) =>
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1090
|
+
{namespaces.map((ns) => {
|
|
1091
|
+
const namespaceName = ns.name ?? ns.displayName;
|
|
1092
|
+
return (
|
|
1093
|
+
<StorageNamespaceSection
|
|
1094
|
+
key={namespaceName}
|
|
1095
|
+
namespace={ns}
|
|
1096
|
+
locale={locale}
|
|
1097
|
+
searchValue={searchValue}
|
|
1098
|
+
remoteSearch={remoteSearchForNamespace(namespaceName)}
|
|
1099
|
+
onContinueRemoteSearch={() => void continueRemoteSearch(ns)}
|
|
1100
|
+
onOpenFile={setOpenFile}
|
|
1101
|
+
/>
|
|
1102
|
+
);
|
|
1103
|
+
})}
|
|
650
1104
|
</CommandList>
|
|
651
1105
|
</Command>
|
|
652
1106
|
</div>
|