diff --git a/htmlui/src/NewSnapshot.js b/htmlui/src/NewSnapshot.js
index 10539c5a7..15179b03b 100644
--- a/htmlui/src/NewSnapshot.js
+++ b/htmlui/src/NewSnapshot.js
@@ -69,23 +69,27 @@ export class NewSnapshot extends Component {
return;
}
- let req = {
- root: this.state.resolvedSource.path,
- maxExamplesPerBucket: 10,
- policyOverride: pe.state.policy,
- }
+ try {
+ let req = {
+ root: this.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,
- estimatingPath: result.data.description,
- estimateTaskVisible: true,
- didEstimate: false,
- })
- }).catch(error => {
- errorAlert(error);
- });
+ 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,
+ didEstimate: false,
+ })
+ }).catch(error => {
+ errorAlert(error);
+ });
+ } catch (e) {
+ errorAlert(e);
+ }
}
snapshotNow(e) {
@@ -101,20 +105,24 @@ export class NewSnapshot extends Component {
return;
}
- axios.post('/api/v1/sources', {
- path: this.state.resolvedSource.path,
- createSnapshot: true,
- policy: pe.state.policy,
- }).then(result => {
- this.props.history.goBack();
- }).catch(error => {
- errorAlert(error);
+ try {
+ axios.post('/api/v1/sources', {
+ path: this.state.resolvedSource.path,
+ createSnapshot: true,
+ policy: pe.getAndValidatePolicy(),
+ }).then(result => {
+ this.props.history.goBack();
+ }).catch(error => {
+ errorAlert(error);
- this.setState({
- error,
- isLoading: false
+ this.setState({
+ error,
+ isLoading: false
+ });
});
- });
+ } catch (e) {
+ errorAlert(e);
+ }
}
render() {
diff --git a/htmlui/src/PolicyEditor.js b/htmlui/src/PolicyEditor.js
index 95a9d2905..e7289f990 100644
--- a/htmlui/src/PolicyEditor.js
+++ b/htmlui/src/PolicyEditor.js
@@ -9,7 +9,7 @@ import Form from 'react-bootstrap/Form';
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 { handleChange, LogDetailSelector, OptionalBoolean, OptionalNumberField, RequiredBoolean, stateProperty, StringList, TimesOfDayList, valueToNumber } from './forms';
import { errorAlert, PolicyEditorLink, sourceQueryStringParams } from './uiutil';
import { getDeepStateProperty } from './deepstate';
@@ -95,7 +95,7 @@ function UpcomingSnapshotTimes(times) {
- {times.map(x => - {moment(x).format('L LT')} ({moment(x).fromNow()})
)}
+ {times.map(x => - {moment(x).format('L LT')} ({moment(x).fromNow()})
)}
>;
}
@@ -125,6 +125,7 @@ export class PolicyEditor extends Component {
this.policyURL = this.policyURL.bind(this);
this.resolvePolicy = this.resolvePolicy.bind(this);
this.PolicyDefinitionPoint = this.PolicyDefinitionPoint.bind(this);
+ this.getAndValidatePolicy = this.getAndValidatePolicy.bind(this);
}
componentDidMount() {
@@ -174,13 +175,17 @@ export class PolicyEditor extends Component {
resolvePolicy(props) {
const u = '/api/v1/policy/resolve?' + sourceQueryStringParams(props);
- axios.post(u, {
- "updates": this.state.policy,
- "numUpcomingSnapshotTimes": 5,
- }).then(result => {
- this.setState({ resolved: result.data });
- }).catch(error => {
- });
+ try {
+ axios.post(u, {
+ "updates": this.getAndValidatePolicy(),
+ "numUpcomingSnapshotTimes": 5,
+ }).then(result => {
+ this.setState({ resolved: result.data });
+ }).catch(error => {
+ });
+ }
+ catch (e) {
+ }
}
PolicyDefinitionPoint(p) {
@@ -195,9 +200,7 @@ export class PolicyEditor extends Component {
return <>Defined by {PolicyEditorLink(p)}>;
}
- saveChanges(e) {
- e.preventDefault()
-
+ getAndValidatePolicy() {
function removeEmpty(l) {
if (!l) {
return l;
@@ -216,7 +219,18 @@ export class PolicyEditor extends Component {
return result;
}
- // clean up policy before saving
+ function validateTimesOfDay(l) {
+ for (const tod of l) {
+ if (!tod.hour) {
+ // unparsed
+ throw Error("invalid time of day: '" + tod + "'")
+ }
+ }
+
+ return l;
+ }
+
+ // clone and clean up policy before saving
let policy = JSON.parse(JSON.stringify(this.state.policy));
if (policy.files) {
if (policy.files.ignore) {
@@ -236,13 +250,32 @@ export class PolicyEditor extends Component {
}
}
- this.setState({ saving: true });
- axios.put(this.policyURL(this.props), policy).then(result => {
- this.props.close();
- }).catch(error => {
- this.setState({ saving: false });
- errorAlert(error, 'Error saving policy');
- });
+ if (policy.scheduling) {
+ if (policy.scheduling.timeOfDay) {
+ policy.scheduling.timeOfDay = validateTimesOfDay(removeEmpty(policy.scheduling.timeOfDay));
+ }
+ }
+
+ return policy;
+ }
+
+ saveChanges(e) {
+ e.preventDefault()
+
+ try {
+ const policy = this.getAndValidatePolicy();
+
+ this.setState({ saving: true });
+ axios.put(this.policyURL(this.props), policy).then(result => {
+ this.props.close();
+ }).catch(error => {
+ this.setState({ saving: false });
+ errorAlert(error, 'Error saving policy');
+ });
+ } catch (e) {
+ errorAlert(e);
+ return
+ }
}
deletePolicy() {
@@ -439,6 +472,13 @@ export class PolicyEditor extends Component {
{EffectiveValue(this, "scheduling.intervalSeconds")}
+
+
+
+ {TimesOfDayList(this, "policy.scheduling.timeOfDay")}
+
+ {EffectiveBooleanValue(this, "scheduling.manual")}
+
diff --git a/htmlui/src/forms.js b/htmlui/src/forms.js
index 16498edbd..1c99177c2 100644
--- a/htmlui/src/forms.js
+++ b/htmlui/src/forms.js
@@ -214,14 +214,67 @@ export function StringList(component, name) {
}
-export function TimesOfDayList(component, name, tempName) {
+export function TimesOfDayList(component, name) {
+ function parseTimeOfDay(v) {
+ var re = /(\d+):(\d+)/;
+
+ const match = re.exec(v);
+ if (match) {
+ const h = parseInt(match[1]);
+ const m = parseInt(match[2]);
+ let valid = (h < 24 && m < 60);
+
+ if (m < 10 && match[2].length === 1) {
+ valid = false;
+ }
+
+ if (valid) {
+ return {hour: h, min: m}
+ }
+ }
+
+ return v;
+ }
+
+ function toMultilineString(v) {
+ if (v) {
+ let tmp = [];
+
+ for (const tod of v) {
+ if (tod.hour) {
+ tmp.push(tod.hour + ":" + (tod.min < 10 ? "0": "") + tod.min);
+ } else {
+ tmp.push(tod);
+ }
+ }
+
+ return tmp.join("\n");
+ }
+
+ return "";
+ }
+
+ function fromMultilineString(target) {
+ const v = target.value;
+ if (v === "") {
+ return undefined;
+ }
+
+ let result = [];
+
+ for (const line of v.split(/\n/)) {
+ result.push(parseTimeOfDay(line));
+ };
+
+ return result;
+ }
+
return
component.handleChange(e, multilineStringToList)}
+ value={toMultilineString(stateProperty(component, name))}
+ onChange={e => component.handleChange(e, fromMultilineString)}
as="textarea"
rows="5">
diff --git a/htmlui/src/uiutil.js b/htmlui/src/uiutil.js
index b96652223..629d27356 100644
--- a/htmlui/src/uiutil.js
+++ b/htmlui/src/uiutil.js
@@ -224,6 +224,8 @@ export function errorAlert(err, prefix) {
if (err.response && err.response.data && err.response.data.error) {
alert(prefix + err.response.data.error);
+ } else if (err instanceof Error) {
+ alert(err);
} else {
alert(prefix + JSON.stringify(err));
}