mirror of
https://github.com/kopia/kopia.git
synced 2026-03-26 10:01:32 -04:00
ui: added editor for snapshot times of day (#1566)
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user