Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): Migrate to functions #215

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 80 additions & 110 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
<Router>
<AppContext.Provider value={this}>
<UIPreferenceProvider initalValue={uiPrefs}>
<Navbar expand="sm" variant="light">
<Navbar.Brand href="/"><img src="/kopia-flat.svg" className="App-logo" alt="logo" /></Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<NavLink data-testid="tab-snapshots" className="nav-link" activeClassName="active" to="/snapshots">Snapshots</NavLink>
<NavLink data-testid="tab-policies" className="nav-link" activeClassName="active" to="/policies">Policies</NavLink>
<NavLink data-testid="tab-tasks" className="nav-link" activeClassName="active" to="/tasks">Tasks <>
{runningTaskCount > 0 && <>({runningTaskCount})</>}
</>
</NavLink>
<NavLink data-testid="tab-repo" className="nav-link" activeClassName="active" to="/repo">Repository</NavLink>
<NavLink data-testid="tab-preferences" className="nav-link" activeClassName="active" to="/preferences">Preferences</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>
return (
<Router>
<AppContext.Provider value={context}>
<UIPreferenceProvider initalValue={uiPrefs}>
<Navbar expand="sm" variant="light">
<Navbar.Brand href="/"><img src="/kopia-flat.svg" className="App-logo" alt="logo" /></Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<NavLink data-testid="tab-snapshots" className="nav-link" activeClassName="active" to="/snapshots">Snapshots</NavLink>
<NavLink data-testid="tab-policies" className="nav-link" activeClassName="active" to="/policies">Policies</NavLink>
<NavLink data-testid="tab-tasks" className="nav-link" activeClassName="active" to="/tasks">Tasks <>
{runningTaskCount > 0 && <>({runningTaskCount})</>}
</>
</NavLink>
<NavLink data-testid="tab-repo" className="nav-link" activeClassName="active" to="/repo">Repository</NavLink>
<NavLink data-testid="tab-preferences" className="nav-link" activeClassName="active" to="/preferences">Preferences</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>

<Container fluid>
<NavLink to="/repo" style={{ color: "inherit", textDecoration: "inherit" }}>
<h5 className="mb-4">{this.state.repoDescription}</h5>
</NavLink>
<Container fluid>
<NavLink to="/repo" style={{ color: "inherit", textDecoration: "inherit" }}>
<h5 className="mb-4">{repoDescription}</h5>
</NavLink>

<Switch>
<Route path="/snapshots/new" component={SnapshotCreate} />
<Route path="/snapshots/single-source/" component={SnapshotHistory} />
<Route path="/snapshots/dir/:oid/restore" component={SnapshotRestore} />
<Route path="/snapshots/dir/:oid" component={SnapshotDirectory} />
<Route path="/snapshots" component={Snapshots} />
<Route path="/policies/edit/" component={Policy} />
<Route path="/policies" component={Policies} />
<Route path="/tasks/:tid" component={Task} />
<Route path="/tasks" component={Tasks} />
<Route path="/repo" component={Repository} />
<Route path="/preferences" component={Preferences} />
<Route exact path="/">
<Redirect to="/snapshots" />
</Route>
</Switch>
</Container>
</UIPreferenceProvider>
</AppContext.Provider>
</Router>
);
}
}
<Switch>
<Route path="/snapshots/new" component={SnapshotCreate} />
<Route path="/snapshots/single-source/" component={SnapshotHistory} />
<Route path="/snapshots/dir/:oid/restore" component={SnapshotRestore} />
<Route path="/snapshots/dir/:oid" component={SnapshotDirectory} />
<Route path="/snapshots" component={Snapshots} />
<Route path="/policies/edit/" component={Policy} />
<Route path="/policies" component={Policies} />
<Route path="/tasks/:tid" component={Task} />
<Route path="/tasks" component={Tasks} />
<Route path="/repo" component={Repository} />
<Route path="/preferences" component={Preferences} />
<Route exact path="/">
<Redirect to="/snapshots" />
</Route>
</Switch>
</Container>
</UIPreferenceProvider>
</AppContext.Provider>
</Router>
);
}
6 changes: 3 additions & 3 deletions src/components/SetupRepository.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 43 additions & 1 deletion src/forms/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDeepStateProperty, setDeepStateProperty } from '../utils/deepstate';
import { getDeepStateProperty, setDeepStateProperty, setDeepStatePropertyReduce } from '../utils/deepstate';

export function validateRequiredFields(component, fields) {
let updateState = {};
Expand All @@ -22,6 +22,48 @@ 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': {
return {
...action.data
};
}
case 'update': {
let updatedState = setDeepStatePropertyReduce(state, action);
return {
...updatedState
}
}
case 'set': {
return {
...state,
...action.data
}
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

/**
*
* @param {The intial state to set} initialState
* @returns
* The initial state
*/
export function initState(initialState) {
return initialState;
}

export function handleChange(event, valueGetter = x => x.value) {
setDeepStateProperty(this, event.target.name, valueGetter(event.target));
}
Expand Down
41 changes: 17 additions & 24 deletions src/pages/Policy.jsx
Original file line number Diff line number Diff line change
@@ -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 <>
<h4>
<GoBackButton onClick={this.props.history.goBack} />
&nbsp;&nbsp;{PolicyTypeName(source)}</h4>
<PolicyEditor ref={this.editorRef} userName={userName} host={host} path={path} close={this.props.history.goBack} />
<Row><Col>&nbsp;</Col></Row>
<Row>
<Col xs={12}>
<CLIEquivalent command={`policy set "${userName}@${host}:${path}"`} />
</Col>
</Row>
</>;
}
return <>
<h4>
<GoBackButton onClick={history.goBack} />
&nbsp;&nbsp;{PolicyTypeName(source)}</h4>
<PolicyEditor ref={editorRef} userName={userName} host={host} path={path} close={history.goBack} />
<Row><Col>&nbsp;</Col></Row>
<Row>
<Col xs={12}>
<CLIEquivalent command={`policy set "${userName}@${host}:${path}"`} />
</Col>
</Row>
</>;
}
Loading