{table.getHeaderGroups().map((headerGroup) => (
@@ -155,13 +181,19 @@ export const ObjectExplorer = ({ objects }: Props): JSX.Element => {
))}
-
+
+ {paddingTop > 0 &&
}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index];
if (!row) return null;
return
;
})}
+ {paddingBottom > 0 &&
}
);
diff --git a/components/providers/object-explorer-provider.tsx b/components/providers/object-explorer-provider.tsx
index c85662b..4465019 100644
--- a/components/providers/object-explorer-provider.tsx
+++ b/components/providers/object-explorer-provider.tsx
@@ -1,8 +1,16 @@
'use client';
-import { createContext, useCallback, useContext, useMemo, useState } from 'react';
+import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
+import { useLocation } from './location-provider';
+
+type UpdateObjectsOpts = { clear?: boolean; cursor?: string };
+
+type ObjectItem = R2Object | string;
export type IObjectExplorerContext = {
+ objects: ObjectItem[] | undefined;
+ updateObjects: (newObjects: ObjectItem[], opts?: UpdateObjectsOpts) => void;
+ tryFetchMoreObjects: () => void;
selectedObjects: Set
;
addSelectedObject: (key: string, shouldClear?: boolean) => void;
removeSelectedObject: (key: string) => void;
@@ -10,6 +18,9 @@ export type IObjectExplorerContext = {
};
const ObjectExplorerContext = createContext({
+ objects: [],
+ updateObjects: () => {},
+ tryFetchMoreObjects: () => {},
selectedObjects: new Set(),
addSelectedObject: () => {},
removeSelectedObject: () => {},
@@ -23,8 +34,60 @@ type Props = {
};
export const ObjectExplorerProvider = ({ children }: Props): JSX.Element => {
+ const { currentBucket, location } = useLocation();
+ const [objects, setObjects] = useState(undefined);
const [selectedObjects, setSelectedObjects] = useState>(new Set());
+ const [, setIsFetchingMoreObjects] = useState(false);
+ const isFetchingMoreObjectsRef = useRef(false);
+ const fetchObjectsCursor = useRef();
+
+ const updateObjects = useCallback((newObjects: ObjectItem[], opts: UpdateObjectsOpts = {}) => {
+ if (opts.clear) {
+ fetchObjectsCursor.current = null;
+ setObjects(newObjects);
+ } else {
+ setObjects((prevObjects) => (prevObjects ?? []).concat(newObjects));
+ }
+
+ fetchObjectsCursor.current = opts.cursor ?? null;
+ }, []);
+
+ const tryFetchMoreObjects = useCallback(() => {
+ if (!fetchObjectsCursor.current || !currentBucket) return undefined;
+ if (isFetchingMoreObjectsRef.current) return undefined;
+
+ isFetchingMoreObjectsRef.current = true;
+ setIsFetchingMoreObjects(true);
+
+ const searchParams = new URLSearchParams({
+ cursor: fetchObjectsCursor.current,
+ dir: location.join('/'),
+ });
+
+ return fetch(`/api/bucket/${currentBucket.raw}?${searchParams.toString()}`)
+ .then((resp) => {
+ if (resp.status !== 200) {
+ throw new Error(`Failed to fetch more objects: ${resp.statusText}`);
+ }
+ return resp.json();
+ })
+ .then((data) => {
+ updateObjects(data.objects, { cursor: data.truncated ? data.cursor : undefined });
+ })
+ .catch((err) => {
+ // TODO: Change to a toast.
+ // eslint-disable-next-line no-console
+ console.error(err);
+ // eslint-disable-next-line no-alert
+ alert(err instanceof Error ? err.message : 'Failed to fetch more objects');
+ })
+ .finally(() => {
+ isFetchingMoreObjectsRef.current = false;
+ setIsFetchingMoreObjects(false);
+ });
+ }, [currentBucket, location, updateObjects]);
+
const addSelectedObject = useCallback(
(key: string, shouldClear?: boolean) => {
if (shouldClear) {
@@ -50,8 +113,24 @@ export const ObjectExplorerProvider = ({ children }: Props): JSX.Element => {
return (
({ selectedObjects, addSelectedObject, removeSelectedObject, clearSelectedObjects }),
- [addSelectedObject, removeSelectedObject, selectedObjects, clearSelectedObjects],
+ () => ({
+ objects,
+ updateObjects,
+ tryFetchMoreObjects,
+ selectedObjects,
+ addSelectedObject,
+ removeSelectedObject,
+ clearSelectedObjects,
+ }),
+ [
+ objects,
+ updateObjects,
+ tryFetchMoreObjects,
+ addSelectedObject,
+ removeSelectedObject,
+ selectedObjects,
+ clearSelectedObjects,
+ ],
)}
>
{children}
diff --git a/utils/compare-alphanumeric-patched.ts b/utils/compare-alphanumeric-patched.ts
new file mode 100644
index 0000000..a15667e
--- /dev/null
+++ b/utils/compare-alphanumeric-patched.ts
@@ -0,0 +1,76 @@
+/**
+ * This is a patched version of the @tanstack/react-table `compareAlphanumeric` function.
+ *
+ * https://github.com/TanStack/table/blob/main/packages/table-core/src/sortingFns.ts
+ *
+ * It it modified to ensure that numbers are always sorted before strings.
+ */
+
+import type { SortingFn } from '@tanstack/react-table';
+import { reSplitAlphaNumeric } from '@tanstack/react-table';
+
+// Mixed sorting is slow, but very inclusive of many edge cases.
+// It handles numbers, mixed alphanumeric combinations, and even
+// null, undefined, and Infinity
+function compareAlphanumeric(aStr: string, bStr: string) {
+ // Split on number groups, but keep the delimiter
+ // Then remove falsey split values
+ const a = aStr.split(reSplitAlphaNumeric).filter(Boolean);
+ const b = bStr.split(reSplitAlphaNumeric).filter(Boolean);
+
+ // While
+ while (a.length && b.length) {
+ const aa = a.shift() as string;
+ const bb = b.shift() as string;
+
+ const an = parseInt(aa, 10);
+ const bn = parseInt(bb, 10);
+
+ const combo = [an, bn].sort();
+
+ // Both are string
+ if (Number.isNaN(combo[0])) {
+ if (aa > bb) {
+ return 1;
+ }
+ if (bb > aa) {
+ return -1;
+ }
+ continue;
+ }
+
+ // One is a string, one is a number
+ if (Number.isNaN(combo[1])) {
+ return Number.isNaN(an) ? 1 : -1; // NOTE: This is the main change from the original.
+ }
+
+ // Both are numbers
+ if (an > bn) {
+ return 1;
+ }
+ if (bn > an) {
+ return -1;
+ }
+ }
+
+ return a.length - b.length;
+}
+
+function toString(a: unknown) {
+ if (typeof a === 'number') {
+ if (Number.isNaN(a) || a === Infinity || a === -Infinity) {
+ return '';
+ }
+ return String(a);
+ }
+ if (typeof a === 'string') {
+ return a;
+ }
+ return '';
+}
+
+export const alphanumeric: SortingFn = (rowA, rowB, columnId) =>
+ compareAlphanumeric(
+ toString(rowA.getValue(columnId)).toLowerCase(),
+ toString(rowB.getValue(columnId)).toLowerCase(),
+ );