@marimo-team/frontend 0.23.10-dev55 → 0.23.10-dev57
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/assets/{edit-page-p8VsTwE3.js → edit-page-CVBh4l1i.js} +3 -3
- package/dist/assets/file-explorer-panel-BCB8cwDd.js +26 -0
- package/dist/assets/{index-CdnLLLYn.js → index-D2v5nOjW.js} +2 -2
- package/dist/assets/{panels-CBe83v0S.js → panels-DYPu5IGJ.js} +1 -1
- package/dist/assets/{run-page-BdSzJWEw.js → run-page-DrqPHnz9.js} +1 -1
- package/dist/assets/state-DyKcYt57.js +1 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/components/editor/cell/code/code-placeholder.css +1 -0
- package/src/components/editor/cell/code/code-placeholder.tsx +1 -0
- package/src/components/storage/__tests__/storage-inspector.test.ts +129 -2
- package/src/components/storage/storage-inspector.tsx +412 -39
- package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +1 -0
- package/src/core/codemirror/editor-mount-scheduler.ts +1 -0
- package/src/utils/schedule-task.ts +1 -0
- package/dist/assets/file-explorer-panel-GJ_Lqi4C.js +0 -26
- package/dist/assets/state-BkLoQLHX.js +0 -1
|
@@ -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,6 +96,10 @@ 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
|
+
|
|
97
103
|
/**
|
|
98
104
|
* Stable, unique identity for an entry row. Prefer the
|
|
99
105
|
* backend's stable id when present and fall back to the list index
|
|
@@ -120,15 +126,19 @@ function entryMatchesSearch(
|
|
|
120
126
|
entry: StorageEntry,
|
|
121
127
|
{ namespace, searchValue, entriesByPath }: SearchContext,
|
|
122
128
|
): boolean {
|
|
123
|
-
const query = searchValue.toLowerCase();
|
|
129
|
+
const query = searchValue.trim().toLowerCase();
|
|
130
|
+
const path = entry.path.toLowerCase();
|
|
131
|
+
const name = displayName(entry.path).toLowerCase();
|
|
124
132
|
|
|
125
|
-
if (
|
|
133
|
+
if (name.includes(query) || path.includes(query)) {
|
|
126
134
|
return true;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
// For directories, check loaded children recursively
|
|
130
138
|
if (entry.kind === "directory") {
|
|
131
|
-
const children = entriesByPath.get(
|
|
139
|
+
const children = entriesByPath.get(
|
|
140
|
+
storagePathKey(namespace, directoryPrefix(entry.path)),
|
|
141
|
+
);
|
|
132
142
|
if (children) {
|
|
133
143
|
return children.some((child) =>
|
|
134
144
|
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
@@ -143,7 +153,7 @@ function entryMatchesSearch(
|
|
|
143
153
|
* Filter entries to those matching the search (or having loaded descendants
|
|
144
154
|
* that match). Returns all entries when there is no active search.
|
|
145
155
|
*/
|
|
146
|
-
function filterEntries(
|
|
156
|
+
export function filterEntries(
|
|
147
157
|
entries: StorageEntry[],
|
|
148
158
|
context: SearchContext,
|
|
149
159
|
): StorageEntry[] {
|
|
@@ -153,6 +163,93 @@ function filterEntries(
|
|
|
153
163
|
return entries.filter((entry) => entryMatchesSearch(entry, context));
|
|
154
164
|
}
|
|
155
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
|
+
function canSearchMoreRemoteEntries({
|
|
191
|
+
hasSearch,
|
|
192
|
+
hasLoadedMatches,
|
|
193
|
+
isPending,
|
|
194
|
+
remoteSearch,
|
|
195
|
+
searchKey,
|
|
196
|
+
entriesByPath,
|
|
197
|
+
pageMetadataByPath,
|
|
198
|
+
}: {
|
|
199
|
+
hasSearch: boolean;
|
|
200
|
+
hasLoadedMatches: boolean;
|
|
201
|
+
isPending: boolean;
|
|
202
|
+
remoteSearch: RemoteSearchState;
|
|
203
|
+
searchKey: StoragePathKey;
|
|
204
|
+
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>;
|
|
205
|
+
pageMetadataByPath: ReadonlyMap<StoragePathKey, StoragePageMetadata>;
|
|
206
|
+
}): boolean {
|
|
207
|
+
if (!hasSearch || hasLoadedMatches || isPending) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (!canRetryRemoteSearch(remoteSearch)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
entriesByPath.get(searchKey) === undefined ||
|
|
216
|
+
pageMetadataByPath.get(searchKey)?.nextPageToken != null
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns the directory prefix to query the backend with for a given search.
|
|
222
|
+
*
|
|
223
|
+
* Object stores like obstore evaluate prefixes on a path-segment basis
|
|
224
|
+
* (`folder/x` would only match `folder/x/...`, never `folder/xsomething`), so
|
|
225
|
+
* for substring searches we list the parent directory and filter on the
|
|
226
|
+
* client. Returns `""` when the search has no directory component.
|
|
227
|
+
*/
|
|
228
|
+
export function remoteSearchPrefix(searchValue: string): string {
|
|
229
|
+
const trimmed = searchValue.trim();
|
|
230
|
+
const lastSlash = trimmed.lastIndexOf("/");
|
|
231
|
+
return lastSlash === -1 ? "" : trimmed.slice(0, lastSlash + 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Shallow check (no recursion into loaded children) used inside the
|
|
236
|
+
* remote-search pagination loop to decide whether a fetched page has
|
|
237
|
+
* any candidates worth surfacing to the user.
|
|
238
|
+
*/
|
|
239
|
+
function entryMatchesQueryShallow(
|
|
240
|
+
entry: StorageEntry,
|
|
241
|
+
searchValue: string,
|
|
242
|
+
): boolean {
|
|
243
|
+
const query = searchValue.trim().toLowerCase();
|
|
244
|
+
if (!query) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return (
|
|
248
|
+
entry.path.toLowerCase().includes(query) ||
|
|
249
|
+
displayName(entry.path).toLowerCase().includes(query)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
156
253
|
const LoadMoreStorageEntries: React.FC<{
|
|
157
254
|
depth: number;
|
|
158
255
|
isLoading: boolean;
|
|
@@ -325,7 +422,7 @@ const StorageEntryRow: React.FC<{
|
|
|
325
422
|
isDir &&
|
|
326
423
|
hasSearch &&
|
|
327
424
|
!!entriesByPath
|
|
328
|
-
.get(storagePathKey(namespace, entry.path))
|
|
425
|
+
.get(storagePathKey(namespace, directoryPrefix(entry.path)))
|
|
329
426
|
?.some((child) =>
|
|
330
427
|
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
331
428
|
);
|
|
@@ -474,7 +571,7 @@ const StorageEntryRow: React.FC<{
|
|
|
474
571
|
protocol={protocol}
|
|
475
572
|
rootPath={rootPath}
|
|
476
573
|
backendType={backendType}
|
|
477
|
-
prefix={entry.path}
|
|
574
|
+
prefix={directoryPrefix(entry.path)}
|
|
478
575
|
depth={depth + 1}
|
|
479
576
|
locale={locale}
|
|
480
577
|
searchValue={selfMatches ? "" : searchValue} // When a parent directory matches the search, we don't need to filter the children.
|
|
@@ -489,10 +586,19 @@ const StorageNamespaceSection: React.FC<{
|
|
|
489
586
|
namespace: StorageNamespace;
|
|
490
587
|
locale: string;
|
|
491
588
|
searchValue: string;
|
|
589
|
+
remoteSearch: RemoteSearchState;
|
|
590
|
+
onContinueRemoteSearch: () => void;
|
|
492
591
|
onOpenFile: (info: OpenFileInfo) => void;
|
|
493
|
-
}> = ({
|
|
592
|
+
}> = ({
|
|
593
|
+
namespace,
|
|
594
|
+
locale,
|
|
595
|
+
searchValue,
|
|
596
|
+
remoteSearch,
|
|
597
|
+
onContinueRemoteSearch,
|
|
598
|
+
onOpenFile,
|
|
599
|
+
}) => {
|
|
494
600
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
495
|
-
const { entriesByPath } = useStorage();
|
|
601
|
+
const { entriesByPath, pageMetadataByPath } = useStorage();
|
|
496
602
|
const { clearNamespaceCache } = useStorageActions();
|
|
497
603
|
const namespaceName = namespace.name ?? namespace.displayName;
|
|
498
604
|
|
|
@@ -523,6 +629,103 @@ const StorageNamespaceSection: React.FC<{
|
|
|
523
629
|
searchValue,
|
|
524
630
|
entriesByPath,
|
|
525
631
|
});
|
|
632
|
+
const searchPrefix = remoteSearchPrefix(searchValue);
|
|
633
|
+
const searchKey = storagePathKey(namespaceName, searchPrefix);
|
|
634
|
+
const remoteEntries =
|
|
635
|
+
searchPrefix === "" ? [] : (entriesByPath.get(searchKey) ?? []);
|
|
636
|
+
// The fetched page is the whole parent directory; we still need to filter
|
|
637
|
+
// it by the full search query before showing entries to the user.
|
|
638
|
+
const filteredRemoteEntries =
|
|
639
|
+
remoteEntries.length > 0
|
|
640
|
+
? filterEntries(remoteEntries, {
|
|
641
|
+
namespace: namespaceName,
|
|
642
|
+
searchValue,
|
|
643
|
+
entriesByPath,
|
|
644
|
+
})
|
|
645
|
+
: remoteEntries;
|
|
646
|
+
const hasSearch = !!searchValue.trim();
|
|
647
|
+
const hasLoadedMatches =
|
|
648
|
+
filtered.length > 0 || filteredRemoteEntries.length > 0;
|
|
649
|
+
const canSearchMore =
|
|
650
|
+
searchPrefix !== "" &&
|
|
651
|
+
canSearchMoreRemoteEntries({
|
|
652
|
+
hasSearch,
|
|
653
|
+
hasLoadedMatches,
|
|
654
|
+
isPending,
|
|
655
|
+
remoteSearch,
|
|
656
|
+
searchKey,
|
|
657
|
+
entriesByPath,
|
|
658
|
+
pageMetadataByPath,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const showRemoteResults = hasSearch && filtered.length === 0;
|
|
662
|
+
const statusRow = (() => {
|
|
663
|
+
if (isPending && entries.length === 0) {
|
|
664
|
+
return (
|
|
665
|
+
<span className="flex items-center gap-1.5">
|
|
666
|
+
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
667
|
+
Loading...
|
|
668
|
+
</span>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (remoteSearch.status === "searching") {
|
|
672
|
+
return (
|
|
673
|
+
<span className="flex items-center gap-1.5">
|
|
674
|
+
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
675
|
+
Searching more entries...
|
|
676
|
+
</span>
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (remoteSearch.status === "error") {
|
|
680
|
+
return (
|
|
681
|
+
<span className="text-destructive">
|
|
682
|
+
Search failed: {remoteSearch.error.message}
|
|
683
|
+
</span>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (remoteSearch.status === "capped") {
|
|
687
|
+
return (
|
|
688
|
+
<span className="flex items-center gap-1.5">
|
|
689
|
+
Searched more entries.
|
|
690
|
+
<Button
|
|
691
|
+
variant="text"
|
|
692
|
+
size="xs"
|
|
693
|
+
className="h-5 px-0 text-xs hover:text-blue-600"
|
|
694
|
+
onClick={onContinueRemoteSearch}
|
|
695
|
+
>
|
|
696
|
+
Continue searching
|
|
697
|
+
</Button>
|
|
698
|
+
<span className="text-[10px]">(or press Enter)</span>
|
|
699
|
+
</span>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (remoteSearch.status === "exhausted" && !hasLoadedMatches) {
|
|
703
|
+
return "No matches";
|
|
704
|
+
}
|
|
705
|
+
if (!hasSearch && !isPending && entries.length === 0 && !error) {
|
|
706
|
+
return "No entries";
|
|
707
|
+
}
|
|
708
|
+
if (canSearchMore) {
|
|
709
|
+
return (
|
|
710
|
+
<span className="flex items-center gap-1.5">
|
|
711
|
+
No loaded matches.
|
|
712
|
+
<Button
|
|
713
|
+
variant="text"
|
|
714
|
+
size="xs"
|
|
715
|
+
className="h-5 px-0 text-xs hover:text-blue-600"
|
|
716
|
+
onClick={onContinueRemoteSearch}
|
|
717
|
+
>
|
|
718
|
+
Search more entries
|
|
719
|
+
</Button>
|
|
720
|
+
<span className="text-[10px]">(or press Enter)</span>
|
|
721
|
+
</span>
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (hasSearch && !hasLoadedMatches && entries.length > 0) {
|
|
725
|
+
return "No matches";
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
})();
|
|
526
729
|
|
|
527
730
|
return (
|
|
528
731
|
<>
|
|
@@ -551,15 +754,6 @@ const StorageNamespaceSection: React.FC<{
|
|
|
551
754
|
</CommandItem>
|
|
552
755
|
{isExpanded && (
|
|
553
756
|
<>
|
|
554
|
-
{isPending && entries.length === 0 && (
|
|
555
|
-
<div
|
|
556
|
-
className="flex items-center gap-1.5 py-1 text-xs text-muted-foreground"
|
|
557
|
-
style={indentStyle(1)}
|
|
558
|
-
>
|
|
559
|
-
<LoaderCircle className="h-3 w-3 animate-spin" />
|
|
560
|
-
Loading...
|
|
561
|
-
</div>
|
|
562
|
-
)}
|
|
563
757
|
{error && entries.length === 0 && (
|
|
564
758
|
<ErrorState
|
|
565
759
|
error={error}
|
|
@@ -568,20 +762,12 @@ const StorageNamespaceSection: React.FC<{
|
|
|
568
762
|
showIcon={false}
|
|
569
763
|
/>
|
|
570
764
|
)}
|
|
571
|
-
{!
|
|
765
|
+
{!error && statusRow && (
|
|
572
766
|
<div
|
|
573
767
|
className="py-1 text-xs text-muted-foreground italic"
|
|
574
768
|
style={indentStyle(1)}
|
|
575
769
|
>
|
|
576
|
-
|
|
577
|
-
</div>
|
|
578
|
-
)}
|
|
579
|
-
{searchValue && filtered.length === 0 && entries.length > 0 && (
|
|
580
|
-
<div
|
|
581
|
-
className="py-1 text-xs text-muted-foreground italic"
|
|
582
|
-
style={indentStyle(1)}
|
|
583
|
-
>
|
|
584
|
-
No matches
|
|
770
|
+
{statusRow}
|
|
585
771
|
</div>
|
|
586
772
|
)}
|
|
587
773
|
{filtered.map((entry) => {
|
|
@@ -602,7 +788,29 @@ const StorageNamespaceSection: React.FC<{
|
|
|
602
788
|
/>
|
|
603
789
|
);
|
|
604
790
|
})}
|
|
605
|
-
{
|
|
791
|
+
{showRemoteResults &&
|
|
792
|
+
filteredRemoteEntries.map((entry) => {
|
|
793
|
+
const rowKey = storageEntryKey(
|
|
794
|
+
entry,
|
|
795
|
+
remoteEntries.indexOf(entry),
|
|
796
|
+
);
|
|
797
|
+
return (
|
|
798
|
+
<StorageEntryRow
|
|
799
|
+
key={`remote-search:${rowKey}`}
|
|
800
|
+
rowKey={`remote-search:${rowKey}`}
|
|
801
|
+
entry={entry}
|
|
802
|
+
namespace={namespaceName}
|
|
803
|
+
protocol={namespace.protocol}
|
|
804
|
+
rootPath={namespace.rootPath}
|
|
805
|
+
backendType={namespace.backendType}
|
|
806
|
+
depth={1}
|
|
807
|
+
locale={locale}
|
|
808
|
+
searchValue={searchValue}
|
|
809
|
+
onOpenFile={onOpenFile}
|
|
810
|
+
/>
|
|
811
|
+
);
|
|
812
|
+
})}
|
|
813
|
+
{hasMore && !canSearchMore && (
|
|
606
814
|
<LoadMoreStorageEntries
|
|
607
815
|
depth={1}
|
|
608
816
|
isLoading={isLoadingMore}
|
|
@@ -617,11 +825,162 @@ const StorageNamespaceSection: React.FC<{
|
|
|
617
825
|
};
|
|
618
826
|
|
|
619
827
|
export const StorageInspector: React.FC = () => {
|
|
620
|
-
const { namespaces } = useStorage();
|
|
828
|
+
const { namespaces, entriesByPath, pageMetadataByPath } = useStorage();
|
|
621
829
|
const { locale } = useLocale();
|
|
622
830
|
const [searchValue, setSearchValue] = useState("");
|
|
831
|
+
const [remoteSearchByNamespace, setRemoteSearchByNamespace] =
|
|
832
|
+
useState<RemoteSearchByNamespace>({});
|
|
623
833
|
const [openFile, setOpenFile] = useState<OpenFileInfo | null>(null);
|
|
834
|
+
const fetchStoragePage = useStoragePageFetcher();
|
|
624
835
|
const hasSearch = !!searchValue.trim();
|
|
836
|
+
const currentQuery = searchValue.trim();
|
|
837
|
+
|
|
838
|
+
const remoteSearchForNamespace = useCallback(
|
|
839
|
+
(namespaceName: string): RemoteSearchState => {
|
|
840
|
+
const remoteSearch = remoteSearchByNamespace[namespaceName];
|
|
841
|
+
if (remoteSearch?.query === currentQuery) {
|
|
842
|
+
return remoteSearch;
|
|
843
|
+
}
|
|
844
|
+
return idleRemoteSearch(currentQuery);
|
|
845
|
+
},
|
|
846
|
+
[currentQuery, remoteSearchByNamespace],
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const setRemoteSearch = useCallback(
|
|
850
|
+
(namespaceName: string, remoteSearch: RemoteSearchState) => {
|
|
851
|
+
setRemoteSearchByNamespace((state) => ({
|
|
852
|
+
...state,
|
|
853
|
+
[namespaceName]: remoteSearch,
|
|
854
|
+
}));
|
|
855
|
+
},
|
|
856
|
+
[],
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
const canContinueRemoteSearch = useCallback(
|
|
860
|
+
(namespace: StorageNamespace): boolean => {
|
|
861
|
+
if (!currentQuery) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const namespaceName = namespace.name ?? namespace.displayName;
|
|
866
|
+
const searchPrefix = remoteSearchPrefix(currentQuery);
|
|
867
|
+
// No directory component in the query - the user is doing a fuzzy
|
|
868
|
+
// search and the backend can't help; rely on local filtering instead.
|
|
869
|
+
if (searchPrefix === "") {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const remoteSearch = remoteSearchForNamespace(namespaceName);
|
|
874
|
+
if (!canRetryRemoteSearch(remoteSearch)) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Already surfacing matches from loaded entries?
|
|
879
|
+
const rootEntries =
|
|
880
|
+
entriesByPath.get(storagePathKey(namespaceName, "")) ??
|
|
881
|
+
namespace.storageEntries;
|
|
882
|
+
const rootMatches = filterEntries(rootEntries, {
|
|
883
|
+
namespace: namespaceName,
|
|
884
|
+
searchValue: currentQuery,
|
|
885
|
+
entriesByPath,
|
|
886
|
+
});
|
|
887
|
+
if (rootMatches.length > 0) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const searchKey = storagePathKey(namespaceName, searchPrefix);
|
|
892
|
+
const prefixEntries = entriesByPath.get(searchKey) ?? [];
|
|
893
|
+
const prefixMatches = filterEntries(prefixEntries, {
|
|
894
|
+
namespace: namespaceName,
|
|
895
|
+
searchValue: currentQuery,
|
|
896
|
+
entriesByPath,
|
|
897
|
+
});
|
|
898
|
+
if (prefixMatches.length > 0) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const canFetchPrefix =
|
|
903
|
+
entriesByPath.get(searchKey) === undefined ||
|
|
904
|
+
pageMetadataByPath.get(searchKey)?.nextPageToken != null;
|
|
905
|
+
return canFetchPrefix;
|
|
906
|
+
},
|
|
907
|
+
[currentQuery, entriesByPath, pageMetadataByPath, remoteSearchForNamespace],
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
const continueRemoteSearch = useCallback(
|
|
911
|
+
async (namespace: StorageNamespace) => {
|
|
912
|
+
if (!canContinueRemoteSearch(namespace)) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const query = currentQuery;
|
|
917
|
+
const namespaceName = namespace.name ?? namespace.displayName;
|
|
918
|
+
const prefix = remoteSearchPrefix(query);
|
|
919
|
+
const key = storagePathKey(namespaceName, prefix);
|
|
920
|
+
const cachedEntries = entriesByPath.get(key);
|
|
921
|
+
let nextPageToken = pageMetadataByPath.get(key)?.nextPageToken ?? null;
|
|
922
|
+
let hasFetchedAny = cachedEntries !== undefined;
|
|
923
|
+
|
|
924
|
+
setRemoteSearch(namespaceName, { query, status: "searching" });
|
|
925
|
+
try {
|
|
926
|
+
for (let page = 0; page < MAX_REMOTE_SEARCH_PAGES; page++) {
|
|
927
|
+
// First iteration with a stale cache hit needs no fetch; just check
|
|
928
|
+
// the cached page before paginating.
|
|
929
|
+
const shouldFetch = !hasFetchedAny || nextPageToken !== null;
|
|
930
|
+
let newEntries: StorageEntry[] = [];
|
|
931
|
+
if (shouldFetch) {
|
|
932
|
+
const result = await fetchStoragePage({
|
|
933
|
+
namespace: namespaceName,
|
|
934
|
+
prefix,
|
|
935
|
+
pageToken: nextPageToken,
|
|
936
|
+
append: hasFetchedAny,
|
|
937
|
+
});
|
|
938
|
+
newEntries = result.entries;
|
|
939
|
+
nextPageToken = result.next_page_token ?? null;
|
|
940
|
+
hasFetchedAny = true;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const entriesToCheck = shouldFetch
|
|
944
|
+
? newEntries
|
|
945
|
+
: (cachedEntries ?? []);
|
|
946
|
+
const hasMatches = entriesToCheck.some((entry) =>
|
|
947
|
+
entryMatchesQueryShallow(entry, query),
|
|
948
|
+
);
|
|
949
|
+
if (hasMatches) {
|
|
950
|
+
setRemoteSearch(namespaceName, { query, status: "found" });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (nextPageToken === null) {
|
|
955
|
+
setRemoteSearch(namespaceName, { query, status: "exhausted" });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
setRemoteSearch(namespaceName, { query, status: "capped" });
|
|
960
|
+
} catch (error) {
|
|
961
|
+
setRemoteSearch(namespaceName, {
|
|
962
|
+
query,
|
|
963
|
+
status: "error",
|
|
964
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
[
|
|
969
|
+
canContinueRemoteSearch,
|
|
970
|
+
currentQuery,
|
|
971
|
+
entriesByPath,
|
|
972
|
+
fetchStoragePage,
|
|
973
|
+
pageMetadataByPath,
|
|
974
|
+
setRemoteSearch,
|
|
975
|
+
],
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
const continueRemoteSearches = useCallback(() => {
|
|
979
|
+
const searchableNamespaces = namespaces.filter(canContinueRemoteSearch);
|
|
980
|
+
for (const namespace of searchableNamespaces) {
|
|
981
|
+
void continueRemoteSearch(namespace);
|
|
982
|
+
}
|
|
983
|
+
}, [canContinueRemoteSearch, continueRemoteSearch, namespaces]);
|
|
625
984
|
|
|
626
985
|
if (namespaces.length === 0) {
|
|
627
986
|
return (
|
|
@@ -680,6 +1039,15 @@ export const StorageInspector: React.FC = () => {
|
|
|
680
1039
|
className="h-6 m-1"
|
|
681
1040
|
value={searchValue}
|
|
682
1041
|
onValueChange={setSearchValue}
|
|
1042
|
+
onKeyDown={(event) => {
|
|
1043
|
+
if (
|
|
1044
|
+
event.key === "Enter" &&
|
|
1045
|
+
namespaces.some(canContinueRemoteSearch)
|
|
1046
|
+
) {
|
|
1047
|
+
event.preventDefault();
|
|
1048
|
+
continueRemoteSearches();
|
|
1049
|
+
}
|
|
1050
|
+
}}
|
|
683
1051
|
rootClassName="flex-1 border-b-0"
|
|
684
1052
|
/>
|
|
685
1053
|
{hasSearch && (
|
|
@@ -693,7 +1061,7 @@ export const StorageInspector: React.FC = () => {
|
|
|
693
1061
|
</Button>
|
|
694
1062
|
)}
|
|
695
1063
|
<Tooltip
|
|
696
|
-
content="Filters loaded entries only. Expand directories to
|
|
1064
|
+
content="Filters loaded entries only. Expand directories or press Enter to search more entries from the backend."
|
|
697
1065
|
delayDuration={200}
|
|
698
1066
|
>
|
|
699
1067
|
<HelpCircleIcon className="h-3.5 w-3.5 shrink-0 cursor-help text-muted-foreground hover:text-foreground mr-2" />
|
|
@@ -709,15 +1077,20 @@ export const StorageInspector: React.FC = () => {
|
|
|
709
1077
|
</AddConnectionDialog>
|
|
710
1078
|
</div>
|
|
711
1079
|
<CommandList className="flex flex-col">
|
|
712
|
-
{namespaces.map((ns) =>
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1080
|
+
{namespaces.map((ns) => {
|
|
1081
|
+
const namespaceName = ns.name ?? ns.displayName;
|
|
1082
|
+
return (
|
|
1083
|
+
<StorageNamespaceSection
|
|
1084
|
+
key={namespaceName}
|
|
1085
|
+
namespace={ns}
|
|
1086
|
+
locale={locale}
|
|
1087
|
+
searchValue={searchValue}
|
|
1088
|
+
remoteSearch={remoteSearchForNamespace(namespaceName)}
|
|
1089
|
+
onContinueRemoteSearch={() => void continueRemoteSearch(ns)}
|
|
1090
|
+
onOpenFile={setOpenFile}
|
|
1091
|
+
/>
|
|
1092
|
+
);
|
|
1093
|
+
})}
|
|
721
1094
|
</CommandList>
|
|
722
1095
|
</Command>
|
|
723
1096
|
</div>
|