diff --git a/package.json b/package.json
index ee74bb8..d8237d9 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.18.0",
"@mui/material": "^5.18.0",
- "@vertexvis/api-client-node": "^0.33.2",
+ "@vertexvis/api-client-node": "^0.40.0",
"@vertexvis/geometry": "^0.24.2",
"@vertexvis/viewer-react": "^0.24.2",
"lodash.debounce": "^4.0",
@@ -17,6 +17,7 @@
"next": "^15.5.7",
"next-connect": "^0.13.0",
"next-iron-session": "^4.2",
+ "pretty-bytes": "^7.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.63.0",
diff --git a/src/components/file/FileDetailsDrawer.tsx b/src/components/file/FileDetailsDrawer.tsx
new file mode 100644
index 0000000..4dd2f43
--- /dev/null
+++ b/src/components/file/FileDetailsDrawer.tsx
@@ -0,0 +1,169 @@
+import { Close } from "@mui/icons-material";
+import {
+ Box,
+ Drawer,
+ IconButton,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+} from "@mui/material";
+import React from "react";
+
+import { toLocaleString } from "../../lib/dates";
+import { File } from "../../lib/files";
+import { toDisplayValue, toFileSizeDisplay } from "../../lib/formatting";
+import { RightDrawerWidth } from "../shared/Layout";
+
+interface Props {
+ readonly file?: File;
+ readonly onClose: () => void;
+ readonly open: boolean;
+}
+
+export function FileDetailsDrawer({
+ file,
+ onClose,
+ open,
+}: Props): JSX.Element {
+ return (
+
+
+
+ File Details
+
+
+
+
+
+ {file ? (
+
+
+
+ ) : (
+ <>>
+ )}
+
+ );
+}
+
+function DetailsRow({
+ label,
+ value,
+}: {
+ readonly label: string;
+ readonly value?: string;
+}): JSX.Element {
+ return (
+
+
+ {label}
+
+ {toDisplayValue(value)}
+
+
+
+ );
+}
+
+function MetadataRow({
+ metadata,
+}: {
+ readonly metadata?: Record;
+}): JSX.Element {
+ const entries = metadata == null ? [] : Object.entries(metadata);
+
+ return (
+
+
+ Metadata
+ {entries.length > 0 ? (
+
+
+
+
+ Key
+
+
+ Value
+
+
+
+
+ {entries.map(([key, value]) => (
+
+
+
+ {toDisplayValue(key)}
+
+
+
+
+ {toDisplayValue(value)}
+
+
+
+ ))}
+
+
+ ) : (
+
+ N/A
+
+ )}
+
+
+ );
+}
diff --git a/src/components/file/FileTable.tsx b/src/components/file/FileTable.tsx
index bd3a167..a1e69cb 100644
--- a/src/components/file/FileTable.tsx
+++ b/src/components/file/FileTable.tsx
@@ -20,7 +20,7 @@ import React from "react";
import useSWR from "swr";
import { toLocaleString } from "../../lib/dates";
-import { toFilePage } from "../../lib/files";
+import { File, toFilePage } from "../../lib/files";
import { SwrProps } from "../../lib/paging";
import { DataLoadError } from "../shared/DataLoadError";
import { DefaultPageSize, DefaultRowHeight } from "../shared/Layout";
@@ -46,7 +46,15 @@ function useFiles({ cursor, pageSize, suppliedId }: SwrProps) {
);
}
-export default function FilesTable(): JSX.Element {
+interface Props {
+ readonly activeFileId?: string;
+ readonly onFileSelected: (file: File) => void;
+}
+
+export default function FilesTable({
+ activeFileId,
+ onFileSelected,
+}: Props): JSX.Element {
const pageSize = DefaultPageSize;
const [curPage, setCurPage] = React.useState(0);
const [cursor, setCursor] = React.useState();
@@ -173,17 +181,23 @@ export default function FilesTable(): JSX.Element {
) : (
page.items.map((row) => {
const isSel = selected.has(row.id);
+ const isActive = activeFileId === row.id;
+
return (
onFileSelected(row)}
>
handleCheck(row.id)}
+ onClick={(e) => {
+ e.stopPropagation();
+ handleCheck(row.id);
+ }}
>
diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts
new file mode 100644
index 0000000..16cf72d
--- /dev/null
+++ b/src/lib/formatting.ts
@@ -0,0 +1,11 @@
+import prettyBytes from "pretty-bytes";
+
+export function toDisplayValue(value?: string): string {
+ return value == null || value.trim().length === 0 ? "N/A" : value;
+}
+
+export function toFileSizeDisplay(size?: number): string | undefined {
+ if (size == null) return undefined;
+
+ return prettyBytes(size);
+}
diff --git a/src/pages/files.tsx b/src/pages/files.tsx
index ed57d3f..c03a2bd 100644
--- a/src/pages/files.tsx
+++ b/src/pages/files.tsx
@@ -1,7 +1,9 @@
import dynamic from "next/dynamic";
import React from "react";
+import { FileDetailsDrawer } from "../components/file/FileDetailsDrawer";
import { Layout } from "../components/shared/Layout";
+import { File } from "../lib/files";
import { defaultServerSideProps } from "../lib/with-session";
const FilesTable = dynamic(() => import("../components/file/FileTable"), {
@@ -9,7 +11,24 @@ const FilesTable = dynamic(() => import("../components/file/FileTable"), {
});
export default function Files(): JSX.Element {
- return } />;
+ const [file, setFile] = React.useState();
+ const drawerOpen = Boolean(file);
+
+ return (
+
+ }
+ rightDrawer={
+ setFile(undefined)}
+ open={drawerOpen}
+ />
+ }
+ rightDrawerOpen={drawerOpen}
+ />
+ );
}
export const getServerSideProps = defaultServerSideProps;
diff --git a/yarn.lock b/yarn.lock
index c603f2b..6e8f734 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -994,12 +994,12 @@
"@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0"
-"@vertexvis/api-client-node@^0.33.2":
- version "0.33.2"
- resolved "https://registry.yarnpkg.com/@vertexvis/api-client-node/-/api-client-node-0.33.2.tgz#bfb15f972270b1aaf5ae80abb146b28b00a2559b"
- integrity sha512-u4Hso8ADOMbk7s/8Qhm0vGzY1sR2MDQ3mWiFc0xxJ051rL2S/UdKjSQA/Yjm3eU2ut1cTmso8ZXWVqvuAqcv6w==
+"@vertexvis/api-client-node@^0.40.0":
+ version "0.40.0"
+ resolved "https://registry.yarnpkg.com/@vertexvis/api-client-node/-/api-client-node-0.40.0.tgz#9563245ab3247ae3789bc972a739f2638b2681da"
+ integrity sha512-z/WtLR296nffQc047nHIiFhC68Vhw4Xs5354t0LWA4j+jPm8ybnf6UkObuJcJv1Nm13qsRe+NAJ08zQzUbtGzQ==
dependencies:
- axios "^1.6.4"
+ axios "1.11.0"
p-limit "^3"
"@vertexvis/frame-streaming-protos@^0.15.1":
@@ -1293,7 +1293,7 @@ axe-core@^4.10.0:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
-axios@1.8.2, axios@^1.6.4:
+axios@1.11.0, axios@1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
@@ -3253,6 +3253,11 @@ prettier@^2.4:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+pretty-bytes@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-7.1.0.tgz#d788c9906241dbdcd4defab51b6d7470243db9bd"
+ integrity sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==
+
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"