mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-11 12:30:25 +08:00
Impl TrialTable in storybook
This commit is contained in:
Generated
+602
-58
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,15 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@mui/system": "^5.15.9",
|
||||
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recoil": "^0.7.7",
|
||||
"sanitize.css": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { loadStorageFromFile } from "./utils/loadStorageFromFile";
|
||||
|
||||
const filePath = "db.sqlite3";
|
||||
const res = await fetch(filePath);
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filePath);
|
||||
const mockStudies: Study[] = [];
|
||||
await loadStorageFromFile(file, (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
mockStudies.push(...value);
|
||||
} else {
|
||||
mockStudies.push(...value([]));
|
||||
}
|
||||
});
|
||||
|
||||
export { mockStudies };
|
||||
@@ -0,0 +1,373 @@
|
||||
import { Clear } from "@mui/icons-material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {
|
||||
Collapse,
|
||||
IconButton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { Fragment, useState } from "react";
|
||||
import { DataGridColumn } from "./DataGridColumn";
|
||||
|
||||
type Order = "asc" | "desc";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
type Value = any;
|
||||
|
||||
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }];
|
||||
|
||||
interface RowFilter {
|
||||
columnIdx: number;
|
||||
value: Value;
|
||||
}
|
||||
|
||||
function DataGrid<T>(props: {
|
||||
columns: DataGridColumn<T>[];
|
||||
rows: T[];
|
||||
keyField: keyof T;
|
||||
dense?: boolean;
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode;
|
||||
initialRowsPerPage?: number;
|
||||
rowsPerPageOption?: Array<number | { value: number; label: string }>;
|
||||
defaultFilter?: (row: T) => boolean;
|
||||
}): React.ReactElement {
|
||||
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props;
|
||||
let { initialRowsPerPage, rowsPerPageOption } = props;
|
||||
const [order, setOrder] = useState<Order>("asc");
|
||||
const [orderBy, setOrderBy] = useState<number>(0); // index of columns
|
||||
const [page, setPage] = useState(0);
|
||||
const [filters, setFilters] = useState<RowFilter[]>([]);
|
||||
|
||||
const getRowIndex = (row: T): number => {
|
||||
return rows.findIndex((row2) => row[keyField] === row2[keyField]);
|
||||
};
|
||||
|
||||
// Pagination
|
||||
rowsPerPageOption = rowsPerPageOption || defaultRowsPerPageOption;
|
||||
initialRowsPerPage = initialRowsPerPage // use first element as default
|
||||
? initialRowsPerPage
|
||||
: isNumber(rowsPerPageOption[0])
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value;
|
||||
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
// Filtering
|
||||
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
|
||||
filters.some((f) => f.columnIdx === columnIdx);
|
||||
|
||||
const handleClickFilterCell = (columnIdx: number, value: Value) => {
|
||||
if (fieldAlreadyFiltered(columnIdx)) {
|
||||
return;
|
||||
}
|
||||
const newFilters = [...filters, { columnIdx: columnIdx, value: value }];
|
||||
setFilters(newFilters);
|
||||
};
|
||||
|
||||
const clearFilter = (columnIdx: number): void => {
|
||||
setFilters(filters.filter((f) => f.columnIdx !== columnIdx));
|
||||
};
|
||||
|
||||
const filteredRows = rows.filter((row, rowIdx) => {
|
||||
if (defaultFilter?.(row)) {
|
||||
return false;
|
||||
}
|
||||
return filters.length === 0
|
||||
? true
|
||||
: filters.some((f) => {
|
||||
if (columns.length <= f.columnIdx) {
|
||||
console.log(
|
||||
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const toCellValue = columns[f.columnIdx].toCellValue;
|
||||
if (toCellValue !== undefined) {
|
||||
return toCellValue(rowIdx) === f.value;
|
||||
}
|
||||
const field = columns[f.columnIdx].field;
|
||||
return row[field] === f.value;
|
||||
});
|
||||
});
|
||||
|
||||
// Sorting
|
||||
const createSortHandler = (columnId: number) => () => {
|
||||
const isAsc = orderBy === columnId && order === "asc";
|
||||
setOrder(isAsc ? "desc" : "asc");
|
||||
setOrderBy(columnId);
|
||||
};
|
||||
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns);
|
||||
const currentPageRows =
|
||||
rowsPerPage > 0
|
||||
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: sortedRows;
|
||||
const emptyRows =
|
||||
rowsPerPage - Math.min(rowsPerPage, sortedRows.length - page * rowsPerPage);
|
||||
|
||||
const RootDiv = styled("div")({
|
||||
width: "100%",
|
||||
});
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
});
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
});
|
||||
return (
|
||||
<RootDiv>
|
||||
<TableContainer>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
size={dense ? "small" : "medium"}
|
||||
aria-label="data grid"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{collapseBody ? <TableCell /> : null}
|
||||
{columns.map((column, columnIdx) => (
|
||||
<TableCell
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={columnIdx}
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={orderBy === column.field ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={orderBy === columnIdx}
|
||||
direction={orderBy === columnIdx ? order : "asc"}
|
||||
onClick={createSortHandler(columnIdx)}
|
||||
>
|
||||
{column.label}
|
||||
{orderBy === column.field ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{column.filterable ? (
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
style={
|
||||
fieldAlreadyFiltered(columnIdx)
|
||||
? {}
|
||||
: { visibility: "hidden" }
|
||||
}
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
clearFilter(columnIdx);
|
||||
}}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentPageRows.map((row) => (
|
||||
<DataGridRow<T>
|
||||
columns={columns}
|
||||
rowIndex={getRowIndex(row)}
|
||||
row={row}
|
||||
keyField={keyField}
|
||||
collapseBody={collapseBody}
|
||||
key={`${row[keyField]}`}
|
||||
handleClickFilterCell={handleClickFilterCell}
|
||||
/>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOption}
|
||||
component="div"
|
||||
count={filteredRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</RootDiv>
|
||||
);
|
||||
}
|
||||
|
||||
function DataGridRow<T>(props: {
|
||||
columns: DataGridColumn<T>[];
|
||||
rowIndex: number;
|
||||
row: T;
|
||||
keyField: keyof T;
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode;
|
||||
handleClickFilterCell: (columnIdx: number, value: Value) => void;
|
||||
}) {
|
||||
const {
|
||||
columns,
|
||||
rowIndex,
|
||||
row,
|
||||
keyField,
|
||||
collapseBody,
|
||||
handleClickFilterCell,
|
||||
} = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const FilterableDiv = styled("div")({
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
<TableRow hover tabIndex={-1}>
|
||||
{collapseBody ? (
|
||||
<TableCell>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
) : null}
|
||||
{columns.map((column, columnIndex) => {
|
||||
const cellItem = column.toCellValue
|
||||
? column.toCellValue(rowIndex)
|
||||
: // TODO(c-bata): Avoid this implicit type conversion.
|
||||
(row[column.field] as number | string | null | undefined);
|
||||
|
||||
return column.filterable ? (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
onClick={() => {
|
||||
const value =
|
||||
column.toCellValue !== undefined
|
||||
? column.toCellValue(rowIndex)
|
||||
: row[column.field];
|
||||
handleClickFilterCell(columnIndex, value);
|
||||
}}
|
||||
>
|
||||
<FilterableDiv>{cellItem}</FilterableDiv>
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
{collapseBody ? (
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{collapseBody(rowIndex)}
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function getComparator<T>(
|
||||
order: Order,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number,
|
||||
): (a: T, b: T) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator<T>(a, b, columns, orderBy)
|
||||
: (a, b) => -descendingComparator<T>(a, b, columns, orderBy);
|
||||
}
|
||||
|
||||
function descendingComparator<T>(
|
||||
a: T,
|
||||
b: T,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number,
|
||||
): number {
|
||||
const field = columns[orderBy].field;
|
||||
if (b[field] < a[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[field] > a[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function stableSort<T>(
|
||||
array: T[],
|
||||
order: Order,
|
||||
orderBy: number,
|
||||
columns: DataGridColumn<T>[],
|
||||
) {
|
||||
// TODO(c-bata): Refactor here by implementing as the same comparator interface.
|
||||
const less = columns[orderBy].less;
|
||||
const comparator = getComparator(order, columns, orderBy);
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order === "asc";
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending);
|
||||
if (result !== 0) return result;
|
||||
} else {
|
||||
const result = comparator(a[0], b[0]);
|
||||
if (result !== 0) return result;
|
||||
}
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map((el) => el[0]);
|
||||
}
|
||||
|
||||
const isNumber = (
|
||||
rowsPerPage: number | { value: number; label: string },
|
||||
): rowsPerPage is number => {
|
||||
return typeof rowsPerPage === "number";
|
||||
};
|
||||
|
||||
export { DataGrid };
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface DataGridColumn<T> {
|
||||
field: keyof T;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
less?: (a: T, b: T, ascending: boolean) => number;
|
||||
filterable?: boolean;
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode;
|
||||
padding?: "normal" | "checkbox" | "none";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { mockStudies } from "../MockStudies";
|
||||
import { TrialTable } from "./TrialTable";
|
||||
|
||||
const meta: Meta<typeof TrialTable> = {
|
||||
component: TrialTable,
|
||||
title: "TrialTable",
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TrialTable>;
|
||||
|
||||
export const MockStudy1: Story = {
|
||||
args: {
|
||||
study: mockStudies[0],
|
||||
},
|
||||
};
|
||||
|
||||
export const MockStudy2: Story = {
|
||||
args: {
|
||||
study: mockStudies[1],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { DataGrid } from "./DataGrid";
|
||||
import { DataGridColumn } from "./DataGridColumn";
|
||||
|
||||
export const TrialTable: FC<{
|
||||
study: Study;
|
||||
initialRowsPerPage?: number;
|
||||
}> = ({ study, initialRowsPerPage }) => {
|
||||
const trials: Trial[] = study.trials;
|
||||
|
||||
const columns: DataGridColumn<Trial>[] = [
|
||||
{ field: "number", label: "Number", sortable: true, padding: "none" },
|
||||
{
|
||||
field: "state",
|
||||
label: "State",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
padding: "none",
|
||||
toCellValue: (i) => trials[i].state.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
if (study === null || study.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[0];
|
||||
const secondVal = secondEl.values?.[0];
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0;
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return ascending ? -1 : 1;
|
||||
} else if (secondVal === undefined) {
|
||||
return ascending ? 1 : -1;
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1;
|
||||
} else if (secondVal === "-inf" || firstVal === "inf") {
|
||||
return -1;
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1;
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null;
|
||||
}
|
||||
return trials[i].values?.[0];
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const objectiveColumns: DataGridColumn<Trial>[] = study.directions.map(
|
||||
(_s, objectiveId) => ({
|
||||
field: "values",
|
||||
label: `Objective ${objectiveId}`,
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[objectiveId];
|
||||
const secondVal = secondEl.values?.[objectiveId];
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0;
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return ascending ? -1 : 1;
|
||||
} else if (secondVal === undefined) {
|
||||
return ascending ? 1 : -1;
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1;
|
||||
} else if (secondVal === "-inf" || firstVal === "inf") {
|
||||
return -1;
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1;
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null;
|
||||
}
|
||||
return trials[i].values?.[objectiveId];
|
||||
},
|
||||
}),
|
||||
);
|
||||
columns.push(...objectiveColumns);
|
||||
}
|
||||
|
||||
study.union_search_space.forEach((s) => {
|
||||
columns.push({
|
||||
field: "params",
|
||||
label: `Param ${s.name}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].params.find((p) => p.name === s.name)?.param_external_value ??
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstVal = firstEl.params.find(
|
||||
(p) => p.name === s.name,
|
||||
)?.param_internal_value;
|
||||
const secondVal = secondEl.params.find(
|
||||
(p) => p.name === s.name,
|
||||
)?.param_internal_value;
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0;
|
||||
} else if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1;
|
||||
} else if (firstVal) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
study.union_user_attrs.forEach((attr_spec) => {
|
||||
columns.push({
|
||||
field: "user_attrs",
|
||||
label: `UserAttribute ${attr_spec.key}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
filterable: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstVal = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key,
|
||||
)?.value;
|
||||
const secondVal = secondEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key,
|
||||
)?.value;
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0;
|
||||
} else if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1;
|
||||
} else if (firstVal) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<DataGrid<Trial>
|
||||
columns={columns}
|
||||
rows={trials}
|
||||
keyField={"trial_id"}
|
||||
dense={false}
|
||||
initialRowsPerPage={initialRowsPerPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,22 +2,31 @@ import { SetterOrUpdater } from "recoil";
|
||||
import { loadJournalStorage } from "./journalStorage";
|
||||
import { loadSQLite3Storage } from "./sqlite3";
|
||||
|
||||
export const loadStorageFromFile = (
|
||||
const readFile = async (file: File) => {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
const arrayBuffer = reader.result as ArrayBuffer | null;
|
||||
if (arrayBuffer !== null) {
|
||||
resolve(arrayBuffer);
|
||||
} else {
|
||||
reject(new Error("Failed to load file"));
|
||||
}
|
||||
});
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadStorageFromFile = async (
|
||||
file: File,
|
||||
setStudies: SetterOrUpdater<Study[]>,
|
||||
): void => {
|
||||
const r = new FileReader();
|
||||
r.addEventListener("load", () => {
|
||||
const arrayBuffer = r.result as ArrayBuffer | null;
|
||||
if (arrayBuffer !== null) {
|
||||
const header = new Uint8Array(arrayBuffer, 0, 16);
|
||||
const headerString = new TextDecoder().decode(header);
|
||||
if (headerString === "SQLite format 3\u0000") {
|
||||
loadSQLite3Storage(arrayBuffer, setStudies);
|
||||
} else {
|
||||
loadJournalStorage(arrayBuffer, setStudies);
|
||||
}
|
||||
}
|
||||
});
|
||||
r.readAsArrayBuffer(file);
|
||||
) => {
|
||||
const arrayBuffer = await readFile(file);
|
||||
const header = new Uint8Array(arrayBuffer, 0, 16);
|
||||
const headerString = new TextDecoder().decode(header);
|
||||
if (headerString === "SQLite format 3\u0000") {
|
||||
await loadSQLite3Storage(arrayBuffer, setStudies);
|
||||
} else {
|
||||
loadJournalStorage(arrayBuffer, setStudies);
|
||||
}
|
||||
};
|
||||
|
||||
+25
-26
@@ -9,11 +9,11 @@ type SQLite3DB = {
|
||||
}): void;
|
||||
};
|
||||
|
||||
export const loadSQLite3Storage = (
|
||||
export const loadSQLite3Storage = async (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
setter: SetterOrUpdater<Study[]>,
|
||||
): void => {
|
||||
sqlite3InitModule({
|
||||
) => {
|
||||
const sqlite3 = await sqlite3InitModule({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
print: (...args: any): void => {
|
||||
console.log(args);
|
||||
@@ -23,30 +23,29 @@ export const loadSQLite3Storage = (
|
||||
console.log(args);
|
||||
},
|
||||
// @ts-ignore
|
||||
}).then((sqlite3) => {
|
||||
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer);
|
||||
const db = new sqlite3.oo1.DB();
|
||||
const rc = sqlite3.capi.sqlite3_deserialize(
|
||||
// @ts-ignore
|
||||
db.pointer,
|
||||
"main",
|
||||
p,
|
||||
arrayBuffer.byteLength,
|
||||
arrayBuffer.byteLength,
|
||||
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE,
|
||||
);
|
||||
db.checkRc(rc);
|
||||
try {
|
||||
const schemaVersion = getSchemaVersion(db);
|
||||
if (!isSupportedSchema(schemaVersion)) {
|
||||
return;
|
||||
}
|
||||
const studies = getStudies(db, schemaVersion);
|
||||
setter((prev) => [...prev, ...studies]);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer);
|
||||
const db = new sqlite3.oo1.DB();
|
||||
const rc = sqlite3.capi.sqlite3_deserialize(
|
||||
// @ts-ignore
|
||||
db.pointer,
|
||||
"main",
|
||||
p,
|
||||
arrayBuffer.byteLength,
|
||||
arrayBuffer.byteLength,
|
||||
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE,
|
||||
);
|
||||
db.checkRc(rc);
|
||||
try {
|
||||
const schemaVersion = getSchemaVersion(db);
|
||||
if (!isSupportedSchema(schemaVersion)) {
|
||||
return;
|
||||
}
|
||||
const studies = getStudies(db, schemaVersion);
|
||||
setter((prev) => [...prev, ...studies]);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
|
||||
const getSchemaVersion = (db: SQLite3DB): string => {
|
||||
|
||||
Reference in New Issue
Block a user