From 39623ce4bf6bb1eecf783bfb31f5329780486216 Mon Sep 17 00:00:00 2001 From: Christophe Date: Thu, 14 Sep 2023 00:36:06 +0200 Subject: [PATCH] refactor: modernize codebase style --- .eslintrc.json | 106 +- .gitignore | 5 +- .yarnrc.yml | 3 + components/Footer.tsx | 37 + components/Identifiable.ts | 5 + ...on.test.tsx => InlineCloseButton.test.tsx} | 3 +- components/InlineCloseButton.tsx | 26 + ...ggestions.tsx => InputWithSuggestions.tsx} | 45 +- components/JsonHighlight.tsx | 24 + components/JsonPreviewModal.tsx | 41 + .../{jsonSelection.tsx => JsonSelection.tsx} | 27 +- components/LoginCheck.tsx | 35 + components/{navHeader.tsx => NavHeader.tsx} | 15 +- ...pagination.test.tsx => Paginator.test.tsx} | 2 +- components/Paginator.tsx | 101 + ...ientWrapper.tsx => QueryClientWrapper.tsx} | 10 +- ...ck.test.tsx => RegistrationCheck.test.tsx} | 5 +- components/RegistrationCheck.tsx | 30 + ...esultEditModal.tsx => ResultEditModal.tsx} | 38 +- ...tReportModal.tsx => ResultReportModal.tsx} | 38 +- ...t.tsx => ResultsPerPageSelection.test.tsx} | 2 +- ...ection.tsx => ResultsPerPageSelection.tsx} | 27 +- .../newTag.tsx => TagSelector/NewTag.tsx} | 29 +- components/TagSelector/PlaceholderTag.tsx | 15 + components/TagSelector/SelectedTag.tsx | 23 + components/TagSelector/UnselectedTag.tsx | 17 + .../index.test.tsx | 13 +- .../{tagSelector => TagSelector}/index.tsx | 48 +- components/TermsOfService.tsx | 105 + ....test.tsx => TermsOfServiceCheck.test.tsx} | 2 +- ...rviceCheck.tsx => TermsOfServiceCheck.tsx} | 35 +- .../{userContext.tsx => UserContext.tsx} | 10 +- ...textWrapper.tsx => UserContextWrapper.tsx} | 17 +- components/footer.tsx | 38 - components/forms/BenchmarkSubmitForm.tsx | 16 +- components/forms/ErrorMessage.tsx | 8 +- components/forms/FlavorSubmitForm.tsx | 22 +- components/forms/ResultSubmitForm.tsx | 30 +- components/forms/SiteSubmitForm.tsx | 18 +- components/identifiable.tsx | 3 - components/inlineCloseButton.tsx | 20 - components/jsonHighlight.test.tsx | 8 +- components/jsonHighlight.tsx | 21 - components/jsonPreviewModal.tsx | 34 - components/loadingOverlay.tsx | 40 +- components/loginCheck.tsx | 31 - components/pagination.tsx | 100 - components/registrationCheck.tsx | 32 - .../BenchmarkInfo.tsx} | 15 +- components/report-view/ClaimInteraction.tsx | 31 + components/report-view/ClaimView.tsx | 42 + .../FlavorInfo.tsx} | 19 +- .../ResultInfo.tsx} | 15 +- .../siteInfo.tsx => report-view/SiteInfo.tsx} | 13 +- components/report-view/SubmitInteraction.tsx | 71 + components/report-view/SubmitView.tsx | 41 + .../{reportView => report-view}/claimInfo.tsx | 25 +- components/reportView/claimInteraction.tsx | 34 - components/reportView/claimView.tsx | 35 - components/reportView/submitInteraction.tsx | 82 - components/reportView/submitView.tsx | 47 - .../ColumnSelectModal.tsx} | 47 +- components/result-search/DiagramCard.tsx | 66 + .../filter.tsx => result-search/Filter.ts} | 4 +- .../FilterEdit.tsx} | 33 +- .../ResultCallbacks.ts} | 8 +- components/result-search/ResultTable.tsx | 219 + .../columns/ActionColumn.tsx | 24 +- .../columns/BenchmarkColumn.tsx | 8 +- .../columns/CheckboxColumn.tsx | 12 +- .../columns/CustomColumn.tsx | 14 +- .../columns/ExecutionDateColumn.module.scss | 5 + .../columns/ExecutionDateColumn.tsx | 20 +- .../columns/SiteColumn.tsx | 8 +- .../columns/SiteFlavorColumn.tsx | 8 +- .../columns/TagsColumn.tsx | 8 +- .../columns/index.ts | 0 .../diagrams/chartjs.tsx | 40 +- .../diagrams/echarts.tsx | 231 +- .../diagrams/helpers.tsx | 68 +- .../diagrams/index.ts | 0 .../jsonKeyHelpers.ts} | 16 +- .../jsonSchema.ts} | 28 +- .../sorting.ts | 0 components/resultSearch/diagramCard.tsx | 68 - components/resultSearch/resultTable.tsx | 203 - .../searchSelectors/BenchmarkSearchSelect.tsx | 59 + .../searchSelectors/FlavorSearchSelect.tsx | 67 + .../{searchForm.tsx => SearchForm.tsx} | 12 +- .../searchSelectors/SiteSearchPopover.tsx | 54 + .../searchSelectors/{table.tsx => Table.tsx} | 29 +- .../searchSelectors/benchmarkSearchSelect.tsx | 66 - .../searchSelectors/flavorSearchSelect.tsx | 75 - components/searchSelectors/index.tsx | 72 +- .../searchSelectors/siteSearchPopover.tsx | 59 - .../FlavorEditor.tsx} | 53 +- .../FlavorList.tsx} | 30 +- .../NewFlavor.tsx} | 15 +- .../SiteEditor.tsx} | 36 +- components/site-editor/SiteSelect.tsx | 20 + .../siteFields.tsx | 50 +- components/siteEditor/siteSelect.tsx | 19 - ...Modal.tsx => BenchmarkSubmissionModal.tsx} | 20 +- ...ionModal.tsx => FlavorSubmissionModal.tsx} | 20 +- .../submissionModals/siteSubmissionModal.tsx | 14 +- components/tagSelector/placeholderTag.tsx | 15 - components/tagSelector/selectedTag.tsx | 19 - components/tagSelector/unselectedTag.tsx | 15 - components/termsOfService.tsx | 113 - components/utility.test.tsx | 2 +- components/utility.tsx | 25 +- {utils => lib}/ensureUnreachable.ts | 0 {utils => lib}/getApiRoute.ts | 2 +- {utils => lib}/gitInfo.js | 2 +- {components => lib}/testData.ts | 2 +- {utils => lib}/useApi.ts | 5 +- lib/useUser.ts | 6 + package.json | 17 +- pages/_app.tsx | 62 +- pages/code-guidelines.tsx | 11 +- pages/oidc-redirect.tsx | 23 +- pages/privacy-policy.tsx | 211 +- pages/registration.tsx | 24 +- pages/report-view.tsx | 139 +- pages/search/result.tsx | 224 +- pages/site-editor.tsx | 32 +- pages/submit/benchmark.tsx | 10 +- pages/submit/result.tsx | 10 +- pages/submit/site.tsx | 10 +- pages/terms-of-service.tsx | 29 +- tsconfig.json | 129 +- yarn.lock | 21989 ++++++++++------ 132 files changed, 16020 insertions(+), 10844 deletions(-) create mode 100644 .yarnrc.yml create mode 100644 components/Footer.tsx create mode 100644 components/Identifiable.ts rename components/{inlineCloseButton.test.tsx => InlineCloseButton.test.tsx} (76%) create mode 100644 components/InlineCloseButton.tsx rename components/{inputWithSuggestions.tsx => InputWithSuggestions.tsx} (60%) create mode 100644 components/JsonHighlight.tsx create mode 100644 components/JsonPreviewModal.tsx rename components/{jsonSelection.tsx => JsonSelection.tsx} (79%) create mode 100644 components/LoginCheck.tsx rename components/{navHeader.tsx => NavHeader.tsx} (95%) rename components/{pagination.test.tsx => Paginator.test.tsx} (97%) create mode 100644 components/Paginator.tsx rename components/{queryClientWrapper.tsx => QueryClientWrapper.tsx} (63%) rename components/{registrationCheck.test.tsx => RegistrationCheck.test.tsx} (90%) create mode 100644 components/RegistrationCheck.tsx rename components/{resultEditModal.tsx => ResultEditModal.tsx} (67%) rename components/{resultReportModal.tsx => ResultReportModal.tsx} (57%) rename components/{resultsPerPageSelection.test.tsx => ResultsPerPageSelection.test.tsx} (91%) rename components/{resultsPerPageSelection.tsx => ResultsPerPageSelection.tsx} (71%) rename components/{tagSelector/newTag.tsx => TagSelector/NewTag.tsx} (72%) create mode 100644 components/TagSelector/PlaceholderTag.tsx create mode 100644 components/TagSelector/SelectedTag.tsx create mode 100644 components/TagSelector/UnselectedTag.tsx rename components/{tagSelector => TagSelector}/index.test.tsx (81%) rename components/{tagSelector => TagSelector}/index.tsx (79%) create mode 100644 components/TermsOfService.tsx rename components/{termsOfServiceCheck.test.tsx => TermsOfServiceCheck.test.tsx} (92%) rename components/{termsOfServiceCheck.tsx => TermsOfServiceCheck.tsx} (69%) rename components/{userContext.tsx => UserContext.tsx} (85%) rename components/{userContextWrapper.tsx => UserContextWrapper.tsx} (83%) delete mode 100644 components/footer.tsx delete mode 100644 components/identifiable.tsx delete mode 100644 components/inlineCloseButton.tsx delete mode 100644 components/jsonHighlight.tsx delete mode 100644 components/jsonPreviewModal.tsx delete mode 100644 components/loginCheck.tsx delete mode 100644 components/pagination.tsx delete mode 100644 components/registrationCheck.tsx rename components/{reportView/benchmarkInfo.tsx => report-view/BenchmarkInfo.tsx} (71%) create mode 100644 components/report-view/ClaimInteraction.tsx create mode 100644 components/report-view/ClaimView.tsx rename components/{reportView/flavorInfo.tsx => report-view/FlavorInfo.tsx} (68%) rename components/{reportView/resultInfo.tsx => report-view/ResultInfo.tsx} (79%) rename components/{reportView/siteInfo.tsx => report-view/SiteInfo.tsx} (74%) create mode 100644 components/report-view/SubmitInteraction.tsx create mode 100644 components/report-view/SubmitView.tsx rename components/{reportView => report-view}/claimInfo.tsx (67%) delete mode 100644 components/reportView/claimInteraction.tsx delete mode 100644 components/reportView/claimView.tsx delete mode 100644 components/reportView/submitInteraction.tsx delete mode 100644 components/reportView/submitView.tsx rename components/{resultSearch/columnSelectModal.tsx => result-search/ColumnSelectModal.tsx} (76%) create mode 100644 components/result-search/DiagramCard.tsx rename components/{resultSearch/filter.tsx => result-search/Filter.ts} (76%) rename components/{resultSearch/filterEdit.tsx => result-search/FilterEdit.tsx} (64%) rename components/{resultSearch/resultCallbacks.tsx => result-search/ResultCallbacks.ts} (77%) create mode 100644 components/result-search/ResultTable.tsx rename components/{resultSearch => result-search}/columns/ActionColumn.tsx (84%) rename components/{resultSearch => result-search}/columns/BenchmarkColumn.tsx (63%) rename components/{resultSearch => result-search}/columns/CheckboxColumn.tsx (73%) rename components/{resultSearch => result-search}/columns/CustomColumn.tsx (51%) create mode 100644 components/result-search/columns/ExecutionDateColumn.module.scss rename components/{resultSearch => result-search}/columns/ExecutionDateColumn.tsx (72%) rename components/{resultSearch => result-search}/columns/SiteColumn.tsx (59%) rename components/{resultSearch => result-search}/columns/SiteFlavorColumn.tsx (64%) rename components/{resultSearch => result-search}/columns/TagsColumn.tsx (70%) rename components/{resultSearch => result-search}/columns/index.ts (100%) rename components/{resultSearch => result-search}/diagrams/chartjs.tsx (90%) rename components/{resultSearch => result-search}/diagrams/echarts.tsx (66%) rename components/{resultSearch => result-search}/diagrams/helpers.tsx (57%) rename components/{resultSearch => result-search}/diagrams/index.ts (100%) rename components/{resultSearch/jsonKeyHelpers.tsx => result-search/jsonKeyHelpers.ts} (73%) rename components/{resultSearch/jsonSchema.tsx => result-search/jsonSchema.ts} (61%) rename components/{resultSearch => result-search}/sorting.ts (100%) delete mode 100644 components/resultSearch/diagramCard.tsx delete mode 100644 components/resultSearch/resultTable.tsx create mode 100644 components/searchSelectors/BenchmarkSearchSelect.tsx create mode 100644 components/searchSelectors/FlavorSearchSelect.tsx rename components/searchSelectors/{searchForm.tsx => SearchForm.tsx} (71%) create mode 100644 components/searchSelectors/SiteSearchPopover.tsx rename components/searchSelectors/{table.tsx => Table.tsx} (59%) delete mode 100644 components/searchSelectors/benchmarkSearchSelect.tsx delete mode 100644 components/searchSelectors/flavorSearchSelect.tsx delete mode 100644 components/searchSelectors/siteSearchPopover.tsx rename components/{siteEditor/flavorEditor.tsx => site-editor/FlavorEditor.tsx} (53%) rename components/{siteEditor/flavorList.tsx => site-editor/FlavorList.tsx} (64%) rename components/{siteEditor/newFlavor.tsx => site-editor/NewFlavor.tsx} (55%) rename components/{siteEditor/siteEditor.tsx => site-editor/SiteEditor.tsx} (54%) create mode 100644 components/site-editor/SiteSelect.tsx rename components/{siteEditor => site-editor}/siteFields.tsx (50%) delete mode 100644 components/siteEditor/siteSelect.tsx rename components/submissionModals/{benchmarkSubmissionModal.tsx => BenchmarkSubmissionModal.tsx} (80%) rename components/submissionModals/{flavorSubmissionModal.tsx => FlavorSubmissionModal.tsx} (74%) delete mode 100644 components/tagSelector/placeholderTag.tsx delete mode 100644 components/tagSelector/selectedTag.tsx delete mode 100644 components/tagSelector/unselectedTag.tsx delete mode 100644 components/termsOfService.tsx rename {utils => lib}/ensureUnreachable.ts (100%) rename {utils => lib}/getApiRoute.ts (84%) rename {utils => lib}/gitInfo.js (94%) rename {components => lib}/testData.ts (84%) rename {utils => lib}/useApi.ts (93%) create mode 100644 lib/useUser.ts diff --git a/.eslintrc.json b/.eslintrc.json index 18254681..4e5b57e9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,103 @@ { - "extends": [ "next/core-web-vitals", "prettier" ], - - "rules": { - "semi": "warn" - } + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/strict", + "plugin:sonarjs/recommended", + "plugin:eslint-comments/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:cypress/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "ecmaFeatures": { + "jsx": true, + "modules": true + }, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "sonarjs", "jsx-a11y", "github", "cypress", "import"], + "rules": { + "@next/next/no-img-element": "off", + "@typescript-eslint/ban-ts-comment": "warn", + "curly": "error", + "no-console": [ + "warn", + { + "allow": ["warn", "error"] + } + ], + "quotes": [ + "warn", + "single", + { + "avoidEscape": true + } + ], + "prefer-template": "warn", + "react/jsx-curly-brace-presence": [ + "warn", + { + "props": "never" + } + ], + "react/jsx-boolean-value": ["error", "never"], + "react/jsx-no-useless-fragment": "warn", + "react/jsx-fragments": "warn", + "eqeqeq": ["warn", "smart"], + "no-lonely-if": "warn", + "no-multi-assign": "warn", + "@typescript-eslint/no-shadow": "warn", + "no-useless-return": "warn", + "no-useless-rename": "warn", + "one-var-declaration-per-line": "warn", + "prefer-object-spread": "warn", + "no-unreachable-loop": "warn", + "no-template-curly-in-string": "warn", + "default-case-last": "warn", + "no-array-constructor": "warn", + "no-else-return": "warn", + // disabled: too generic to really provide helpful warnings + //'no-negated-condition': 'warn', + "array-callback-return": "warn", + "@typescript-eslint/consistent-type-definitions": ["warn", "type"], + + // check if useful + //"no-secrets/no-secrets": "warn", + + // based on https://github.com/github/eslint-plugin-github/blob/main/lib/configs/react.js + "github/a11y-no-generic-link-text": "error", + + // TODO: why is this one not working? + //"github/role-supports-aria-props": "error", + // disabled for the one above + //"jsx-a11y/role-supports-aria-props": "off" + + // maybe consider https://github.com/brendanmorrell/eslint-plugin-styled-components-a11y + + "import/first": "error", + "import/no-absolute-path": "error", + // disabled: slow linting + /*"import/no-cycle": "error",*/ + "import/no-duplicates": "error", + + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports", + "fixStyle": "inline-type-imports" + } + ], + + "sonarjs/cognitive-complexity": "off", + // typically only triggered on more complex switch cases where introducing a variable only worsens readability + "sonarjs/no-duplicate-string": "off" + }, + "ignorePatterns": [".eslintrc.js"] } diff --git a/.gitignore b/.gitignore index 42a96417..a3ab3f90 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ yarn-error.log* /.idea/ .npmrc -utils/generatedGitInfo.json +lib/generatedGitInfo.json + +.yarn/ +!.yarn/releases diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..0563839d --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-3.6.3.cjs diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 00000000..d545ac3c --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,37 @@ +import { type FC } from 'react'; +import Link from 'next/link'; +import generatedGitInfo from 'lib/generatedGitInfo.json'; + +/** + * Static footer component rendered at the bottom of every page + */ +const Footer: FC = () => ( + +); + +export default Footer; diff --git a/components/Identifiable.ts b/components/Identifiable.ts new file mode 100644 index 00000000..a7af865b --- /dev/null +++ b/components/Identifiable.ts @@ -0,0 +1,5 @@ +type Identifiable = { + readonly id: string; +}; + +export default Identifiable; diff --git a/components/inlineCloseButton.test.tsx b/components/InlineCloseButton.test.tsx similarity index 76% rename from components/inlineCloseButton.test.tsx rename to components/InlineCloseButton.test.tsx index b57af37d..ef624acc 100644 --- a/components/inlineCloseButton.test.tsx +++ b/components/InlineCloseButton.test.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { InlineCloseButton } from 'components/inlineCloseButton'; +import InlineCloseButton from 'components/InlineCloseButton'; import { act, render, screen } from '@testing-library/react'; test('interaction', () => { diff --git a/components/InlineCloseButton.tsx b/components/InlineCloseButton.tsx new file mode 100644 index 00000000..1c2448c6 --- /dev/null +++ b/components/InlineCloseButton.tsx @@ -0,0 +1,26 @@ +import { type FC } from 'react'; +import { X } from 'react-bootstrap-icons'; +import actionable from 'styles/actionable.module.css'; +import clsx from 'clsx'; + +type InlineCloseButtonProps = { onClose: () => void }; + +// TODO: use unstyled button instead of div + +/** + * Small X button representing a close action that is rendered inline + * + * @param props + * @param props.onClose callback to call when button is pressed + */ +const InlineCloseButton: FC = ({ onClose }) => ( +
onClose()} + className={clsx('d-inline-block', actionable['actionable'])} + role="button" + > + +
+); + +export default InlineCloseButton; diff --git a/components/inputWithSuggestions.tsx b/components/InputWithSuggestions.tsx similarity index 60% rename from components/inputWithSuggestions.tsx rename to components/InputWithSuggestions.tsx index d7e8e54e..8d4797af 100644 --- a/components/inputWithSuggestions.tsx +++ b/components/InputWithSuggestions.tsx @@ -1,21 +1,28 @@ -import React, { ReactElement, ReactNode, useState } from 'react'; +import { type FC, type PropsWithChildren, useState } from 'react'; import { Dropdown, FormControl, InputGroup } from 'react-bootstrap'; -import { Suggestion } from './resultSearch/jsonSchema'; +import { type Suggestion } from 'components/result-search/jsonSchema'; import { truthyOrNoneTag } from './utility'; -export function InputWithSuggestions(props: { +type InputWithSuggestionsProps = { setInput: (input: string) => void; suggestions?: Suggestion[]; placeholder?: string; - children?: ReactNode; value?: string; -}): ReactElement { - const [input, setInput] = useState(props.value); +}; - function updateInput(input: string) { - setInput(input); - props.setInput(input); - } +const InputWithSuggestions: FC> = ({ + setInput, + suggestions, + placeholder, + value, + children, +}) => { + const [input, setLocalInput] = useState(value); + + const updateInput = (newInput: string) => { + setLocalInput(newInput); + setInput(newInput); + }; return ( { - updateInput(e.target.value); - }} + onChange={(e) => updateInput(e.target.value)} /> - {props.suggestions !== undefined && props.suggestions.length > 0 && ( + {suggestions !== undefined && suggestions.length > 0 && ( <> - {props.suggestions.map((suggestion) => ( + {suggestions.map((suggestion) => ( {suggestion.field}
@@ -52,7 +57,9 @@ export function InputWithSuggestions(props: { )} {/* TODO: clean up, find alternative for this (this is used in filters) */} - {props.children} + {children}
); -} +}; + +export default InputWithSuggestions; diff --git a/components/JsonHighlight.tsx b/components/JsonHighlight.tsx new file mode 100644 index 00000000..97934217 --- /dev/null +++ b/components/JsonHighlight.tsx @@ -0,0 +1,24 @@ +import { type FC, type PropsWithChildren, useEffect } from 'react'; + +import prism from 'prismjs'; +import 'prismjs/components/prism-json'; + +/** + * Display JSON text with syntax highlighting + * + * @param props + * @param props.children + */ +const JsonHighlight: FC = ({ children }) => { + useEffect(() => { + prism.highlightAll(); + }, []); + + return ( +
+            {children}
+        
+ ); +}; + +export default JsonHighlight; diff --git a/components/JsonPreviewModal.tsx b/components/JsonPreviewModal.tsx new file mode 100644 index 00000000..0ee3ae44 --- /dev/null +++ b/components/JsonPreviewModal.tsx @@ -0,0 +1,41 @@ +import { type FC } from 'react'; +import { Button, Modal } from 'react-bootstrap'; +import JsonHighlight from 'components/JsonHighlight'; +import { type Result } from '@eosc-perf/eosc-perf-client'; + +type JsonPreviewModalProps = { + result: Result | null; + show: boolean; + closeModal: () => void; +}; + +/** + * Modal to view the JSON data of a result + * + * @param props + * @param props.result + * @param props.show + * @param props.closeModal + */ +const JsonPreviewModal: FC = ({ result, show, closeModal }) => { + return ( + + + JSON Data + + + {result !== null && ( + {JSON.stringify(result.json, null, 4)} + )} + {result == null &&
Loading...
} +
+ + + +
+ ); +}; + +export default JsonPreviewModal; diff --git a/components/jsonSelection.tsx b/components/JsonSelection.tsx similarity index 79% rename from components/jsonSelection.tsx rename to components/JsonSelection.tsx index 8c862826..5d09edbe 100644 --- a/components/jsonSelection.tsx +++ b/components/JsonSelection.tsx @@ -1,31 +1,34 @@ -import React, { ChangeEvent, ReactElement, useState } from 'react'; +import { type ChangeEvent, type FC, useState } from 'react'; import { Form, ProgressBar } from 'react-bootstrap'; +type JsonSelectionProps = { + fileContents?: string; + setFileContents: (file?: string) => void; +}; + /** * Form component to select a JSON file for upload + * + * @param props * @param props.fileContents string containing the json file * @param props.setFileContents callback to update the string containing the json - * @constructor */ -export function JsonSelection(props: { - fileContents?: string; - setFileContents: (file?: string) => void; -}): ReactElement { +const JsonSelection: FC = ({ fileContents, setFileContents }) => { const [progress, setProgress] = useState(100.0); function loadFile(file?: File) { if (file === undefined) { - props.setFileContents(undefined); + setFileContents(undefined); return; } const reader = new FileReader(); reader.addEventListener('load', (e) => { - if (e.target && e.target.result) { + if (e.target?.result) { // readAsText guarantees string - props.setFileContents(e.target.result as string); + setFileContents(e.target.result as string); } else { - props.setFileContents(undefined); + setFileContents(undefined); } setProgress(100.0); }); @@ -52,4 +55,6 @@ export function JsonSelection(props: { {/*props.fileContents !== undefined ? props.fileContents :
No file loaded.
*/} ); -} +}; + +export default JsonSelection; diff --git a/components/LoginCheck.tsx b/components/LoginCheck.tsx new file mode 100644 index 00000000..596ddcee --- /dev/null +++ b/components/LoginCheck.tsx @@ -0,0 +1,35 @@ +import { type FC } from 'react'; +import { Alert, Button, Col, Row } from 'react-bootstrap'; +import useUser from 'lib/useUser'; + +const DEFAULT_MESSAGE = + 'You must be logged in to submit data to the platform! Log in using the dropdown in the top right of the page.'; + +type LoginCheckProps = { message?: string }; + +/** + * Warning banner that displays if the user is not logged in. + * @constructor + */ +const LoginCheck: FC = ({ message = DEFAULT_MESSAGE }) => { + const auth = useUser(); + + if (auth.loading || auth.loggedIn) { + return null; + } + + return ( + + + {message} + + + + + + ); +}; + +export default LoginCheck; diff --git a/components/navHeader.tsx b/components/NavHeader.tsx similarity index 95% rename from components/navHeader.tsx rename to components/NavHeader.tsx index 131ee241..e0775156 100644 --- a/components/navHeader.tsx +++ b/components/NavHeader.tsx @@ -1,18 +1,17 @@ -import React, { ReactElement, useContext } from 'react'; -import { UserContext } from 'components/userContext'; +import { type FC } from 'react'; import { Nav, Navbar, NavDropdown } from 'react-bootstrap'; -import logo from '../public/images/eosc-perf-logo.4.svg'; +import logo from 'public/images/eosc-perf-logo.4.svg'; import { useAuth } from 'react-oidc-context'; import { Wrench } from 'react-bootstrap-icons'; import Link from 'next/link'; import Image from 'next/image'; +import useUser from 'lib/useUser'; /** * Navigation header rendered at the top of every page - * @constructor */ -export function NavHeader(): ReactElement { - const auth = useContext(UserContext); +const NavHeader: FC = () => { + const auth = useUser(); const authentication = useAuth(); return ( @@ -119,4 +118,6 @@ export function NavHeader(): ReactElement { ); -} +}; + +export default NavHeader; diff --git a/components/pagination.test.tsx b/components/Paginator.test.tsx similarity index 97% rename from components/pagination.test.tsx rename to components/Paginator.test.tsx index e4ba497e..552ec4d5 100644 --- a/components/pagination.test.tsx +++ b/components/Paginator.test.tsx @@ -1,5 +1,5 @@ import { act, render, screen } from '@testing-library/react'; -import { Paginatable, Paginator } from './pagination'; +import Paginator, { type Paginatable } from './Paginator'; describe('pagination', () => { const pagination: Paginatable = { diff --git a/components/Paginator.tsx b/components/Paginator.tsx new file mode 100644 index 00000000..8b6eefe7 --- /dev/null +++ b/components/Paginator.tsx @@ -0,0 +1,101 @@ +import { type FC } from 'react'; +import { Pagination } from 'react-bootstrap'; + +/** + * Representation of an OpenAPI pagination object + */ +export type Paginatable = { + /** + * Page index of next page + */ + readonly next_num: number; + + /** + * Page index of previous page + */ + readonly prev_num: number; + + /** + * Total number of pages + */ + readonly total: number; + + /** + * Maximum number of items per page + */ + per_page?: number; + + /** + * Whether there is a next page + */ + readonly has_next: boolean; + + /** + * Whether there is a previous page + */ + readonly has_prev: boolean; + + /** + * Total number of pages available + */ + readonly pages: number; + + /** + * The current page + */ + page?: number; +}; + +/** + * Generic for pagination of a specific object type + */ +export type Paginated = { items: Type[] } & Paginatable; + +type PaginatorProps = { + pagination: Paginatable; + navigateTo: (pageIndex: number) => void; +}; + +/** + * Component to navigate between pages of a pagination object + * + * @param props + * @param props.pagination paginatable object to navigate through + * @param props.navigateTo callback to navigate to another page in the pagination + */ +const Paginator: FC = ({ pagination, navigateTo }) => ( + + navigateTo(1)} + data-testid="paginator-first" + /> + navigateTo(pagination.prev_num)} + data-testid="paginator-prev" + /> + {/* TODO: don't show all pages, only nearby 3-5? */} + {[...Array(pagination.pages).keys()].map((n: number) => ( + navigateTo(n + 1)} + key={n + 1} + > + {n + 1} + + ))} + navigateTo(pagination.next_num)} + data-testid="paginator-next" + /> + navigateTo(pagination.pages)} + data-testid="paginator-last" + /> + +); + +export default Paginator; diff --git a/components/queryClientWrapper.tsx b/components/QueryClientWrapper.tsx similarity index 63% rename from components/queryClientWrapper.tsx rename to components/QueryClientWrapper.tsx index b734e1a0..4de22458 100644 --- a/components/queryClientWrapper.tsx +++ b/components/QueryClientWrapper.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import { type FC, type PropsWithChildren } from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { ReactQueryDevtools } from 'react-query/devtools'; @@ -10,10 +10,12 @@ const queryClient = new QueryClient({ }, }); -export function QueryClientWrapper(props: { children: ReactNode }) { +const QueryClientWrapper: FC = ({ children }) => { return ( - {props.children} + {children} ); -} +}; + +export default QueryClientWrapper; diff --git a/components/registrationCheck.test.tsx b/components/RegistrationCheck.test.tsx similarity index 90% rename from components/registrationCheck.test.tsx rename to components/RegistrationCheck.test.tsx index f574972b..ffbaf629 100644 --- a/components/registrationCheck.test.tsx +++ b/components/RegistrationCheck.test.tsx @@ -1,7 +1,6 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; -import { RegistrationCheck } from './registrationCheck'; -import { UserContext, UserInfo } from './userContext'; +import RegistrationCheck from './RegistrationCheck'; +import UserContext, { type UserInfo } from './UserContext'; const legalUser: UserInfo = { token: '__access__token__', diff --git a/components/RegistrationCheck.tsx b/components/RegistrationCheck.tsx new file mode 100644 index 00000000..9510605f --- /dev/null +++ b/components/RegistrationCheck.tsx @@ -0,0 +1,30 @@ +import { type FC } from 'react'; +import { Alert, Button, Col, Row } from 'react-bootstrap'; +import Link from 'next/link'; +import useUser from 'lib/useUser'; + +/** + * Warning banner that displays if the user has not completed registration + */ +const RegistrationCheck: FC = () => { + const auth = useUser(); + + if (auth.loading || !auth.loggedIn || auth.registered) { + return null; + } + + return ( + + + You must register before submitting data to the services on this website! + + + + + + + + ); +}; + +export default RegistrationCheck; diff --git a/components/resultEditModal.tsx b/components/ResultEditModal.tsx similarity index 67% rename from components/resultEditModal.tsx rename to components/ResultEditModal.tsx index c63a902a..1f474117 100644 --- a/components/resultEditModal.tsx +++ b/components/ResultEditModal.tsx @@ -1,28 +1,26 @@ -import React, { useContext, useEffect, useState } from 'react'; +import { type FC, useEffect, useState } from 'react'; import { Button, Modal } from 'react-bootstrap'; import { useMutation } from 'react-query'; -import { UserContext } from 'components/userContext'; -import { JsonHighlight } from 'components/jsonHighlight'; -import TagSelector from './tagSelector'; -import { Result, Tag, TagsIds } from '@eosc-perf/eosc-perf-client'; -import useApi from '../utils/useApi'; +import JsonHighlight from 'components/JsonHighlight'; +import TagSelector from './TagSelector'; +import { type Result, type Tag, type TagsIds } from '@eosc-perf/eosc-perf-client'; +import useApi from 'lib/useApi'; +import useUser from 'lib/useUser'; -export function ResultEditModal({ - result, - show, - closeModal, -}: { +type ResultEditModalProps = { result: Result; show: boolean; closeModal: () => void; -}) { - const auth = useContext(UserContext); +}; + +const ResultEditModal: FC = ({ result, show, closeModal }) => { + const auth = useUser(); const api = useApi(auth.token); const [selectedTags, setSelectedTags] = useState([]); useEffect(() => { - setSelectedTags(result?.tags ?? []); + setSelectedTags(result.tags ?? []); }, [result]); const { mutate } = useMutation((data: TagsIds) => api.results.updateResult(result.id, data), { @@ -31,12 +29,14 @@ export function ResultEditModal({ }, }); - function submitEdit() { + const submitEdit = () => { mutate({ tags_ids: selectedTags.map((tag) => tag.id) }); - } + }; + + // TODO: this is checking for result = null: why? return ( - + Edit result @@ -61,4 +61,6 @@ export function ResultEditModal({ ); -} +}; + +export default ResultEditModal; diff --git a/components/resultReportModal.tsx b/components/ResultReportModal.tsx similarity index 57% rename from components/resultReportModal.tsx rename to components/ResultReportModal.tsx index 9f6bccd4..731660a9 100644 --- a/components/resultReportModal.tsx +++ b/components/ResultReportModal.tsx @@ -1,27 +1,27 @@ -import React, { useContext, useState } from 'react'; +import { type FC, useState } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; import { useMutation } from 'react-query'; -import { UserContext } from 'components/userContext'; -import { JsonHighlight } from 'components/jsonHighlight'; -import useApi from '../utils/useApi'; -import { CreateClaim, Result } from '@eosc-perf/eosc-perf-client'; +import JsonHighlight from 'components/JsonHighlight'; +import useApi from 'lib/useApi'; +import { type CreateClaim, type Result } from '@eosc-perf/eosc-perf-client'; +import useUser from 'lib/useUser'; -export function ResultReportModal(props: { +type ResultReportModalProps = { result: Result; show: boolean; closeModal: () => void; -}) { - const auth = useContext(UserContext); +}; + +const ResultReportModal: FC = ({ result, show, closeModal }) => { + const auth = useUser(); const api = useApi(auth.token); const [message, setMessage] = useState(''); const { mutate } = useMutation( - (data: CreateClaim) => api.results.claimReport(props.result.id, data), + (data: CreateClaim) => api.results.claimReport(result.id, data), { - onSuccess: () => { - props.closeModal(); - }, + onSuccess: closeModal, } ); @@ -30,7 +30,7 @@ export function ResultReportModal(props: { } return ( - + Report result @@ -45,19 +45,21 @@ export function ResultReportModal(props: { /> - {props.result !== null && ( - {JSON.stringify(props.result.json, null, 4)} + {result !== null && ( + {JSON.stringify(result.json, null, 4)} )} - {props.result == null &&
Loading...
} + {result == null &&
Loading...
} -
); -} +}; + +export default ResultReportModal; diff --git a/components/resultsPerPageSelection.test.tsx b/components/ResultsPerPageSelection.test.tsx similarity index 91% rename from components/resultsPerPageSelection.test.tsx rename to components/ResultsPerPageSelection.test.tsx index 4f35af3e..2b746a75 100644 --- a/components/resultsPerPageSelection.test.tsx +++ b/components/ResultsPerPageSelection.test.tsx @@ -1,4 +1,4 @@ -import { ResultsPerPageSelection } from './resultsPerPageSelection'; +import ResultsPerPageSelection from './ResultsPerPageSelection'; import { render } from '@testing-library/react'; describe('results per page selection', () => { diff --git a/components/resultsPerPageSelection.tsx b/components/ResultsPerPageSelection.tsx similarity index 71% rename from components/resultsPerPageSelection.tsx rename to components/ResultsPerPageSelection.tsx index 67c4938a..160e62de 100644 --- a/components/resultsPerPageSelection.tsx +++ b/components/ResultsPerPageSelection.tsx @@ -1,16 +1,21 @@ -import React, { ChangeEvent } from 'react'; +import { FC } from 'react'; import { Col, Form, Row } from 'react-bootstrap'; +type ResultsPerPageSelectionProps = { + onChange: (resultsPerPage: number) => void; + currentSelection: number; +}; + /** * Dropdown component to select the number of items to display in a page + * @param props * @param props.onChange callback to be called with the new number of items per page * @param props.currentSelection the current number of items displayed per page - * @constructor */ -export function ResultsPerPageSelection(props: { - onChange: (resultsPerPage: number) => void; - currentSelection: number; -}) { +const ResultsPerPageSelection: FC = ({ + onChange, + currentSelection, +}) => { const options = [10, 15, 20, 50, 100]; return ( @@ -18,10 +23,8 @@ export function ResultsPerPageSelection(props: { Results per page: ) => - props.onChange(parseInt(e.target.value)) - } - value={props.currentSelection} + onChange={(e) => onChange(parseInt(e.target.value))} + value={currentSelection} > {options.map((n: number) => (