Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMemo } from 'react'
import { Config, ConfigProvider } from '../hooks/useConfig.js'
import { getHttpSource } from '../lib/sources/httpSource.js'
import { getHyperparamSource } from '../lib/sources/hyperparamSource.js'
import Page from './Page.js'
Expand All @@ -10,20 +12,20 @@ export default function App() {

const source = getHttpSource(sourceId) ?? getHyperparamSource(sourceId, { endpoint: location.origin })

// Memoize the config to avoid creating a new object on each render
const config: Config = useMemo(() => ({
routes: {
getSourceRouteUrl: ({ sourceId }) => `/files?key=${sourceId}`,
getCellRouteUrl: ({ sourceId, col, row }) => `/files?key=${sourceId}&col=${col}&row=${row}`,
},
}), [])

if (!source) {
return <div>Could not load a data source. You have to pass a valid source in the url.</div>
}
return (
<Page
source={source}
navigation={{ row, col }}
config={{
slidePanel: {},
routes: {
getSourceRouteUrl: ({ sourceId }) => `/files?key=${sourceId}`,
getCellRouteUrl: ({ sourceId, col, row }) => `/files?key=${sourceId}&col=${col}&row=${row}`,
},
}}
/>
<ConfigProvider value={config}>
<Page source={source} navigation={{ row, col }} />
</ConfigProvider>
)
}
10 changes: 5 additions & 5 deletions src/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { ReactNode } from 'react'
import type { RoutesConfig } from '../lib/routes.js'
import { useConfig } from '../hooks/useConfig.js'
import type { Source } from '../lib/sources/types.js'

export type BreadcrumbConfig = RoutesConfig
interface BreadcrumbProps {
source: Source,
config?: BreadcrumbConfig
children?: ReactNode
}

/**
* Breadcrumb navigation
*/
export default function Breadcrumb({ source, config, children }: BreadcrumbProps) {
export default function Breadcrumb({ source, children }: BreadcrumbProps) {
const { routes } = useConfig()

return <nav className='top-header top-header-divided'>
<div className='path'>
{source.sourceParts.map((part, depth) =>
<a href={config?.routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
)}
</div>
{children}
Expand Down
9 changes: 3 additions & 6 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@ import { asyncBufferFromUrl, parquetMetadataAsync } from 'hyparquet'
import { useEffect, useState } from 'react'
import type { FileSource } from '../lib/sources/types.js'
import { parquetDataFrame } from '../lib/tableProvider.js'
import Breadcrumb, { BreadcrumbConfig } from './Breadcrumb.js'
import Breadcrumb from './Breadcrumb.js'
import Layout from './Layout.js'

export type CellConfig = BreadcrumbConfig

interface CellProps {
source: FileSource;
row: number;
col: number;
config?: CellConfig
}

/**
* Cell viewer displays a single cell from a table.
*/
export default function CellView({ source, row, col, config }: CellProps) {
export default function CellView({ source, row, col }: CellProps) {
const [text, setText] = useState<string | undefined>()
const [progress, setProgress] = useState<number>()
const [error, setError] = useState<Error>()
Expand Down Expand Up @@ -69,7 +66,7 @@ export default function CellView({ source, row, col, config }: CellProps) {

return (
<Layout progress={progress} error={error} title={fileName}>
<Breadcrumb source={source} config={config} />
<Breadcrumb source={source} />

{/* <Highlight text={text || ''} /> */}
<pre className="viewer text">{text}</pre>
Expand Down
13 changes: 5 additions & 8 deletions src/components/File.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { useState } from 'react'
import type { FileSource } from '../lib/sources/types.js'
import Breadcrumb, { BreadcrumbConfig } from './Breadcrumb.js'
import Breadcrumb from './Breadcrumb.js'
import Layout from './Layout.js'
import Viewer, { ViewerConfig } from './viewers/Viewer.js'

export type FileConfig = ViewerConfig & BreadcrumbConfig
import Viewer from './viewers/Viewer.js'

interface FileProps {
source: FileSource
config?: FileConfig
}

/**
* File viewer page
*/
export default function File({ source, config }: FileProps) {
export default function File({ source }: FileProps) {
const [progress, setProgress] = useState<number>()
const [error, setError] = useState<Error>()

return <Layout progress={progress} error={error} title={source.fileName}>
<Breadcrumb source={source} config={config} />
<Viewer source={source} setProgress={setProgress} setError={setError} config={config} />
<Breadcrumb source={source} />
<Viewer source={source} setProgress={setProgress} setError={setError} />
</Layout>
}
14 changes: 7 additions & 7 deletions src/components/Folder.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { useEffect, useRef, useState } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import type { DirSource, FileMetadata } from '../lib/sources/types.js'
import { cn, formatFileSize, getFileDate, getFileDateShort } from '../lib/utils.js'
import Breadcrumb, { BreadcrumbConfig } from './Breadcrumb.js'
import Breadcrumb from './Breadcrumb.js'
import Layout, { Spinner } from './Layout.js'

export type FolderConfig = BreadcrumbConfig

interface FolderProps {
source: DirSource
config?: FolderConfig
}

/**
* Folder browser page
*/
export default function Folder({ source, config }: FolderProps) {
export default function Folder({ source }: FolderProps) {
// State to hold file listing
const [files, setFiles] = useState<FileMetadata[]>()
const [error, setError] = useState<Error>()
const [searchQuery, setSearchQuery] = useState('')
const searchRef = useRef<HTMLInputElement>(null)
const listRef = useRef<HTMLUListElement>(null)

const { routes } = useConfig()

// Fetch files on component mount
useEffect(() => {
source.listFiles()
Expand Down Expand Up @@ -83,7 +83,7 @@ export default function Folder({ source, config }: FolderProps) {
}, [])

return <Layout error={error} title={source.prefix}>
<Breadcrumb source={source} config={config}>
<Breadcrumb source={source}>
<div className='top-actions'>
<input autoFocus className='search' placeholder='Search...' ref={searchRef} />
</div>
Expand All @@ -92,7 +92,7 @@ export default function Folder({ source, config }: FolderProps) {
{files && files.length > 0 && <ul className='file-list' ref={listRef}>
{filtered?.map((file, index) =>
<li key={index}>
<a href={config?.routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
<a href={routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
<span className={cn('file-name', 'file', file.kind === 'directory' && 'folder')}>
{file.name}
</span>
Expand Down
12 changes: 5 additions & 7 deletions src/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Source } from '../lib/sources/types.js'
import Cell from './Cell.js'
import File, { FileConfig } from './File.js'
import File from './File.js'
import Folder from './Folder.js'

export type PageConfig = FileConfig
export interface Navigation {
col?: number
row?: number
Expand All @@ -12,18 +11,17 @@ export interface Navigation {
interface PageProps {
source: Source,
navigation?: Navigation,
config?: PageConfig
}

export default function Page({ source, navigation, config }: PageProps) {
export default function Page({ source, navigation }: PageProps) {
if (source.kind === 'directory') {
return <Folder source={source} config={config}/>
return <Folder source={source} />
}
if (navigation?.row !== undefined && navigation.col !== undefined) {
// cell view
return <Cell source={source} row={navigation.row} col={navigation.col} config={config} />
return <Cell source={source} row={navigation.row} col={navigation.col} />
} else {
// file view
return <File source={source} config={config} />
return <File source={source} />
}
}
11 changes: 5 additions & 6 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import Breadcrumb, { BreadcrumbConfig } from './Breadcrumb.js'
import Cell, { CellConfig } from './Cell.js'
import File, { FileConfig } from './File.js'
import Folder, { FolderConfig } from './Folder.js'
import Breadcrumb from './Breadcrumb.js'
import Cell from './Cell.js'
import File from './File.js'
import Folder from './Folder.js'
import Layout, { ErrorBar, Spinner } from './Layout.js'
import Markdown from './Markdown.js'
import Page, { PageConfig } from './Page.js'
import Page from './Page.js'
export * from './viewers/index.js'
export { Breadcrumb, Cell, ErrorBar, File, Folder, Layout, Markdown, Page, Spinner }
export type { BreadcrumbConfig, CellConfig, FileConfig, FolderConfig, PageConfig }
23 changes: 8 additions & 15 deletions src/components/viewers/ParquetView.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import HighTable, { DataFrame, rowCache } from 'hightable'
import { asyncBufferFromUrl, parquetMetadataAsync } from 'hyparquet'
import React, { useCallback, useEffect, useState } from 'react'
import { RoutesConfig, appendSearchParams } from '../../lib/routes.js'
import { useConfig } from '../../hooks/useConfig.js'
import { appendSearchParams } from '../../lib/routes.js'
import { FileSource } from '../../lib/sources/types.js'
import { parquetDataFrame } from '../../lib/tableProvider.js'
import { cn } from '../../lib/utils.js'
import styles from '../../styles/ParquetView.module.css'
import { Spinner } from '../Layout.js'
import CellPanel from './CellPanel.js'
import ContentHeader, { ContentSize } from './ContentHeader.js'
import SlidePanel, { SlidePanelConfig } from './SlidePanel.js'

interface HighTableConfig {
hightable?: {
className?: string;
}
}
export type ParquetViewConfig = SlidePanelConfig & RoutesConfig & HighTableConfig
import SlidePanel from './SlidePanel.js'

interface ViewerProps {
source: FileSource
setProgress: (progress: number | undefined) => void
setError: (error: Error | undefined) => void
config?: ParquetViewConfig
}

interface Content extends ContentSize {
Expand All @@ -32,10 +25,11 @@ interface Content extends ContentSize {
/**
* Parquet file viewer
*/
export default function ParquetView({ source, setProgress, setError, config }: ViewerProps) {
export default function ParquetView({ source, setProgress, setError }: ViewerProps) {
const [isLoading, setIsLoading] = useState<boolean>(true)
const [content, setContent] = useState<Content>()
const [cell, setCell] = useState<{ row: number, col: number } | undefined>()
const { highTable, routes } = useConfig()

useEffect(() => {
async function loadParquetDataFrame() {
Expand Down Expand Up @@ -77,12 +71,12 @@ export default function ParquetView({ source, setProgress, setError, config }: V

const { sourceId } = source
const getCellRouteUrl = useCallback(({ col, row }: {col: number, row: number}) => {
const url = config?.routes?.getCellRouteUrl?.({ sourceId, col, row })
const url = routes?.getCellRouteUrl?.({ sourceId, col, row })
if (url) {
return url
}
return appendSearchParams({ col: col.toString(), row: row.toString() })
}, [config, sourceId])
}, [routes, sourceId])

const onDoubleClickCell = useCallback((_event: React.MouseEvent, col: number, row: number) => {
if (cell?.col === col && cell.row === row) {
Expand All @@ -108,7 +102,7 @@ export default function ParquetView({ source, setProgress, setError, config }: V
onDoubleClickCell={onDoubleClickCell}
onMouseDownCell={onMouseDownCell}
onError={setError}
className={cn(styles.hightable, config?.hightable?.className)}
className={cn(styles.hightable, highTable?.className)}
/>}

{isLoading && <div className='center'><Spinner /></div>}
Expand All @@ -132,7 +126,6 @@ export default function ParquetView({ source, setProgress, setError, config }: V
isPanelOpen={!!(content?.dataframe && cell)}
mainContent={mainContent}
panelContent={panelContent}
config={config}
/>
)
}
18 changes: 5 additions & 13 deletions src/components/viewers/SlidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import React, { ReactNode, useCallback, useEffect, useState } from 'react'

export interface SlidePanelConfig {
slidePanel?: {
minWidth?: number
defaultWidth?: number
}
}
import { useConfig } from '../../hooks/useConfig.js'

interface SlidePanelProps {
mainContent: ReactNode
panelContent: ReactNode
isPanelOpen: boolean
config?: SlidePanelConfig
}

const WIDTH = {
Expand All @@ -22,17 +15,16 @@ const WIDTH = {
/**
* Slide out panel component with resizing.
*/
export default function SlidePanel({
mainContent, panelContent, isPanelOpen, config,
}: SlidePanelProps) {
const minWidth = config?.slidePanel?.minWidth && config.slidePanel.minWidth > 0 ? config.slidePanel.minWidth : WIDTH.MIN
export default function SlidePanel({ mainContent, panelContent, isPanelOpen }: SlidePanelProps) {
const { slidePanel } = useConfig()
const minWidth = slidePanel?.minWidth && slidePanel.minWidth > 0 ? slidePanel.minWidth : WIDTH.MIN
function validWidth(width?: number): number | undefined {
if (width && minWidth <= width) {
return width
}
return undefined
}
const defaultWidth = validWidth(config?.slidePanel?.defaultWidth) ?? WIDTH.DEFAULT
const defaultWidth = validWidth(slidePanel?.defaultWidth) ?? WIDTH.DEFAULT
const [resizingClientX, setResizingClientX] = useState(-1)
const panelRef = React.createRef<HTMLDivElement>()

Expand Down
21 changes: 3 additions & 18 deletions src/components/viewers/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,25 @@ import AvroView from './AvroView.js'
import ImageView from './ImageView.js'
import JsonView from './JsonView.js'
import MarkdownView from './MarkdownView.js'
import TableView, { ParquetViewConfig } from './ParquetView.js'
import TableView from './ParquetView.js'
import TextView from './TextView.js'

export type ViewerConfig = ParquetViewConfig

interface ViewerProps {
source: FileSource;
setError: (error: Error | undefined) => void;
setProgress: (progress: number | undefined) => void;
config?: ViewerConfig;
}

/**
* Get a viewer for a file.
* Chooses viewer based on content type.
*/
export default function Viewer({
source,
setError,
setProgress,
config,
}: ViewerProps) {
export default function Viewer({ source, setError, setProgress }: ViewerProps) {
const { fileName } = source
if (fileName.endsWith('.md')) {
return <MarkdownView source={source} setError={setError} />
} else if (fileName.endsWith('.parquet')) {
return (
<TableView
source={source}
setError={setError}
setProgress={setProgress}
config={config}
/>
)
return <TableView source={source} setError={setError} setProgress={setProgress} />
} else if (fileName.endsWith('.json')) {
return <JsonView source={source} setError={setError} />
} else if (fileName.endsWith('.avro')) {
Expand Down
Loading