From f7b80d7351085f7a7ff0ae883a62b695e2ed643f Mon Sep 17 00:00:00 2001 From: Christoph Anderson Date: Sun, 7 May 2023 15:52:07 +0200 Subject: [PATCH] Added some minor code refactorings. This PR comes with the following fixes: - Pastel theme should now be darker - Added a preference tab - Font size of the selector has been reduced - Fixed css - Moved AppContext under src/contexts - Renamed themes to provide backwards compatibility --- src/App.css | 14 ++++--- src/App.jsx | 23 +++++------ src/EstimateResults.jsx | 4 +- src/NewSnapshot.jsx | 6 +-- src/PoliciesTable.jsx | 6 +-- src/Preferences.jsx | 41 +++++++++++++++++++ src/RepoStatus.jsx | 2 +- src/SetupRepository.jsx | 2 +- src/SnapshotsTable.jsx | 8 ++-- src/SourcesTable.jsx | 4 +- src/TaskDetails.jsx | 4 +- src/TaskLogs.jsx | 5 +-- src/TasksTable.jsx | 4 +- src/Theme.css | 24 +++++------ src/ThemeSelector.tsx | 35 ++++------------ .../AppContext.tsx} | 0 src/contexts/UIPreferencesContext.tsx | 39 ++++++++++++------ src/uiutil.jsx | 21 ++++++---- 18 files changed, 139 insertions(+), 103 deletions(-) create mode 100644 src/Preferences.jsx rename src/{AppContext.js => contexts/AppContext.tsx} (100%) diff --git a/src/App.css b/src/App.css index c154ef7..88a750e 100644 --- a/src/App.css +++ b/src/App.css @@ -11,11 +11,8 @@ body { color: var(--text-color-body); } -.select { - color: var(--text-color-selector); -} - -.select select:hover { +.select_theme { + font-size: 90%; color: var(--text-color-selector); } @@ -50,7 +47,7 @@ body { .btn-warning { background-color: var(--color-warning); - border-color: var(--color-primary); + border-color: var(--color-warning); } .btn-danger { @@ -136,6 +133,11 @@ body { font-weight: bold; } +.label-description { + padding-top: 4px; + padding-bottom: 8px; +} + .label-help { padding-top: 4px; padding-bottom: 8px; diff --git a/src/App.jsx b/src/App.jsx index 03848d2..6e9ee7f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,10 @@ -import axios from 'axios'; import 'bootstrap/dist/css/bootstrap.min.css'; -import { React, Component } from 'react'; -import { Navbar, Nav, Container} from 'react-bootstrap'; -import { BrowserRouter as Router, NavLink, Redirect, Route, Switch } from 'react-router-dom'; import './Theme.css'; import './App.css'; +import axios from 'axios'; +import { React, Component } from 'react'; +import { Navbar, Nav, Container } from 'react-bootstrap'; +import { BrowserRouter as Router, NavLink, Redirect, Route, Switch } from 'react-router-dom'; import { BeginRestore } from './BeginRestore'; import { DirectoryObject } from "./DirectoryObject"; import { PoliciesTable } from "./PoliciesTable"; @@ -15,9 +15,8 @@ import { TaskDetails } from './TaskDetails'; import { TasksTable } from './TasksTable'; import { NewSnapshot } from './NewSnapshot'; import { PolicyEditorPage } from './PolicyEditorPage'; -import { ThemeSelector } from './ThemeSelector'; -import { AppContext } from './AppContext'; - +import { Preferences } from './Preferences'; +import { AppContext } from './contexts/AppContext'; import { UIPreferenceProvider } from './contexts/UIPreferencesContext'; export default class App extends Component { @@ -50,7 +49,6 @@ export default class App extends Component { } this.fetchInitialRepositoryDescription(); - this.taskSummaryInterval = window.setInterval(this.fetchTaskSummary, 5000); } @@ -61,7 +59,7 @@ export default class App extends Component { repoDescription: result.data.description, }); } - }).catch(error => { /* ignore */}); + }).catch(error => { /* ignore */ }); } fetchTaskSummary() { @@ -110,9 +108,7 @@ export default class App extends Component { Repository - - @@ -133,8 +129,9 @@ export default class App extends Component { + - + diff --git a/src/EstimateResults.jsx b/src/EstimateResults.jsx index 9d9d5a8..24d5fee 100644 --- a/src/EstimateResults.jsx +++ b/src/EstimateResults.jsx @@ -7,7 +7,7 @@ import Button from 'react-bootstrap/Button'; import Spinner from 'react-bootstrap/esm/Spinner'; import Form from 'react-bootstrap/Form'; import { TaskLogs } from './TaskLogs'; -import { cancelTask, redirectIfNotConnected, sizeDisplayName } from './uiutil'; +import { cancelTask, redirect, sizeDisplayName } from './uiutil'; export class EstimateResults extends Component { constructor() { @@ -55,7 +55,7 @@ export class EstimateResults extends Component { this.interval = null; } }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false diff --git a/src/NewSnapshot.jsx b/src/NewSnapshot.jsx index 71f148d..1a62fd9 100644 --- a/src/NewSnapshot.jsx +++ b/src/NewSnapshot.jsx @@ -7,7 +7,7 @@ import Row from 'react-bootstrap/Row'; import { handleChange } from './forms'; import { PolicyEditor } from './PolicyEditor'; import { EstimateResults } from './EstimateResults'; -import { CLIEquivalent, DirectorySelector, errorAlert, GoBackButton, redirectIfNotConnected } from './uiutil'; +import { CLIEquivalent, DirectorySelector, errorAlert, GoBackButton, redirect } from './uiutil'; export class NewSnapshot extends Component { constructor() { @@ -35,7 +35,7 @@ export class NewSnapshot extends Component { localHost: result.data.localHost, }); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); }); } @@ -54,7 +54,7 @@ export class NewSnapshot extends Component { // while we were resolving this.maybeResolveCurrentPath(currentPath); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); }); } else { this.setState({ diff --git a/src/PoliciesTable.jsx b/src/PoliciesTable.jsx index 283012f..c7aac11 100644 --- a/src/PoliciesTable.jsx +++ b/src/PoliciesTable.jsx @@ -11,7 +11,7 @@ import Row from 'react-bootstrap/Row'; import { Link } from 'react-router-dom'; import { handleChange } from './forms'; import MyTable from './Table'; -import { CLIEquivalent, compare, DirectorySelector, isAbsolutePath, ownerName, policyEditorURL, redirectIfNotConnected } from './uiutil'; +import { CLIEquivalent, compare, DirectorySelector, isAbsolutePath, ownerName, policyEditorURL, redirect } from './uiutil'; const applicablePolicies = "Applicable Policies" const localPolicies = "Local Path Policies" @@ -68,7 +68,7 @@ export class PoliciesTable extends Component { isLoading: false, }); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false @@ -87,7 +87,7 @@ export class PoliciesTable extends Component { isLoading: false, }); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false diff --git a/src/Preferences.jsx b/src/Preferences.jsx new file mode 100644 index 0000000..348ac31 --- /dev/null +++ b/src/Preferences.jsx @@ -0,0 +1,41 @@ +import { Component } from 'react'; +import { ThemeSelector } from './ThemeSelector'; +import { UIPreferencesContext } from './contexts/UIPreferencesContext'; + +export class Preferences extends Component { + static contextType = UIPreferencesContext; + constructor() { + super(); + this.state = { + status: {}, + error: null, + }; + } + + render() { + const { pageSize, bytesStringBase2 } = this.context; + return <> +
+
+ + + The current active theme +
+
+
+ + + Represents bytes to the base of 2 +
+
+
+ + + The number of items shown in tables +
+
+ + } +} \ No newline at end of file diff --git a/src/RepoStatus.jsx b/src/RepoStatus.jsx index b95e324..b7dc6e2 100644 --- a/src/RepoStatus.jsx +++ b/src/RepoStatus.jsx @@ -13,7 +13,7 @@ import { cancelTask, CLIEquivalent } from './uiutil'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faChevronCircleDown, faChevronCircleUp, faWindowClose } from '@fortawesome/free-solid-svg-icons'; import { TaskLogs } from './TaskLogs'; -import { AppContext } from './AppContext'; +import { AppContext } from './contexts/AppContext'; export class RepoStatus extends Component { constructor() { diff --git a/src/SetupRepository.jsx b/src/SetupRepository.jsx index b8b1688..d172d72 100644 --- a/src/SetupRepository.jsx +++ b/src/SetupRepository.jsx @@ -8,7 +8,7 @@ import Collapse from 'react-bootstrap/Collapse'; import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; import Spinner from 'react-bootstrap/Spinner'; -import { AppContext } from './AppContext'; +import { AppContext } from './contexts/AppContext'; import { handleChange, validateRequiredFields } from './forms'; import { RequiredBoolean } from './forms/RequiredBoolean'; import { RequiredField } from './forms/RequiredField'; diff --git a/src/SnapshotsTable.jsx b/src/SnapshotsTable.jsx index 397cc56..d4a4ea9 100644 --- a/src/SnapshotsTable.jsx +++ b/src/SnapshotsTable.jsx @@ -8,7 +8,7 @@ import Col from 'react-bootstrap/Col'; import Spinner from 'react-bootstrap/Spinner'; import { Link } from "react-router-dom"; import MyTable from './Table'; -import { CLIEquivalent, compare, errorAlert, GoBackButton, objectLink, parseQuery, redirectIfNotConnected, rfc3339TimestampForDisplay, sizeWithFailures, sourceQueryStringParams } from './uiutil'; +import { CLIEquivalent, compare, errorAlert, GoBackButton, objectLink, parseQuery, redirect, rfc3339TimestampForDisplay, sizeWithFailures, sourceQueryStringParams } from './uiutil'; import { faSync, faThumbtack } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Modal from 'react-bootstrap/Modal'; @@ -142,7 +142,7 @@ export class SnapshotsTable extends Component { this.fetchSnapshots(); } }).catch(error => { - redirectIfNotConnected(error); + redirect(error); errorAlert(error); }); @@ -164,7 +164,7 @@ export class SnapshotsTable extends Component { axios.post('/api/v1/snapshots/delete', req).then(result => { this.props.history.goBack(); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); errorAlert(error); }); } @@ -307,7 +307,7 @@ export class SnapshotsTable extends Component { editingDescriptionFor: undefined, savingSnapshot: false, }); - redirectIfNotConnected(e); + redirect(e); errorAlert(e); }); } diff --git a/src/SourcesTable.jsx b/src/SourcesTable.jsx index 1d44c4e..02a4479 100644 --- a/src/SourcesTable.jsx +++ b/src/SourcesTable.jsx @@ -12,7 +12,7 @@ import Spinner from 'react-bootstrap/Spinner'; import { Link } from 'react-router-dom'; import { handleChange } from './forms'; import MyTable from './Table'; -import { CLIEquivalent, compare, errorAlert, ownerName, policyEditorURL, redirectIfNotConnected, sizeDisplayName, sizeWithFailures, sourceQueryStringParams } from './uiutil'; +import { CLIEquivalent, compare, errorAlert, ownerName, policyEditorURL, redirect, sizeDisplayName, sizeWithFailures, sourceQueryStringParams } from './uiutil'; const localSnapshots = "Local Snapshots" const allSnapshots = "All Snapshots" @@ -60,7 +60,7 @@ export class SourcesTable extends Component { isRefreshing: false, }); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isRefreshing: false, diff --git a/src/TaskDetails.jsx b/src/TaskDetails.jsx index 6f5460a..7eef578 100644 --- a/src/TaskDetails.jsx +++ b/src/TaskDetails.jsx @@ -11,7 +11,7 @@ import Row from 'react-bootstrap/Row'; import Table from 'react-bootstrap/Table'; import Spinner from 'react-bootstrap/Spinner'; import { TaskLogs } from './TaskLogs'; -import { cancelTask, formatDuration, GoBackButton, redirectIfNotConnected, sizeDisplayName } from './uiutil'; +import { cancelTask, formatDuration, GoBackButton, redirect, sizeDisplayName } from './uiutil'; export class TaskDetails extends Component { constructor() { @@ -60,7 +60,7 @@ export class TaskDetails extends Component { this.interval = null; } }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false diff --git a/src/TaskLogs.jsx b/src/TaskLogs.jsx index 50f19be..38e5209 100644 --- a/src/TaskLogs.jsx +++ b/src/TaskLogs.jsx @@ -1,9 +1,8 @@ - import axios from 'axios'; import React, { Component } from 'react'; import Table from 'react-bootstrap/Table'; import { handleChange } from './forms'; -import { redirectIfNotConnected } from './uiutil'; +import { redirect } from './uiutil'; export class TaskLogs extends Component { constructor() { @@ -54,7 +53,7 @@ export class TaskLogs extends Component { this.scrollToBottom(); } }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false diff --git a/src/TasksTable.jsx b/src/TasksTable.jsx index 69f0be6..d4a9198 100644 --- a/src/TasksTable.jsx +++ b/src/TasksTable.jsx @@ -12,7 +12,7 @@ import Row from 'react-bootstrap/Row'; import { Link } from 'react-router-dom'; import { handleChange } from './forms'; import MyTable from './Table'; -import { redirectIfNotConnected, taskStatusSymbol } from './uiutil'; +import { redirect, taskStatusSymbol } from './uiutil'; export class TasksTable extends Component { constructor() { @@ -66,7 +66,7 @@ export class TasksTable extends Component { isLoading: false, }); }).catch(error => { - redirectIfNotConnected(error); + redirect(error); this.setState({ error, isLoading: false diff --git a/src/Theme.css b/src/Theme.css index 6eb4084..d644143 100644 --- a/src/Theme.css +++ b/src/Theme.css @@ -1,5 +1,5 @@ /*Supported themes */ -.light-theme { +.light { --background-color: #ffffff; --color-primary: #2A7FFF; --color-secondary: #23a25a; @@ -16,7 +16,7 @@ --color-danger: #d2475a; } -.dark-theme { +.dark { --background-color: #343c43; --color-primary: #1162b1; --color-secondary: #0d9aab; @@ -33,24 +33,24 @@ --color-danger: #b31f04; } -.pastel-theme { +.pastel { --background-color: #ffffff; - --color-primary: #9feae7; - --color-secondary: #d7a5e9; - --color-warning: #ffe070; - --color-success: #d7a5e9; - --color-submit: #00B4D8; + --color-primary: #9bf6ff; + --color-secondary: #bdb2ff; + --color-warning: #ffd6a5; + --color-success: #bdb2ff; + --color-submit: #a0c4ff; --text-color: #000000; --text-color-selector: #000000; --text-color-body: #000000; - --text-color-nav: #d7a5e9; + --text-color-nav: #c095e4; - --color-error: #b31f04; - --color-danger: #b31f04; + --color-error: #ffadad; + --color-danger: #ffadad; } -.ocean-theme { +.ocean { --background-color: #ffffff; --color-primary: #03045E; --color-secondary: #0077B6; diff --git a/src/ThemeSelector.tsx b/src/ThemeSelector.tsx index 9db8ec4..e6eb0b6 100644 --- a/src/ThemeSelector.tsx +++ b/src/ThemeSelector.tsx @@ -1,21 +1,20 @@ -import Select, { components } from 'react-select' +import Select from 'react-select' import { useContext } from 'react'; -import { Theme, UIPreferencesContext } from './contexts/UIPreferencesContext'; +import { UIPreferencesContext } from './contexts/UIPreferencesContext'; export function ThemeSelector() { const { theme, setTheme } = useContext(UIPreferencesContext); //Contains all supported themes const themes = [ - { value: 'light-theme', label: 'light' }, - { value: 'dark-theme', label: 'dark' }, - { value: 'pastel-theme', label: 'pastel' }, - { value: 'ocean-theme', label: 'ocean' } + { value: 'light', label: 'light' }, + { value: 'dark', label: 'dark' }, + { value: 'pastel', label: 'pastel' }, + { value: 'ocean', label: 'ocean' } ] - + //Finds the current selected theme within supported themes const currentTheme = themes.find(o => o.value === theme); - updateTheme(theme) /** * Handles the theme selection by the user @@ -23,26 +22,8 @@ export function ThemeSelector() { */ const handleTheme = (event: any) => { var selectedTheme = event.value; - updateTheme(selectedTheme) setTheme(selectedTheme) }; - return } \ No newline at end of file diff --git a/src/AppContext.js b/src/contexts/AppContext.tsx similarity index 100% rename from src/AppContext.js rename to src/contexts/AppContext.tsx diff --git a/src/contexts/UIPreferencesContext.tsx b/src/contexts/UIPreferencesContext.tsx index 873b0a0..6a91506 100644 --- a/src/contexts/UIPreferencesContext.tsx +++ b/src/contexts/UIPreferencesContext.tsx @@ -2,9 +2,12 @@ import React, { ReactNode, useEffect, useState } from 'react'; import axios from 'axios'; export const PAGE_SIZES = [10, 20, 30, 40, 50, 100]; +export const UIPreferencesContext = React.createContext({} as UIPreferences); -export type Theme = "light-theme" | "dark-theme" | "pastel-theme" | "ocean-theme"; +const DEFAULT_PREFERENCES = { pageSize: PAGE_SIZES[0], theme: getDefaultTheme() } as SerializedUIPreferences; +const PREFERENCES_URL = '/api/v1/ui-preferences'; +export type Theme = "light" | "dark" | "pastel" | "ocean"; export type PageSize = 10 | 20 | 30 | 40 | 50 | 100; export interface UIPreferences { @@ -19,8 +22,6 @@ interface SerializedUIPreferences { theme: Theme | undefined } -export const UIPreferencesContext = React.createContext({} as UIPreferences); - export interface UIPreferenceProviderProps { children: ReactNode, initalValue: UIPreferences | undefined @@ -32,10 +33,9 @@ export interface UIPreferenceProviderProps { */ function getDefaultTheme(): Theme { if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { - return "dark-theme"; + return "dark"; } - - return "light-theme"; + return "light"; } function normalizePageSize(pageSize: number): PageSize { @@ -51,14 +51,9 @@ function normalizePageSize(pageSize: number): PageSize { return PAGE_SIZES[index - 1] as PageSize; } } - return 100; } -const PREFERENCES_URL = '/api/v1/ui-preferences'; - -const DEFAULT_PREFERENCES = { pageSize: PAGE_SIZES[0], theme: getDefaultTheme() } as SerializedUIPreferences; - export function UIPreferenceProvider(props: UIPreferenceProviderProps) { const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); @@ -73,9 +68,10 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) { } else { storedPreferences.pageSize = normalizePageSize(storedPreferences.pageSize); } - + syncTheme(storedPreferences.theme) setPreferences(storedPreferences); }).catch(err => console.error(err)); + }, []); useEffect(() => { @@ -83,10 +79,27 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) { return; } - axios.put(PREFERENCES_URL, preferences).then(result => {}).catch(err => console.error(err)); + axios.put(PREFERENCES_URL, preferences).then(result => { }).catch(err => console.error(err)); }, [preferences]); + /** + * Synchronizes the selected theme with the html class + * + * @param theme + * The theme to be set + */ + const syncTheme = (theme: Theme) => { + var doc = document.querySelector("html")!; + doc.className = theme + } + + /** + * + * @param theme + * @returns + */ const setTheme = (theme: Theme) => setPreferences(oldPreferences => { + syncTheme(theme); return { ...oldPreferences, theme }; }); diff --git a/src/uiutil.jsx b/src/uiutil.jsx index b13c17f..573e76d 100644 --- a/src/uiutil.jsx +++ b/src/uiutil.jsx @@ -9,24 +9,24 @@ import InputGroup from 'react-bootstrap/InputGroup'; import Spinner from 'react-bootstrap/Spinner'; import { Link } from 'react-router-dom'; -const base10UnitPrefixes = ["", "K", "M", "G", "T"]; - // locale to use for number formatting (undefined would use default locale, but we stick to EN for now) const locale = "en-US" -function niceNumber(f) { +const base10UnitPrefixes = ["", "K", "M", "G", "T"]; + +function formatNumber(f) { return (Math.round(f * 10) / 10.0) + ''; } function toDecimalUnitString(f, thousand, prefixes, suffix) { for (var i = 0; i < prefixes.length; i++) { if (f < 0.9 * thousand) { - return niceNumber(f) + ' ' + prefixes[i] + suffix; + return formatNumber(f) + ' ' + prefixes[i] + suffix; } f /= thousand } - return niceNumber(f) + ' ' + prefixes[prefixes.length - 1] + suffix; + return formatNumber(f) + ' ' + prefixes[prefixes.length - 1] + suffix; } export function sizeWithFailures(size, summ) { @@ -105,10 +105,13 @@ export function compare(a, b) { return (a < b ? -1 : (a > b ? 1 : 0)); } -export function redirectIfNotConnected(e) { - if (e && e.response && e.response.data && e.response.data.code === "NOT_CONNECTED") { - window.location.replace("/repo"); - return; +/** + * In case of an error, redirect to the repository selection + * @param {error} The error that was returned + */ +export function redirect(e) { + if (e.response.data.code === "NOT_CONNECTED") { + window.location.replace("/repo") } }