mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-28 11:56:28 +00:00
fix: form bugs in UI e.g. awkward behavior when modifying hooks
This commit is contained in:
@@ -4,6 +4,20 @@ import { Button, Card, Collapse, CollapseProps, Form, FormListFieldData, Input,
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Rule } from 'antd/es/form';
|
||||
|
||||
export interface HookFormData {
|
||||
hooks: {
|
||||
conditions: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface HookFields {
|
||||
conditions: string[];
|
||||
actionCommand?: any;
|
||||
actionGotify?: any;
|
||||
actionDiscord?: any;
|
||||
actionWebhook?: any;
|
||||
}
|
||||
|
||||
export const hooksListTooltipText = <>
|
||||
Hooks are actions that can execute on backup lifecycle events.
|
||||
|
||||
@@ -34,19 +48,15 @@ export const hooksListTooltipText = <>
|
||||
</ul>
|
||||
</>
|
||||
|
||||
|
||||
/**
|
||||
* HooksFormList is a UI component for editing a list of hooks that can apply either at the repo level or at the plan level.
|
||||
*/
|
||||
export const HooksFormList = (props: { hooks: Hook[] }) => {
|
||||
const [hooks, _] = useState([...props.hooks] || []);
|
||||
|
||||
return <Form.List name="hooks" initialValue={props.hooks || []}>
|
||||
export const HooksFormList = () => {
|
||||
return <Form.List name="hooks">
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
console.log(index, field);
|
||||
const hook = hooks[index];
|
||||
if (!hook) return null;
|
||||
return <Card key={index} title={<>
|
||||
Hook {index}
|
||||
<MinusCircleOutlined
|
||||
@@ -56,13 +66,12 @@ export const HooksFormList = (props: { hooks: Hook[] }) => {
|
||||
/>
|
||||
</>
|
||||
} size="small" >
|
||||
<Form.Item name={[field.name, "conditions"]} initialValue={hook.conditions}>
|
||||
<Form.Item name={[field.name, "conditions"]} >
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Runs when..."
|
||||
defaultValue={hook.conditions}
|
||||
options={[
|
||||
{ label: "On Finish Snapshot", value: Hook_Condition.SNAPSHOT_END },
|
||||
{ label: "On Start Snapshot", value: Hook_Condition.SNAPSHOT_START },
|
||||
@@ -71,7 +80,11 @@ export const HooksFormList = (props: { hooks: Hook[] }) => {
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<HookBuilder hook={hook} field={field} />
|
||||
<Form.Item shouldUpdate={(prevValues, curValues) => {
|
||||
return prevValues.hooks[index] !== curValues.hooks[index];
|
||||
}}>
|
||||
<HookBuilder field={field} />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
})}
|
||||
<Form.Item>
|
||||
@@ -79,11 +92,7 @@ export const HooksFormList = (props: { hooks: Hook[] }) => {
|
||||
content={<>
|
||||
{hookTypes.map((hookType, index) => {
|
||||
return <Button key={index} onClick={() => {
|
||||
const hook = new Hook({
|
||||
action: hookType.action
|
||||
});
|
||||
hooks.push(hook);
|
||||
add(hook);
|
||||
add(structuredClone(hookType.template));
|
||||
}}>
|
||||
{hookType.name}
|
||||
</Button>
|
||||
@@ -105,78 +114,79 @@ export const HooksFormList = (props: { hooks: Hook[] }) => {
|
||||
|
||||
const hookTypes: {
|
||||
name: string,
|
||||
action: typeof Hook.prototype.action,
|
||||
template: HookFields,
|
||||
}[] = [
|
||||
{
|
||||
name: "Command", action: {
|
||||
case: "actionCommand",
|
||||
value: new Hook_Command({
|
||||
name: "Command", template: {
|
||||
actionCommand: {
|
||||
command: "echo {{ .ShellEscape .Summary }}",
|
||||
}),
|
||||
}
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Discord", action: {
|
||||
case: "actionDiscord",
|
||||
value: new Hook_Discord({
|
||||
name: "Discord", template: {
|
||||
actionDiscord: {
|
||||
webhookUrl: "",
|
||||
template: "{{ .Summary }}",
|
||||
}),
|
||||
},
|
||||
conditions: [],
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Gotify", action: {
|
||||
case: "actionGotify",
|
||||
value: new Hook_Gotify({
|
||||
name: "Gotify", template: {
|
||||
actionGotify: {
|
||||
baseUrl: "",
|
||||
token: "",
|
||||
template: "{{ .Summary }}",
|
||||
titleTemplate: "Backrest {{ .EventName .Event }} in plan {{ .Plan.Id }}",
|
||||
}),
|
||||
},
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const HookBuilder = ({ field, hook }: { field: FormListFieldData, hook: Hook }) => {
|
||||
let component: React.ReactNode;
|
||||
switch (hook.action.case) {
|
||||
case "actionDiscord":
|
||||
return <>
|
||||
<Form.Item name={[field.name, "action", "value", "webhookUrl"]} rules={[requiredField("webhook URL is required")]} >
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Discord Webhook</div>} />
|
||||
</Form.Item >
|
||||
Text Template:
|
||||
<Form.Item name={[field.name, "action", "value", "template"]} >
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item >
|
||||
</>
|
||||
case "actionCommand":
|
||||
return <>
|
||||
<Tooltip title="Script to execute. Commands will not work in the docker build of Backrest.">
|
||||
Script:
|
||||
</Tooltip>
|
||||
<Form.Item name={[field.name, "action", "value", "command"]} rules={[requiredField("command is required")]}>
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
case "actionGotify":
|
||||
return <>
|
||||
<Form.Item name={[field.name, "action", "value", "baseUrl"]} rules={[requiredField("gotify base URL is required"), { type: "url" }]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Gotify Base URL</div>} />
|
||||
</Form.Item >
|
||||
<Form.Item name={[field.name, "action", "value", "token"]} rules={[requiredField("gotify token is required")]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Gotify Token</div>} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[field.name, "action", "value", "titleTemplate"]} rules={[requiredField("gotify title template is required")]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Title Template</div>} />
|
||||
</Form.Item>
|
||||
Text Template:
|
||||
<Form.Item name={[field.name, "action", "value", "template"]}>
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
default:
|
||||
return <p>Unknown hook {hook.action.case}</p>
|
||||
const HookBuilder = ({ field }: { field: FormListFieldData }) => {
|
||||
const form = Form.useFormInstance();
|
||||
const hookData = form.getFieldValue(["hooks", field.name]) as HookFields;
|
||||
|
||||
if (hookData.actionDiscord) {
|
||||
return <>
|
||||
<Form.Item name={[field.name, "action", "value", "webhookUrl"]} rules={[requiredField("webhook URL is required")]} >
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Discord Webhook</div>} />
|
||||
</Form.Item >
|
||||
Text Template:
|
||||
<Form.Item name={[field.name, "action", "value", "template"]} >
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item >
|
||||
</>
|
||||
} else if (hookData.actionCommand) {
|
||||
return <>
|
||||
<Tooltip title="Script to execute. Commands will not work in the docker build of Backrest.">
|
||||
Script:
|
||||
</Tooltip>
|
||||
<Form.Item name={[field.name, "action", "value", "command"]} rules={[requiredField("command is required")]}>
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
} else if (hookData.actionGotify) {
|
||||
return <>
|
||||
<Form.Item name={[field.name, "action", "value", "baseUrl"]} rules={[requiredField("gotify base URL is required"), { type: "url" }]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Gotify Base URL</div>} />
|
||||
</Form.Item >
|
||||
<Form.Item name={[field.name, "action", "value", "token"]} rules={[requiredField("gotify token is required")]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Gotify Token</div>} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[field.name, "action", "value", "titleTemplate"]} rules={[requiredField("gotify title template is required")]}>
|
||||
<Input addonBefore={<div style={{ width: "8em" }}>Title Template</div>} />
|
||||
</Form.Item>
|
||||
Text Template:
|
||||
<Form.Item name={[field.name, "action", "value", "template"]}>
|
||||
<Input.TextArea style={{ width: "100%", fontFamily: "monospace" }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
} else {
|
||||
return <p>Unknown hook</p>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { ValidateErrorEntity } from "rc-field-form/lib/interface";
|
||||
|
||||
export const validateForm = async <T>(form: FormInstance<T>) => {
|
||||
try {
|
||||
return await form.validateFields();
|
||||
await form.validateFields();
|
||||
return form.getFieldsValue();
|
||||
} catch (e: any) {
|
||||
if (e.errorFields) {
|
||||
const firstError = (e as ValidateErrorEntity).errorFields?.[0]
|
||||
|
||||
+123
-128
@@ -14,7 +14,7 @@ import {
|
||||
Collapse,
|
||||
FormInstance,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
import { Plan, RetentionPolicy } from "../../gen/ts/v1/config_pb";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
@@ -30,13 +30,16 @@ import { backrestService } from "../api";
|
||||
export const AddPlanModal = ({
|
||||
template,
|
||||
}: {
|
||||
template: Partial<Plan> | null;
|
||||
template: Plan | null;
|
||||
}) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const showModal = useShowModal();
|
||||
const alertsApi = useAlertApi()!;
|
||||
const [config, setConfig] = useConfig();
|
||||
const [form] = Form.useForm<Plan>();
|
||||
const [form] = Form.useForm();
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(template ? JSON.parse(template.toJsonString()) : {});
|
||||
}, [template])
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
@@ -77,7 +80,12 @@ export const AddPlanModal = ({
|
||||
setConfirmLoading(true);
|
||||
|
||||
try {
|
||||
let plan = new Plan(await validateForm<Plan>(form));
|
||||
let planFormData = await validateForm(form);
|
||||
const plan = new Plan().fromJsonString(JSON.stringify(planFormData), { ignoreUnknownFields: false });
|
||||
|
||||
if (plan.retention && plan.retention.equals(new RetentionPolicy())) {
|
||||
delete plan.retention;
|
||||
}
|
||||
|
||||
// Merge the new plan (or update) into the config
|
||||
if (template) {
|
||||
@@ -330,14 +338,13 @@ export const AddPlanModal = ({
|
||||
</Tooltip>
|
||||
|
||||
{/* Plan.retention */}
|
||||
<RetentionPolicyView policy={template?.retention} form={form} />
|
||||
|
||||
<RetentionPolicyView />
|
||||
|
||||
{/* Plan.hooks */}
|
||||
<Form.Item
|
||||
label={<Tooltip title={hooksListTooltipText}>Hooks</Tooltip>}
|
||||
>
|
||||
<HooksFormList hooks={template?.hooks || []} />
|
||||
<HooksFormList />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -360,145 +367,133 @@ export const AddPlanModal = ({
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Modal >
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form: FormInstance }) => {
|
||||
enum PolicyType {
|
||||
TimeBased,
|
||||
CountBased,
|
||||
None,
|
||||
}
|
||||
const RetentionPolicyView = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const retention = form.getFieldValue("retention") as RetentionPolicy | undefined;
|
||||
const [mode, setMode] = useState(!retention ? 2 : retention.keepLastN ? 0 : 1);
|
||||
|
||||
policy = policy || new RetentionPolicy();
|
||||
|
||||
let defaultPolicyType = PolicyType.None;
|
||||
if (policy.keepLastN) {
|
||||
defaultPolicyType = PolicyType.CountBased;
|
||||
} else if (policy) {
|
||||
defaultPolicyType = PolicyType.TimeBased;
|
||||
}
|
||||
|
||||
const [policyType, setPolicyType] = useState<PolicyType>(defaultPolicyType);
|
||||
|
||||
let elem = null;
|
||||
switch (policyType) {
|
||||
case PolicyType.TimeBased:
|
||||
elem = (
|
||||
<Form.Item
|
||||
required={true}
|
||||
>
|
||||
<Row>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
name={["retention", "keepYearly"]}
|
||||
initialValue={policy.keepYearly || 0}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Yearly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepMonthly"]}
|
||||
initialValue={policy.keepMonthly || 3}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Monthly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepWeekly"]}
|
||||
initialValue={policy.keepWeekly || 4}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Weekly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={11} offset={1}>
|
||||
<Form.Item
|
||||
name={["retention", "keepDaily"]}
|
||||
initialValue={policy.keepDaily || 7}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Daily</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepHourly"]}
|
||||
initialValue={policy.keepHourly || 24}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Hourly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item >
|
||||
);
|
||||
break;
|
||||
case PolicyType.CountBased:
|
||||
elem = (
|
||||
<Form.Item
|
||||
name={["retention", "keepLastN"]}
|
||||
initialValue={policy.keepLastN || 30}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input keep last N",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber addonBefore={<div style={{ width: "5em" }}>Count</div>} type="number" />
|
||||
</Form.Item>
|
||||
);
|
||||
break;
|
||||
case PolicyType.None:
|
||||
elem = <p>All backups are retained e.g. for append-only repos. Ensure that you manually forget / prune backups elsewhere. Backrest will register forgets performed externally on the next backup.</p>
|
||||
let elem: React.ReactNode = null;
|
||||
if (mode === 2) {
|
||||
elem = <p>All backups are retained e.g. for append-only repos. Ensure that you manually forget / prune backups elsewhere. Backrest will register forgets performed externally on the next backup.</p>;
|
||||
} else if (mode === 0) {
|
||||
elem = (
|
||||
<Form.Item
|
||||
name={["retention", "keepLastN"]}
|
||||
initialValue={30}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input keep last N",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber addonBefore={<div style={{ width: "5em" }}>Count</div>} type="number" />
|
||||
</Form.Item>
|
||||
);
|
||||
} else {
|
||||
elem = (
|
||||
<Form.Item
|
||||
required={true}
|
||||
>
|
||||
<Row>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
name={["retention", "keepYearly"]}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
initialValue={0}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Yearly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepMonthly"]}
|
||||
initialValue={3}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Monthly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepWeekly"]}
|
||||
initialValue={4}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Weekly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={11} offset={1}>
|
||||
<Form.Item
|
||||
name={["retention", "keepDaily"]}
|
||||
initialValue={7}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Daily</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepHourly"]}
|
||||
initialValue={24}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
addonBefore={<div style={{ width: "5em" }}>Hourly</div>}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item >
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item label="Retention Policy">
|
||||
<Row>
|
||||
<Radio.Group
|
||||
value={policyType}
|
||||
onChange={(e) => {
|
||||
setPolicyType(e.target.value);
|
||||
if (e.target.value === PolicyType.None) {
|
||||
form.resetFields(["retention"]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={PolicyType.CountBased}>
|
||||
<Radio.Group value={mode} onChange={e => {
|
||||
const selected = e.target.value;
|
||||
if (selected === 0) {
|
||||
setMode(0);
|
||||
form.setFieldValue("retention", { keepLastN: 30 });
|
||||
} else if (selected === 1) {
|
||||
setMode(1);
|
||||
form.setFieldValue("retention", { keepYearly: 0, keepMonthly: 3, keepWeekly: 4, keepDaily: 7, keepHourly: 24 });
|
||||
} else {
|
||||
setMode(2);
|
||||
form.setFieldValue("retention", null);
|
||||
}
|
||||
}}>
|
||||
<Radio.Button value={0}>
|
||||
<Tooltip title="The last N snapshots will be kept by restic. Retention policy is applied to drop older snapshots after each backup run.">
|
||||
By Count
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={PolicyType.TimeBased}>
|
||||
<Radio.Button value={1}>
|
||||
<Tooltip title="Snapshots older than the specified time period will be dropped by restic. Retention policy is applied to drop older snapshots after each backup run." >
|
||||
By Time Period
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={PolicyType.None}>
|
||||
<Radio.Button value={2}>
|
||||
<Tooltip title="All backups will be retained. Note that this may result in slow backups if the set of snapshots grows very large.">
|
||||
None
|
||||
</Tooltip>
|
||||
@@ -509,7 +504,7 @@ const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form:
|
||||
<Row>
|
||||
{elem}
|
||||
</Row>
|
||||
</Form.Item>
|
||||
</Form.Item >
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
FormInstance,
|
||||
Collapse,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
import { Repo } from "../../gen/ts/v1/config_pb";
|
||||
import { Hook, Repo } from "../../gen/ts/v1/config_pb";
|
||||
import { URIAutocomplete } from "../components/URIAutocomplete";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
@@ -24,6 +24,7 @@ import { backrestService } from "../api";
|
||||
import {
|
||||
HooksFormList,
|
||||
hooksListTooltipText,
|
||||
HookFormData,
|
||||
} from "../components/HooksFormList";
|
||||
import { ConfirmButton } from "../components/SpinButton";
|
||||
import { useConfig } from "../components/ConfigProvider";
|
||||
@@ -31,13 +32,16 @@ import { useConfig } from "../components/ConfigProvider";
|
||||
export const AddRepoModal = ({
|
||||
template,
|
||||
}: {
|
||||
template: Partial<Repo> | null;
|
||||
template: Repo | null;
|
||||
}) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const showModal = useShowModal();
|
||||
const alertsApi = useAlertApi()!;
|
||||
const [config, setConfig] = useConfig();
|
||||
const [form] = Form.useForm<Repo>();
|
||||
const [form] = Form.useForm();
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(template ? JSON.parse(template.toJsonString()) : {});
|
||||
}, [template])
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
@@ -88,7 +92,8 @@ export const AddRepoModal = ({
|
||||
setConfirmLoading(true);
|
||||
|
||||
try {
|
||||
let repo = await validateForm<Repo>(form);
|
||||
let repoFormData = await validateForm(form);
|
||||
const repo = new Repo().fromJsonString(JSON.stringify(repoFormData), { ignoreUnknownFields: false });
|
||||
|
||||
if (template !== null) {
|
||||
// We are in the edit repo flow, update the repo in the config
|
||||
@@ -97,7 +102,7 @@ export const AddRepoModal = ({
|
||||
alertsApi.error("Can't update repo, not found");
|
||||
return;
|
||||
}
|
||||
config.repos![idx] = new Repo(repo);
|
||||
config.repos![idx] = repo;
|
||||
setConfig(await backrestService.setConfig(config));
|
||||
showModal(null);
|
||||
alertsApi.success("Updated repo " + repo.uri);
|
||||
@@ -166,7 +171,6 @@ export const AddRepoModal = ({
|
||||
hasFeedback
|
||||
name="id"
|
||||
label="Repo Name"
|
||||
initialValue={template ? template.id : ""}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
rules={[
|
||||
{
|
||||
@@ -219,7 +223,6 @@ export const AddRepoModal = ({
|
||||
hasFeedback
|
||||
name="uri"
|
||||
label="Repository URI"
|
||||
initialValue={template ? template.uri : ""}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
rules={[
|
||||
{
|
||||
@@ -239,7 +242,6 @@ export const AddRepoModal = ({
|
||||
<Form.Item<Repo>
|
||||
hasFeedback
|
||||
name="password"
|
||||
initialValue={template ? template.password : ""}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
>
|
||||
<Input disabled={!!template} />
|
||||
@@ -276,16 +278,15 @@ export const AddRepoModal = ({
|
||||
},
|
||||
},
|
||||
]}
|
||||
initialValue={template ? template.env : []}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
console.log("FIELD: ", field),
|
||||
<Form.Item key={field.key}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
initialValue={""}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -305,7 +306,7 @@ export const AddRepoModal = ({
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
onClick={() => remove(index)}
|
||||
style={{ paddingLeft: "5px" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -313,7 +314,7 @@ export const AddRepoModal = ({
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
onClick={() => add("THIS IS A NEW VALUE")}
|
||||
style={{ width: "90%" }}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
@@ -330,7 +331,6 @@ export const AddRepoModal = ({
|
||||
<Form.Item label="Flags">
|
||||
<Form.List
|
||||
name="flags"
|
||||
initialValue={template ? template.flags : []}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
@@ -339,7 +339,6 @@ export const AddRepoModal = ({
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
initialValue={""}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -355,7 +354,7 @@ export const AddRepoModal = ({
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
onClick={() => remove(index)}
|
||||
style={{ paddingLeft: "5px" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -398,7 +397,7 @@ export const AddRepoModal = ({
|
||||
>
|
||||
<Form.Item
|
||||
name={["prunePolicy", "maxFrequencyDays"]}
|
||||
initialValue={template?.prunePolicy?.maxFrequencyDays || 7}
|
||||
initialValue={7}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
@@ -409,7 +408,7 @@ export const AddRepoModal = ({
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["prunePolicy", "maxUnusedPercent"]}
|
||||
initialValue={template?.prunePolicy?.maxUnusedPercent || 25}
|
||||
initialValue={25}
|
||||
required={false}
|
||||
>
|
||||
<InputNumber
|
||||
@@ -425,7 +424,7 @@ export const AddRepoModal = ({
|
||||
<Form.Item
|
||||
label={<Tooltip title={hooksListTooltipText}>Hooks</Tooltip>}
|
||||
>
|
||||
<HooksFormList hooks={template?.hooks || []} />
|
||||
<HooksFormList />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate label="Preview">
|
||||
@@ -438,9 +437,7 @@ export const AddRepoModal = ({
|
||||
label: "Repo Config as JSON",
|
||||
children: (
|
||||
<Typography>
|
||||
<pre>{new Repo(form.getFieldsValue()).toJsonString({
|
||||
prettySpaces: 2,
|
||||
})}</pre>
|
||||
<pre>{JSON.stringify(form.getFieldsValue(), undefined, 2)}</pre>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
@@ -461,13 +458,20 @@ const expectedEnvVars: { [scheme: string]: string[] } = {
|
||||
gs: ["GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_PROJECT_ID"],
|
||||
};
|
||||
|
||||
const envVarSetValidator = (form: FormInstance<Repo>, envVars: string[]) => {
|
||||
const envVarSetValidator = (form: FormInstance<FormData>, envVars: string[]) => {
|
||||
if (!envVars) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let uri = form.getFieldValue("uri");
|
||||
if (!uri) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const envVarNames = envVars.map((e) => {
|
||||
if (!e) {
|
||||
return "";
|
||||
}
|
||||
let idx = e.indexOf("=");
|
||||
if (idx === -1) {
|
||||
return "";
|
||||
|
||||
@@ -121,7 +121,7 @@ export const App: React.FC = () => {
|
||||
</small>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ marginLeft: "10px" }}
|
||||
style={{ marginLeft: "10px", color: "white" }}
|
||||
onClick={() => {
|
||||
setAuthToken("");
|
||||
window.location.reload();
|
||||
|
||||
@@ -11,6 +11,7 @@ export const GettingStartedGuide = () => {
|
||||
<>
|
||||
<Typography.Text>
|
||||
<h1>Getting Started</h1>
|
||||
<p><a href="https://github.com/garethgeorge/backrest">Backrest documentation on GitHub</a></p>
|
||||
<Divider orientation="left">Overview</Divider>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
Reference in New Issue
Block a user