Skip to content

Commit

Permalink
fix(ui): Fixed minor bugs in html-ui and added QOL changes (#177)
Browse files Browse the repository at this point in the history
* - Fixed minor bugs in html-ui
- Added placeholders within the policy editor for a unified representation
- Added headers for tables in policy and snapshot overview
- Minor refactorings

* - Fixed test of PolicyEditor due to changed import

---------

Co-authored-by: Christoph Anderson <[email protected]>
  • Loading branch information
lupusA and lupusA authored Sep 5, 2023
1 parent 672f3a2 commit 52e30d9
Show file tree
Hide file tree
Showing 13 changed files with 82 additions and 58 deletions.
4 changes: 4 additions & 0 deletions public/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
div#appVersion {
display: none
}

3 changes: 2 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="index.css">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Kopia UI" />
Expand All @@ -15,7 +16,7 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="appVersion" style="display: none">
<div id="appVersion">
<hr />
<p class="version-info">Version %REACT_APP_FULL_VERSION_INFO%</p>
</div>
Expand Down
28 changes: 14 additions & 14 deletions src/PoliciesTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class PoliciesTable extends Component {
constructor() {
super();
this.state = {
items: [],
policies: [],
isLoading: false,
error: null,
editorTarget: null,
Expand Down Expand Up @@ -64,7 +64,7 @@ export class PoliciesTable extends Component {
fetchPolicies() {
axios.get('/api/v1/policies').then(result => {
this.setState({
items: result.data.policies,
policies: result.data.policies,
isLoading: false,
});
}).catch(error => {
Expand Down Expand Up @@ -166,7 +166,7 @@ export class PoliciesTable extends Component {
}

render() {
let { items, sources, isLoading, error } = this.state;
let { policies, sources, isLoading, error } = this.state;
if (error) {
return <p>{error.message}</p>;
}
Expand All @@ -189,31 +189,31 @@ export class PoliciesTable extends Component {
break;

case globalPolicy:
items = items.filter(x => this.isGlobalPolicy(x));
policies = policies.filter(x => this.isGlobalPolicy(x));
break;

case localPolicies:
items = items.filter(x => this.isLocalUserPolicy(x));
policies = policies.filter(x => this.isLocalUserPolicy(x));
break;

case applicablePolicies:
items = items.filter(x => this.isLocalUserPolicy(x) || this.isLocalHostPolicy(x) || this.isGlobalPolicy(x));
policies = policies.filter(x => this.isLocalUserPolicy(x) || this.isLocalHostPolicy(x) || this.isGlobalPolicy(x));
break;

case perUserPolicies:
items = items.filter(x => !!x.target.userName && !!x.target.host && !x.target.path);
policies = policies.filter(x => !!x.target.userName && !!x.target.host && !x.target.path);
break;

case perHostPolicies:
items = items.filter(x => !x.target.userName && !!x.target.host && !x.target.path);
policies = policies.filter(x => !x.target.userName && !!x.target.host && !x.target.path);
break;

default:
items = items.filter(x => ownerName(x.target) === this.state.selectedOwner);
policies = policies.filter(x => ownerName(x.target) === this.state.selectedOwner);
break;
};

items.sort((l,r) => {
policies.sort((l,r) => {
const hc = compare(l.target.host,r.target.host);
if (hc) {
return hc;
Expand Down Expand Up @@ -242,7 +242,7 @@ export class PoliciesTable extends Component {
accessor: x => this.policySummary(x),
}, {
id: 'edit',
Header: '',
Header: 'Actions',
width: 50,
Cell: x => <Button data-testid="edit-policy" as={Link} to={policyEditorURL(x.row.original.target)} variant="primary" size="sm">Edit</Button>
}]
Expand Down Expand Up @@ -284,9 +284,9 @@ export class PoliciesTable extends Component {
</Form>
</div>}

{items.length > 0 ? <div>
<p>Found {items.length} policies matching criteria.</p>
<MyTable data={items} columns={columns} />
{policies.length > 0 ? <div>
<p>Found {policies.length} policies matching criteria.</p>
<MyTable data={policies} columns={columns} />
</div> : ((this.state.selectedOwner === localPolicies && this.state.policyPath) ? <p>
No policy found for directory <code>{this.state.policyPath}</code>. Click <b>Set Policy</b> to define it.
</p> : <p>No policies found.</p>)}
Expand Down
22 changes: 9 additions & 13 deletions src/PolicyEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ export class PolicyEditor extends Component {
<Accordion.Body>
<SectionHeaderRow />
<Row>
<LabelColumn name="Ignore Files" help="List of file and directory names to ignore (specified as relative to the directory they are in and not absolute; wildcards are allowed)" />
<WideValueColumn>{StringList(this, "policy.files.ignore")}</WideValueColumn>
<LabelColumn name="Ignore Files" help={<> List of file and directory names to ignore. <br /> (See <a target="_blank" rel="noreferrer" href="https://kopia.io/docs/advanced/kopiaignore/">documentation on ignoring files</a>).</>} />
<WideValueColumn>{StringList(this, "policy.files.ignore", { placeholder: "e.g. /file.txt" })}</WideValueColumn>
{EffectiveTextAreaValue(this, "files.ignore")}
</Row>
<Row>
Expand All @@ -311,7 +311,7 @@ export class PolicyEditor extends Component {
</Row>
<Row>
<LabelColumn name="Ignore Rule Files" help="List of additional files containing ignore rules (each file configures ignore rules for the directory and its subdirectories)" />
<ValueColumn>{StringList(this, "policy.files.ignoreDotFiles")}</ValueColumn>
<ValueColumn>{StringList(this, "policy.files.ignoreDotFiles", { placeholder: "e.g. .kopiaignore" })}</ValueColumn>
{EffectiveTextAreaValue(this, "files.ignoreDotFiles")}
</Row>
<Row>
Expand Down Expand Up @@ -382,14 +382,14 @@ export class PolicyEditor extends Component {
<Row>
<LabelColumn name="Only Compress Extensions" help="Only compress files with the following file extensions (one extension per line)" />
<WideValueColumn>
{StringList(this, "policy.compression.onlyCompress")}
{StringList(this, "policy.compression.onlyCompress", { placeholder: "e.g. *.txt" })}
</WideValueColumn>
{EffectiveTextAreaValue(this, "compression.onlyCompress")}
</Row>
<Row>
<LabelColumn name="Never Compress Extensions" help="Never compress the following file extensions (one extension per line)" />
<WideValueColumn>
{StringList(this, "policy.compression.neverCompress")}
{StringList(this, "policy.compression.neverCompress", { placeholder: "e.g. *.mp4" })}
</WideValueColumn>
{EffectiveTextAreaValue(this, "compression.neverCompress")}
</Row>
Expand Down Expand Up @@ -420,19 +420,17 @@ export class PolicyEditor extends Component {
{EffectiveValue(this, "scheduling.intervalSeconds")}
</Row>
<Row>
<LabelColumn name="Times Of Day" help="Create snapshots at the specified times of day (one per line and listed in 24hr time, ex: 17:00)" />
<LabelColumn name="Times Of Day" help="Create snapshots at the specified times of day (24hr format)" />
<ValueColumn>
{TimesOfDayList(this, "policy.scheduling.timeOfDay")}
{TimesOfDayList(this, "policy.scheduling.timeOfDay", { placeholder: "e.g. 17:00" })}
</ValueColumn>
{EffectiveTimesOfDayValue(this, "scheduling.timeOfDay")}
</Row>
<Row>
<LabelColumn name="Cron Expressions" help={<>Snapshot schedules using UNIX crontab syntax (one per line):
<br/><br/><pre>minute hour day month weekday #comment</pre>

See <a target="_blank" rel="noreferrer" href="https://github.com/hashicorp/cronexpr#implementation">supported format details</a>.</>} />
<br /> See <a target="_blank" rel="noreferrer" href="https://github.com/hashicorp/cronexpr#implementation">supported format details</a>.</>} />
<ValueColumn>
{StringList(this, "policy.scheduling.cron")}
{StringList(this, "policy.scheduling.cron", { placeholder: "minute hour day month weekday #comment" })}
</ValueColumn>
{EffectiveListValue(this, "scheduling.cron")}
</Row>
Expand Down Expand Up @@ -572,8 +570,6 @@ export class PolicyEditor extends Component {
&nbsp;
<Spinner animation="border" variant="primary" size="sm" />
</>}
{/* <pre className="debug-json">{JSON.stringify(this.state, null, 4)}
</pre> */}
</Form>
</>;
}
Expand Down
4 changes: 2 additions & 2 deletions src/PreferencesView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class PreferencesView extends Component {
<form>
<div className='form-group'>
<label className='label-description' id='themeLabel'>Theme</label>
<select className="select_theme, form-select form-select-sm" value={theme} onChange={e => setTheme(e.target.value)}>
<select className="select_theme, form-select form-select-sm" title='Select theme' value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">light</option>
<option value="dark">dark</option>
<option value="pastel">pastel</option>
Expand All @@ -19,7 +19,7 @@ export class PreferencesView extends Component {
<br />
<div className='form-group'>
<label className='label-description'>Byte representation</label>
<select className="form-select form-select-sm" id='bytesBaseInput' value={bytesStringBase2} onChange={e => setByteStringBase(e.target.value)}>
<select className="form-select form-select-sm" title='Select byte representation' id='bytesBaseInput' value={bytesStringBase2} onChange={e => setByteStringBase(e.target.value)}>
<option value="true">Base-2 (KiB, MiB, GiB, TiB)</option>
<option value="false">Base-10 (KB, MB, GB, TB)</option>
</select>
Expand Down
14 changes: 7 additions & 7 deletions src/RepoStatus.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ export class RepoStatus extends Component {
if (this.state.status.initTaskID) {
return <><h4><Spinner animation="border" variant="primary" size="sm" />&nbsp;Initializing Repository...</h4>
{this.state.showLog ? <>
<Button size="sm" variant="light" onClick={() => this.setState({ showLog: false })}><FontAwesomeIcon icon={faChevronCircleUp} /> Hide Log</Button>
<TaskLogs taskID={this.state.status.initTaskID} />
</> : <Button size="sm" variant="light" onClick={() => this.setState({ showLog: true })}><FontAwesomeIcon icon={faChevronCircleDown} /> Show Log</Button>}
<hr/>
<Button size="sm" variant="light" onClick={() => this.setState({ showLog: false })}><FontAwesomeIcon icon={faChevronCircleUp} /> Hide Log</Button>
<TaskLogs taskID={this.state.status.initTaskID} />
</> : <Button size="sm" variant="light" onClick={() => this.setState({ showLog: true })}><FontAwesomeIcon icon={faChevronCircleDown} /> Show Log</Button>}
<hr />
<Button size="sm" variant="danger" icon={faWindowClose} title="Cancel" onClick={() => cancelTask(this.state.status.initTaskID)}>Cancel Connection</Button>
</>;
}
Expand All @@ -143,7 +143,7 @@ export class RepoStatus extends Component {
<FontAwesomeIcon icon={faCheck} style={{ marginRight: 4 }} />
<span>Connected To Repository</span>
</p>
<Form onSubmit={this.updateDescription}>
<Form>
<Row>
<Form.Group as={Col}>
<InputGroup>
Expand All @@ -155,7 +155,7 @@ export class RepoStatus extends Component {
onChange={this.handleChange}
size="sm" />
&nbsp;
<Button data-testid='update-description' size="sm" type="submit">Update Description</Button>
<Button data-testid='update-description' size="sm" onClick={this.updateDescription} type="button">Update Description</Button>
</InputGroup>
<Form.Control.Feedback type="invalid">Description Is Required</Form.Control.Feedback>
</Form.Group>
Expand Down Expand Up @@ -209,7 +209,7 @@ export class RepoStatus extends Component {
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Error Correction Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.ecc||"-"} />
<Form.Control readOnly defaultValue={this.state.status.ecc || "-"} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Internal Compression</Form.Label>
Expand Down
4 changes: 2 additions & 2 deletions src/SnapshotsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export class SnapshotsTable extends Component {

const columns = [{
id: 'selected',
Header: '',
Header: 'Selected',
width: 20,
align: "center",
Cell: x => <div className="form-check multiselect"><input type="checkbox" className="form-check-input" checked={this.isSelected(x.row.original)} onChange={() => this.toggleSelected(x.row.original)} /></div>,
Expand Down Expand Up @@ -397,7 +397,7 @@ export class SnapshotsTable extends Component {
</Col>
<Col xs="auto">
<Button size="sm" variant="primary">
{this.state.isRefreshing ? <Spinner animation="border" variant="light" size="sm" /> : <FontAwesomeIcon icon={faSync} onClick={this.fetchSnapshots} />}
{this.state.isRefreshing ? <Spinner animation="border" variant="light" size="sm" /> : <FontAwesomeIcon icon={faSync} title="Fetch snapshots" onClick={this.fetchSnapshots} />}
</Button>
</Col>
</Row>
Expand Down
28 changes: 26 additions & 2 deletions src/SourcesTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,31 @@ export class SourcesTable extends Component {
});
}

/**
* Sets the header of an cell dynamically based on it's status
* @param x - the cell which status is interpreted
* @returns - the header of the cell
*/
setHeader(x) {
switch (x.cell.value) {
case "IDLE":
case "PAUSED":
return x.cell.column.Header = "Actions"
case "PENDING":
case "UPLOADING":
return x.cell.column.Header = "Status"
default:
return x.cell.column.Header = ""
}
}

/**
* Sets the content an cell dynamically based on it's status
* @param x - the cell which content is changed
* @returns - the content of the cell
*/
statusCell(x, parent, bytesStringBase2) {
this.setHeader(x)
switch (x.cell.value) {
case "IDLE":
case "PAUSED":
Expand Down Expand Up @@ -262,7 +286,7 @@ export class SourcesTable extends Component {
Header: '',
width: 300,
accessor: x => x.status,
Cell: x => this.statusCell(x, this, bytesStringBase2),
Cell: x => this.statusCell(x, this, bytesStringBase2)
}]

return <>
Expand All @@ -288,7 +312,7 @@ export class SourcesTable extends Component {
<Col>
</Col>
<Col xs="auto">
<Button size="sm" variant="primary">
<Button size="sm" title="Synchronize" variant="primary">
{this.state.isRefreshing ? <Spinner animation="border" variant="light" size="sm" /> : <FontAwesomeIcon icon={faSync} onClick={this.sync} />}
</Button>
</Col>
Expand Down
16 changes: 7 additions & 9 deletions src/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,15 @@ export default function MyTable({ columns, data }) {
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(
(row, i) => {
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
}
<tr {...row.getRowProps()} key={i}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)}
)}
</tbody>
</Table>
Expand Down
5 changes: 3 additions & 2 deletions src/forms/StringList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ function multilineStringToList(target) {
return v.split(/\n/);
}

export function StringList(component, name) {
export function StringList(component, name, props = {}) {
return <Form.Group as={Col}>
<Form.Control
size="sm"
name={name}
value={listToMultilineString(stateProperty(component, name))}
onChange={e => component.handleChange(e, multilineStringToList)}
as="textarea"
rows="5">
rows="5"
{...props}>
</Form.Control>
</Form.Group>;
}
5 changes: 3 additions & 2 deletions src/forms/TimesOfDayList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Form from 'react-bootstrap/Form';
import FormGroup from 'react-bootstrap/FormGroup';
import { stateProperty } from '.';

export function TimesOfDayList(component, name) {
export function TimesOfDayList(component, name, props = {}) {
function parseTimeOfDay(v) {
var re = /(\d+):(\d+)/;

Expand Down Expand Up @@ -65,7 +65,8 @@ export function TimesOfDayList(component, name) {
value={toMultilineString(stateProperty(component, name))}
onChange={e => component.handleChange(e, fromMultilineString)}
as="textarea"
rows="5">
rows="5"
{...props}>
</Form.Control>
<Form.Control.Feedback type="invalid">Invalid Times of Day</Form.Control.Feedback>
</FormGroup>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, waitFor, logDOM } from '@testing-library/react';
import React from 'react';
import { PolicyEditor } from '.';
import { PolicyEditor } from '../PolicyEditor/.';
import { MemoryRouter } from 'react-router-dom';
import { setupAPIMock } from '../tests/api_mocks';
import moment from 'moment';
Expand Down
Loading

0 comments on commit 52e30d9

Please sign in to comment.