diff --git a/bin/serve.js b/bin/serve.js index a5f3abaf..55de0f63 100644 --- a/bin/serve.js +++ b/bin/serve.js @@ -32,6 +32,12 @@ const mimeTypes = { '.woff2': 'font/woff2', } +// Paths to ignore for logging +const ignorePrefix = [ + '/favicon.svg', + '/assets/', +] + /** * @template T * @typedef {T | Promise} Awaitable @@ -286,6 +292,11 @@ function startServer(port, path) { pipe(content, res) } + // Ignored logging paths + if (ignorePrefix.some(p => req.url?.startsWith(p))) { + return + } + // log request const endTime = new Date() const ms = endTime.getTime() - startTime.getTime() diff --git a/index.html b/index.html index ddedd5ae..8932fa4e 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,9 @@ hyperparam - + - + diff --git a/package.json b/package.json index d195dcc4..96e56308 100644 --- a/package.json +++ b/package.json @@ -58,15 +58,15 @@ "hightable": "0.20.1", "hyparquet": "1.19.0", "hyparquet-compressors": "1.1.1", - "icebird": "0.3.0" + "icebird": "0.3.1" }, "devDependencies": { "@eslint/js": "9.37.0", "@storybook/react-vite": "9.1.10", "@testing-library/react": "16.3.0", - "@types/node": "24.7.0", - "@types/react": "19.2.0", - "@types/react-dom": "19.2.0", + "@types/node": "24.7.2", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.1", "@vitejs/plugin-react": "5.0.4", "@vitest/coverage-v8": "3.2.4", "eslint": "9.37.0", @@ -82,7 +82,7 @@ "react-dom": "19.2.0", "storybook": "9.1.10", "typescript": "5.9.3", - "typescript-eslint": "8.45.0", + "typescript-eslint": "8.46.0", "vite": "7.1.9", "vitest": "3.2.4" }, diff --git a/public/favicon.png b/public/favicon.png deleted file mode 100644 index cd162fdd..00000000 Binary files a/public/favicon.png and /dev/null differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..8d5c22dd --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Breadcrumb/Breadcrumb.module.css b/src/components/Breadcrumb/Breadcrumb.module.css index 26beb468..34dcb4c6 100644 --- a/src/components/Breadcrumb/Breadcrumb.module.css +++ b/src/components/Breadcrumb/Breadcrumb.module.css @@ -1,16 +1,23 @@ +:root { + --color-background-dark: #333; + --border-radius-lg: 8px; + --space-3xs: clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem); +} + .breadcrumb { /* top navbar */ align-items: center; display: flex; - font-size: 18px; - height: 32px; + font-size: 16px; + height: 3em; justify-content: space-between; gap: 10px; min-height: 32px; - padding-left: 20px; - padding-right: 10px; border-bottom: 1px solid #ddd; - background: #eee; + background: var(--color-background-dark); + padding: 0 10px 0 20px; + border-radius: var(--border-radius-lg); + margin: var(--space-3xs); /* TODO(SL): forbid overflow? */ h1 { @@ -32,10 +39,9 @@ display: none; } a { - color: #222622; - font-family: "Courier New", Courier, monospace; - font-weight: 600; - font-size: 18px; + color: #eee; + font-weight: 500; + font-size: 12pt; text-overflow: ellipsis; white-space: nowrap; text-decoration-thickness: 1px; diff --git a/src/components/ContentWrapper/ContentWrapper.module.css b/src/components/ContentWrapper/ContentWrapper.module.css index 0a287614..0f4bb97e 100644 --- a/src/components/ContentWrapper/ContentWrapper.module.css +++ b/src/components/ContentWrapper/ContentWrapper.module.css @@ -7,13 +7,13 @@ & > header:first-child { align-items: center; - background-color: #f2f2f2; color: #444; display: flex; + font-size: 10pt; gap: 16px; height: 24px; overflow: hidden; - padding: 0 16px; + padding: 0 8px; /* all one line */ text-overflow: ellipsis; white-space: nowrap; diff --git a/src/components/Dropdown/Dropdown.module.css b/src/components/Dropdown/Dropdown.module.css index ef6f1fd7..ef36b559 100644 --- a/src/components/Dropdown/Dropdown.module.css +++ b/src/components/Dropdown/Dropdown.module.css @@ -20,11 +20,6 @@ overflow-x: hidden; padding: 0; } -.dropdownButton:active, -.dropdownButton:focus, -.dropdownButton:hover { - color: #113; -} /* caret */ .dropdownButton::before { diff --git a/src/components/Folder/Folder.module.css b/src/components/Folder/Folder.module.css index 2ca7809b..dce2135e 100644 --- a/src/components/Folder/Folder.module.css +++ b/src/components/Folder/Folder.module.css @@ -70,7 +70,7 @@ .search { background: #fff url("../../assets/search.svg") no-repeat center right 8px; border: 1px solid transparent; - border-radius: 8px; + border-radius: 12px; flex-shrink: 1; font-size: 12px; height: 24px; diff --git a/src/components/SideBar/SideBar.module.css b/src/components/SideBar/SideBar.module.css index 44d2c759..a9209ea8 100644 --- a/src/components/SideBar/SideBar.module.css +++ b/src/components/SideBar/SideBar.module.css @@ -7,7 +7,7 @@ height: 100vh; } .sideBar > div { - background-image: linear-gradient(to bottom, #f2f2f2, #e4e4e4); + background: #eef0f9; box-shadow: 0 0 6px rgba(10, 10, 10, 0.4); height: 100vh; position: absolute; @@ -23,7 +23,7 @@ filter: drop-shadow(0 0 2px #bbb); font-family: "Century Gothic", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1.1em; - font-weight: 500; + font-weight: 600; text-orientation: mixed; letter-spacing: 0.3px; padding: 10px 12px; diff --git a/src/lib/sources/hyperparamSource.ts b/src/lib/sources/hyperparamSource.ts index bb742661..3bd8cce9 100644 --- a/src/lib/sources/hyperparamSource.ts +++ b/src/lib/sources/hyperparamSource.ts @@ -8,12 +8,6 @@ export interface HyperparamFileMetadata { lastModified: string } -function canParse(sourceId: string): boolean { - /// we expect relative paths, such as path/to/file or path/to/dir/ - /// let's just check that it is empty or starts with a "word" character - return sourceId === '' || /^[\w]/.test(sourceId) -} - function getSourceParts(sourceId: string): SourcePart[] { const parts = sourceId.split('/') const sourceParts = [ @@ -66,12 +60,10 @@ async function listFiles(prefix: string, { endpoint, requestInit }: {endpoint: s } export function getHyperparamSource(sourceId: string, { endpoint, requestInit }: {endpoint: string, requestInit?: RequestInit}): FileSource | DirSource | undefined { - if (!URL.canParse(endpoint)) { - throw new Error('Invalid endpoint') - } - if (!canParse(sourceId)) { - return undefined - } + if (!URL.canParse(endpoint)) throw new Error('Invalid endpoint') + if (sourceId.startsWith('/')) throw new Error('Source cannot start with a /') + if (sourceId.includes('..')) throw new Error('Source cannot include ..') + const sourceParts = getSourceParts(sourceId) if (getKind(sourceId) === 'file') { return { diff --git a/test/lib/sources/hyperparamSource.test.ts b/test/lib/sources/hyperparamSource.test.ts index 81e80010..623ff6d6 100644 --- a/test/lib/sources/hyperparamSource.test.ts +++ b/test/lib/sources/hyperparamSource.test.ts @@ -6,27 +6,26 @@ globalThis.fetch = vi.fn() describe('getHyperparamSource', () => { const endpoint = 'http://localhost:3000' - test.for([ - 'test.txt', - 'no-extension', - 'folder/subfolder/test.txt', - ])('recognizes a local file path', (sourceId: string) => { - expect(getHyperparamSource(sourceId, { endpoint })?.kind).toBe('file') + it('recognizes local files', () => { + expect(getHyperparamSource('test.txt', { endpoint })?.kind).toBe('file') + expect(getHyperparamSource('no-extension', { endpoint })?.kind).toBe('file') + expect(getHyperparamSource('folder/subfolder/test.txt', { endpoint })?.kind).toBe('file') }) - test.for([ - '', - 'folder1/', - 'folder1/folder2/', - ])('recognizes a folder', (sourceId: string) => { - expect(getHyperparamSource(sourceId, { endpoint })?.kind).toBe('directory') + it('recognizes folders', () => { + expect(getHyperparamSource('', { endpoint })?.kind).toBe('directory') + expect(getHyperparamSource('folder1/', { endpoint })?.kind).toBe('directory') + expect(getHyperparamSource('folder1/folder2/', { endpoint })?.kind).toBe('directory') }) - test.for([ - '/', - '////', - ])('does not support a heading slash', (sourceId: string) => { - expect(getHyperparamSource(sourceId, { endpoint })).toBeUndefined() + it('throws on leading slash', () => { + expect(() => getHyperparamSource('/', { endpoint })).toThrow('Source cannot start with a /') + expect(() => getHyperparamSource('/folder/', { endpoint })).toThrow('Source cannot start with a /') + }) + + it('throws on .. in path', () => { + expect(() => getHyperparamSource('..', { endpoint })).toThrow('Source cannot include ..') + expect(() => getHyperparamSource('folder/../file.txt', { endpoint })).toThrow('Source cannot include ..') }) test.for([