diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx
index 218a45ec..5cf03d3f 100644
--- a/webui/src/components/OperationRow.tsx
+++ b/webui/src/components/OperationRow.tsx
@@ -623,7 +623,7 @@ const BackupOperationStatus = ({
console.error("GOT UNEXPECTED STATUS: ", status);
return (
<>
- {m.op_row_unexpected_status} {status}
+ {m.op_row_unexpected_status() + JSON.stringify(status)}
>
);
}
diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx
index ca4aa27c..017c3aa2 100644
--- a/webui/src/components/ScheduleFormItem.tsx
+++ b/webui/src/components/ScheduleFormItem.tsx
@@ -1,9 +1,9 @@
import {
Checkbox,
+ Flex,
Form,
InputNumber,
Radio,
- Row,
Tooltip,
Typography,
} from "antd";
@@ -169,8 +169,8 @@ export const ScheduleFormItem = ({
}
return (
- <>
-
+
+
{
@@ -217,26 +217,25 @@ export const ScheduleFormItem = ({
-
- Clock for schedule:{" "}
-
+
+
+ Clock for schedule:
- Clock provides the time that the schedule is evaluated relative
- to.
+ Clock provides the time that the schedule is evaluated relative to.
Local - current time in the local timezone.
UTC - current time in the UTC timezone.
- Last Run Time - relative to the last time the task ran. Good
- for devices that aren't always powered on e.g. laptops.
+ Last Run Time - relative to the last time the task ran. Good for
+ devices that aren't always powered on e.g. laptops.
>
}
>
-
+
-
-
-
- {elem}
-
- >
+
+ {elem && (
+
+
{elem}
+
+ )}
+
);
};
diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx
index 65501daa..02558018 100644
--- a/webui/src/views/AddPlanModal.tsx
+++ b/webui/src/views/AddPlanModal.tsx
@@ -13,6 +13,7 @@ import {
Collapse,
Checkbox,
AutoComplete,
+ Flex,
} from "antd";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useShowModal } from "../components/ModalManager";
@@ -53,112 +54,7 @@ import * as m from "../paraglide/messages";
const { TextArea } = Input;
const sep = isWindows ? "\\" : "/";
-const PathsTextArea = ({ value, onChange, ...props }: any) => {
- const [options, setOptions] = useState<{ value: string }[]>([]);
- const [currentLine, setCurrentLine] = useState("");
- const [cursorPosition, setCursorPosition] = useState(0);
- // selectingRef acts as a lock to prevent the AutoComplete's default behavior (replacing the entire value)
- // from overwriting the multi-line merge logic in onSelect.
- const selectingRef = useRef(false);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const handleSearch = useMemo(
- () =>
- debounce((searchValue: string) => {
- if (!searchValue) {
- setOptions([]);
- return;
- }
-
- const lastSlash = searchValue.lastIndexOf(sep);
- let searchPath = searchValue;
- if (lastSlash !== -1) {
- searchPath = searchValue.substring(0, lastSlash);
- }
-
- backrestService
- .pathAutocomplete({ value: searchPath + sep })
- .then((res: StringList) => {
- if (!res.values) {
- return;
- }
- const vals = res.values.map((v) => {
- return {
- value: searchPath + sep + v,
- };
- });
- setOptions(vals.filter((o) => o.value.indexOf(searchValue) !== -1));
- })
- .catch((e) => {
- console.log("Path autocomplete error: ", e);
- });
- }, 200),
- []
- );
-
- const handleTextAreaChange = (e: React.ChangeEvent) => {
- // If we are currently handling a selection, ignore the change event that AutoComplete triggers
- // to replace the text with the selected item.
- if (selectingRef.current) {
- selectingRef.current = false;
- return;
- }
-
- const newValue = e.target.value;
- const cursorPos = e.target.selectionStart || 0;
-
- // Find the current line based on cursor position
- const lines = newValue.substring(0, cursorPos).split("\n");
- const currentLineValue = lines[lines.length - 1];
-
- setCurrentLine(currentLineValue);
- setCursorPosition(cursorPos);
-
- // Trigger autocomplete for the current line
- handleSearch(currentLineValue);
-
- // Update the form value
- if (onChange) {
- onChange(newValue);
- }
- };
-
- const onSelect = (selectedValue: string) => {
- // Set the flag to ignore the next onChange event (which contains the raw selected value)
- selectingRef.current = true;
-
- const lines = (value || "").split("\n");
- const beforeCursor = (value || "").substring(0, cursorPosition);
- const afterCursor = (value || "").substring(cursorPosition);
- const linesBeforeCursor = beforeCursor.split("\n");
-
- // Replace the current line with the selected value
- linesBeforeCursor[linesBeforeCursor.length - 1] = selectedValue;
- const newValue = linesBeforeCursor.join("\n") + afterCursor;
-
- if (onChange) {
- onChange(newValue);
- }
- setOptions([]);
- };
-
- return (
- {}} // We handle search in textarea change
- {...props}
- >
-
-
- );
-};
const planDefaults = create(PlanSchema, {
schedule: {
@@ -191,15 +87,7 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
? toJson(PlanSchema, template, { alwaysEmitImplicit: true })
: toJson(PlanSchema, planDefaults, { alwaysEmitImplicit: true });
- // Convert paths array to newline-separated string for the textarea
- const formDataObj = formData as any;
- if (formDataObj?.paths && Array.isArray(formDataObj.paths)) {
- formDataObj.pathsText = formDataObj.paths.join("\n");
- } else {
- formDataObj.pathsText = "";
- }
-
- form.setFieldsValue(formDataObj);
+ form.setFieldsValue(formData);
}, [template]);
if (!config) {
@@ -244,17 +132,6 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
try {
let planFormData = await validateForm(form);
- // Convert pathsText back to paths array
- if (planFormData.pathsText) {
- planFormData.paths = planFormData.pathsText
- .split("\n")
- .map((path: string) => path.trim())
- .filter((path: string) => path.length > 0);
- delete planFormData.pathsText;
- } else {
- planFormData.paths = [];
- }
-
const plan = fromJson(PlanSchema, planFormData, {
ignoreUnknownFields: false,
});
@@ -342,8 +219,8 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
{
- if (!value || !value.trim()) {
- throw new Error(m.add_plan_modal_validation_paths_required());
- }
- const paths = value
- .split("\n")
- .map((p: string) => p.trim())
- .filter((p: string) => p.length > 0);
- if (paths.length === 0) {
- throw new Error(
- m.add_plan_modal_validation_paths_valid_required()
- );
- }
- },
- },
- ]}
>
-
+ {
+ if (!paths || paths.length === 0) {
+ throw new Error(
+ m.add_plan_modal_validation_paths_required()
+ );
+ }
+ },
+ },
+ ]}
+ initialValue={template ? template.paths : []}
+ >
+ {(fields, { add, remove }, { errors }) => (
+ <>
+ {fields.map((field, index) => {
+ const { key, ...restField } = field;
+ return (
+
+
+
+ form.validateFields()}
+ globAllowed={true}
+ />
+
+ remove(field.name)}
+ />
+
+
+ );
+ })}
+
+ add()}
+ block
+ icon={ }
+ >
+ Add Path
+
+
+
+ >
+ )}
+
{/* Plan.excludes */}
@@ -460,28 +380,29 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
const { key, ...restField } = field;
return (
-
- form.validateFields()}
- globAllowed={true}
+
+
+ form.validateFields()}
+ globAllowed={true}
+ />
+
+ remove(field.name)}
/>
-
- remove(field.name)}
- style={{ paddingLeft: "5px" }}
- />
+
);
})}
@@ -489,7 +410,7 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
add()}
- style={{ width: "90%" }}
+ block
icon={ }
>
{m.add_plan_modal_field_excludes_add()}
@@ -529,28 +450,29 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
const { key, ...restField } = field;
return (
-
- form.validateFields()}
- globAllowed={true}
+
+
+ form.validateFields()}
+ globAllowed={true}
+ />
+
+ remove(field.name)}
/>
-
- remove(field.name)}
- style={{ paddingLeft: "5px" }}
- />
+
);
})}
@@ -558,7 +480,7 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
add()}
- style={{ width: "90%" }}
+ block
icon={ }
>
{m.add_plan_modal_field_iexcludes_add()}
@@ -593,30 +515,31 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
const { key, ...restField } = field;
return (
-
-
+
+
+
+ remove(index)}
/>
-
- remove(index)}
- style={{ paddingLeft: "5px" }}
- />
+
);
})}
@@ -624,7 +547,7 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
add()}
- style={{ width: "90%" }}
+ block
icon={ }
>
{m.add_plan_modal_field_backup_flags_add()}
diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx
index e871aeba..725a42c8 100644
--- a/webui/src/views/AddRepoModal.tsx
+++ b/webui/src/views/AddRepoModal.tsx
@@ -15,6 +15,7 @@ import {
Checkbox,
Select,
Space,
+ Flex,
} from "antd";
import React, { useEffect, useState } from "react";
import { useShowModal } from "../components/ModalManager";
@@ -243,8 +244,8 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
-
-
-
- hasFeedback
- name="password"
- validateTrigger={["onChange", "onBlur"]}
- >
-
-
-
-
+
+ hasFeedback
+ name="password"
+ validateTrigger={["onChange", "onBlur"]}
+ noStyle
>
- {
- if (template) return;
- form.setFieldsValue({
- password: cryptoRandomPassword(),
- });
- }}
- >
- {m.add_repo_modal_button_generate()}
-
-
-
+
+
+ {
+ if (template) return;
+ form.setFieldsValue({
+ password: cryptoRandomPassword(),
+ });
+ }}
+ >
+ {m.add_repo_modal_button_generate()}
+
+
{/* Repo.env */}
@@ -404,31 +398,31 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
const { key, ...restField } = field;
return (
-
- form.validateFields()}
- style={{ width: "90%" }}
+
+
+ form.validateFields()}
+ style={{ flex: 1 }}
+ />
+
+ remove(index)}
/>
-
- remove(index)}
- style={{ paddingLeft: "5px" }}
- />
+
);
})}
@@ -436,7 +430,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
add("")}
- style={{ width: "90%" }}
+ block
icon={ }
>
{m.add_repo_modal_button_set_env()}
@@ -458,30 +452,30 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
const { key, ...restField } = field;
return (
-
-
+
+
+
+ remove(index)}
/>
-
- remove(index)}
- style={{ paddingLeft: "5px" }}
- />
+
);
})}
@@ -489,7 +483,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
add()}
- style={{ width: "90%" }}
+ block
icon={ }
>
{m.add_repo_modal_button_set_flag()}
@@ -501,6 +495,23 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
+ {/* Repo.autoUnlock */}
+
+ {m.add_repo_modal_field_auto_unlock()}
+
+ }
+ name="autoUnlock"
+ valuePropName="checked"
+ >
+
+
+
{/* Repo.prunePolicy */}
{
)}
-
- {m.add_repo_modal_field_auto_unlock()}
-
- }
- name="autoUnlock"
- valuePropName="checked"
- >
-
-
-
{m.add_plan_modal_field_hooks()}}
>
diff --git a/webui/src/views/SettingsModal.tsx b/webui/src/views/SettingsModal.tsx
index 00ee4a21..411ef472 100644
--- a/webui/src/views/SettingsModal.tsx
+++ b/webui/src/views/SettingsModal.tsx
@@ -98,6 +98,7 @@ export const SettingsModal = () => {
user.passwordBcrypt = hash.value;
delete user.needsBcrypt;
}
+ delete user.isExisting;
}
}