From 7a7de5c3f4beeefc2ee37ddd7110c004ac1d911d Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Sun, 5 Dec 2021 12:45:05 -0800 Subject: [PATCH] ui: added editor for snapshot times of day (#1566) --- htmlui/src/NewSnapshot.js | 64 +++++++++++++++++------------- htmlui/src/PolicyEditor.js | 80 ++++++++++++++++++++++++++++---------- htmlui/src/forms.js | 61 +++++++++++++++++++++++++++-- htmlui/src/uiutil.js | 2 + 4 files changed, 155 insertions(+), 52 deletions(-) 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) { ; } @@ -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)); }