diff --git a/htmlui/src/App.css b/htmlui/src/App.css
index 27497a70f..8ab4ea15a 100644
--- a/htmlui/src/App.css
+++ b/htmlui/src/App.css
@@ -143,8 +143,7 @@ div.tab-body {
}
.logs-table {
- border: 1px solid #ccd;
- background-color: #f0f0ff;
+ background-color: #f8f8ff;
font-family: monospace;
font-size: 12px;
font-weight: bold;
@@ -175,12 +174,9 @@ div.tab-body {
padding-left: 40px;
}
-.estimate-results {
- margin: 10px;
- margin-left: 15px;
- padding: 15px;
- border: 1px solid #ccc;
- background-color: #f8f8ff;
+.estimateResults {
+ color: #008;
+ font-size: 95%;
}
.list-actions {
diff --git a/htmlui/src/App.js b/htmlui/src/App.js
index 5dda500ab..ca997748f 100644
--- a/htmlui/src/App.js
+++ b/htmlui/src/App.js
@@ -14,9 +14,9 @@ import { RepoStatus } from "./RepoStatus";
import { SnapshotsTable } from "./SnapshotsTable";
import { SourcesTable } from "./SourcesTable";
import { TaskDetails } from './TaskDetails';
-import { TasksTable } from './TasksTable';
+import { TasksTable } from './TasksTable';
import { NewSnapshot } from './NewSnapshot';
-import { PolicyEditor } from './PolicyEditor';
+import { PolicyEditorPage } from './PolicyEditorPage';
function useInterval(callback, delay) {
const savedCallback = useRef();
@@ -72,9 +72,9 @@ function App() {
-
+
-
+
diff --git a/htmlui/src/EstimateResults.js b/htmlui/src/EstimateResults.js
new file mode 100644
index 000000000..da1841c8b
--- /dev/null
+++ b/htmlui/src/EstimateResults.js
@@ -0,0 +1,116 @@
+
+import { faChevronCircleDown, faChevronCircleUp, faStopCircle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import axios from 'axios';
+import React, { Component } from 'react';
+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';
+
+export class EstimateResults extends Component {
+ constructor() {
+ super();
+ this.state = {
+ isLoading: true,
+ error: null,
+ showLog: false,
+ };
+
+ this.taskID = this.taskID.bind(this);
+ this.fetchTask = this.fetchTask.bind(this);
+
+ // poll frequently, we will stop as soon as the task ends.
+ this.interval = window.setInterval(() => this.fetchTask(this.props), 500);
+ }
+
+ componentDidMount() {
+ this.setState({
+ isLoading: true,
+ });
+
+ this.fetchTask(this.props);
+ }
+
+ componentWillUnmount() {
+ if (this.interval) {
+ window.clearInterval(this.interval);
+ }
+ }
+
+ taskID(props) {
+ return props.taskID || props.match.params.tid;
+ }
+
+ fetchTask(props) {
+ axios.get('/api/v1/tasks/' + this.taskID(props)).then(result => {
+ this.setState({
+ task: result.data,
+ isLoading: false,
+ });
+
+ if (result.data.endTime) {
+ window.clearInterval(this.interval);
+ this.interval = null;
+ }
+ }).catch(error => {
+ redirectIfNotConnected(error);
+ this.setState({
+ error,
+ isLoading: false
+ });
+ });
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps !== this.props) {
+ this.fetchTask(this.props);
+ }
+ }
+
+ taskStatusDescription(task) {
+ if (task.status === "RUNNING") {
+ return <> >
+ }
+
+ if (task.status === "SUCCESS") {
+ return "Total"
+ }
+
+ if (task.status === "CANCELED") {
+ return "(Canceled)"
+ }
+
+ return task.status;
+ }
+
+ render() {
+ const { task, isLoading, error } = this.state;
+ if (error) {
+ return
{error.message}
;
+ }
+
+ if (isLoading) {
+ return Loading ...
;
+ }
+
+ return <>
+ {task.counters &&
+ {this.taskStatusDescription(task)} Bytes: {sizeDisplayName(task.counters["Bytes"]?.value)} ({sizeDisplayName(task.counters["Excluded Bytes"]?.value)} excluded)
+ Files: {task.counters["Files"]?.value} ({task.counters["Excluded Files"]?.value} excluded)
+ Directories: {task.counters["Directories"]?.value} ({task.counters["Excluded Directories"]?.value} excluded)
+ Errors: {task.counters["Errors"]?.value} ({task.counters["Ignored Errors"]?.value} ignored)
+
+ }
+ {task.status === "RUNNING" && <>
+ cancelTask(task.id)} > Cancel
+ >}
+ {this.state.showLog ? <>
+ this.setState({ showLog: false })}> Hide Log
+
+ > : this.setState({ showLog: true })}> Show Log }
+ >
+ ;
+ }
+}
diff --git a/htmlui/src/NewSnapshot.js b/htmlui/src/NewSnapshot.js
index d4b79cee9..10539c5a7 100644
--- a/htmlui/src/NewSnapshot.js
+++ b/htmlui/src/NewSnapshot.js
@@ -1,15 +1,13 @@
-import { faWindowClose } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import axios from 'axios';
import React, { Component } 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, validateRequiredFields } from './forms';
+import { handleChange } from './forms';
import { PolicyEditor } from './PolicyEditor';
-import { TaskDetails } from './TaskDetails';
-import { cancelTask, DirectorySelector, errorAlert, GoBackButton, redirectIfNotConnected } from './uiutil';
+import { EstimateResults } from './EstimateResults';
+import { DirectorySelector, errorAlert, GoBackButton, redirectIfNotConnected } from './uiutil';
export class NewSnapshot extends Component {
constructor() {
@@ -18,6 +16,7 @@ export class NewSnapshot extends Component {
path: "",
estimateTaskID: null,
estimateTaskVisible: false,
+ lastEstimatedPath: "",
policyEditorVisibleFor: "n/a",
localUsername: null,
};
@@ -26,8 +25,6 @@ export class NewSnapshot extends Component {
this.handleChange = handleChange.bind(this);
this.estimate = this.estimate.bind(this);
this.snapshotNow = this.snapshotNow.bind(this);
- this.cancelEstimate = this.cancelEstimate.bind(this);
- this.togglePolicyEditor = this.togglePolicyEditor.bind(this);
}
componentDidMount() {
@@ -41,36 +38,46 @@ export class NewSnapshot extends Component {
});
}
- togglePolicyEditor() {
- if (!this.state.path) {
- return;
+ componentDidUpdate() {
+ if (this.state.lastResolvedPath !== this.state.path) {
+ axios.post('/api/v1/paths/resolve', { path: this.state.path }).then(result => {
+ this.setState({
+ lastResolvedPath: this.state.path,
+ resolvedSource: result.data.source,
+ });
+ }).catch(error => {
+ redirectIfNotConnected(error);
+ });
}
- if (this.state.policyEditorVisibleFor === this.state.path) {
+ if (this.state.estimateTaskVisible && this.state.lastEstimatedPath !== this.state.resolvedSource.path) {
this.setState({
- policyEditorVisibleFor: "n/a",
- });
- } else {
- this.setState({
- policyEditorVisibleFor: this.state.path,
- });
+ estimateTaskVisible: false,
+ })
}
}
estimate(e) {
e.preventDefault();
- if (!validateRequiredFields(this, ["path"])) {
+ if (!this.state.resolvedSource.path) {
+ return;
+ }
+
+ const pe = this.policyEditorRef.current;
+ if (!pe) {
return;
}
let req = {
- root: this.state.path,
+ root: this.state.resolvedSource.path,
maxExamplesPerBucket: 10,
+ policyOverride: pe.state.policy,
}
axios.post('/api/v1/estimate', req).then(result => {
this.setState({
+ lastEstimatedPath: this.state.resolvedSource.path,
estimateTaskID: result.data.id,
estimatingPath: result.data.description,
estimateTaskVisible: true,
@@ -81,22 +88,23 @@ export class NewSnapshot extends Component {
});
}
- cancelEstimate() {
- this.setState({ estimateTaskVisible: false });
- cancelTask(this.state.estimateTaskID);
- }
-
snapshotNow(e) {
e.preventDefault();
- if (!this.state.path) {
+ if (!this.state.resolvedSource.path) {
alert('Must specify directory to snapshot.');
return
}
+ const pe = this.policyEditorRef.current;
+ if (!pe) {
+ return;
+ }
+
axios.post('/api/v1/sources', {
- path: this.state.path,
+ path: this.state.resolvedSource.path,
createSnapshot: true,
+ policy: pe.state.policy,
}).then(result => {
this.props.history.goBack();
}).catch(error => {
@@ -121,62 +129,40 @@ export class NewSnapshot extends Component {
- this.setState({ path: p })} autoFocus placeholder="enter path to snapshot" name="path" value={this.state.path} onChange={this.handleChange}
- readOnly={this.state.policyEditorVisible || this.state.estimateTaskVisible} />
-
- Click the Estimate button to estimate the size of the snapshot.
- To specify frequency of snapshots or exclude some files, click Policy.
-
+ this.setState({ path: p })} autoFocus placeholder="enter path to snapshot" name="path" value={this.state.path} onChange={this.handleChange} />
Estimate
-
-
- Policy
+ onClick={this.estimate}>Estimate
+
+ Snapshot Now
{this.state.estimateTaskID && this.state.estimateTaskVisible &&
-
-
-
- Estimate Snapshot Size for {this.state.estimatingPath}...
-
-
-
+
}
- {this.state.path && this.state.policyEditorVisibleFor === this.state.path &&
-
-
-
-
}
+ {this.state.resolvedSource &&
+
+ {this.state.resolvedSource ? this.state.resolvedSource.path : this.state.path}
+
-
-
- Snapshot Now
-
-
+
+ }
>;
}
}
diff --git a/htmlui/src/PoliciesTable.js b/htmlui/src/PoliciesTable.js
index ababcfa2d..4ed35bd06 100644
--- a/htmlui/src/PoliciesTable.js
+++ b/htmlui/src/PoliciesTable.js
@@ -8,10 +8,10 @@ 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 { PolicyEditor } from './PolicyEditor';
import MyTable from './Table';
-import { compare, DirectorySelector, isAbsolutePath, ownerName, redirectIfNotConnected } from './uiutil';
+import { compare, DirectorySelector, isAbsolutePath, ownerName, policyEditorURL, redirectIfNotConnected } from './uiutil';
const localPolicies = "Local Policies"
const allPolicies = "All Policies"
@@ -31,7 +31,6 @@ export class PoliciesTable extends Component {
sources: [],
};
- this.editorClosed = this.editorClosed.bind(this);
this.editPolicyForPath = this.editPolicyForPath.bind(this);
this.handleChange = handleChange.bind(this);
this.fetchPolicies = this.fetchPolicies.bind(this);
@@ -94,13 +93,6 @@ export class PoliciesTable extends Component {
});
}
- editorClosed() {
- this.setState({
- editorTarget: null,
- });
- this.fetchPolicies();
- }
-
editPolicyForPath(e) {
e.preventDefault();
@@ -113,13 +105,11 @@ export class PoliciesTable extends Component {
return;
}
- this.setState({
- editorTarget: {
- userName: this.state.localUsername,
- host: this.state.localHost,
- path: this.state.policyPath,
- },
- })
+ this.props.history.push(policyEditorURL({
+ userName: this.state.localUsername,
+ host: this.state.localHost,
+ path: this.state.policyPath,
+ }));
}
selectOwner(h) {
@@ -233,12 +223,7 @@ export class PoliciesTable extends Component {
id: 'edit',
Header: '',
width: 50,
- Cell: x => {
- this.setState({
- editorTarget: x.row.original.target,
- selectedRowIsNew: false,
- })
- }}>Edit ,
+ Cell: x => Edit ,
}]
return
@@ -277,14 +262,12 @@ export class PoliciesTable extends Component {
}
- {items.length > 0 ?
+ {items.length > 0 ?
Found {items.length} policies matching criteria.
: ((this.state.selectedOwner === localPolicies && this.state.policyPath) ?
No policy found for directory {this.state.policyPath}. Click Set Policy to define it.
:
No policies found.
)}
-
- {this.state.editorTarget &&
}
;
}
}
diff --git a/htmlui/src/PolicyEditor.js b/htmlui/src/PolicyEditor.js
index d0a3c5e96..95a9d2905 100644
--- a/htmlui/src/PolicyEditor.js
+++ b/htmlui/src/PolicyEditor.js
@@ -1,4 +1,4 @@
-import { faCalendarTimes, faChevronLeft, faClock, faExclamationTriangle, faFileAlt, faFileArchive, faFolderOpen, faMagic } from '@fortawesome/free-solid-svg-icons';
+import { faCalendarTimes, faClock, faExclamationTriangle, faFileAlt, faFileArchive, faFolderOpen, faMagic } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import axios from 'axios';
import moment from 'moment';
@@ -10,24 +10,9 @@ import Row from 'react-bootstrap/Row';
import Spinner from 'react-bootstrap/Spinner';
import Accordion from 'react-bootstrap/Accordion';
import { handleChange, LogDetailSelector, OptionalBoolean, OptionalNumberField, RequiredBoolean, stateProperty, StringList, valueToNumber } from './forms';
-import { errorAlert, sourceQueryStringParams } from './uiutil';
+import { errorAlert, PolicyEditorLink, sourceQueryStringParams } from './uiutil';
import { getDeepStateProperty } from './deepstate';
-function PolicyTypeName(s) {
- if (!s.host && !s.userName) {
- return "Global Policy"
- }
-
- if (!s.userName) {
- return "Host: " + s.host;
- }
-
- if (!s.path) {
- return "User: " + s.userName + "@" + s.host;
- }
-
- return "Directory: " + s.userName + "@" + s.host + ":" + s.path;
-}
function LabelColumn(props) {
return
@@ -118,7 +103,7 @@ function UpcomingSnapshotTimes(times) {
function SectionHeaderRow() {
return
- Values
+
Effective
;
}
@@ -152,7 +137,11 @@ export class PolicyEditor extends Component {
});
}
- componentDidUpdate() {
+ componentDidUpdate(prevProps) {
+ if (sourceQueryStringParams(this.props) !== sourceQueryStringParams(prevProps)) {
+ this.fetchPolicy(this.props);
+ }
+
const pjs = JSON.stringify(this.state.policy);
if (pjs !== this.lastResolvedPolicy) {
this.resolvePolicy(this.props);
@@ -203,7 +192,7 @@ export class PolicyEditor extends Component {
return "(Defined by this policy)";
}
- return <>Defined by {PolicyTypeName(p)} >;
+ return <>Defined by {PolicyEditorLink(p)}>;
}
saveChanges(e) {
@@ -288,10 +277,7 @@ export class PolicyEditor extends Component {
}
return
- {!this.props.embedded &&
Return
- {PolicyTypeName(this.props)}}