mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-12 12:40:33 +08:00
Add standalone app
This commit is contained in:
+2
-1
@@ -1 +1,2 @@
|
||||
optuna_dashboard/ts/components/PlotlyDarkMode.ts
|
||||
optuna_dashboard/ts/components/PlotlyDarkMode.ts
|
||||
standalone_app/PlotlyDarkMode.ts
|
||||
|
||||
+4
-2
@@ -5,8 +5,10 @@
|
||||
"description": "Dashboard for Optuna",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"fmt": "prettier --write \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\"",
|
||||
"lint": "eslint . --ext .ts,.tsx && prettier --list-different \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\"",
|
||||
"fmt": "prettier --write \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\" \"standalone_app/*.{ts,tsx}\"",
|
||||
"lint": "npm run lint:eslint && npm run lint:fmt",
|
||||
"lint:eslint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fmt": "prettier --list-different \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\" \"standalone_app/*.{ts,tsx}\"",
|
||||
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
|
||||
"build": "webpack",
|
||||
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Optuna Dashboard (Wasm ver.)</title>
|
||||
<script defer type="module" src="/bundle.js"></script>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+6087
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "optuna-dashboard-wasm",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
|
||||
"serve": "python3 -m http.server 9000 --directory ./public",
|
||||
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
|
||||
"build:prd": "NODE_ENV=production webpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/plotly.js": "^2.12.18",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"esbuild-loader": "^3.0.1",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.2",
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.8",
|
||||
"@emotion/styled": "^11.10.8",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/lab": "^5.0.0-alpha.128",
|
||||
"@mui/material": "^5.12.2",
|
||||
"@sqlite.org/sqlite-wasm": "^3.41.2-build11",
|
||||
"module-workers-polyfill": "^0.3.2",
|
||||
"notistack": "^3.0.1",
|
||||
"plotly.js-dist-min": "^2.22.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.11.0",
|
||||
"recoil": "^0.7.7",
|
||||
"optuna": "../rustlib/pkg"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import "./index.css"
|
||||
import { App } from "./components/App"
|
||||
import { RecoilRoot } from "recoil"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<App />
|
||||
</RecoilRoot>
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { FC, useMemo, useState, useEffect } from "react"
|
||||
import { HashRouter as Router, Routes, Route } from "react-router-dom"
|
||||
import { SnackbarProvider } from "notistack"
|
||||
import blue from "@mui/material/colors/blue"
|
||||
import pink from "@mui/material/colors/pink"
|
||||
import {
|
||||
createTheme,
|
||||
useMediaQuery,
|
||||
ThemeProvider,
|
||||
Box,
|
||||
CssBaseline,
|
||||
} from "@mui/material"
|
||||
|
||||
import { StudyDetail } from "./StudyDetail"
|
||||
import { StudyList } from "./StudyList"
|
||||
|
||||
export const App: FC = () => {
|
||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)")
|
||||
const [colorMode, setColorMode] = useState<"light" | "dark">("light")
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette: {
|
||||
mode: colorMode,
|
||||
primary: blue,
|
||||
secondary: pink,
|
||||
},
|
||||
}),
|
||||
[colorMode]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setColorMode(prefersDarkMode ? "dark" : "light")
|
||||
}, [prefersDarkMode])
|
||||
|
||||
const toggleColorMode = () => {
|
||||
setColorMode(colorMode === "dark" ? "light" : "dark")
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box>
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
element={<StudyList toggleColorMode={toggleColorMode} />}
|
||||
/>
|
||||
<Route
|
||||
path=":idx"
|
||||
element={<StudyDetail toggleColorMode={toggleColorMode} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
</SnackbarProvider>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import React from "react"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
Collapse,
|
||||
IconButton,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import { styled } from "@mui/system"
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
|
||||
import { Clear } from "@mui/icons-material"
|
||||
|
||||
type Order = "asc" | "desc"
|
||||
|
||||
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
|
||||
|
||||
interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T) => number
|
||||
filterable?: boolean
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
}
|
||||
|
||||
interface RowFilter<T> {
|
||||
columnIdx: number
|
||||
value: any
|
||||
}
|
||||
|
||||
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
|
||||
}) {
|
||||
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props
|
||||
let { initialRowsPerPage, rowsPerPageOption } = props
|
||||
const [order, setOrder] = React.useState<Order>("asc")
|
||||
const [orderBy, setOrderBy] = React.useState<number>(0) // index of columns
|
||||
const [page, setPage] = React.useState(0)
|
||||
const [filters, setFilters] = React.useState<RowFilter<T>[]>([])
|
||||
|
||||
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] = React.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: any) => {
|
||||
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 !== undefined && 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) => (event: React.MouseEvent<unknown>) => {
|
||||
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
|
||||
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={(e) => {
|
||||
clearFilter(columnIdx)
|
||||
}}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentPageRows.map((row, index) => (
|
||||
<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: any) => void
|
||||
}) {
|
||||
const {
|
||||
columns,
|
||||
rowIndex,
|
||||
row,
|
||||
keyField,
|
||||
collapseBody,
|
||||
handleClickFilterCell,
|
||||
} = props
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
const FilterableDiv = styled("div")({
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
})
|
||||
return (
|
||||
<React.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={(e) => {
|
||||
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}
|
||||
</React.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 result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0])
|
||||
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, DataGridColumn }
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { TextField, TextFieldProps } from "@mui/material"
|
||||
|
||||
export const DebouncedInputTextField: FC<{
|
||||
onChange: (s: string, valid: boolean) => void
|
||||
delay: number
|
||||
textFieldProps: TextFieldProps
|
||||
}> = ({ onChange, delay, textFieldProps }) => {
|
||||
const [text, setText] = React.useState<string>("")
|
||||
const [valid, setValidity] = React.useState<boolean>(true)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onChange(text, valid)
|
||||
}, delay)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [text, delay])
|
||||
return (
|
||||
<TextField
|
||||
onChange={(e) => {
|
||||
setText(e.target.value)
|
||||
setValidity(e.target.validity.valid)
|
||||
}}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import {
|
||||
Grid,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
MenuItem,
|
||||
Switch,
|
||||
Select,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
SelectChangeEvent,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
|
||||
|
||||
const plotDomId = "plot-history"
|
||||
|
||||
export const PlotHistory: FC<{
|
||||
study: Study | null
|
||||
}> = ({ study = null }) => {
|
||||
const theme = useTheme()
|
||||
const [xAxis, setXAxis] = useState<string>("number")
|
||||
const [objectiveId, setObjectiveId] = useState<number>(0)
|
||||
const [logScale, setLogScale] = useState<boolean>(false)
|
||||
const [filterCompleteTrial, setFilterCompleteTrial] = useState<boolean>(false)
|
||||
const [filterPrunedTrial, setFilterPrunedTrial] = useState<boolean>(false)
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<number>) => {
|
||||
setObjectiveId(event.target.value as number)
|
||||
}
|
||||
|
||||
const handleXAxisChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setXAxis(e.target.value)
|
||||
}
|
||||
|
||||
const handleLogScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLogScale(!logScale)
|
||||
}
|
||||
|
||||
const handleFilterCompleteChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterCompleteTrial(!filterCompleteTrial)
|
||||
}
|
||||
|
||||
const handleFilterPrunedChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterPrunedTrial(!filterPrunedTrial)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (study !== null) {
|
||||
plotHistory(
|
||||
study,
|
||||
objectiveId,
|
||||
xAxis,
|
||||
logScale,
|
||||
filterCompleteTrial,
|
||||
filterPrunedTrial,
|
||||
theme.palette.mode
|
||||
)
|
||||
}
|
||||
}, [
|
||||
study,
|
||||
objectiveId,
|
||||
logScale,
|
||||
xAxis,
|
||||
filterPrunedTrial,
|
||||
filterCompleteTrial,
|
||||
theme.palette.mode,
|
||||
])
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
container
|
||||
direction="column"
|
||||
sx={{ paddingRight: theme.spacing(2) }}
|
||||
>
|
||||
<Typography variant="h6" sx={{ margin: "1em 0", fontWeight: 600 }}>
|
||||
History
|
||||
</Typography>
|
||||
{study !== null && study.directions.length !== 1 ? (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">Objective ID:</FormLabel>
|
||||
<Select value={objectiveId} onChange={handleObjectiveChange}>
|
||||
{study.directions.map((d, i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{i}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">Log y scale:</FormLabel>
|
||||
<Switch
|
||||
checked={logScale}
|
||||
onChange={handleLogScaleChange}
|
||||
value="enable"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">Filter state:</FormLabel>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!filterCompleteTrial}
|
||||
onChange={handleFilterCompleteChange}
|
||||
/>
|
||||
}
|
||||
label="Complete"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!filterPrunedTrial}
|
||||
onChange={handleFilterPrunedChange}
|
||||
/>
|
||||
}
|
||||
label="Pruned"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">X-axis:</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="gender"
|
||||
name="gender1"
|
||||
value={xAxis}
|
||||
onChange={handleXAxisChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="number"
|
||||
control={<Radio />}
|
||||
label="Number"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="datetime_start"
|
||||
control={<Radio />}
|
||||
label="Datetime start"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="datetime_complete"
|
||||
control={<Radio />}
|
||||
label="Datetime complete"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<div id={plotDomId} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const filterFunc = (trial: Trial, objectiveId: number): boolean => {
|
||||
if (trial.state !== "Complete" && trial.state !== "Pruned") {
|
||||
return false
|
||||
}
|
||||
if (trial.values === undefined) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
trial.values.length > objectiveId &&
|
||||
trial.values[objectiveId] !== "inf" &&
|
||||
trial.values[objectiveId] !== "-inf"
|
||||
)
|
||||
}
|
||||
|
||||
const plotHistory = (
|
||||
study: Study,
|
||||
objectiveId: number,
|
||||
xAxis: string,
|
||||
logScale: boolean,
|
||||
filterCompleteTrial: boolean,
|
||||
filterPrunedTrial: boolean,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 0,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Objective Value",
|
||||
type: logScale ? "log" : "linear",
|
||||
},
|
||||
xaxis: {
|
||||
title: xAxis === "number" ? "Trial" : "Time",
|
||||
type: xAxis === "number" ? "linear" : "date",
|
||||
},
|
||||
showlegend: true,
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
let filteredTrials = study.trials.filter((t) => filterFunc(t, objectiveId))
|
||||
if (filterCompleteTrial) {
|
||||
filteredTrials = filteredTrials.filter((t) => t.state !== "Complete")
|
||||
}
|
||||
if (filterPrunedTrial) {
|
||||
filteredTrials = filteredTrials.filter((t) => t.state !== "Pruned")
|
||||
}
|
||||
if (filteredTrials.length === 0) {
|
||||
plotly.react(plotDomId, [], layout)
|
||||
return
|
||||
}
|
||||
|
||||
const getAxisX = (trial: Trial): number | Date => {
|
||||
return xAxis === "number"
|
||||
? trial.number
|
||||
: xAxis === "datetime_start"
|
||||
? trial.datetime_start!
|
||||
: trial.datetime_complete!
|
||||
}
|
||||
|
||||
const xForLinePlot: (number | Date)[] = []
|
||||
const yForLinePlot: number[] = []
|
||||
let currentBest: number | null = null
|
||||
for (let i = 0; i < filteredTrials.length; i++) {
|
||||
const t = filteredTrials[i]
|
||||
if (currentBest === null) {
|
||||
currentBest = t.values![objectiveId] as number
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(t.values![objectiveId] as number)
|
||||
} else if (
|
||||
study.directions[objectiveId] === "maximize" &&
|
||||
t.values![objectiveId] > currentBest
|
||||
) {
|
||||
const p = filteredTrials[i - 1]
|
||||
if (!xForLinePlot.includes(getAxisX(p))) {
|
||||
xForLinePlot.push(getAxisX(p))
|
||||
yForLinePlot.push(currentBest)
|
||||
}
|
||||
currentBest = t.values![objectiveId] as number
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(t.values![objectiveId] as number)
|
||||
} else if (
|
||||
study.directions[objectiveId] === "minimize" &&
|
||||
t.values![objectiveId] < currentBest
|
||||
) {
|
||||
const p = filteredTrials[i - 1]
|
||||
if (!xForLinePlot.includes(getAxisX(p))) {
|
||||
xForLinePlot.push(getAxisX(p))
|
||||
yForLinePlot.push(currentBest)
|
||||
}
|
||||
currentBest = t.values![objectiveId] as number
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(t.values![objectiveId] as number)
|
||||
}
|
||||
}
|
||||
xForLinePlot.push(getAxisX(filteredTrials[filteredTrials.length - 1]))
|
||||
yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1])
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = [
|
||||
{
|
||||
x: filteredTrials.map(getAxisX),
|
||||
y: filteredTrials.map(
|
||||
(t: Trial): number => t.values![objectiveId] as number
|
||||
),
|
||||
name: "Objective Value",
|
||||
mode: "markers",
|
||||
type: "scatter",
|
||||
},
|
||||
{
|
||||
x: xForLinePlot,
|
||||
y: yForLinePlot,
|
||||
name: "Best Value",
|
||||
mode: "lines",
|
||||
type: "scatter",
|
||||
},
|
||||
]
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import { Typography, useTheme, Box, Card, CardContent } from "@mui/material"
|
||||
import init, { wasm_fanova_calculate } from "optuna"
|
||||
|
||||
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
|
||||
const plotDomId = "graph-hyperparameter-importances"
|
||||
|
||||
export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
|
||||
const theme = useTheme()
|
||||
const nObjectives = study.directions.length
|
||||
const objectiveNames: string[] = study.directions.map(
|
||||
(d, i) => `Objective ${i}`
|
||||
)
|
||||
const [importance, setImportance] = useState<ParamImportance[][]>([])
|
||||
|
||||
useEffect(() => {
|
||||
async function run_wasm() {
|
||||
await init()
|
||||
|
||||
const x: ParamImportance[][] = study.directions.map((d, objectiveId) => {
|
||||
let filteredTrials = study.trials.filter((t) =>
|
||||
filterFunc(t, objectiveId)
|
||||
)
|
||||
if (filteredTrials.length === 0) {
|
||||
return study.union_search_space.map((s) => {
|
||||
return {
|
||||
name: s.name,
|
||||
importance: 0.5,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const features = study.intersection_search_space.map((s) =>
|
||||
filteredTrials
|
||||
.map((t) => t.params.find((p) => p.name === s.name) as TrialParam)
|
||||
.map((p) => p.param_internal_value)
|
||||
)
|
||||
const values = filteredTrials.map(
|
||||
(t) => t.values?.[objectiveId] as number
|
||||
)
|
||||
const importance = wasm_fanova_calculate(features, values)
|
||||
return study.intersection_search_space.map((s, i) => ({
|
||||
name: s.name,
|
||||
importance: importance[i],
|
||||
}))
|
||||
})
|
||||
setImportance(x)
|
||||
}
|
||||
|
||||
run_wasm()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (importance.length > 0) {
|
||||
plotParamImportancesBeta(importance, objectiveNames, theme.palette.mode)
|
||||
}
|
||||
}, [nObjectives, importance, theme.palette.mode])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Hyperparameter Importance
|
||||
</Typography>
|
||||
<Box id={plotDomId} sx={{ height: "450px" }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const filterFunc = (trial: Trial, objectiveId: number): boolean => {
|
||||
if (trial.state !== "Complete" && trial.state !== "Pruned") {
|
||||
return false
|
||||
}
|
||||
if (trial.values === undefined) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
trial.values.length > objectiveId &&
|
||||
trial.values[objectiveId] !== "inf" &&
|
||||
trial.values[objectiveId] !== "-inf"
|
||||
)
|
||||
}
|
||||
|
||||
const plotParamImportancesBeta = (
|
||||
importances: ParamImportance[][],
|
||||
objectiveNames: string[],
|
||||
mode: string
|
||||
) => {
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: "Hyperparameter Importance",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Hyperparameter",
|
||||
automargin: true,
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
barmode: "group",
|
||||
bargap: 0.15,
|
||||
bargroupgap: 0.1,
|
||||
uirevision: "true",
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
const traces: Partial<plotly.PlotData>[] = importances.map(
|
||||
(importance, i) => {
|
||||
const reversed = [...importance].reverse()
|
||||
const importance_values = reversed.map((p) => p.importance)
|
||||
const param_names = reversed.map((p) => p.name)
|
||||
const param_hover_templates = reversed.map(
|
||||
(p) => `${p.name}): ${p.importance} <extra></extra>`
|
||||
)
|
||||
return {
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
name: objectiveNames[i],
|
||||
x: importance_values,
|
||||
y: param_names,
|
||||
text: importance_values.map((v) => String(v.toFixed(2))),
|
||||
textposition: "outside",
|
||||
hovertemplate: param_hover_templates,
|
||||
}
|
||||
}
|
||||
)
|
||||
plotly.react(plotDomId, traces, layout)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
DragEventHandler,
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { loadStorage } from "../sqlite3"
|
||||
import { useSetRecoilState } from "recoil"
|
||||
import { studiesState } from "../state"
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile"
|
||||
|
||||
export const StorageLoader: FC<{}> = () => {
|
||||
const theme = useTheme()
|
||||
const [dragOver, setDragOver] = useState<boolean>(false)
|
||||
|
||||
const setStudies = useSetRecoilState<Study[]>(studiesState)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const loadStorageFromFile = (file: File): void => {
|
||||
const r = new FileReader()
|
||||
r.addEventListener("load", () => {
|
||||
const arrayBuffer = r.result as ArrayBuffer | null
|
||||
if (arrayBuffer !== null) {
|
||||
loadStorage(arrayBuffer, setStudies)
|
||||
}
|
||||
})
|
||||
r.readAsArrayBuffer(file)
|
||||
}
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0]
|
||||
if (!f) {
|
||||
return
|
||||
}
|
||||
loadStorageFromFile(f)
|
||||
}
|
||||
const handleClick: MouseEventHandler = (e) => {
|
||||
if (!inputRef || !inputRef.current) {
|
||||
return
|
||||
}
|
||||
inputRef.current.click()
|
||||
}
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const file = e.dataTransfer.files?.[0]
|
||||
setDragOver(false)
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
loadStorageFromFile(file)
|
||||
}
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(true)
|
||||
}
|
||||
const handleDragLeave: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
border: dragOver
|
||||
? `3px dashed ${theme.palette.mode === "dark" ? "white" : "black"}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<CardActionArea onClick={handleClick}>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<UploadFileIcon
|
||||
sx={{ fontSize: 80, marginBottom: theme.spacing(2) }}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Typography>Load an Optuna Storage</Typography>
|
||||
<Typography
|
||||
sx={{ textAlign: "center", color: theme.palette.grey.A400 }}
|
||||
>
|
||||
Drag your SQLite3 file here or click to browse.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { FC } from "react"
|
||||
import { Link, useParams } from "react-router-dom"
|
||||
import {
|
||||
AppBar,
|
||||
Typography,
|
||||
Container,
|
||||
Toolbar,
|
||||
Box,
|
||||
IconButton,
|
||||
useTheme,
|
||||
Card,
|
||||
CardContent,
|
||||
} from "@mui/material"
|
||||
import { Home } from "@mui/icons-material"
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4"
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import { studiesState } from "../state"
|
||||
import { TrialTable } from "./TrialTable"
|
||||
import { PlotHistory } from "./PlotHistory"
|
||||
import { PlotImportance } from "./PlotImportance"
|
||||
|
||||
const useStudyValue = (idx: number): Study | null => {
|
||||
const studies = useRecoilValue<Study[]>(studiesState)
|
||||
return studies[idx] || null
|
||||
}
|
||||
|
||||
export const StudyDetail: FC<{
|
||||
toggleColorMode: () => void
|
||||
}> = ({ toggleColorMode }) => {
|
||||
const theme = useTheme()
|
||||
const { idx } = useParams<{ idx: string }>()
|
||||
const idxNumber = parseInt(idx || "", 10)
|
||||
const study = useStudyValue(idxNumber)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppBar position="static">
|
||||
<Container
|
||||
sx={{
|
||||
["@media (min-width: 1280px)"]: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Typography variant="h6">Optuna Dashboard</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
toggleColorMode()
|
||||
}}
|
||||
color="inherit"
|
||||
title={
|
||||
theme.palette.mode === "dark"
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode"
|
||||
}
|
||||
>
|
||||
{theme.palette.mode === "dark" ? (
|
||||
<Brightness7Icon />
|
||||
) : (
|
||||
<Brightness4Icon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
component={Link}
|
||||
to={"/"}
|
||||
color="inherit"
|
||||
title="Return to the top"
|
||||
>
|
||||
<Home />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
<Container
|
||||
sx={{
|
||||
["@media (min-width: 1280px)"]: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
margin: `${theme.spacing(4)} ${theme.spacing(2)}`,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
fontSize: "1.8rem",
|
||||
...(theme.palette.mode === "dark" && {
|
||||
color: theme.palette.primary.light,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{study?.study_name || "Not Found"}
|
||||
</Typography>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<PlotHistory study={study} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && <PlotImportance study={study} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && <TrialTable study={study} initialRowsPerPage={10} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import React, { FC, useState } from "react"
|
||||
import {
|
||||
AppBar,
|
||||
Typography,
|
||||
Container,
|
||||
Toolbar,
|
||||
Box,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActionArea,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
SvgIcon,
|
||||
} from "@mui/material"
|
||||
import { styled } from "@mui/system"
|
||||
import SortIcon from "@mui/icons-material/Sort"
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4"
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import { studiesState } from "../state"
|
||||
import { Link } from "react-router-dom"
|
||||
import { DebouncedInputTextField } from "./Debounce"
|
||||
import { Search } from "@mui/icons-material"
|
||||
import { StorageLoader } from "./StorageLoader"
|
||||
|
||||
export const StudyList: FC<{
|
||||
toggleColorMode: () => void
|
||||
}> = ({ toggleColorMode }) => {
|
||||
const theme = useTheme()
|
||||
const studies = useRecoilValue<Study[]>(studiesState)
|
||||
|
||||
const [studyFilterText, setStudyFilterText] = useState<string>("")
|
||||
const [sortBy, setSortBy] = useState<"id-asc" | "id-desc">("id-asc")
|
||||
const studyFilter = (row: Study): boolean => {
|
||||
const keywords = studyFilterText.split(" ")
|
||||
return !keywords.every((k) => {
|
||||
if (k === "") {
|
||||
return true
|
||||
}
|
||||
return row.study_name.indexOf(k) >= 0
|
||||
})
|
||||
}
|
||||
let filteredStudies: Study[] = studies.filter((s) => !studyFilter(s))
|
||||
if (sortBy === "id-desc") {
|
||||
filteredStudies = filteredStudies.reverse()
|
||||
}
|
||||
|
||||
const Select = styled(TextField)(({ theme }) => ({
|
||||
"& .MuiInputBase-input": {
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||
},
|
||||
}))
|
||||
const sortBySelect = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
margin: theme.spacing(0, 2),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(0, 2),
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<SortIcon />
|
||||
</Box>
|
||||
<Select
|
||||
select
|
||||
value={sortBy}
|
||||
onChange={(e) => {
|
||||
setSortBy(e.target.value as "id-asc" | "id-desc")
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"id-asc"}>Sort ascending</MenuItem>
|
||||
<MenuItem value={"id-desc"}>Sort descending</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppBar position="static">
|
||||
<Container
|
||||
sx={{
|
||||
["@media (min-width: 1280px)"]: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Typography variant="h6">Optuna Dashboard (Wasm ver.)</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
toggleColorMode()
|
||||
}}
|
||||
color="inherit"
|
||||
title={
|
||||
theme.palette.mode === "dark"
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode"
|
||||
}
|
||||
>
|
||||
{theme.palette.mode === "dark" ? (
|
||||
<Brightness7Icon />
|
||||
) : (
|
||||
<Brightness4Icon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
<Container
|
||||
sx={{
|
||||
["@media (min-width: 1280px)"]: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<DebouncedInputTextField
|
||||
onChange={(s) => {
|
||||
setStudyFilterText(s)
|
||||
}}
|
||||
delay={500}
|
||||
textFieldProps={{
|
||||
fullWidth: true,
|
||||
id: "search-study",
|
||||
variant: "outlined",
|
||||
placeholder: "Search study",
|
||||
sx: { maxWidth: 500 },
|
||||
InputProps: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SvgIcon fontSize="small" color="action">
|
||||
<Search />
|
||||
</SvgIcon>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{sortBySelect}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
{filteredStudies.map((study, idx) => (
|
||||
<Card
|
||||
key={study.study_id}
|
||||
sx={{ margin: theme.spacing(2), width: "500px" }}
|
||||
>
|
||||
<CardActionArea component={Link} to={`/${idx}`}>
|
||||
<CardContent>
|
||||
<Typography variant="h5">
|
||||
{study.study_id}. {study.study_name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="text.secondary"
|
||||
component="div"
|
||||
>
|
||||
{"Direction: " +
|
||||
study.directions.map((d) => d.toUpperCase()).join(", ")}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
{!IS_VSCODE && <StorageLoader />}
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { FC } from "react"
|
||||
|
||||
import { DataGridColumn, DataGrid } from "./DataGrid"
|
||||
|
||||
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(),
|
||||
},
|
||||
]
|
||||
|
||||
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_internal_value ||
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
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
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (study === null || study.directions.length == 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.values?.[0]
|
||||
const secondVal = secondEl.values?.[0]
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
} else if (secondVal === undefined) {
|
||||
return 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): number => {
|
||||
const firstVal = firstEl.values?.[objectiveId]
|
||||
const secondVal = secondEl.values?.[objectiveId]
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
} else if (secondVal === undefined) {
|
||||
return 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)
|
||||
}
|
||||
|
||||
return (
|
||||
<DataGrid<Trial>
|
||||
columns={columns}
|
||||
rows={trials}
|
||||
keyField={"trial_id"}
|
||||
dense={false}
|
||||
initialRowsPerPage={initialRowsPerPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// @ts-ignore
|
||||
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
|
||||
import { SetterOrUpdater } from "recoil"
|
||||
|
||||
export const loadStorage = (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
setter: SetterOrUpdater<Study[]>
|
||||
): void => {
|
||||
sqlite3InitModule({
|
||||
print: (...args: any): void => {
|
||||
console.log(args)
|
||||
},
|
||||
printErr: (...args: any): void => {
|
||||
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 {
|
||||
// Check version_info table
|
||||
let supported = true
|
||||
db.exec({
|
||||
sql: "SELECT schema_version FROM version_info LIMIT 1",
|
||||
callback: (vals: any[]) => {
|
||||
if (vals[0] != 12) {
|
||||
supported = false
|
||||
}
|
||||
},
|
||||
})
|
||||
if (!supported) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get studies
|
||||
let studies: Study[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
|
||||
" FROM studies AS s INNER JOIN study_directions AS sd" +
|
||||
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
|
||||
callback: (vals: any[]) => {
|
||||
const study_id = vals[0]
|
||||
const study_name = vals[1]
|
||||
const direction: StudyDirection = vals[2].toLowerCase()
|
||||
const objective = vals[3]
|
||||
let index = 0
|
||||
|
||||
if (objective === 0) {
|
||||
studies.push({
|
||||
study_id: study_id,
|
||||
study_name: study_name,
|
||||
directions: [direction],
|
||||
union_search_space: [],
|
||||
intersection_search_space: [],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
trials: [],
|
||||
})
|
||||
} else {
|
||||
index = studies.findIndex((s) => s.study_id === study_id)
|
||||
studies[index].directions.push(direction)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
studies.forEach((s) => {
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT t.trial_id, t.number, t.study_id, t.state, t.datetime_start, t.datetime_complete," +
|
||||
" tv.objective, tv.value, tv.value_type" +
|
||||
" FROM trials AS t LEFT JOIN trial_values AS tv ON tv.trial_id = t.trial_id" +
|
||||
` WHERE t.study_id = ${s.study_id}` +
|
||||
" ORDER BY t.number",
|
||||
callback: (vals: any[]) => {
|
||||
const state: TrialState =
|
||||
vals[3] === "COMPLETE"
|
||||
? "Complete"
|
||||
: "PRUNED"
|
||||
? "Pruned"
|
||||
: "RUNNING"
|
||||
? "Running"
|
||||
: "WAITING"
|
||||
? "Waiting"
|
||||
: "Fail"
|
||||
const trial: Trial = {
|
||||
trial_id: vals[0],
|
||||
number: vals[1],
|
||||
study_id: vals[2],
|
||||
state: state,
|
||||
params: [],
|
||||
intermediate_values: [],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
}
|
||||
s.trials.push(trial)
|
||||
},
|
||||
})
|
||||
const union_search_space: SearchSpaceItem[] = []
|
||||
let intersection_search_space: Set<SearchSpaceItem> = new Set()
|
||||
s.trials.forEach((trial) => {
|
||||
const params: TrialParam[] = []
|
||||
const param_names = new Set<string>()
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT param_name, param_value" +
|
||||
` FROM trial_params WHERE trial_id = ${trial.trial_id}`,
|
||||
callback: (vals: any[]) => {
|
||||
const param_name = vals[0]
|
||||
params.push({
|
||||
name: param_name,
|
||||
param_internal_value: vals[1],
|
||||
})
|
||||
|
||||
param_names.add(param_name)
|
||||
if (
|
||||
union_search_space.findIndex((s) => s.name === param_name) == -1
|
||||
) {
|
||||
union_search_space.push({ name: param_name })
|
||||
}
|
||||
},
|
||||
})
|
||||
if (intersection_search_space.size === 0) {
|
||||
param_names.forEach((s) => {
|
||||
intersection_search_space.add({
|
||||
name: s,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
intersection_search_space = new Set(
|
||||
Array.from(intersection_search_space).filter((s) =>
|
||||
param_names.has(s.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
trial.params = params
|
||||
const values: TrialValueNumber[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT value, value_type" +
|
||||
` FROM trial_values WHERE trial_id = ${trial.trial_id}` +
|
||||
" ORDER BY objective",
|
||||
callback: (vals: any[]) => {
|
||||
values.push(
|
||||
vals[1] === "INF_NEG"
|
||||
? "-inf"
|
||||
: vals[1] === "INF_POS"
|
||||
? "+inf"
|
||||
: vals[0]
|
||||
)
|
||||
},
|
||||
})
|
||||
if (s.directions.length === values.length) {
|
||||
trial.values = values
|
||||
}
|
||||
})
|
||||
s.union_search_space = union_search_space
|
||||
s.intersection_search_space = Array.from(intersection_search_space)
|
||||
})
|
||||
|
||||
setter((prev) => [...prev, ...studies])
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from "recoil"
|
||||
|
||||
export const studiesState = atom<Study[]>({
|
||||
key: "studies",
|
||||
default: [],
|
||||
})
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
declare const IS_VSCODE: boolean
|
||||
|
||||
type TrialValueNumber = number | "inf" | "-inf"
|
||||
type TrialIntermediateValueNumber = number | "inf" | "-inf" | "nan"
|
||||
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
|
||||
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
|
||||
type StudyDirection = "maximize" | "minimize" | "not_set"
|
||||
|
||||
type FloatDistribution = {
|
||||
type: "FloatDistribution"
|
||||
low: number
|
||||
high: number
|
||||
step: number
|
||||
log: boolean
|
||||
}
|
||||
|
||||
type IntDistribution = {
|
||||
type: "IntDistribution"
|
||||
low: number
|
||||
high: number
|
||||
step: number
|
||||
log: boolean
|
||||
}
|
||||
|
||||
type CategoricalDistribution = {
|
||||
type: "CategoricalDistribution"
|
||||
choices: { pytype: string; value: string }[]
|
||||
}
|
||||
|
||||
type Distribution =
|
||||
| FloatDistribution
|
||||
| IntDistribution
|
||||
| CategoricalDistribution
|
||||
|
||||
type Attribute = {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type Study = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
directions: StudyDirection[]
|
||||
user_attrs: Attribute[]
|
||||
union_search_space: SearchSpaceItem[]
|
||||
intersection_search_space: SearchSpaceItem[]
|
||||
system_attrs: Attribute[]
|
||||
datetime_start?: Date
|
||||
trials: Trial[]
|
||||
}
|
||||
|
||||
type Trial = {
|
||||
trial_id: number
|
||||
number: number
|
||||
study_id: number
|
||||
state: TrialState
|
||||
values?: TrialValueNumber[]
|
||||
params: TrialParam[]
|
||||
intermediate_values: TrialIntermediateValue[]
|
||||
datetime_start?: Date
|
||||
datetime_complete?: Date
|
||||
user_attrs: Attribute[]
|
||||
system_attrs: Attribute[]
|
||||
}
|
||||
|
||||
type TrialParam = {
|
||||
name: string
|
||||
param_internal_value: number
|
||||
}
|
||||
|
||||
type SearchSpaceItem = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type ParamImportance = {
|
||||
name: string
|
||||
importance: number
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { FC, useEffect } from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import "./index.css"
|
||||
import { App } from "./components/App"
|
||||
import { RecoilRoot, useSetRecoilState, SetterOrUpdater } from "recoil"
|
||||
import { studiesState } from "./state"
|
||||
import { loadStorage } from "./sqlite3"
|
||||
|
||||
export const AppWrapper: FC = () => {
|
||||
const setStudies = useSetRecoilState<Study[]>(studiesState)
|
||||
|
||||
const onceSetStudies: SetterOrUpdater<Study[]> = (
|
||||
setter: ((currVal: Study) => Study) | Study
|
||||
): void => {
|
||||
const studies = setter([])
|
||||
setStudies(studies)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", (event) => {
|
||||
const message = event.data
|
||||
switch (message.type) {
|
||||
case "optunaStorage":
|
||||
const fileContentBase64 = message.content
|
||||
const binaryString = atob(fileContentBase64)
|
||||
const len = binaryString.length
|
||||
const bytes = new Uint8Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
const arrayBuffer = bytes.buffer
|
||||
loadStorage(arrayBuffer, onceSetStudies)
|
||||
break
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
return <App />
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<AppWrapper />
|
||||
</RecoilRoot>
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"paths": {
|
||||
"plotly.js-dist-min": ["node_modules/@types/plotly.js"]
|
||||
},
|
||||
"noImplicitAny": true,
|
||||
"lib": ["dom", "esnext"],
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es5",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
},
|
||||
"files": [
|
||||
"./src/browser_app_entry.tsx",
|
||||
"./src/vscode_entry.tsx"
|
||||
],
|
||||
"include": [
|
||||
"./src/types/**/*"
|
||||
],
|
||||
"types": ["node"]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
const isDev = mode === 'development';
|
||||
|
||||
const typeScriptLoader = process.env.TYPESCRIPT_LOADER === "esbuild-loader" ? {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
loader: 'tsx',
|
||||
tsconfigRaw: require('./tsconfig.json')
|
||||
}
|
||||
} : {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: __dirname + '/tsconfig.json',
|
||||
transpileOnly: isDev,
|
||||
happyPackMode: true
|
||||
}
|
||||
}
|
||||
|
||||
var config = [
|
||||
{
|
||||
mode,
|
||||
experiments: {
|
||||
syncWebAssembly: true,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
entry: [__dirname + '/src/browser_app_entry.tsx'],
|
||||
output: {
|
||||
path: __dirname + '/public/',
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [{ oneOf: [typeScriptLoader] }]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(false)})
|
||||
]
|
||||
},
|
||||
{
|
||||
mode,
|
||||
experiments: {
|
||||
syncWebAssembly: true,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
entry: [__dirname + '/src/vscode_entry.tsx'],
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../vscode/assets/'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ oneOf: [typeScriptLoader] },
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: "asset/inline",
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(true)})
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
if (isDev) {
|
||||
config[0].devtool = 'source-map';
|
||||
config[0].cache = {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
}
|
||||
}
|
||||
console.log('= = = = = = = = = = = = = = = = = = =');
|
||||
console.log('DEVELOPMENT BUILD');
|
||||
console.log(process.env.TYPESCRIPT_LOADER === 'esbuild-loader' ? 'esbuild-loader' : 'ts-loader');
|
||||
console.log('= = = = = = = = = = = = = = = = = = =');
|
||||
} else {
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
config[0].plugins.push(new CompressionPlugin())
|
||||
config[1].plugins.push(new CompressionPlugin())
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user