diff --git a/webui/src/components/HooksFormList.tsx b/webui/src/components/HooksFormList.tsx index d002c8af..2b421a25 100644 --- a/webui/src/components/HooksFormList.tsx +++ b/webui/src/components/HooksFormList.tsx @@ -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 = <> + /** * 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 +export const HooksFormList = () => { + return {(fields, { add, remove }, { errors }) => ( <> {fields.map((field, index) => { - console.log(index, field); - const hook = hooks[index]; - if (!hook) return null; return Hook {index} { /> } size="small" > - + Discord Webhook} /> - - Text Template: - - - - - case "actionCommand": - return <> - - Script: - - - - - - case "actionGotify": - return <> - - Gotify Base URL} /> - - - Gotify Token} /> - - - Title Template} /> - - Text Template: - - - - - default: - return

Unknown hook {hook.action.case}

+const HookBuilder = ({ field }: { field: FormListFieldData }) => { + const form = Form.useFormInstance(); + const hookData = form.getFieldValue(["hooks", field.name]) as HookFields; + + if (hookData.actionDiscord) { + return <> + + Discord Webhook} /> + + Text Template: + + + + + } else if (hookData.actionCommand) { + return <> + + Script: + + + + + + } else if (hookData.actionGotify) { + return <> + + Gotify Base URL} /> + + + Gotify Token} /> + + + Title Template} /> + + Text Template: + + + + + } else { + return

Unknown hook

} } diff --git a/webui/src/lib/formutil.ts b/webui/src/lib/formutil.ts index 2cfaddce..7384dba5 100644 --- a/webui/src/lib/formutil.ts +++ b/webui/src/lib/formutil.ts @@ -3,7 +3,8 @@ import type { ValidateErrorEntity } from "rc-field-form/lib/interface"; export const validateForm = async (form: FormInstance) => { try { - return await form.validateFields(); + await form.validateFields(); + return form.getFieldsValue(); } catch (e: any) { if (e.errorFields) { const firstError = (e as ValidateErrorEntity).errorFields?.[0] diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx index d42af624..83562266 100644 --- a/webui/src/views/AddPlanModal.tsx +++ b/webui/src/views/AddPlanModal.tsx @@ -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 | null; + template: Plan | null; }) => { const [confirmLoading, setConfirmLoading] = useState(false); const showModal = useShowModal(); const alertsApi = useAlertApi()!; const [config, setConfig] = useConfig(); - const [form] = Form.useForm(); + 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(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 = ({ {/* Plan.retention */} - - + {/* Plan.hooks */} Hooks} > - + @@ -360,145 +367,133 @@ export const AddPlanModal = ({ )}
- + ); }; -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(defaultPolicyType); - - let elem = null; - switch (policyType) { - case PolicyType.TimeBased: - elem = ( - - - - - Yearly} - type="number" - /> - - - Monthly} - type="number" - /> - - - Weekly} - type="number" - /> - - - - - Daily} - type="number" - /> - - - Hourly} - type="number" - /> - - - - - ); - break; - case PolicyType.CountBased: - elem = ( - - Count} type="number" /> - - ); - break; - case PolicyType.None: - elem =

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.

+ let elem: React.ReactNode = null; + if (mode === 2) { + elem =

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.

; + } else if (mode === 0) { + elem = ( + + Count} type="number" /> + + ); + } else { + elem = ( + + + + + Yearly} + type="number" + /> + + + Monthly} + type="number" + /> + + + Weekly} + type="number" + /> + + + + + Daily} + type="number" + /> + + + Hourly} + type="number" + /> + + + + + ); } return ( <> - { - setPolicyType(e.target.value); - if (e.target.value === PolicyType.None) { - form.resetFields(["retention"]); - } - }} - > - + { + 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); + } + }}> + By Count - + By Time Period - + None @@ -509,7 +504,7 @@ const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form: {elem} - + ); }; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index e832bef1..f97e9de9 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -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 | null; + template: Repo | null; }) => { const [confirmLoading, setConfirmLoading] = useState(false); const showModal = useShowModal(); const alertsApi = useAlertApi()!; const [config, setConfig] = useConfig(); - const [form] = Form.useForm(); + 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(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 = ({ hasFeedback name="password" - initialValue={template ? template.password : ""} validateTrigger={["onChange", "onBlur"]} > @@ -276,16 +278,15 @@ export const AddRepoModal = ({ }, }, ]} - initialValue={template ? template.env : []} > {(fields, { add, remove }, { errors }) => ( <> {fields.map((field, index) => ( + console.log("FIELD: ", field), remove(field.name)} + onClick={() => remove(index)} style={{ paddingLeft: "5px" }} /> @@ -313,7 +314,7 @@ export const AddRepoModal = ({