diff --git a/optuna_dashboard/static/action.ts b/optuna_dashboard/static/action.ts index 5f1061c9..4f398497 100644 --- a/optuna_dashboard/static/action.ts +++ b/optuna_dashboard/static/action.ts @@ -5,6 +5,7 @@ import { getStudySummariesAPI, createNewStudyAPI, deleteStudyAPI, + saveNoteAPI, } from "./apiClient" import { studyDetailsState, studySummariesState } from "./state" @@ -95,11 +96,32 @@ export const actionCreator = () => { }) } + const saveNote = (studyId: number, note: Note) => { + saveNoteAPI(studyId, note) + .then(() => { + const newStudy = Object.assign({}, studyDetails[studyId]) + newStudy.note = note + const newStudies = Object.assign({}, studyDetails) + newStudies[studyId] = newStudy + setStudyDetails(newStudies) + enqueueSnackbar(`Success to save the note`, { + variant: "success", + }) + }) + .catch((err) => { + const reason = err.response?.data.reason + enqueueSnackbar(`Failed: ${reason}`, { + variant: "error", + }) + }) + } + return { updateStudyDetail, updateStudySummaries, createNewStudy, deleteStudy, + saveNote, } } diff --git a/optuna_dashboard/static/components/Note.tsx b/optuna_dashboard/static/components/Note.tsx index 2b2a7f3a..0e01d1a9 100644 --- a/optuna_dashboard/static/components/Note.tsx +++ b/optuna_dashboard/static/components/Note.tsx @@ -1,18 +1,91 @@ -import {Card, CardContent, useTheme, TextField} from "@mui/material"; -import React, {FC} from "react"; +import { Box, Button, TextField, Typography, useTheme } from "@mui/material" +import React, { FC, createRef, useState, useEffect } from "react" +import LoadingButton from "@mui/lab/LoadingButton" +import SaveIcon from "@mui/icons-material/Save" +import { actionCreator } from "../action" -export const Note: FC<{studyId: number}> = ({studyId}) => { - const theme = useTheme() - return ( - = ({ studyId, latestNote }) => { + const theme = useTheme() + const [disable, setDisable] = useState(true) + const [curNote, setCurNote] = useState({ version: 0, body: "" }) + const textAreaRef = createRef() + const action = actionCreator() + const notLatest = latestNote.version > curNote.version + + useEffect(() => { + setCurNote(latestNote) + }, []) + const handleSave = () => { + const newNote = { + version: curNote.version + 1, + body: textAreaRef.current ? textAreaRef.current.value : "" + } + setCurNote(newNote) + action.saveNote(studyId, newNote) + } + const handleRefresh = () => { + if (!textAreaRef.current) { + console.log("Unexpectedly, textarea is not found.") + return + } + textAreaRef.current.value = latestNote.body + setCurNote(latestNote) + } + + return ( + <> + { + const cur = textAreaRef.current ? textAreaRef.current.value : "" + setDisable(cur === latestNote.body) + }} + /> + + {notLatest && ( + <> + + The text you are editing has updated. Do you want to discard your + changes and refresh the textarea? + + + + )} + + } + variant="contained" + disabled={disable} > - - - - - ) -} \ No newline at end of file + Save + + + + ) +} diff --git a/optuna_dashboard/static/components/StudyDetail.tsx b/optuna_dashboard/static/components/StudyDetail.tsx index b7f4abc4..1796bcec 100644 --- a/optuna_dashboard/static/components/StudyDetail.tsx +++ b/optuna_dashboard/static/components/StudyDetail.tsx @@ -37,6 +37,7 @@ import { GraphIntermediateValues } from "./GraphIntermediateValues" import { GraphSlice } from "./GraphSlice" import { GraphHistory } from "./GraphHistory" import { GraphParetoFront } from "./GraphParetoFront" +import { Note } from "./Note" import { actionCreator } from "../action" import { studyDetailsState } from "../state" @@ -382,6 +383,14 @@ export const StudyDetail: FC<{ + + + Note + {studyDetail !== null && ( + + )} + + diff --git a/package-lock.json b/package-lock.json index 90d2df10..4c468579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@mui/icons-material": "^5.4.4", + "@mui/lab": "^5.0.0-alpha.73", "@mui/material": "^5.4.4", "axios": "^0.21.2", "notistack": "^2.0.3", @@ -2740,6 +2741,75 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@date-io/core": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz", + "integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg==" + }, + "node_modules/@date-io/date-fns": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz", + "integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==", + "dependencies": { + "@date-io/core": "^2.13.1" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + } + } + }, + "node_modules/@date-io/dayjs": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz", + "integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==", + "dependencies": { + "@date-io/core": "^2.13.1" + }, + "peerDependencies": { + "dayjs": "^1.8.17" + }, + "peerDependenciesMeta": { + "dayjs": { + "optional": true + } + } + }, + "node_modules/@date-io/luxon": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz", + "integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==", + "dependencies": { + "@date-io/core": "^2.13.1" + }, + "peerDependencies": { + "luxon": "^1.21.3 || ^2.x" + }, + "peerDependenciesMeta": { + "luxon": { + "optional": true + } + } + }, + "node_modules/@date-io/moment": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz", + "integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==", + "dependencies": { + "@date-io/core": "^2.13.1" + }, + "peerDependencies": { + "moment": "^2.24.0" + }, + "peerDependenciesMeta": { + "moment": { + "optional": true + } + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", @@ -3681,6 +3751,91 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.73", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz", + "integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@date-io/date-fns": "^2.13.1", + "@date-io/dayjs": "^2.13.1", + "@date-io/luxon": "^2.13.1", + "@date-io/moment": "^2.13.1", + "@mui/base": "5.0.0-alpha.72", + "@mui/system": "^5.5.1", + "@mui/utils": "^5.4.4", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^16.8.6 || ^17.0.0", + "date-fns": "^2.25.0", + "dayjs": "^1.10.7", + "luxon": "^1.28.0 || ^2.0.0", + "moment": "^2.29.1", + "react": "^17.0.0", + "react-dom": "^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@mui/lab/node_modules/@mui/base": { + "version": "5.0.0-alpha.72", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz", + "integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@emotion/is-prop-valid": "^1.1.2", + "@mui/utils": "^5.4.4", + "@popperjs/core": "^2.11.3", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^17.0.0", + "react-dom": "^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.4.4.tgz", @@ -3782,17 +3937,17 @@ } }, "node_modules/@mui/system": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.4.tgz", - "integrity": "sha512-Zjbztq2o/VRuRRCWjG44juRrPKYLQMqtQpMHmMttGu5BnvK6PAPW3WOY0r1JCAwLhbd8Kug9nyhGQYKETjo+tQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz", + "integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==", "dependencies": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.4.4", "@mui/styled-engine": "^5.4.4", - "@mui/types": "^7.1.2", + "@mui/types": "^7.1.3", "@mui/utils": "^5.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.10", + "csstype": "^3.0.11", "prop-types": "^15.7.2" }, "engines": { @@ -3821,9 +3976,9 @@ } }, "node_modules/@mui/types": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.2.tgz", - "integrity": "sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz", + "integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==", "peerDependencies": { "@types/react": "*" }, @@ -3891,9 +4046,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -10287,6 +10442,14 @@ "node": ">=0.10.0" } }, + "node_modules/rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -13674,6 +13837,43 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@date-io/core": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz", + "integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg==" + }, + "@date-io/date-fns": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz", + "integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/dayjs": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz", + "integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/luxon": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz", + "integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/moment": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz", + "integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, "@discoveryjs/json-ext": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", @@ -14375,6 +14575,42 @@ "@babel/runtime": "^7.17.2" } }, + "@mui/lab": { + "version": "5.0.0-alpha.73", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz", + "integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==", + "requires": { + "@babel/runtime": "^7.17.2", + "@date-io/date-fns": "^2.13.1", + "@date-io/dayjs": "^2.13.1", + "@date-io/luxon": "^2.13.1", + "@date-io/moment": "^2.13.1", + "@mui/base": "5.0.0-alpha.72", + "@mui/system": "^5.5.1", + "@mui/utils": "^5.4.4", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + }, + "dependencies": { + "@mui/base": { + "version": "5.0.0-alpha.72", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz", + "integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==", + "requires": { + "@babel/runtime": "^7.17.2", + "@emotion/is-prop-valid": "^1.1.2", + "@mui/utils": "^5.4.4", + "@popperjs/core": "^2.11.3", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + } + } + }, "@mui/material": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.4.4.tgz", @@ -14415,24 +14651,24 @@ } }, "@mui/system": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.4.4.tgz", - "integrity": "sha512-Zjbztq2o/VRuRRCWjG44juRrPKYLQMqtQpMHmMttGu5BnvK6PAPW3WOY0r1JCAwLhbd8Kug9nyhGQYKETjo+tQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz", + "integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==", "requires": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.4.4", "@mui/styled-engine": "^5.4.4", - "@mui/types": "^7.1.2", + "@mui/types": "^7.1.3", "@mui/utils": "^5.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.10", + "csstype": "^3.0.11", "prop-types": "^15.7.2" } }, "@mui/types": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.2.tgz", - "integrity": "sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz", + "integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==", "requires": {} }, "@mui/utils": { @@ -14474,9 +14710,9 @@ } }, "@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==" + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -19265,6 +19501,12 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "requires": {} + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 3338baf6..39663336 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "MIT", "dependencies": { "@mui/icons-material": "^5.4.4", + "@mui/lab": "^5.0.0-alpha.73", "@mui/material": "^5.4.4", "axios": "^0.21.2", "notistack": "^2.0.3",