From 08189609f0aded4a5ba4f5e19e1e59c0e638c59b Mon Sep 17 00:00:00 2001 From: Frank Niessink <frank@niessink.com> Date: Mon, 30 Sep 2024 18:02:00 +0200 Subject: [PATCH] Migrate frontend to MUI. - Settings - Menubar Partially addresses #9796. --- components/frontend/package-lock.json | 350 +++++++++++++----- components/frontend/package.json | 4 +- components/frontend/src/App.js | 4 +- components/frontend/src/App.test.js | 34 +- components/frontend/src/AppUI.js | 97 ++--- components/frontend/src/AppUI.test.js | 2 +- .../src/header_footer/CollapseButton.js | 32 -- .../frontend/src/header_footer/Menubar.css | 21 -- .../frontend/src/header_footer/Menubar.js | 133 ++----- .../src/header_footer/Menubar.test.js | 14 +- .../src/header_footer/ResetSettingsButton.js | 43 --- .../src/header_footer/SettingsPanel.test.js | 8 - .../frontend/src/header_footer/UIModeMenu.js | 66 +++- .../src/header_footer/UIModeMenu.test.js | 4 + .../header_footer/buttons/CollapseButton.js | 25 ++ .../{ => buttons}/CollapseButton.test.js | 8 +- .../header_footer/buttons/DatePickerButton.js | 48 +++ .../{ => buttons}/DownloadAsPDFButton.js | 52 ++- .../{ => buttons}/DownloadAsPDFButton.test.js | 22 +- .../src/header_footer/buttons/HomeButton.js | 29 ++ .../buttons/ResetSettingsButton.js | 33 ++ .../{ => buttons}/ResetSettingsButton.test.js | 6 +- .../header_footer/buttons/SettingsButton.js | 20 + .../settings_menu/SettingsMenu.css | 4 - .../settings_menu/SettingsMenu.js | 44 +-- .../settings_menu/SortColumnMenu.js | 11 +- components/frontend/src/widgets/IconCombi.js | 17 - 27 files changed, 657 insertions(+), 474 deletions(-) delete mode 100644 components/frontend/src/header_footer/CollapseButton.js delete mode 100644 components/frontend/src/header_footer/ResetSettingsButton.js create mode 100644 components/frontend/src/header_footer/buttons/CollapseButton.js rename components/frontend/src/header_footer/{ => buttons}/CollapseButton.test.js (87%) create mode 100644 components/frontend/src/header_footer/buttons/DatePickerButton.js rename components/frontend/src/header_footer/{ => buttons}/DownloadAsPDFButton.js (61%) rename components/frontend/src/header_footer/{ => buttons}/DownloadAsPDFButton.test.js (78%) create mode 100644 components/frontend/src/header_footer/buttons/HomeButton.js create mode 100644 components/frontend/src/header_footer/buttons/ResetSettingsButton.js rename components/frontend/src/header_footer/{ => buttons}/ResetSettingsButton.test.js (88%) create mode 100644 components/frontend/src/header_footer/buttons/SettingsButton.js delete mode 100644 components/frontend/src/header_footer/settings_menu/SettingsMenu.css delete mode 100644 components/frontend/src/widgets/IconCombi.js diff --git a/components/frontend/package-lock.json b/components/frontend/package-lock.json index 198fe0f493..eeb368e37c 100644 --- a/components/frontend/package-lock.json +++ b/components/frontend/package-lock.json @@ -11,15 +11,17 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.1", + "@mui/lab": "^6.0.0-beta.10", "@mui/material": "^6.1.1", + "@mui/x-date-pickers": "^7.18.0", "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.3", "history": "^5.3.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^7.4.0", "react-dom": "^18.3.1", - "react-focus-lock": "^2.13.0", "react-grid-layout": "^1.4.4", "react-hash-link": "1.0.2", "react-is": "^18.3.1", @@ -4821,6 +4823,68 @@ "dev": true, "license": "MIT" }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.58.tgz", + "integrity": "sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@floating-ui/react-dom": "^2.1.1", + "@mui/types": "^7.2.15", + "@mui/utils": "6.0.0-rc.0", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/base/node_modules/@mui/utils": { + "version": "6.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.0-rc.0.tgz", + "integrity": "sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz", @@ -4857,6 +4921,51 @@ } } }, + "node_modules/@mui/lab": { + "version": "6.0.0-beta.10", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.10.tgz", + "integrity": "sha512-eqCBz5SZS8Un9To3UcjH01AxkOOgvme/g0ZstFC8Nz1Kg5/EJMA0ByhKS5AvUMzUKrv0FXMdbuPqbBvF3bVrXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/base": "5.0.0-beta.58", + "@mui/system": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^6.1.1", + "@mui/material-pigment-css": "^6.1.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz", @@ -5050,6 +5159,152 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.18.0.tgz", + "integrity": "sha512-12tXIoMj9vpS8fS/bS3kWPCoVrH38vNGCxgplI0vOnUrN9rJuYJz3agLPJe1S0xciTw+9W8ZSe3soaW+owoz1Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6", + "@mui/x-internals": "7.18.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.18.0.tgz", + "integrity": "sha512-lzCHOWIR0cAIY1bGrWSprYerahbnH5C31ql/2OWCEjcngL2NAV1M6oKI2Vp4HheqzJ822c60UyWyapvyjSzY/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -8771,6 +9026,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "dev": true, @@ -8936,10 +9197,6 @@ "dev": true, "license": "MIT" }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/detect-port-alt": { "version": "1.1.6", "dev": true, @@ -10701,16 +10958,6 @@ "dev": true, "license": "ISC" }, - "node_modules/focus-lock": { - "version": "1.3.5", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/follow-redirects": { "version": "1.15.6", "dev": true, @@ -19528,16 +19775,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-clientside-effect": { - "version": "1.2.6", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-datepicker": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.4.0.tgz", @@ -19710,29 +19947,6 @@ "version": "3.2.2", "license": "MIT" }, - "node_modules/react-focus-lock": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.2.tgz", - "integrity": "sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^1.3.5", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.6", - "use-callback-ref": "^1.3.2", - "use-sidecar": "^1.1.2" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-grid-layout": { "version": "1.4.4", "license": "MIT", @@ -23611,6 +23825,7 @@ }, "node_modules/tslib": { "version": "2.6.2", + "dev": true, "license": "0BSD" }, "node_modules/tsutils": { @@ -23917,45 +24132,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, diff --git a/components/frontend/package.json b/components/frontend/package.json index db08bdc282..a2539922e3 100644 --- a/components/frontend/package.json +++ b/components/frontend/package.json @@ -7,15 +7,17 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.1", + "@mui/lab": "^6.0.0-beta.10", "@mui/material": "^6.1.1", + "@mui/x-date-pickers": "^7.18.0", "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.3", "history": "^5.3.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^7.4.0", "react-dom": "^18.3.1", - "react-focus-lock": "^2.13.0", "react-grid-layout": "^1.4.4", "react-hash-link": "1.0.2", "react-is": "^18.3.1", diff --git a/components/frontend/src/App.js b/components/frontend/src/App.js index 5241036e43..41d86df088 100644 --- a/components/frontend/src/App.js +++ b/components/frontend/src/App.js @@ -19,7 +19,9 @@ const theme = createTheme({ colorSchemes: { dark: true, // Add a dark theme (light theme is available by default) }, - components: { MuiTooltip: { defaultProps: { arrow: true }, styleOverrides: { tooltip: { fontSize: "1em" } } } }, + components: { + MuiTooltip: { defaultProps: { arrow: true }, styleOverrides: { tooltip: { fontSize: "1em" } } }, + }, }) class App extends Component { diff --git a/components/frontend/src/App.test.js b/components/frontend/src/App.test.js index 592a87d34c..0579e832b7 100644 --- a/components/frontend/src/App.test.js +++ b/components/frontend/src/App.test.js @@ -1,5 +1,4 @@ import { act, fireEvent, render, screen } from "@testing-library/react" -import userEvent from "@testing-library/user-event" import history from "history/browser" import * as auth from "./api/auth" @@ -71,30 +70,49 @@ it("resets the user when the user clicks logout", async () => { it("handles a date change", async () => { render(<App />) - await userEvent.type(screen.getByPlaceholderText("YYYY-MM-DD"), "2020-03-13") - expect(screen.getAllByDisplayValue("2020-03-13").length).toBe(1) + await act(async () => { + fireEvent.click(screen.getByLabelText("Report date")) + }) + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Previous month" })) + }) + await act(async () => { + fireEvent.click(screen.getAllByRole("gridcell", { name: "15" })[0]) + }) + expect(screen.getByLabelText("Report date").textContent).toMatch(/15/) }) it("handles a date change between two dates in the past", async () => { history.push("/?report_date=2022-03-13") render(<App />) - await userEvent.type(screen.getByPlaceholderText("YYYY-MM-DD"), "{Backspace}4") - expect(screen.getAllByDisplayValue("2022-03-14").length).toBe(1) + await act(async () => { + fireEvent.click(screen.getByLabelText("Report date")) + }) + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Previous month" })) + }) + await act(async () => { + fireEvent.click(screen.getAllByRole("gridcell", { name: "15" })[0]) + }) + expect(screen.getByLabelText("Report date").textContent).toMatch(/15/) }) it("reads the report date query parameter", () => { history.push("/?report_date=2020-03-13") render(<App />) - expect(screen.getAllByDisplayValue("2020-03-13").length).toBe(1) + expect(screen.getByLabelText("Report date").textContent).toMatch(/2020/) }) it("handles a date reset", async () => { history.push("/?report_date=2020-03-13") render(<App />) await act(async () => { - fireEvent.click(screen.getByRole("button", { name: "Close" })) + fireEvent.click(screen.getByLabelText("Report date")) + }) + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Today" })) }) - expect(screen.queryAllByDisplayValue("2020-03-13").length).toBe(0) + expect(screen.getByLabelText("Report date").textContent).toMatch(/today/) }) it("handles the nr of measurements event source", async () => { diff --git a/components/frontend/src/AppUI.js b/components/frontend/src/AppUI.js index 93ec29bead..4b35645ba8 100644 --- a/components/frontend/src/AppUI.js +++ b/components/frontend/src/AppUI.js @@ -2,6 +2,9 @@ import "react-toastify/dist/ReactToastify.css" import "./App.css" import { useColorScheme } from "@mui/material/styles" +import { LocalizationProvider } from "@mui/x-date-pickers" +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs" +import { locale_en_gb } from "dayjs/locale/en-gb" import { bool, func, number, object, string } from "prop-types" import HashLinkObserver from "react-hash-link" import { ToastContainer } from "react-toastify" @@ -73,52 +76,54 @@ export function AppUI({ backgroundColor: backgroundColor, }} > - <DarkMode.Provider value={darkMode}> - <HashLinkObserver /> - <Menubar - email={email} - handleDateChange={handleDateChange} - openReportsOverview={openReportsOverview} - onDate={handleDateChange} - report_date={report_date} - report_uuid={report_uuid} - set_user={set_user} - user={user} - panel={ - <SettingsPanel - atReportsOverview={atReportsOverview} - handleSort={handleSort} - settings={settings} - tags={getReportsTags(reports)} - /> - } - settings={settings} - setUIMode={setMode} - uiMode={mode} - /> - <ToastContainer theme="colored" /> - <Permissions.Provider value={user_permissions}> - <DataModel.Provider value={dataModel}> - <PageContent - changed_fields={changed_fields} - current_report={current_report} - handleSort={handleSort} - lastUpdate={lastUpdate} - loading={loading} - nrMeasurements={nrMeasurements} - openReportsOverview={openReportsOverview} - openReport={openReport} - reload={reload} - report_date={report_date} - report_uuid={report_uuid} - reports={reports} - reports_overview={reports_overview} - settings={settings} - /> - </DataModel.Provider> - </Permissions.Provider> - <Footer lastUpdate={lastUpdate} report={current_report} /> - </DarkMode.Provider> + <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale_en_gb}> + <DarkMode.Provider value={darkMode}> + <HashLinkObserver /> + <Menubar + email={email} + handleDateChange={handleDateChange} + openReportsOverview={openReportsOverview} + onDate={handleDateChange} + report_date={report_date} + report_uuid={report_uuid} + set_user={set_user} + user={user} + panel={ + <SettingsPanel + atReportsOverview={atReportsOverview} + handleSort={handleSort} + settings={settings} + tags={getReportsTags(reports)} + /> + } + settings={settings} + setUIMode={setMode} + uiMode={mode} + /> + <ToastContainer theme="colored" /> + <Permissions.Provider value={user_permissions}> + <DataModel.Provider value={dataModel}> + <PageContent + changed_fields={changed_fields} + current_report={current_report} + handleSort={handleSort} + lastUpdate={lastUpdate} + loading={loading} + nrMeasurements={nrMeasurements} + openReportsOverview={openReportsOverview} + openReport={openReport} + reload={reload} + report_date={report_date} + report_uuid={report_uuid} + reports={reports} + reports_overview={reports_overview} + settings={settings} + /> + </DataModel.Provider> + </Permissions.Provider> + <Footer lastUpdate={lastUpdate} report={current_report} /> + </DarkMode.Provider> + </LocalizationProvider> </div> ) } diff --git a/components/frontend/src/AppUI.test.js b/components/frontend/src/AppUI.test.js index 2084e29dd4..861dd0dba8 100644 --- a/components/frontend/src/AppUI.test.js +++ b/components/frontend/src/AppUI.test.js @@ -59,6 +59,6 @@ async function renderAppUI() { it("resets all settings", async () => { history.push("?date_interval=2") await act(async () => await renderAppUI()) - fireEvent.click(screen.getByLabelText("Reset reports overview settings")) + fireEvent.click(screen.getByText("Reset settings")) expect(history.location.search).toBe("") }) diff --git a/components/frontend/src/header_footer/CollapseButton.js b/components/frontend/src/header_footer/CollapseButton.js deleted file mode 100644 index d4b6c48cfe..0000000000 --- a/components/frontend/src/header_footer/CollapseButton.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Button, Icon } from "semantic-ui-react" - -import { Popup } from "../semantic_ui_react_wrappers" -import { stringsURLSearchQueryPropType } from "../sharedPropTypes" - -export function CollapseButton({ expandedItems }) { - const label = "Collapse all headers and metrics" - return ( - <Popup - on={["hover", "focus"]} - trigger={ - <span // We need a span here to prevent the popup from becoming disabled whenever the button is disabled - > - <Button - aria-label={label} - basic - disabled={expandedItems.equals([])} - icon - onClick={() => expandedItems.reset()} - inverted - > - <Icon name="angle double up" /> Collapse all - </Button> - </span> - } - content={label} - /> - ) -} -CollapseButton.propTypes = { - expandedItems: stringsURLSearchQueryPropType, -} diff --git a/components/frontend/src/header_footer/Menubar.css b/components/frontend/src/header_footer/Menubar.css index 18f8aec0ff..299ea3fa50 100644 --- a/components/frontend/src/header_footer/Menubar.css +++ b/components/frontend/src/header_footer/Menubar.css @@ -3,24 +3,3 @@ display: none !important; } } - -.menubar { - opacity: 0.98; -} - -.menu .center { - display: grid; - place-content: center; -} - -.panel { - background-color: black; - border: 0px; - left: 0px; - margin: 0px; - opacity: 0.98; - position: fixed; - top: 64px; - width: 100%; - z-index: 4; -} diff --git a/components/frontend/src/header_footer/Menubar.js b/components/frontend/src/header_footer/Menubar.js index 854a6eb4f8..6012bb7de7 100644 --- a/components/frontend/src/header_footer/Menubar.js +++ b/components/frontend/src/header_footer/Menubar.js @@ -1,18 +1,20 @@ import "./Menubar.css" +import { AppBar, Button, Drawer, Stack, Toolbar } from "@mui/material" import { element, func, string } from "prop-types" import { useEffect, useState } from "react" -import FocusLock from "react-focus-lock" -import { Button, Dropdown, Icon, Image, Menu, Portal } from "semantic-ui-react" +import { Dropdown, Icon } from "semantic-ui-react" import { login, logout } from "../api/auth" -import { Form, Message, Modal, Popup } from "../semantic_ui_react_wrappers" +import { Form, Message, Modal } from "../semantic_ui_react_wrappers" import { optionalDatePropType, settingsPropType, uiModePropType } from "../sharedPropTypes" import { Avatar } from "../widgets/Avatar" -import { DatePicker } from "../widgets/DatePicker" -import { CollapseButton } from "./CollapseButton" -import { DownloadAsPDFButton } from "./DownloadAsPDFButton" -import { ResetSettingsButton } from "./ResetSettingsButton" +import { CollapseButton } from "./buttons/CollapseButton" +import { DatePickerButton } from "./buttons/DatePickerButton" +import { DownloadAsPDFButton } from "./buttons/DownloadAsPDFButton" +import { HomeButton } from "./buttons/HomeButton" +import { ResetSettingsButton } from "./buttons/ResetSettingsButton" +import { SettingsButton } from "./buttons/SettingsButton" import { UIModeMenu } from "./UIModeMenu" function Login({ set_user }) { @@ -49,7 +51,7 @@ function Login({ set_user }) { return ( <Modal trigger={ - <Button secondary> + <Button color="inherit"> <Icon name="user" /> Login </Button> @@ -144,113 +146,42 @@ export function Menubar({ const atReportsOverview = report_uuid === "" return ( <> - <Menu fluid className="menubar" inverted fixed="top"> - <Menu.Menu position="left"> - <Popup - content="Go to reports overview" - disabled={atReportsOverview} - trigger={ - <div - onBeforeInput={(event) => { - event.preventDefault() - setSettingsPanelVisible(false) - openReportsOverview() - }} - tabIndex={atReportsOverview ? -1 : 0} - > - <Menu.Item - header - onClick={ - atReportsOverview - ? null - : () => { - setSettingsPanelVisible(false) - openReportsOverview() - } - } - > - <Image size="mini" src="/favicon.ico" alt="Go home" /> - <span style={{ paddingLeft: "6mm", fontSize: "2em" }}>Quality-time</span> - </Menu.Item> - </div> - } - /> - <FocusLock group="settingsPanel" disabled={!settingsPanelVisible} className="center"> - <div - onBeforeInput={(event) => { - event.preventDefault() - setSettingsPanelVisible(!settingsPanelVisible) - }} - > - <Menu.Item - onClick={(event) => { - event.stopPropagation() - setSettingsPanelVisible(!settingsPanelVisible) - }} - tabIndex={0} - > - <Icon size="large" name={`caret ${settingsPanelVisible ? "down" : "right"}`} /> - Settings - </Menu.Item> - </div> - </FocusLock> - <Menu.Item> + <AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}> + <Toolbar> + <Stack direction="row" spacing={2} sx={{ flexGrow: 1 }}> + <HomeButton + atReportsOverview={atReportsOverview} + openReportsOverview={openReportsOverview} + setSettingsPanelVisible={setSettingsPanelVisible} + /> + <SettingsButton + setSettingsPanelVisible={setSettingsPanelVisible} + settingsPanelVisible={settingsPanelVisible} + /> <ResetSettingsButton atReportsOverview={atReportsOverview} handleDateChange={handleDateChange} reportDate={report_date} settings={settings} /> - </Menu.Item> - <Menu.Item> <CollapseButton expandedItems={settings.expandedItems} /> - </Menu.Item> - <Menu.Item> <DownloadAsPDFButton report_uuid={report_uuid} /> - </Menu.Item> - </Menu.Menu> - <Menu.Menu position="right"> - <Popup - content="Show the report as it was on the selected date" - position="left center" - trigger={ - <Menu.Item> - <Form> - <DatePicker - isClearable={true} - maxDate={new Date()} - onChange={onDate} - selected={report_date} - /> - </Form> - </Menu.Item> - } - /> - <Menu.Item> + </Stack> + <Stack direction="row" spacing={2}> + <DatePickerButton onChange={(date) => onDate(date ? date.$d : null)} reportDate={report_date} /> <UIModeMenu setUIMode={setUIMode} uiMode={uiMode} /> - </Menu.Item> - <Menu.Item> {user !== null ? ( <Logout email={email} user={user} set_user={set_user} /> ) : ( <Login set_user={set_user} /> )} - </Menu.Item> - </Menu.Menu> - </Menu> - <Portal - closeOnTriggerClick={true} - open={settingsPanelVisible} - onClose={(event) => { - event.stopPropagation() - setSettingsPanelVisible(false) - }} - unmountOnHide - > - <div className="panel"> - <FocusLock group="settingsPanel">{panel}</FocusLock> - </div> - </Portal> + </Stack> + </Toolbar> + </AppBar> + <Drawer anchor="top" open={settingsPanelVisible} onClose={() => setSettingsPanelVisible(false)}> + <Toolbar /* Add an empty toolbar to the drawer so the panel is not partly hidden by the appbar. */ /> + {panel} + </Drawer> </> ) } diff --git a/components/frontend/src/header_footer/Menubar.test.js b/components/frontend/src/header_footer/Menubar.test.js index afdd1328c5..7a0090744b 100644 --- a/components/frontend/src/header_footer/Menubar.test.js +++ b/components/frontend/src/header_footer/Menubar.test.js @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen } from "@testing-library/react" +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import history from "history/browser" @@ -99,7 +99,7 @@ it("does not go to home page if on reports overview", async () => { const openReportsOverview = jest.fn() renderMenubar({ report_uuid: "", openReportsOverview: openReportsOverview }) act(() => { - fireEvent.click(screen.getByAltText(/Go home/)) + fireEvent.click(screen.getByAltText(/Go to reports overview/)) }) expect(openReportsOverview).not.toHaveBeenCalled() }) @@ -108,7 +108,7 @@ it("goes to home page if on report", async () => { const openReportsOverview = jest.fn() renderMenubar({ openReportsOverview: openReportsOverview }) await act(async () => { - fireEvent.click(screen.getByAltText(/Go home/)) + fireEvent.click(screen.getByAltText(/Go to reports overview/)) }) expect(openReportsOverview).toHaveBeenCalled() }) @@ -116,7 +116,7 @@ it("goes to home page if on report", async () => { it("goes to home page on keypress", async () => { const openReportsOverview = jest.fn() renderMenubar({ openReportsOverview: openReportsOverview }) - await userEvent.type(screen.getByAltText(/Go home/), "{Enter}") + await userEvent.type(screen.getByAltText(/Go to reports overview/), "{Enter}") expect(openReportsOverview).toHaveBeenCalled() }) @@ -132,12 +132,12 @@ it("shows the view panel on space", async () => { expect(screen.getAllByText(/Hello/).length).toBe(1) }) -it("hides the view panel on click", () => { +it("hides the view panel on click", async () => { renderMenubar({ panel: <div>Hello</div> }) fireEvent.click(screen.getByText(/Settings/)) expect(screen.getAllByText(/Hello/).length).toBe(1) fireEvent.click(screen.getByText(/Settings/)) - expect(screen.queryAllByText(/Hello/).length).toBe(0) + await waitFor(() => expect(screen.queryAllByText(/Hello/).length).toBe(0)) }) it("hides the view panel on escape", async () => { @@ -145,5 +145,5 @@ it("hides the view panel on escape", async () => { fireEvent.click(screen.getByText(/Settings/)) expect(screen.getAllByText(/Hello/).length).toBe(1) await userEvent.keyboard("{Escape}") - expect(screen.queryAllByText(/Hello/).length).toBe(0) + await waitFor(() => expect(screen.queryAllByText(/Hello/).length).toBe(0)) }) diff --git a/components/frontend/src/header_footer/ResetSettingsButton.js b/components/frontend/src/header_footer/ResetSettingsButton.js deleted file mode 100644 index e4982f9f6e..0000000000 --- a/components/frontend/src/header_footer/ResetSettingsButton.js +++ /dev/null @@ -1,43 +0,0 @@ -import { bool, func } from "prop-types" -import { Button, Icon } from "semantic-ui-react" - -import { Popup } from "../semantic_ui_react_wrappers" -import { optionalDatePropType, settingsPropType } from "../sharedPropTypes" - -export function ResetSettingsButton({ atReportsOverview, handleDateChange, reportDate, settings }) { - const label = `Reset ${atReportsOverview ? "reports overview" : "this report's"} settings` - return ( - <Popup - on={["hover", "focus"]} - trigger={ - <span // We need a span here to prevent the popup from becoming disabled whenever the button is disabled - > - <Button - aria-label={label} - basic - disabled={settings.allDefault() && reportDate === null} - icon - onClick={() => { - handleDateChange(null) - settings.reset() - }} - inverted - > - <Icon.Group> - <Icon name="undo alternate" /> - <Icon name="setting" size="tiny" /> - </Icon.Group> - Reset settings - </Button> - </span> - } - content={label} - /> - ) -} -ResetSettingsButton.propTypes = { - atReportsOverview: bool, - handleDateChange: func, - reportDate: optionalDatePropType, - settings: settingsPropType, -} diff --git a/components/frontend/src/header_footer/SettingsPanel.test.js b/components/frontend/src/header_footer/SettingsPanel.test.js index 2a02fe6ba0..2cb0b4e16c 100644 --- a/components/frontend/src/header_footer/SettingsPanel.test.js +++ b/components/frontend/src/header_footer/SettingsPanel.test.js @@ -127,14 +127,6 @@ it("sorts a column by keypress", async () => { expect(handleSort).toHaveBeenCalledWith("comment") }) -it("ignores a keypress if the menu item is disabled", async () => { - history.push("?hidden_columns=comment") - const handleSort = jest.fn() - renderSettingsPanel({ handleSort: handleSort }) - await userEvent.type(screen.getAllByText(/Comment/)[1], " ") - expect(handleSort).not.toHaveBeenCalledWith("comment") -}) - it("sets the number of dates", async () => { history.push("?nr_dates=2") renderSettingsPanel() diff --git a/components/frontend/src/header_footer/UIModeMenu.js b/components/frontend/src/header_footer/UIModeMenu.js index 960889f80b..2996814565 100644 --- a/components/frontend/src/header_footer/UIModeMenu.js +++ b/components/frontend/src/header_footer/UIModeMenu.js @@ -1,25 +1,59 @@ +import Brightness4Icon from "@mui/icons-material/Brightness4" +import { IconButton, Menu, MenuItem, Tooltip } from "@mui/material" import { func } from "prop-types" -import { Dropdown } from "semantic-ui-react" +import { useState } from "react" import { uiModePropType } from "../sharedPropTypes" -import { IconCombi } from "../widgets/IconCombi" export function UIModeMenu({ setUIMode, uiMode }) { + const [anchorEl, setAnchorEl] = useState() + const handleMenu = (event) => setAnchorEl(event.currentTarget) + const handleClose = () => setAnchorEl(null) return ( - <Dropdown icon={<IconCombi iconBottomRight="moon" iconTopLeft="sun" label="Dark/light mode" />}> - <Dropdown.Menu> - <Dropdown.Header>Dark/light mode</Dropdown.Header> - <Dropdown.Item active={uiMode === "system"} onClick={() => setUIMode("system")}> - Follow OS setting - </Dropdown.Item> - <Dropdown.Item active={uiMode === "dark"} onClick={() => setUIMode("dark")}> - Dark mode - </Dropdown.Item> - <Dropdown.Item active={uiMode === "light"} onClick={() => setUIMode("light")}> - Light mode - </Dropdown.Item> - </Dropdown.Menu> - </Dropdown> + <Tooltip placement="left" title="Change dark/light mode"> + <span> + <IconButton + aria-label="Dark/light mode" + aria-controls="dark-light-menu" + aria-haspopup="true" + color="inherit" + onClick={handleMenu} + size="large" + sx={{ height: "100%" }} + > + <Brightness4Icon /> + </IconButton> + <Menu id="dark-light-menu" anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}> + <MenuItem + onClick={() => { + handleClose() + setUIMode("system") + }} + selected={uiMode === "system"} + > + Follow OS setting + </MenuItem> + <MenuItem + onClick={() => { + handleClose() + setUIMode("dark") + }} + selected={uiMode === "dark"} + > + Dark mode + </MenuItem> + <MenuItem + onClick={() => { + handleClose() + setUIMode("light") + }} + selected={uiMode === "light"} + > + Light mode + </MenuItem> + </Menu> + </span> + </Tooltip> ) } UIModeMenu.propTypes = { diff --git a/components/frontend/src/header_footer/UIModeMenu.test.js b/components/frontend/src/header_footer/UIModeMenu.test.js index b53829ae82..594ba2a9f2 100644 --- a/components/frontend/src/header_footer/UIModeMenu.test.js +++ b/components/frontend/src/header_footer/UIModeMenu.test.js @@ -6,6 +6,7 @@ import { UIModeMenu } from "./UIModeMenu" it("sets dark mode", () => { const setUIMode = jest.fn() render(<UIModeMenu setUIMode={setUIMode} />) + fireEvent.click(screen.getByLabelText(/Dark\/light mode/)) fireEvent.click(screen.getByText(/Dark mode/)) expect(setUIMode).toHaveBeenCalledWith("dark") }) @@ -13,6 +14,7 @@ it("sets dark mode", () => { it("sets light mode", () => { const setUIMode = jest.fn() render(<UIModeMenu setUIMode={setUIMode} uiMode="dark" />) + fireEvent.click(screen.getByLabelText(/Dark\/light mode/)) fireEvent.click(screen.getByText(/Light mode/)) expect(setUIMode).toHaveBeenCalledWith("light") }) @@ -20,6 +22,7 @@ it("sets light mode", () => { it("sets follows os mode", () => { const setUIMode = jest.fn() render(<UIModeMenu setUIMode={setUIMode} uiMode="dark" />) + fireEvent.click(screen.getByLabelText(/Dark\/light mode/)) fireEvent.click(screen.getByText(/Follow OS/)) expect(setUIMode).toHaveBeenCalledWith("system") }) @@ -27,6 +30,7 @@ it("sets follows os mode", () => { it("sets dark mode on keypress", async () => { const setUIMode = jest.fn() render(<UIModeMenu setUIMode={setUIMode} />) + fireEvent.click(screen.getByLabelText(/Dark\/light mode/)) await userEvent.type(screen.getByText(/Dark mode/), " ") expect(setUIMode).toHaveBeenCalledWith("dark") }) diff --git a/components/frontend/src/header_footer/buttons/CollapseButton.js b/components/frontend/src/header_footer/buttons/CollapseButton.js new file mode 100644 index 0000000000..e3b21eedbf --- /dev/null +++ b/components/frontend/src/header_footer/buttons/CollapseButton.js @@ -0,0 +1,25 @@ +import UnfoldLessIcon from "@mui/icons-material/UnfoldLess" +import { Button, Tooltip } from "@mui/material" + +import { stringsURLSearchQueryPropType } from "../../sharedPropTypes" + +export function CollapseButton({ expandedItems }) { + return ( + <Tooltip title={"Collapse all headers and metrics"}> + <span /* https://mui.com/material-ui/react-tooltip/#disabled-elements */> + <Button + color="inherit" + disabled={expandedItems.equals([])} + onClick={() => expandedItems.reset()} + startIcon={<UnfoldLessIcon />} + sx={{ height: "100%" }} + > + Collapse all + </Button> + </span> + </Tooltip> + ) +} +CollapseButton.propTypes = { + expandedItems: stringsURLSearchQueryPropType, +} diff --git a/components/frontend/src/header_footer/CollapseButton.test.js b/components/frontend/src/header_footer/buttons/CollapseButton.test.js similarity index 87% rename from components/frontend/src/header_footer/CollapseButton.test.js rename to components/frontend/src/header_footer/buttons/CollapseButton.test.js index 0d9ee2bc54..2bd044e4b2 100644 --- a/components/frontend/src/header_footer/CollapseButton.test.js +++ b/components/frontend/src/header_footer/buttons/CollapseButton.test.js @@ -1,8 +1,8 @@ import { fireEvent, render, renderHook, screen } from "@testing-library/react" import history from "history/browser" -import { createTestableSettings } from "../__fixtures__/fixtures" -import { useExpandedItemsSearchQuery } from "../app_ui_settings" +import { createTestableSettings } from "../../__fixtures__/fixtures" +import { useExpandedItemsSearchQuery } from "../../app_ui_settings" import { CollapseButton } from "./CollapseButton" beforeEach(() => { @@ -19,7 +19,7 @@ it("resets the expanded items", () => { const expandedItems = renderHook(() => useExpandedItemsSearchQuery()) expect(expandedItems.result.current.value).toStrictEqual(["tab"]) renderCollapseButton({ expandedItems: expandedItems.result.current }) - fireEvent.click(screen.getByRole("button", { name: "Collapse all headers and metrics" })) + fireEvent.click(screen.getByRole("button", { name: "Collapse all" })) expandedItems.rerender() expect(expandedItems.result.current.value).toStrictEqual([]) }) @@ -28,7 +28,7 @@ it("doesn't change the expanded items if there are none", () => { const expandedItems = renderHook(() => useExpandedItemsSearchQuery()) expect(expandedItems.result.current.value).toStrictEqual([]) renderCollapseButton({ expandedItems: expandedItems.result.current }) - fireEvent.click(screen.getByRole("button", { name: "Collapse all headers and metrics" })) + fireEvent.click(screen.getByRole("button", { name: "Collapse all" })) expandedItems.rerender() expect(expandedItems.result.current.value).toStrictEqual([]) }) diff --git a/components/frontend/src/header_footer/buttons/DatePickerButton.js b/components/frontend/src/header_footer/buttons/DatePickerButton.js new file mode 100644 index 0000000000..f523ff988e --- /dev/null +++ b/components/frontend/src/header_footer/buttons/DatePickerButton.js @@ -0,0 +1,48 @@ +import EventIcon from "@mui/icons-material/Event" +import { Button, Menu, Tooltip } from "@mui/material" +import { StaticDatePicker } from "@mui/x-date-pickers/StaticDatePicker" +import { func } from "prop-types" +import { useState } from "react" + +import { datePropType } from "../../sharedPropTypes" + +export function DatePickerButton({ onChange, reportDate }) { + const [anchorEl, setAnchorEl] = useState() + const handleMenu = (event) => setAnchorEl(event.currentTarget) + const handleClose = () => setAnchorEl(null) + return ( + <Tooltip placement="left" title="Show the report as it was on the selected date"> + <span> + <Button + aria-label="Report date" + aria-controls="date-picker-button-menu" + color="inherit" + onClick={handleMenu} + startIcon={<EventIcon />} + sx={{ height: "100%" }} + > + {reportDate ? reportDate.toDateString() : "today"} + </Button> + <Menu id="date-picker-button-menu" anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}> + <StaticDatePicker + disableFuture + onChange={(value) => { + handleClose() + onChange(value) + }} + slots={{ toolbar: null }} + slotProps={{ + actionBar: { + actions: ["today"], + }, + }} + /> + </Menu> + </span> + </Tooltip> + ) +} +DatePickerButton.propTypes = { + onChange: func, + reportDate: datePropType, +} diff --git a/components/frontend/src/header_footer/DownloadAsPDFButton.js b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js similarity index 61% rename from components/frontend/src/header_footer/DownloadAsPDFButton.js rename to components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js index 565e9f7181..a096386531 100644 --- a/components/frontend/src/header_footer/DownloadAsPDFButton.js +++ b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.js @@ -1,11 +1,12 @@ +import PictureAsPdf from "@mui/icons-material/PictureAsPdf" +import { LoadingButton } from "@mui/lab" +import { Tooltip } from "@mui/material" import { string } from "prop-types" import { useState } from "react" -import { Icon } from "semantic-ui-react" -import { get_report_pdf } from "../api/report" -import { registeredURLSearchParams } from "../hooks/url_search_query" -import { Button, Popup } from "../semantic_ui_react_wrappers" -import { showMessage } from "../widgets/toast" +import { get_report_pdf } from "../../api/report" +import { registeredURLSearchParams } from "../../hooks/url_search_query" +import { showMessage } from "../../widgets/toast" function download_pdf(report_uuid, query_string, callback) { const reportId = report_uuid ? `report-${report_uuid}` : "reports-overview" @@ -41,29 +42,24 @@ export function DownloadAsPDFButton({ report_uuid }) { const itemType = report_uuid ? "report" : "reports overview" const label = `Download ${itemType} as PDF` return ( - <Popup - on={["hover", "focus"]} - trigger={ - <Button - aria-label={label} - basic - icon - loading={loading} - onClick={() => { - if (!loading) { - setLoading(true) - download_pdf(report_uuid, `?${query.toString()}`, () => { - setLoading(false) - }) - } - }} - inverted - > - <Icon name="file pdf" /> Download as PDF - </Button> - } - content={`Generate a PDF version of the ${itemType} as currently displayed. This may take some time.`} - /> + <Tooltip title={`Generate a PDF version of the ${itemType} as currently displayed. This may take some time.`}> + <LoadingButton + aria-label={label} + color="inherit" + loading={loading} + onClick={() => { + if (!loading) { + setLoading(true) + download_pdf(report_uuid, `?${query.toString()}`, () => { + setLoading(false) + }) + } + }} + startIcon={<PictureAsPdf />} + > + Download as PDF + </LoadingButton> + </Tooltip> ) } DownloadAsPDFButton.propTypes = { diff --git a/components/frontend/src/header_footer/DownloadAsPDFButton.test.js b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js similarity index 78% rename from components/frontend/src/header_footer/DownloadAsPDFButton.test.js rename to components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js index d8b8abb8e0..712b4e577f 100644 --- a/components/frontend/src/header_footer/DownloadAsPDFButton.test.js +++ b/components/frontend/src/header_footer/buttons/DownloadAsPDFButton.test.js @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from "@testing-library/react" import history from "history/browser" -import * as fetch_server_api from "../api/fetch_server_api" +import * as fetch_server_api from "../../api/fetch_server_api" import { DownloadAsPDFButton } from "./DownloadAsPDFButton" beforeEach(() => { @@ -25,9 +25,9 @@ const test_report = { report_uuid: "report_uuid" } test("DownloadAsPDFButton indicates loading on click", async () => { render(<DownloadAsPDFButton report={test_report} report_uuid="report_uuid" />) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download as PDF/)) }) - expect(screen.getByLabelText(/Download/).className).toContain("loading") + expect(screen.getByLabelText(/Download as PDF/).className).toContain("MuiCircularProgress-indeterminate") expect(fetch_server_api.fetch_server_api).toHaveBeenCalledWith( "get", "report/report_uuid/pdf?report_url=http%3A%2F%2Flocalhost%2F", @@ -40,7 +40,7 @@ test("DownloadAsPDFButton ignores unregistered query parameters", async () => { history.push("?unregister_key=value&nr_dates=4") render(<DownloadAsPDFButton report={test_report} report_uuid="report_uuid" />) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download/)) }) expect(fetch_server_api.fetch_server_api).toHaveBeenCalledWith( "get", @@ -53,12 +53,12 @@ test("DownloadAsPDFButton ignores unregistered query parameters", async () => { test("DownloadAsPDFButton ignores a second click", async () => { render(<DownloadAsPDFButton report={test_report} />) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download as PDF/)) }) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download as PDF/)) }) - expect(screen.getByLabelText(/Download/).className).toContain("loading") + expect(screen.getByLabelText(/Download as PDF/).className).toContain("MuiCircularProgress-indeterminate") }) test("DownloadAsPDFButton stops loading after returning pdf", async () => { @@ -67,16 +67,16 @@ test("DownloadAsPDFButton stops loading after returning pdf", async () => { window.URL.createObjectURL = jest.fn() render(<DownloadAsPDFButton report={test_report} />) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download as PDF/)) }) - expect(screen.getByLabelText(/Download/).className).not.toContain("loading") + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") }) test("DownloadAsPDFButton stops loading after receiving error", async () => { fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: false }) render(<DownloadAsPDFButton report={test_report} />) await act(async () => { - fireEvent.click(screen.getByLabelText(/Download/)) + fireEvent.click(screen.getByText(/Download as PDF/)) }) - expect(screen.getByLabelText(/Download/).className).not.toContain("loading") + expect(screen.getByLabelText(/Download/).className).not.toContain("MuiCircularProgress-indeterminate") }) diff --git a/components/frontend/src/header_footer/buttons/HomeButton.js b/components/frontend/src/header_footer/buttons/HomeButton.js new file mode 100644 index 0000000000..d7de1fa8a3 --- /dev/null +++ b/components/frontend/src/header_footer/buttons/HomeButton.js @@ -0,0 +1,29 @@ +import { Button, Tooltip, Typography } from "@mui/material" +import { bool, func } from "prop-types" + +export function HomeButton({ atReportsOverview, openReportsOverview, setSettingsPanelVisible }) { + const label = "Go to reports overview" + return ( + <Tooltip title={label}> + <span /* https://mui.com/material-ui/react-tooltip/#disabled-elements */> + <Button + color="inherit" + disabled={atReportsOverview} + onClick={() => { + setSettingsPanelVisible(false) + openReportsOverview() + }} + startIcon={<img height="28px" width="28px" src="/favicon.ico" alt={label} />} + sx={{ textTransform: "none" }} + > + <Typography variant="h4">Quality-time</Typography> + </Button> + </span> + </Tooltip> + ) +} +HomeButton.propTypes = { + atReportsOverview: bool, + openReportsOverview: func, + setSettingsPanelVisible: func, +} diff --git a/components/frontend/src/header_footer/buttons/ResetSettingsButton.js b/components/frontend/src/header_footer/buttons/ResetSettingsButton.js new file mode 100644 index 0000000000..fad6c8fea9 --- /dev/null +++ b/components/frontend/src/header_footer/buttons/ResetSettingsButton.js @@ -0,0 +1,33 @@ +import SettingsBackupRestoreIcon from "@mui/icons-material/SettingsBackupRestore" +import { Button, Tooltip } from "@mui/material" +import { bool, func } from "prop-types" + +import { optionalDatePropType, settingsPropType } from "../../sharedPropTypes" + +export function ResetSettingsButton({ atReportsOverview, handleDateChange, reportDate, settings }) { + const label = `Reset ${atReportsOverview ? "reports overview" : "this report's"} settings` + return ( + <Tooltip title={label}> + <span /* https://mui.com/material-ui/react-tooltip/#disabled-elements */> + <Button + color="inherit" + disabled={settings.allDefault() && reportDate === null} + startIcon={<SettingsBackupRestoreIcon />} + onClick={() => { + handleDateChange(null) + settings.reset() + }} + sx={{ height: "100%" }} + > + Reset settings + </Button> + </span> + </Tooltip> + ) +} +ResetSettingsButton.propTypes = { + atReportsOverview: bool, + handleDateChange: func, + reportDate: optionalDatePropType, + settings: settingsPropType, +} diff --git a/components/frontend/src/header_footer/ResetSettingsButton.test.js b/components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js similarity index 88% rename from components/frontend/src/header_footer/ResetSettingsButton.test.js rename to components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js index 25630f0bd9..f1aa984f18 100644 --- a/components/frontend/src/header_footer/ResetSettingsButton.test.js +++ b/components/frontend/src/header_footer/buttons/ResetSettingsButton.test.js @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from "@testing-library/react" import history from "history/browser" -import { createTestableSettings } from "../__fixtures__/fixtures" +import { createTestableSettings } from "../../__fixtures__/fixtures" import { ResetSettingsButton } from "./ResetSettingsButton" beforeEach(() => { @@ -38,7 +38,7 @@ it("resets the settings", async () => { reportDate: new Date("2023-01-01"), settings: settings, }) - fireEvent.click(screen.getAllByLabelText(/Reset reports overview settings/)[0]) + fireEvent.click(screen.getAllByText(/Reset settings/)[0]) expect(history.location.search).toEqual("") expect(handleDateChange).toHaveBeenCalledWith(null) }) @@ -51,6 +51,6 @@ it("does not reset the settings when all have the default value", async () => { handleDateChange: handleDateChange, settings: settings, }) - fireEvent.click(screen.getAllByLabelText(/Reset this report's settings/)[0]) + fireEvent.click(screen.getAllByText(/Reset settings/)[0]) expect(handleDateChange).not.toHaveBeenCalled() }) diff --git a/components/frontend/src/header_footer/buttons/SettingsButton.js b/components/frontend/src/header_footer/buttons/SettingsButton.js new file mode 100644 index 0000000000..0ad445148a --- /dev/null +++ b/components/frontend/src/header_footer/buttons/SettingsButton.js @@ -0,0 +1,20 @@ +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" +import ArrowRightIcon from "@mui/icons-material/ArrowRight" +import { Button } from "@mui/material" +import { bool, func } from "prop-types" + +export function SettingsButton({ settingsPanelVisible, setSettingsPanelVisible }) { + return ( + <Button + color="inherit" + startIcon={settingsPanelVisible ? <ArrowDropDownIcon /> : <ArrowRightIcon />} + onClick={() => setSettingsPanelVisible(!settingsPanelVisible)} + > + Settings + </Button> + ) +} +SettingsButton.propTypes = { + settingsPanelVisible: bool, + setSettingsPanelVisible: func, +} diff --git a/components/frontend/src/header_footer/settings_menu/SettingsMenu.css b/components/frontend/src/header_footer/settings_menu/SettingsMenu.css deleted file mode 100644 index 5a1ef65c1b..0000000000 --- a/components/frontend/src/header_footer/settings_menu/SettingsMenu.css +++ /dev/null @@ -1,4 +0,0 @@ -.ui.horizontal.segments.equal.width > .ui.segment { - flex-grow: 1; - width: 0; -} diff --git a/components/frontend/src/header_footer/settings_menu/SettingsMenu.js b/components/frontend/src/header_footer/settings_menu/SettingsMenu.js index 87e6fa4d0c..f690e46f67 100644 --- a/components/frontend/src/header_footer/settings_menu/SettingsMenu.js +++ b/components/frontend/src/header_footer/settings_menu/SettingsMenu.js @@ -1,18 +1,13 @@ -import "./SettingsMenu.css" - +import { MenuItem, MenuList, Stack, Tooltip, Typography } from "@mui/material" import { bool, func, number, oneOfType, string } from "prop-types" -import { Header, Menu, Segment } from "semantic-ui-react" -import { Popup } from "../../semantic_ui_react_wrappers" import { childrenPropType, popupContentPropType } from "../../sharedPropTypes" -const activeColor = "grey" - export function SettingsMenuGroup({ children }) { return ( - <Segment.Group horizontal className="equal width" style={{ margin: "0px", border: "0px" }}> + <Stack direction="row" sx={{ justifyContent: "space-between", padding: "20px" }}> {children} - </Segment.Group> + </Stack> ) } SettingsMenuGroup.propTypes = { @@ -20,12 +15,11 @@ SettingsMenuGroup.propTypes = { } export function SettingsMenu({ children, title }) { - const menuProps = { compact: true, vertical: true, inverted: true, secondary: true } return ( - <Segment inverted color="black"> - <Header size="small">{title}</Header> - <Menu {...menuProps}>{children}</Menu> - </Segment> + <Stack> + <Typography variant="h6">{title}</Typography> + <MenuList>{children}</MenuList> + </Stack> ) } SettingsMenu.propTypes = { @@ -36,8 +30,6 @@ SettingsMenu.propTypes = { export function SettingsMenuItem({ active, children, disabled, disabledHelp, help, onClick, onClickData }) { // A menu item that can can show help when disabled so users can see why the menu item is disabled const props = { - active: active, - color: activeColor, disabled: disabled, onBeforeInput: (event) => { event.preventDefault() @@ -49,25 +41,21 @@ export function SettingsMenuItem({ active, children, disabled, disabledHelp, hel event.preventDefault() onClick(onClickData) }, + selected: active, tabIndex: 0, } if (help || (disabledHelp && disabled)) { - props["style"] = { marginLeft: 0, marginRight: 0, marginBottom: 5 } // Compensate for the span return ( - <Popup - content={disabledHelp || help} - inverted - position="left center" - // We need a span here to prevent the popup from becoming disabled when the menu item is disabled: - trigger={ - <span> - <Menu.Item {...props}>{children}</Menu.Item> - </span> - } - /> + <Tooltip placement="left" title={disabledHelp || help}> + <span + // We need a span here to prevent the popup from becoming disabled when the menu item is disabled + > + <MenuItem {...props}>{children}</MenuItem> + </span> + </Tooltip> ) } - return <Menu.Item {...props}>{children}</Menu.Item> + return <MenuItem {...props}>{children}</MenuItem> } SettingsMenuItem.propTypes = { active: bool, diff --git a/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js b/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js index 4d8077d84f..df378d6cc9 100644 --- a/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js +++ b/components/frontend/src/header_footer/settings_menu/SortColumnMenu.js @@ -1,6 +1,7 @@ +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" +import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp" import { bool, func, string } from "prop-types" -import { Icon } from "../../semantic_ui_react_wrappers" import { popupContentPropType, settingsPropType, @@ -85,11 +86,7 @@ SortColumnMenu.propTypes = { function SortColumnMenuItem({ column, disabled, sortColumn, sortDirection, handleSort, help }) { let sortIndicator = null if (sortColumn.equals(column) && sortDirection.value) { - // We use a triangle because the sort down and up icons are not at the same height - const iconDirection = sortDirection.equals("ascending") ? "up" : "down" - sortIndicator = ( - <Icon disabled={disabled} name={`triangle ${iconDirection}`} aria-label={`sorted ${sortDirection.value}`} /> - ) + sortIndicator = sortDirection.equals("ascending") ? <ArrowDropDownIcon /> : <ArrowDropUpIcon /> } return ( <SettingsMenuItem @@ -99,7 +96,7 @@ function SortColumnMenuItem({ column, disabled, sortColumn, sortDirection, handl onClick={handleSort} onClickData={column} > - {capitalize(column === "name" ? "metric" : column).replaceAll("_", " ")} <span>{sortIndicator}</span> + {capitalize(column === "name" ? "metric" : column).replaceAll("_", " ")} {sortIndicator} </SettingsMenuItem> ) } diff --git a/components/frontend/src/widgets/IconCombi.js b/components/frontend/src/widgets/IconCombi.js deleted file mode 100644 index 3ac1972780..0000000000 --- a/components/frontend/src/widgets/IconCombi.js +++ /dev/null @@ -1,17 +0,0 @@ -import { string } from "prop-types" -import { Icon } from "semantic-ui-react" - -export function IconCombi({ iconBottomRight, iconTopLeft, label }) { - const style = { textShadow: "0px 0px" } - return ( - <Icon.Group aria-label={label} size="big"> - <Icon corner="top left" name={iconTopLeft} style={style} /> - <Icon corner="bottom right" name={iconBottomRight} style={style} /> - </Icon.Group> - ) -} -IconCombi.propTypes = { - iconBottomRight: string, - iconTopLeft: string, - label: string, -}