ui: added editor for snapshot times of day (#1566)

This commit is contained in:
Jarek Kowalski
2021-12-05 12:45:05 -08:00
committed by GitHub
parent 5f04fad003
commit 7a7de5c3f4
4 changed files with 155 additions and 52 deletions

View File

@@ -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() {

View File

@@ -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) {
<LabelColumn name-="Upcoming" />
<ul data-testid="upcoming-snapshot-times">
{times.map(x => <li key={x}>{moment(x).format('L LT')} ({moment(x).fromNow()})</li>)}
{times.map(x => <li key={x}>{moment(x).format('L LT')} ({moment(x).fromNow()})</li>)}
</ul>
</>;
}
@@ -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 {
</WideValueColumn>
{EffectiveValue(this, "scheduling.intervalSeconds")}
</Row>
<Row>
<LabelColumn name="Times Of Day" help="Create snapshots at the provided times of day, one per line, 24h time (e.g. 17:00,18:00)" />
<ValueColumn>
{TimesOfDayList(this, "policy.scheduling.timeOfDay")}
</ValueColumn>
{EffectiveBooleanValue(this, "scheduling.manual")}
</Row>
<Row>
<LabelColumn name="Manual Snapshots Only" help="Only create snapshots manually (disables scheduled snapshots)." />
<ValueColumn>

View File

@@ -214,14 +214,67 @@ export function StringList(component, name) {
</Form.Group>
}
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 <FormGroup>
<Form.Control
size="sm"
name={name}
isInvalid={true}
value={listToMultilineString(stateProperty(component, name))}
onChange={e => component.handleChange(e, multilineStringToList)}
value={toMultilineString(stateProperty(component, name))}
onChange={e => component.handleChange(e, fromMultilineString)}
as="textarea"
rows="5">
</Form.Control>

View File

@@ -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));
}