From 6936141b22843a8b0ca321f67d701999d26b46d1 Mon Sep 17 00:00:00 2001 From: lupusA Date: Mon, 20 Nov 2023 20:11:56 +0100 Subject: [PATCH 1/6] Start migrate to functions instead of components --- src/pages/Policy.jsx | 41 ++++++++++------------- src/pages/Preferences.jsx | 69 +++++++++++++++++++-------------------- 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/src/pages/Policy.jsx b/src/pages/Policy.jsx index e759e78..3dae8e9 100644 --- a/src/pages/Policy.jsx +++ b/src/pages/Policy.jsx @@ -1,31 +1,24 @@ -import React, { Component, createRef } from 'react'; +import { useRef } from 'react'; import Col from 'react-bootstrap/esm/Col'; import Row from 'react-bootstrap/esm/Row'; import { PolicyEditor } from '../components/policy-editor/PolicyEditor'; import { CLIEquivalent, GoBackButton, parseQuery, PolicyTypeName } from '../utils/uiutil'; -export class Policy extends Component { - constructor() { - super(); +export function Policy({ location, history }) { + const source = parseQuery(location.search); + const { userName, host, path } = source; + const editorRef = useRef(); - this.editorRef = createRef(); - } - - render() { - const source = parseQuery(this.props.location.search); - const { userName, host, path } = source; - - return <> -

- -   {PolicyTypeName(source)}

- -   - - - - - - ; - } + return <> +

+ +   {PolicyTypeName(source)}

+ +   + + + + + + ; } diff --git a/src/pages/Preferences.jsx b/src/pages/Preferences.jsx index 958d0f3..ad99ee5 100644 --- a/src/pages/Preferences.jsx +++ b/src/pages/Preferences.jsx @@ -1,42 +1,39 @@ -import { Component } from 'react'; +import { useContext } from 'react'; import { UIPreferencesContext } from '../contexts/UIPreferencesContext'; /** * Class that exports preferences */ -export class Preferences extends Component { - render() { - const { pageSize, theme, bytesStringBase2, setByteStringBase, setTheme } = this.context; - return <> -
-
- - - The current active theme -
-
-
- - - Specifies the representation of bytes -
-
-
- - - Specifies the pagination size in tables -
-
- - } +export function Preferences() { + const { pageSize, theme, bytesStringBase2, setByteStringBase, setTheme } = useContext(UIPreferencesContext); + return <> +
+
+ + + The current active theme +
+
+
+ + + Specifies the representation of bytes +
+
+
+ + + Specifies the pagination size in tables +
+
+ } -Preferences.contextType = UIPreferencesContext From 902475e1a1c832fb0aaa2df4984a501abd092632 Mon Sep 17 00:00:00 2001 From: lupusA Date: Sat, 25 Nov 2023 17:17:51 +0100 Subject: [PATCH 2/6] Migrated pages/Tasks from component to function --- src/pages/Tasks.jsx | 168 ++++++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 92 deletions(-) diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.jsx index 427df65..44e21db 100644 --- a/src/pages/Tasks.jsx +++ b/src/pages/Tasks.jsx @@ -1,49 +1,47 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useState, useLayoutEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; import axios from 'axios'; import moment from 'moment'; -import React, { Component } from 'react'; import Alert from 'react-bootstrap/Alert'; import Col from 'react-bootstrap/Col'; import Dropdown from 'react-bootstrap/Dropdown'; import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; -import { Link } from 'react-router-dom'; -import { handleChange } from '../forms'; import KopiaTable from '../utils/KopiaTable'; import { redirect, taskStatusSymbol } from '../utils/uiutil'; -export class Tasks extends Component { - constructor() { - super(); - this.state = { - items: [], - isLoading: false, - error: null, - showKind: "All", - showStatus: "All", - uniqueKinds: [], - }; - - this.handleChange = handleChange.bind(this); - this.fetchTasks = this.fetchTasks.bind(this); - this.interval = window.setInterval(this.fetchTasks, 3000); - } +export function Tasks() { + const [isLoading, setIsLoading] = useState(false); + const [searchDescription, setDescription] = useState("") + const [response, setResponse] = useState({ items:[], kinds:[]}); + const [kind, setKind] = useState("All"); + const [status, setStatus] = useState("All"); + const [error, setError] = useState(); - componentDidMount() { - this.setState({ - isLoading: true, + const fetchTasks = useCallback(() => { + axios.get('/api/v1/tasks').then(result => { + setIsLoading(false); + setResponse({items: result.data.tasks, kinds:getUniqueKinds(result.data.tasks)}); + }).catch(error => { + redirect(error); + setError(error); + setIsLoading(false); }); + }, []) + + useLayoutEffect(() => { + setIsLoading(true) + fetchTasks() + let interval = setInterval(fetchTasks, 5000) + return () => { + window.clearInterval(interval); + }; + }, [fetchTasks]); - this.fetchTasks(); - } - - componentWillUnmount() { - window.clearInterval(this.interval); - } - - getUniqueKinds(tasks) { + function getUniqueKinds(tasks) { let o = {}; for (const tsk of tasks) { @@ -58,112 +56,98 @@ export class Tasks extends Component { return result; } - fetchTasks() { - axios.get('/api/v1/tasks').then(result => { - this.setState({ - items: result.data.tasks, - uniqueKinds: this.getUniqueKinds(result.data.tasks), - isLoading: false, - }); - }).catch(error => { - redirect(error); - this.setState({ - error, - isLoading: false - }); - }); + function handleDescription(desc) { + setDescription(desc.target.value) } - taskMatches(t) { - if (this.state.showKind !== "All" && t.kind !== this.state.showKind) { + function taskMatches(t) { + if (kind !== "All" && t.kind !== kind) { return false; } - if (this.state.showStatus !== "All" && t.status.toLowerCase() !== this.state.showStatus.toLowerCase()) { + if (status !== "All" && t.status.toLowerCase() !== status.toLowerCase()) { return false; } - if (this.state.searchDescription && t.description.indexOf(this.state.searchDescription) < 0) { + if (searchDescription && t.description.indexOf(searchDescription) < 0) { return false; } return true } - filterItems(items) { - return items.filter(c => this.taskMatches(c)) + function filterItems(items) { + return items.filter(c => taskMatches(c)) } - render() { - const { items, isLoading, error } = this.state; - if (error) { - return

{error.message}

; - } - if (isLoading) { - return

Loading ...

; - } + if (error) { + return

{error.message}

; + } + if (isLoading) { + return

Loading ...

; + } - const columns = [{ - Header: 'Start Time', - width: 160, - accessor: x => - {moment(x.startTime).fromNow()} - - }, { - Header: 'Status', - width: 240, - accessor: x => taskStatusSymbol(x), - }, { - Header: 'Kind', - width: "", - accessor: x =>

{x.kind}

, - }, { - Header: 'Description', - width: "", - accessor: x =>

{x.description}

, - }] - - const filteredItems = this.filterItems(items) - - return <> + const columns = [{ + Header: 'Start Time', + width: 160, + accessor: x => + {moment(x.startTime).fromNow()} + + }, { + Header: 'Status', + width: 240, + accessor: x => taskStatusSymbol(x), + }, { + Header: 'Kind', + width: "", + accessor: x =>

{x.kind}

, + }, { + Header: 'Description', + width: "", + accessor: x =>

{x.description}

, + }] + + const filteredItems = filterItems(response.items) + + return ( + <>
- Status: {this.state.showStatus} + Status: {status} - this.setState({ showStatus: "All" })}>All + setStatus("All")}>All - this.setState({ showStatus: "Running" })}>Running - this.setState({ showStatus: "Failed" })}>Failed + setStatus("Running")}>Running + setStatus("Failed")}>Failed - Kind: {this.state.showKind} + Kind: {kind} - this.setState({ showKind: "All" })}>All + setKind("All")}>All - {this.state.uniqueKinds.map(k => this.setState({ showKind: k })}>{k})} + {response.kinds.map(kind => setKind(kind)}>{kind})} - +
- {!items.length ? + {!response.items.length ? A list of tasks will appear here when you create snapshots, restore, run maintenance, etc. : }
- ; - } + ); } From 0a6962f9c9a4c26246f2e5177bf66df66f3276de Mon Sep 17 00:00:00 2001 From: lupusA Date: Mon, 27 Nov 2023 21:07:39 +0100 Subject: [PATCH 3/6] - Further migrating to functions. - deepstate need improvement --- src/App.jsx | 190 +++++++--------- src/components/SetupRepository.jsx | 6 +- src/forms/index.jsx | 21 +- src/pages/Repository.jsx | 346 +++++++++++++---------------- src/utils/deepstate.js | 24 ++ src/utils/uiutil.jsx | 9 + 6 files changed, 288 insertions(+), 308 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 00c456a..7a31e53 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,7 +2,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import './css/Theme.css'; import './css/App.css'; import axios from 'axios'; -import { React, Component } from 'react'; +import { React, useCallback, useLayoutEffect, useState, useContext } from 'react'; import { Navbar, Nav, Container } from 'react-bootstrap'; import { BrowserRouter as Router, NavLink, Redirect, Route, Switch } from 'react-router-dom'; import { Policy } from './pages/Policy'; @@ -19,129 +19,99 @@ import { SnapshotRestore } from './pages/SnapshotRestore'; import { AppContext } from './contexts/AppContext'; import { UIPreferenceProvider } from './contexts/UIPreferencesContext'; -export default class App extends Component { - constructor() { - super(); +export default function App({ uiPrefs }) { + const context = useContext(AppContext); + const [runningTaskCount, setRunningTaskCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [repoDescription, setRepoDescription] = useState(""); - this.state = { - runningTaskCount: 0, - isFetching: false, - repoDescription: "", - }; - - this.fetchTaskSummary = this.fetchTaskSummary.bind(this); - this.repositoryUpdated = this.repositoryUpdated.bind(this); - this.repositoryDescriptionUpdated = this.repositoryDescriptionUpdated.bind(this); - this.fetchInitialRepositoryDescription = this.fetchInitialRepositoryDescription.bind(this); - - const tok = document.head.querySelector('meta[name="kopia-csrf-token"]'); - if (tok && tok.content) { - axios.defaults.headers.common['X-Kopia-Csrf-Token'] = tok.content; - } else { - axios.defaults.headers.common['X-Kopia-Csrf-Token'] = "-"; - } - } - - componentDidMount() { - const av = document.getElementById('appVersion'); - if (av) { - // show app version after mounting the component to avoid flashing of unstyled content. - av.style.display = "block"; - } - - this.fetchInitialRepositoryDescription(); - this.taskSummaryInterval = window.setInterval(this.fetchTaskSummary, 5000); + const token = document.head.querySelector('meta[name="kopia-csrf-token"]'); + if (token && token.content) { + axios.defaults.headers.common['X-Kopia-Csrf-Token'] = token.content; + } else { + axios.defaults.headers.common['X-Kopia-Csrf-Token'] = "-"; } - fetchInitialRepositoryDescription() { + const fetchRepositoryDescription = useCallback(() => { axios.get('/api/v1/repo/status').then(result => { if (result.data.description) { - this.setState({ - repoDescription: result.data.description, - }); + setRepoDescription(result.data.description) } - }).catch(error => { /* ignore */ }); - } + }).catch(error => { /* ignore */ }) + }, []) - fetchTaskSummary() { - if (!this.state.isFetching) { - this.setState({ isFetching: true }); + const fetchTaskSummary = useCallback(() => { + if (!isLoading) { + setIsLoading(true); axios.get('/api/v1/tasks-summary').then(result => { - this.setState({ isFetching: false, runningTaskCount: result.data["RUNNING"] || 0 }); + setIsLoading(false); + setRunningTaskCount(result.data["RUNNING"] || 0); }).catch(error => { - this.setState({ isFetching: false, runningTaskCount: -1 }); + setIsLoading(false); + setRunningTaskCount(-1); }); } - } - - componentWillUnmount() { - window.clearInterval(this.taskSummaryInterval); - } + }, [isLoading]) - // this is invoked via AppContext whenever repository is connected, disconnected, etc. - repositoryUpdated(isConnected) { - if (isConnected) { - window.location.replace("/snapshots"); - } else { - window.location.replace("/repo"); + useLayoutEffect(() => { + const appVersion = document.getElementById('appVersion'); + if (appVersion) { + // show app version after mounting the component to avoid flashing of unstyled content. + appVersion.style.display = "block"; } - } - repositoryDescriptionUpdated(desc) { - this.setState({ - repoDescription: desc, - }); - } - - render() { - const { uiPrefs, runningTaskCount } = this.state; + let interval = setInterval(fetchTaskSummary, 5000) + fetchRepositoryDescription(); + return () => { + window.clearInterval(interval); + }; + }, [fetchRepositoryDescription, fetchTaskSummary]); - return ( - - - - - logo - - - - - + return ( + + + + + logo + + + + + - - -
{this.state.repoDescription}
-
+ + +
{repoDescription}
+
- - - - - - - - - - - - - - - - -
-
-
-
- ); - } -} + + + + + + + + + + + + + + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/SetupRepository.jsx b/src/components/SetupRepository.jsx index 485c153..43dbcd6 100644 --- a/src/components/SetupRepository.jsx +++ b/src/components/SetupRepository.jsx @@ -22,7 +22,7 @@ import { SetupRepositoryS3 } from './SetupRepositoryS3'; import { SetupRepositorySFTP } from './SetupRepositorySFTP'; import { SetupRepositoryToken } from './SetupRepositoryToken'; import { SetupRepositoryWebDAV } from './SetupRepositoryWebDAV'; -import { toAlgorithmOption } from '../utils/uiutil'; +import { toAlgorithmOption, repositoryUpdated } from '../utils/uiutil'; const supportedProviders = [ { provider: "filesystem", description: "Local Directory or NAS", component: SetupRepositoryFilesystem }, @@ -144,7 +144,7 @@ export class SetupRepository extends Component { request.clientOptions = this.clientOptions(); axios.post('/api/v1/repo/create', request).then(result => { - this.context.repositoryUpdated(true); + repositoryUpdated(true); }).catch(error => { if (error.response.data) { this.setState({ @@ -191,7 +191,7 @@ export class SetupRepository extends Component { this.setState({ isLoading: true }); axios.post('/api/v1/repo/connect', request).then(result => { this.setState({ isLoading: false }); - this.context.repositoryUpdated(true); + repositoryUpdated(true); }).catch(error => { this.setState({ isLoading: false }); if (error.response.data) { diff --git a/src/forms/index.jsx b/src/forms/index.jsx index ec5c418..b914018 100644 --- a/src/forms/index.jsx +++ b/src/forms/index.jsx @@ -1,4 +1,4 @@ -import { getDeepStateProperty, setDeepStateProperty } from '../utils/deepstate'; +import { getDeepStateProperty, setDeepStateProperty, setDeepStatePropertyReduce } from '../utils/deepstate'; export function validateRequiredFields(component, fields) { let updateState = {}; @@ -22,6 +22,25 @@ export function validateRequiredFields(component, fields) { return true; } +export function reducer(state, action) { + switch (action.type) { + case 'initial': { + return { + data: action.data + }; + } + case 'update': { + let st = setDeepStatePropertyReduce(state, action); + return { + data: st.data + } + } + default: { + throw Error('Unknown action: ' + action.type); + } + } +} + export function handleChange(event, valueGetter = x => x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } diff --git a/src/pages/Repository.jsx b/src/pages/Repository.jsx index 5e646cb..fc124da 100644 --- a/src/pages/Repository.jsx +++ b/src/pages/Repository.jsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import React, { Component } from 'react'; +import React, { useCallback, useLayoutEffect, useState, useContext, useReducer } from 'react'; import Badge from 'react-bootstrap/Badge'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; @@ -7,240 +7,198 @@ import Row from 'react-bootstrap/Row'; import Form from 'react-bootstrap/Form'; import InputGroup from 'react-bootstrap/InputGroup'; import Spinner from 'react-bootstrap/Spinner'; -import { handleChange } from '../forms'; import { SetupRepository } from '../components/SetupRepository'; -import { cancelTask, CLIEquivalent } from '../utils/uiutil'; +import { cancelTask, CLIEquivalent, repositoryUpdated } from '../utils/uiutil'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faChevronCircleDown, faChevronCircleUp, faWindowClose } from '@fortawesome/free-solid-svg-icons'; import { Logs } from '../components/Logs'; import { AppContext } from '../contexts/AppContext'; +import { reducer } from '../forms' -export class Repository extends Component { - constructor() { - super(); +export function Repository() { + const context = useContext(AppContext) + const [state, dispatch] = useReducer(reducer, {}) + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [showLog, setShowLog] = useState(false); - this.state = { - status: {}, - isLoading: true, - error: null, - provider: "", - description: "", - }; - - this.mounted = false; - this.disconnect = this.disconnect.bind(this); - this.updateDescription = this.updateDescription.bind(this); - this.handleChange = handleChange.bind(this); - this.fetchStatus = this.fetchStatus.bind(this); - this.fetchStatusWithoutSpinner = this.fetchStatusWithoutSpinner.bind(this); - } - - componentDidMount() { - this.mounted = true; - this.fetchStatus(this.props); - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchStatus() { - if (this.mounted) { - this.setState({ - isLoading: true, - }); - } - - this.fetchStatusWithoutSpinner(); - } - - fetchStatusWithoutSpinner() { + let mounted = false; + const fetchStatusWithoutSpinner = useCallback(() => { axios.get('/api/v1/repo/status').then(result => { - if (this.mounted) { - this.setState({ - status: result.data, - isLoading: false, + if (mounted) { + setIsLoading(false); + dispatch({ + type: 'initial', + data: result.data }); - // Update the app context to reflect the successfully-loaded description. - this.context.repositoryDescriptionUpdated(result.data.description); - + context.repoDescription = result.data.description; if (result.data.initTaskID) { window.setTimeout(() => { - this.fetchStatusWithoutSpinner(); + fetchStatusWithoutSpinner(); }, 1000); } } }).catch(error => { - if (this.mounted) { - this.setState({ - error, - isLoading: false - }) + if (mounted) { + setError(error); + setIsLoading(false); } }); - } + }, [context, mounted]) + + useLayoutEffect(() => { + mounted = true; + setIsLoading(true) + fetchStatusWithoutSpinner(); + return () => { + mounted = false + }; + }, [mounted, fetchStatusWithoutSpinner]); - disconnect() { - this.setState({ isLoading: true }) + function disconnect() { + setIsLoading(true) axios.post('/api/v1/repo/disconnect', {}).then(result => { - this.context.repositoryUpdated(false); - }).catch(error => this.setState({ - error, - isLoading: false - })); - } - - selectProvider(provider) { - this.setState({ provider }); - } - - updateDescription() { - this.setState({ - isLoading: true + repositoryUpdated(false); + }).catch(error => { + setError(error); + setIsLoading(false); }); + } + function updateDescription() { + setIsLoading(true); axios.post('/api/v1/repo/description', { - "description": this.state.status.description, + "description": state.data.description, }).then(result => { - // Update the app context to reflect the successfully-saved description. - this.context.repositoryDescriptionUpdated(result.data.description); - - this.setState({ - isLoading: false, - }); + context.repoDescription = result.data.description; + setIsLoading(false) }).catch(error => { - this.setState({ - isLoading: false, - }); + setIsLoading(false) }); } - render() { - let { isLoading, error } = this.state; - if (error) { - return

{error.message}

; - } - - if (isLoading) { - return ; - } - - if (this.state.status.initTaskID) { - return <>

 Initializing Repository...

- {this.state.showLog ? <> - - - : } -
- - ; - } + if (error) { + return

{error.message}

; + } + if (isLoading) { + return ; + } + if (state.data.initTaskID) { + return <>

 Initializing Repository...

+ {showLog ? <> + + + : } +
+ + ; + } - if (this.state.status.connected) { - return <> -

- - Connected To Repository -

-
+ if (state.data.connected) { + return <> +

+ + Connected To Repository +

+ + + + + dispatch({ + type: 'update', + source: e.target.name, + data: e.target.value + })} + size="sm" /> +   + + + Description Is Required + + + {state.data.readonly && + Repository is read-only + } +
+
+
+ {state.data.apiServerURL ? <> - - -   - - - Description Is Required + Server URL + - {this.state.status.readonly && - Repository is read-only - } -
-
-
- {this.state.status.apiServerURL ? <> - - - Server URL - - - - : <> - - - Config File - - - - - - Provider - - - - Encryption Algorithm - - - - Hash Algorithm - - - - Splitter Algorithm - - - - - - Repository Format - - - - Error Correction Overhead - 0 ? this.state.status.eccOverheadPercent + "%" : "Disabled"} /> - - - Error Correction Algorithm - - - - Internal Compression - - - - } + : <> - Connected as: - + Config File + -   - - - + + Provider + + + + Encryption Algorithm + + + + Hash Algorithm + + + + Splitter Algorithm + + -
+ + + Repository Format + + + + Error Correction Overhead + 0 ? state.data.eccOverheadPercent + "%" : "Disabled"} /> + + + Error Correction Algorithm + + + + Internal Compression + + + + } + + + Connected as: + + +   - - + + - ; - } - - return ; + +   + + + + + + ; } -} - -Repository.contextType = AppContext; + return ; +} \ No newline at end of file diff --git a/src/utils/deepstate.js b/src/utils/deepstate.js index 42c8af4..9a7d541 100644 --- a/src/utils/deepstate.js +++ b/src/utils/deepstate.js @@ -6,6 +6,30 @@ // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true +export function setDeepStatePropertyReduce(state, action) { + let newState = state; + let st = newState; + + const parts = action.source.split(/\./); + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + + if (st[part] === undefined) { + st[part] = {} + } else { + st[part] = { ...st[part] } + } + + st = st[part] + } + + const part = parts[parts.length - 1] + st[part] = action.data; + + return newState +} + export function setDeepStateProperty(component, name, value) { let newState = { ...component.state }; let st = newState; diff --git a/src/utils/uiutil.jsx b/src/utils/uiutil.jsx index 09562c1..04e936d 100644 --- a/src/utils/uiutil.jsx +++ b/src/utils/uiutil.jsx @@ -118,6 +118,15 @@ export function redirect(e) { } } + +export function repositoryUpdated(isConnected) { + if (isConnected) { + window.location.replace("/snapshots"); + } else { + window.location.replace("/repo"); + } +}; + /** * Convert a number of milliseconds into a string containing multiple units. * From 686104277ac5b835d37d8295b5068597d757ffe6 Mon Sep 17 00:00:00 2001 From: lupusA Date: Sun, 3 Dec 2023 16:08:09 +0100 Subject: [PATCH 4/6] Migrated SnapshotCreate. Improvements needed --- src/forms/index.jsx | 18 ++- src/pages/Repository.jsx | 53 ++++----- src/pages/SnapshotCreate.jsx | 215 +++++++++++++++++++---------------- 3 files changed, 155 insertions(+), 131 deletions(-) diff --git a/src/forms/index.jsx b/src/forms/index.jsx index b914018..97fb280 100644 --- a/src/forms/index.jsx +++ b/src/forms/index.jsx @@ -24,15 +24,21 @@ export function validateRequiredFields(component, fields) { export function reducer(state, action) { switch (action.type) { - case 'initial': { + case 'init': { return { - data: action.data + ...action.data }; } case 'update': { - let st = setDeepStatePropertyReduce(state, action); + let updatedState = setDeepStatePropertyReduce(state, action); return { - data: st.data + ...updatedState + } + } + case 'set': { + return { + ...state, + ...action.data } } default: { @@ -41,6 +47,10 @@ export function reducer(state, action) { } } +export function init(initialState) { + return initialState; +} + export function handleChange(event, valueGetter = x => x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } diff --git a/src/pages/Repository.jsx b/src/pages/Repository.jsx index fc124da..1a1d088 100644 --- a/src/pages/Repository.jsx +++ b/src/pages/Repository.jsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import React, { useCallback, useLayoutEffect, useState, useContext, useReducer } from 'react'; +import React, { useCallback, useLayoutEffect, useState, useContext, useReducer, useRef } from 'react'; import Badge from 'react-bootstrap/Badge'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; @@ -22,13 +22,14 @@ export function Repository() { const [error, setError] = useState(null); const [showLog, setShowLog] = useState(false); - let mounted = false; + let mounted = useRef(false); + const fetchStatusWithoutSpinner = useCallback(() => { axios.get('/api/v1/repo/status').then(result => { if (mounted) { setIsLoading(false); dispatch({ - type: 'initial', + type: 'init', data: result.data }); // Update the app context to reflect the successfully-loaded description. @@ -48,11 +49,11 @@ export function Repository() { }, [context, mounted]) useLayoutEffect(() => { - mounted = true; + mounted.current = true; setIsLoading(true) fetchStatusWithoutSpinner(); return () => { - mounted = false + mounted.current = false; }; }, [mounted, fetchStatusWithoutSpinner]); @@ -69,7 +70,7 @@ export function Repository() { function updateDescription() { setIsLoading(true); axios.post('/api/v1/repo/description', { - "description": state.data.description, + "description": state.description, }).then(result => { context.repoDescription = result.data.description; setIsLoading(false) @@ -84,18 +85,18 @@ export function Repository() { if (isLoading) { return ; } - if (state.data.initTaskID) { + if (state.initTaskID) { return <>

 Initializing Repository...

{showLog ? <> - + : }
- + ; } - if (state.data.connected) { + if (state.connected) { return <>

@@ -107,9 +108,9 @@ export function Repository() { dispatch({ type: 'update', source: e.target.name, @@ -122,67 +123,67 @@ export function Repository() { Description Is Required - {state.data.readonly && + {state.readonly && Repository is read-only }


- {state.data.apiServerURL ? <> + {state.apiServerURL ? <> Server URL - + : <> Config File - + Provider - + Encryption Algorithm - + Hash Algorithm - + Splitter Algorithm - + Repository Format - + Error Correction Overhead - 0 ? state.data.eccOverheadPercent + "%" : "Disabled"} /> + 0 ? state.eccOverheadPercent + "%" : "Disabled"} /> Error Correction Algorithm - + Internal Compression - + } Connected as: - +   diff --git a/src/pages/SnapshotCreate.jsx b/src/pages/SnapshotCreate.jsx index 7a85f1b..2a86812 100644 --- a/src/pages/SnapshotCreate.jsx +++ b/src/pages/SnapshotCreate.jsx @@ -1,109 +1,111 @@ import axios from 'axios'; -import React, { Component } from 'react'; +import React, { useState, useReducer, useEffect, useRef, useCallback } from 'react'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; -import { handleChange } from '../forms'; import { PolicyEditor } from '../components/policy-editor/PolicyEditor'; import { SnapshotEstimation } from '../components/SnapshotEstimation'; import { CLIEquivalent, DirectorySelector, errorAlert, GoBackButton, redirect } from '../utils/uiutil'; - -export class SnapshotCreate extends Component { - constructor() { - super(); - this.state = { - path: "", - estimateTaskID: null, - estimateTaskVisible: false, - lastEstimatedPath: "", - policyEditorVisibleFor: "n/a", - localUsername: null, - }; - - this.policyEditorRef = React.createRef(); - this.handleChange = handleChange.bind(this); - this.estimate = this.estimate.bind(this); - this.snapshotNow = this.snapshotNow.bind(this); - this.maybeResolveCurrentPath = this.maybeResolveCurrentPath.bind(this); - } - - componentDidMount() { +import { reducer, init } from '../forms' +import { useHistory } from 'react-router-dom/cjs/react-router-dom.min'; + +const initalState = { + localHost: "", + localUsername: "", + path: "", + lastEstimatedPath: "", + estimatingPath: "", + didEstimate: false, + estimateTaskID: null, + estimateTaskVisible: false, + policyEditorVisibleFor: "n/a" +}; + +export function SnapshotCreate() { + const [state, dispatch] = useReducer(reducer, initalState, init); + const [error, setError] = useState(null); + let history = useHistory() + + const policyEditorRef = useRef(); + const mounted = useRef(false); + + const fetchUser = useCallback(() => { axios.get('/api/v1/sources').then(result => { - this.setState({ + let newState = { localUsername: result.data.localUsername, - localHost: result.data.localHost, + localHost: result.data.localHost + }; + dispatch({ + type: 'set', + data: newState }); }).catch(error => { redirect(error); + setError(error); }); - } - - maybeResolveCurrentPath(lastResolvedPath) { - const currentPath = this.state.path; + }, []) + const resolvePath = useCallback((lastResolvedPath) => { + const currentPath = state.path; if (lastResolvedPath !== currentPath) { - if (this.state.path) { + if (state.path) { axios.post('/api/v1/paths/resolve', { path: currentPath }).then(result => { - this.setState({ + let newState = { lastResolvedPath: currentPath, - resolvedSource: result.data.source, + resolvedSource: result.data.source + }; + dispatch({ + type: 'set', + data: newState }); - - // check again, it's possible that this.state.path has changed - // while we were resolving - this.maybeResolveCurrentPath(currentPath); + resolvePath(currentPath); }).catch(error => { redirect(error); }); } else { - this.setState({ + let newState = { lastResolvedPath: currentPath, - resolvedSource: null, + resolvedSource: null + } + dispatch({ + type: 'set', + data: newState }); - - this.maybeResolveCurrentPath(currentPath); + resolvePath(currentPath); } } - } - - componentDidUpdate() { - this.maybeResolveCurrentPath(this.state.lastResolvedPath); - - if (this.state.estimateTaskVisible && this.state.lastEstimatedPath !== this.state.resolvedSource.path) { - this.setState({ - estimateTaskVisible: false, - }) - } - } + }, [state.path]) - estimate(e) { + function estimate(e) { e.preventDefault(); - - if (!this.state.resolvedSource.path) { + if (!state.resolvedSource.path) { return; } - - const pe = this.policyEditorRef.current; + const pe = policyEditorRef.current; if (!pe) { return; } - try { let req = { - root: this.state.resolvedSource.path, + root: state.resolvedSource.path, maxExamplesPerBucket: 10, policyOverride: pe.getAndValidatePolicy(), } axios.post('/api/v1/estimate', req).then(result => { - this.setState({ - lastEstimatedPath: this.state.resolvedSource.path, - estimateTaskID: result.data.id, + let newState = { + lastEstimatedPath: state.resolvedSource.path, estimatingPath: result.data.description, - estimateTaskVisible: true, didEstimate: false, - }) + estimateTaskID: result.data.id, + estimateTaskVisible: true + } + dispatch({ + type: 'set', + data: newState + }); + }).catch(error => { errorAlert(error); }); @@ -112,44 +114,46 @@ export class SnapshotCreate extends Component { } } - snapshotNow(e) { + function snapshotNow(e) { e.preventDefault(); - - if (!this.state.resolvedSource.path) { + if (!state.resolvedSource.path) { alert('Must specify directory to snapshot.'); return } - - const pe = this.policyEditorRef.current; - if (!pe) { + if (!policyEditorRef.current) { return; } - try { axios.post('/api/v1/sources', { - path: this.state.resolvedSource.path, + path: state.resolvedSource.path, createSnapshot: true, - policy: pe.getAndValidatePolicy(), - }).then(result => { - this.props.history.goBack(); + policy: policyEditorRef.current.getAndValidatePolicy(), + }).then(_ => { + history.goBack() }).catch(error => { errorAlert(error); - - this.setState({ - error, - isLoading: false - }); + setError(error); }); } catch (e) { errorAlert(e); + setError(error); } } - render() { - return <> + useEffect(() => { + if (!mounted.current) { + fetchUser() + mounted.current = true; + } else { + resolvePath(state.lastResolvedPath) + } + }, [mounted, fetchUser, resolvePath, state.lastResolvedPath]); + + return ( + <> - +    

New Snapshot

@@ -157,47 +161,56 @@ export class SnapshotCreate extends Component { - this.setState({ path: p })} autoFocus placeholder="enter path to snapshot" name="path" value={this.state.path} onChange={this.handleChange} /> + dispatch({ + type: 'update', + source: e.target.name, + data: e.target.value + })} + + onChange={e => dispatch({ + type: 'update', + source: e.target.name, + data: e.target.value + })} + autoFocus placeholder="enter path to snapshot" name="path" value={state.path} /> + onClick={estimate}>Estimate   + onClick={snapshotNow}>Snapshot Now - {this.state.estimateTaskID && this.state.estimateTaskVisible && - + {state.estimateTaskID && state.estimateTaskVisible && + }
- - {this.state.resolvedSource && + {state.resolvedSource && - {this.state.resolvedSource ? this.state.resolvedSource.path : this.state.path} + {state.resolvedSource ? state.resolvedSource.path : state.path} - + host={state.resolvedSource.host} + userName={state.resolvedSource.userName} + path={state.resolvedSource.path} /> } -   - - - ; - } + + + ) } From a6f091aac4affbc614f2667b88b9602b8891ac45 Mon Sep 17 00:00:00 2001 From: lupusA Date: Sun, 3 Dec 2023 16:45:51 +0100 Subject: [PATCH 5/6] Minor refactoring --- src/pages/SnapshotCreate.jsx | 74 +++++++++++++++++------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/pages/SnapshotCreate.jsx b/src/pages/SnapshotCreate.jsx index 2a86812..65d71a2 100644 --- a/src/pages/SnapshotCreate.jsx +++ b/src/pages/SnapshotCreate.jsx @@ -32,13 +32,12 @@ export function SnapshotCreate() { const fetchUser = useCallback(() => { axios.get('/api/v1/sources').then(result => { - let newState = { - localUsername: result.data.localUsername, - localHost: result.data.localHost - }; dispatch({ type: 'set', - data: newState + data: { + localUsername: result.data.localUsername, + localHost: result.data.localHost + } }); }).catch(error => { redirect(error); @@ -51,59 +50,54 @@ export function SnapshotCreate() { if (lastResolvedPath !== currentPath) { if (state.path) { axios.post('/api/v1/paths/resolve', { path: currentPath }).then(result => { - let newState = { - lastResolvedPath: currentPath, - resolvedSource: result.data.source - }; dispatch({ type: 'set', - data: newState + data: { + lastResolvedPath: currentPath, + resolvedSource: result.data.source + } }); resolvePath(currentPath); }).catch(error => { redirect(error); }); } else { - let newState = { - lastResolvedPath: currentPath, - resolvedSource: null - } dispatch({ type: 'set', - data: newState + data: { + lastResolvedPath: currentPath, + resolvedSource: null + } }); resolvePath(currentPath); } } }, [state.path]) - function estimate(e) { + function estimatePath(e) { e.preventDefault(); if (!state.resolvedSource.path) { return; } - const pe = policyEditorRef.current; - if (!pe) { + if (!policyEditorRef.current) { return; } try { - let req = { + let request = { root: state.resolvedSource.path, maxExamplesPerBucket: 10, - policyOverride: pe.getAndValidatePolicy(), + policyOverride: policyEditorRef.current.getAndValidatePolicy(), } - - axios.post('/api/v1/estimate', req).then(result => { - let newState = { - lastEstimatedPath: state.resolvedSource.path, - estimatingPath: result.data.description, - didEstimate: false, - estimateTaskID: result.data.id, - estimateTaskVisible: true - } + axios.post('/api/v1/estimate', request).then(result => { dispatch({ type: 'set', - data: newState + data: { + lastEstimatedPath: state.resolvedSource.path, + estimatingPath: result.data.description, + didEstimate: false, + estimateTaskID: result.data.id, + estimateTaskVisible: true + } }); }).catch(error => { @@ -114,7 +108,7 @@ export function SnapshotCreate() { } } - function snapshotNow(e) { + function snapshotPath(e) { e.preventDefault(); if (!state.resolvedSource.path) { alert('Must specify directory to snapshot.'); @@ -155,9 +149,10 @@ export function SnapshotCreate() { -    

New Snapshot

+
+
New Snapshot
-
+
@@ -183,7 +178,7 @@ export function SnapshotCreate() { disabled={!state.resolvedSource?.path} title="Estimate" variant="secondary" - onClick={estimate}>Estimate + onClick={estimatePath}>Estimate   + onClick={snapshotPath}>Snapshot Now {state.estimateTaskID && state.estimateTaskVisible && } -
+
{state.resolvedSource && - {state.resolvedSource ? state.resolvedSource.path : state.path} + {state.resolvedSource ? state.resolvedSource.path : state.path} - } -   + ) -} +} \ No newline at end of file From b08e8a4d77782251278f7db5f7d8345075b6a8f3 Mon Sep 17 00:00:00 2001 From: lupusA Date: Sun, 3 Dec 2023 19:58:58 +0100 Subject: [PATCH 6/6] Further migrating SnapshotDirectory.jsx --- src/forms/index.jsx | 15 ++- src/pages/SnapshotCreate.jsx | 55 +++++---- src/pages/SnapshotDirectory.jsx | 202 +++++++++++++++++--------------- 3 files changed, 156 insertions(+), 116 deletions(-) diff --git a/src/forms/index.jsx b/src/forms/index.jsx index 97fb280..bfb2dd7 100644 --- a/src/forms/index.jsx +++ b/src/forms/index.jsx @@ -22,6 +22,13 @@ export function validateRequiredFields(component, fields) { return true; } +/** + * + * @param {*} state + * @param {*} action + * @returns + * The new state + */ export function reducer(state, action) { switch (action.type) { case 'init': { @@ -47,7 +54,13 @@ export function reducer(state, action) { } } -export function init(initialState) { +/** + * + * @param {The intial state to set} initialState + * @returns + * The initial state + */ +export function initState(initialState) { return initialState; } diff --git a/src/pages/SnapshotCreate.jsx b/src/pages/SnapshotCreate.jsx index 65d71a2..cb099f6 100644 --- a/src/pages/SnapshotCreate.jsx +++ b/src/pages/SnapshotCreate.jsx @@ -7,7 +7,7 @@ import Row from 'react-bootstrap/Row'; import { PolicyEditor } from '../components/policy-editor/PolicyEditor'; import { SnapshotEstimation } from '../components/SnapshotEstimation'; import { CLIEquivalent, DirectorySelector, errorAlert, GoBackButton, redirect } from '../utils/uiutil'; -import { reducer, init } from '../forms' +import { reducer, initState } from '../forms' import { useHistory } from 'react-router-dom/cjs/react-router-dom.min'; const initalState = { @@ -23,7 +23,7 @@ const initalState = { }; export function SnapshotCreate() { - const [state, dispatch] = useReducer(reducer, initalState, init); + const [state, dispatch] = useReducer(reducer, initalState, initState); const [error, setError] = useState(null); let history = useHistory() @@ -74,6 +74,11 @@ export function SnapshotCreate() { } }, [state.path]) + /** + * + * @param {*} e + * @returns + */ function estimatePath(e) { e.preventDefault(); if (!state.resolvedSource.path) { @@ -108,6 +113,11 @@ export function SnapshotCreate() { } } + /** + * + * @param {*} e + * @returns + */ function snapshotPath(e) { e.preventDefault(); if (!state.resolvedSource.path) { @@ -146,13 +156,8 @@ export function SnapshotCreate() { return ( <> - - - -
-
New Snapshot
+
New Snapshot
-
@@ -179,7 +184,6 @@ export function SnapshotCreate() { title="Estimate" variant="secondary" onClick={estimatePath}>Estimate -   + {state.mountInfo.path ? <> + {window.kopiaUI && <> - + } - - + + : <> - + }     @@ -145,9 +159,9 @@ export class SnapshotDirectory extends Component {   - + - + - } + ) }