diff --git a/.github/workflows/typescript-tests.yml b/.github/workflows/typescript-tests.yml index 971a4c5b..5873f024 100644 --- a/.github/workflows/typescript-tests.yml +++ b/.github/workflows/typescript-tests.yml @@ -22,9 +22,8 @@ jobs: uses: actions/setup-node@v2 with: node-version: '16' - - run: | - npm install - npm run lint + - run: npm install + - run: npm run lint build: name: JS build check diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 6d5aa0d8..d9420bf7 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -219,7 +219,6 @@ export const actionCreator = () => { }) }) .catch((err) => { - console.dir(err) if (err.response.status === 409) { const index = studyDetails[studyId].trials.findIndex( (t) => t.trial_id === trialId @@ -233,7 +232,7 @@ export const actionCreator = () => { ) return } - setTrialNote(studyId, index, note) + setTrialNote(studyId, index, err.response.data.note) } const reason = err.response?.data.reason if (reason !== undefined) { diff --git a/optuna_dashboard/ts/components/App.tsx b/optuna_dashboard/ts/components/App.tsx index 53c2a5f4..02fb6898 100644 --- a/optuna_dashboard/ts/components/App.tsx +++ b/optuna_dashboard/ts/components/App.tsx @@ -47,7 +47,6 @@ export const App: FC = () => { backgroundColor: colorMode === "dark" ? "#121212" : "#ffffff", width: "100%", minHeight: "100vh", - paddingBottom: theme.spacing(2), }} > diff --git a/optuna_dashboard/ts/components/BestTrialsCard.tsx b/optuna_dashboard/ts/components/BestTrialsCard.tsx new file mode 100644 index 00000000..d3e8499e --- /dev/null +++ b/optuna_dashboard/ts/components/BestTrialsCard.tsx @@ -0,0 +1,133 @@ +import React, { FC } from "react" +import { + Box, + Button, + Card, + CardContent, + Divider, + List, + ListItem, + ListItemButton, + ListItemText, + Typography, + useTheme, +} from "@mui/material" +import { Link } from "react-router-dom" +import LinkIcon from "@mui/icons-material/Link" + +export const BestTrialsCard: FC<{ + studyDetail: StudyDetail | null +}> = ({ studyDetail }) => { + const theme = useTheme() + + let header = "Best Trials" + let content: React.ReactNode = null + if (studyDetail !== null && studyDetail.best_trials.length === 1) { + const bestTrial = studyDetail.best_trials[0] + header = `Best Trial (number=${bestTrial.number})` + content = ( + <> + + {bestTrial.values} + + + Params = [ + {bestTrial.params.map((p) => `${p.name}: ${p.value}`).join(", ")}] + + + Intermediate Values = [ + {studyDetail.best_trials[0].intermediate_values + .map((p) => `${p.step}: ${p.value}`) + .join(", ")} + ] + + + User Attributes = [ + {studyDetail.best_trials[0].user_attrs + .map((p) => `${p.key}: ${p.value}`) + .join(", ")} + ] + + + + ) + } else if (studyDetail !== null && studyDetail.best_trials.length > 1) { + const bestTrials = studyDetail.best_trials + content = ( + <> + + + + {bestTrials.map((trial) => ( + + + Trial {trial.number} + } + secondary={ + <> + + Objective Values = [{trial.values?.join(", ")}] + + + Params = [ + {trial.params + .map((p) => `${p.name}: ${p.value}`) + .join(", ")} + ] + + + } + /> + + + ))} + + + + ) + } + return ( + + + + {header} + + {content} + + + ) +} diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index 813365bd..a26c3e16 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -214,7 +214,9 @@ const plotContour = ( const filteredTrials = trials.filter((t) => filterFunc(t, objectiveId)) if (filteredTrials.length === 0) { - plotly.react(plotDomId, []) + plotly.react(plotDomId, [], { + template: mode === "dark" ? plotlyDarkTemplate : {}, + }) return } diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index 47b82bee..c266de67 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -80,7 +80,9 @@ const plotEdf = (study: StudyDetail, objectiveId: number, mode: string) => { const filteredTrials = trials.filter((t) => filterFunc(t, objectiveId)) if (filteredTrials.length === 0) { - plotly.react(plotDomId, []) + plotly.react(plotDomId, [], { + template: mode === "dark" ? plotlyDarkTemplate : {}, + }) return } diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index da431167..03907c12 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -224,7 +224,7 @@ const plotHistory = ( filteredTrials = filteredTrials.filter((t) => t.state !== "Pruned") } if (filteredTrials.length === 0) { - plotly.react(plotDomId, []) + plotly.react(plotDomId, [], layout) return } diff --git a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx index 373add6c..faf090fe 100644 --- a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx +++ b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx @@ -56,7 +56,7 @@ export const GraphHyperparameterImportanceBeta: FC<{ {title} diff --git a/optuna_dashboard/ts/components/Note.tsx b/optuna_dashboard/ts/components/Note.tsx index 47f9a9f6..24bd61c9 100644 --- a/optuna_dashboard/ts/components/Note.tsx +++ b/optuna_dashboard/ts/components/Note.tsx @@ -4,6 +4,11 @@ import { Card, CardContent, CardHeader, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, IconButton, SxProps, TextField, @@ -13,21 +18,46 @@ import { import React, { FC, createRef, useState, useEffect } from "react" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" +import remarkMath from "remark-math" +import rehypeMathjax from "rehype-mathjax" import LoadingButton from "@mui/lab/LoadingButton" import SaveIcon from "@mui/icons-material/Save" -import EditIcon from "@mui/icons-material/Edit" import CloseIcon from "@mui/icons-material/Close" +import EditIcon from "@mui/icons-material/Edit" import Divider from "@mui/material/Divider" import { Theme } from "@mui/material/styles" import { CodeComponent, ReactMarkdownNames, } from "react-markdown/lib/ast-to-react" +import HtmlIcon from "@mui/icons-material/Html" +import ModeEditIcon from "@mui/icons-material/ModeEdit" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism" import { actionCreator } from "../action" +const placeholder = `## What is this feature for? + +Here you can freely take a note in *(GitHub flavored) Markdown format*. +In addition, **code blocks with syntax highlights** and **formula** are also supported here, as shown below. + +### Code-block with Syntax Highlights + +\`\`\`python +def objective(trial): + x = trial.suggest_float("x", -10, 10) + y = trial.suggest_float("y", -10, 10) + return (x - 5) ** 2 + (y + 5) ** 2 +\`\`\` + +### Formula + +$$ +L = \\frac{1}{2} \\rho v^2 S C_L +$$ +` + const CodeBlock: CodeComponent | ReactMarkdownNames = ({ inline, className, @@ -62,7 +92,6 @@ export const TrialNote: FC<{ studyId={studyId} trialId={trialId} latestNote={latestNote} - minRows={10} cardSx={cardSx} /> ) @@ -71,35 +100,86 @@ export const TrialNote: FC<{ export const StudyNote: FC<{ studyId: number latestNote: Note - minRows: number cardSx?: SxProps -}> = ({ studyId, latestNote, minRows, cardSx }) => { - return ( - - ) +}> = ({ studyId, latestNote, cardSx }) => { + return } -const NoteBase: FC<{ +const useConfirmCloseDialog = ( + handleClose: () => void +): [() => void, () => React.ReactNode] => { + const theme = useTheme() + const [open, setOpen] = useState(false) + + const zIndex = theme.zIndex.snackbar - 1 + const openDialog = () => { + setOpen(true) + } + const renderDialog = () => { + return ( + + Unsaved changes + + + Do you want to save or discard your changes? + + + + + + + + ) + } + return [openDialog, renderDialog] +} + +const MarkdownRenderer: FC<{ body: string }> = ({ body }) => ( + +) + +const MarkdownEditorModal: FC<{ studyId: number trialId?: number latestNote: Note - minRows: number - cardSx?: SxProps -}> = ({ studyId, trialId, latestNote, minRows, cardSx }) => { + setEditorUnmount: () => void +}> = ({ studyId, trialId, latestNote, setEditorUnmount }) => { const theme = useTheme() - const [renderMarkdown, setRenderMarkdown] = useState(true) + const action = actionCreator() + const [openConfirmCloseDialog, renderConfirmCloseDialog] = + useConfirmCloseDialog(() => { + setEditorUnmount() + window.onbeforeunload = null + }) + const [saving, setSaving] = useState(false) const [edited, setEdited] = useState(false) const [curNote, setCurNote] = useState({ version: 0, body: "" }) const textAreaRef = createRef() - const action = actionCreator() const notLatest = latestNote.version > curNote.version + const [previewMarkdown, setPreviewMarkdown] = useState("") + const [preview, setPreview] = useState(false) + useEffect(() => { setCurNote(latestNote) }, []) @@ -119,7 +199,6 @@ const NoteBase: FC<{ body: textAreaRef.current ? textAreaRef.current.value : "", } setSaving(true) - let actionResponse: Promise if (trialId === undefined) { actionResponse = action.saveStudyNote(studyId, newNote) @@ -129,8 +208,8 @@ const NoteBase: FC<{ actionResponse .then(() => { setCurNote(newNote) - setRenderMarkdown(true) window.onbeforeunload = null + setEditorUnmount() }) .finally(() => { setSaving(false) @@ -146,103 +225,173 @@ const NoteBase: FC<{ window.onbeforeunload = null } - let content - if (renderMarkdown) { - const defaultBody = - "*A markdown editor for taking a memo, related to the study. Click the 'Edit' button in the upper right corner to access the editor.*" - content = ( - - ) - } else { - content = ( - <> - { - const cur = textAreaRef.current ? textAreaRef.current.value : "" - setEdited(cur !== curNote.body) - }} - /> - - {notLatest && !saving && ( - <> - - The text you are editing has updated. Do you want to discard - your changes and refresh the textarea? - - - - )} - - } - variant="contained" - disabled={!edited} - > - Save - - - - ) - } + // See https://github.com/iamhosseindhv/notistack/issues/231#issuecomment-825924840 + const zIndex = theme.zIndex.snackbar - 2 return ( - + + { + setPreview(!preview) + setPreviewMarkdown( + textAreaRef.current ? textAreaRef.current.value : "" + ) + }} + > + {preview ? ( + + ) : ( + + )} + + } + title="Markdown Editor" + /> + + + + { + const cur = textAreaRef.current ? textAreaRef.current.value : "" + if (edited !== (cur !== curNote.body)) { + setEdited(cur !== curNote.body) + } + }} + /> + + {notLatest && !saving && ( + <> + + The text you are editing has updated. Do you want to discard your + changes and refresh the textarea? + + + + )} + + + } + variant="contained" + disabled={!edited || notLatest} + sx={{ marginLeft: theme.spacing(1) }} + > + Save + + + {renderConfirmCloseDialog()} + + ) +} + +const NoteBase: FC<{ + studyId: number + trialId?: number + latestNote: Note + cardSx?: SxProps +}> = ({ studyId, trialId, latestNote, cardSx }) => { + const theme = useTheme() + const [editorMounted, setEditorMounted] = useState(false) + + const defaultBody = "" + return ( + { - setRenderMarkdown(true) - }} - > - - - ) : ( - setRenderMarkdown(false)}> - - - ) + { + setEditorMounted(true) + }} + > + + } sx={{ paddingBottom: 0 }} /> - {content} + + {editorMounted && ( + { + setEditorMounted(false) + }} + /> + )} ) } diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index ad06611a..ae37f699 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -235,11 +235,7 @@ export const StudyDetail: FC<{ {studyDetail !== null ? ( - + ) : null} diff --git a/optuna_dashboard/ts/components/StudyDetailBeta.tsx b/optuna_dashboard/ts/components/StudyDetailBeta.tsx index d2c1b349..75a2eedd 100644 --- a/optuna_dashboard/ts/components/StudyDetailBeta.tsx +++ b/optuna_dashboard/ts/components/StudyDetailBeta.tsx @@ -34,6 +34,7 @@ import { DataGrid, DataGridColumn } from "./DataGrid" import { GraphIntermediateValues } from "./GraphIntermediateValues" import { Edf } from "./GraphEdf" import { TrialList } from "./TrialList" +import { BestTrialsCard } from "./BestTrialsCard" interface ParamTypes { studyId: string @@ -123,103 +124,12 @@ export const StudyDetailBeta: FC<{ graphHeight="450px" /> - - - {studyDetail !== null && - studyDetail.best_trials.length === 1 && ( - <> - - Best Trial - - - {studyDetail.best_trials[0].values} - - - number={studyDetail.best_trials[0].number} - - - trial_id={studyDetail.best_trials[0].trial_id} - - - Params = [ - {studyDetail.best_trials[0].params - .map((p) => `${p.name}: ${p.value}`) - .join(", ")} - ] - - - Intermediate Values = [ - {studyDetail.best_trials[0].intermediate_values - .map((p) => `${p.step}: ${p.value}`) - .join(", ")} - ] - - - User Attributes = [ - {studyDetail.best_trials[0].user_attrs - .map((p) => `${p.key}: ${p.value}`) - .join(", ")} - ] - - - )} - {studyDetail !== null && studyDetail.directions.length > 1 && ( - <> - - Best Trials ({studyDetail.best_trials.length} trials) - - {studyDetail.best_trials.map((trial, i) => ( - - - - Trial number={trial.number} (trial_id= - {trial.trial_id}) - - - Objective Values = [{trial.values?.join(", ")}] - - - Params = [ - {trial.params - .map((p) => `${p.name}: ${p.value}`) - .join(", ")} - ] - - - - ))} - - )} - - + ) diff --git a/optuna_dashboard/ts/components/StudyListBeta.tsx b/optuna_dashboard/ts/components/StudyListBeta.tsx index 02175a19..2e25d562 100644 --- a/optuna_dashboard/ts/components/StudyListBeta.tsx +++ b/optuna_dashboard/ts/components/StudyListBeta.tsx @@ -144,7 +144,6 @@ export const StudyListBeta: FC<{