diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts
index 056049eaa..1fa68d3cd 100644
--- a/app/portainer/react/views/index.ts
+++ b/app/portainer/react/views/index.ts
@@ -10,6 +10,7 @@ import { EdgeComputeSettingsView } from '@/react/portainer/settings/EdgeComputeV
import { withI18nSuspense } from '@/react-tools/withI18nSuspense';
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
+import { BackupSettingsPanel } from '@/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel';
import { wizardModule } from './wizard';
import { teamsModule } from './teams';
@@ -49,4 +50,8 @@ export const viewsModule = angular
.component(
'environmentsListView',
r2a(withUIRouter(withReactQuery(withCurrentUser(EnvironmentsListView))), [])
+ )
+ .component(
+ 'backupSettingsPanel',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(BackupSettingsPanel))), [])
).name;
diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html
index 888f0daaf..9e990907e 100644
--- a/app/portainer/views/settings/settings.html
+++ b/app/portainer/views/settings/settings.html
@@ -152,241 +152,5 @@
-
+
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js
index 07ccc30a0..7c09aec28 100644
--- a/app/portainer/views/settings/settingsController.js
+++ b/app/portainer/views/settings/settingsController.js
@@ -1,24 +1,17 @@
import angular from 'angular';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
-import { options } from '@/react/portainer/settings/SettingsView/backup-options';
angular.module('portainer.app').controller('SettingsController', [
'$scope',
'Notifications',
'SettingsService',
'StateManager',
- 'BackupService',
- 'FileSaver',
- function ($scope, Notifications, SettingsService, StateManager, BackupService, FileSaver) {
- $scope.s3BackupFeatureId = FeatureId.S3_BACKUP_SETTING;
- $scope.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS;
+ function ($scope, Notifications, SettingsService, StateManager) {
$scope.updateSettings = updateSettings;
$scope.handleSuccess = handleSuccess;
$scope.requireNoteOnApplications = FeatureId.K8S_REQUIRE_NOTE_ON_APPLICATIONS;
- $scope.backupOptions = options;
-
$scope.state = {
actionInProgress: false,
availableKubeconfigExpiryOptions: [
@@ -48,28 +41,12 @@ angular.module('portainer.app').controller('SettingsController', [
showHTTPS: !window.ddExtension,
};
- $scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
-
$scope.formValues = {
KubeconfigExpiry: undefined,
HelmRepositoryURL: undefined,
BlackListedLabels: [],
labelName: '',
labelValue: '',
- passwordProtect: false,
- password: '',
- backupFormType: $scope.BACKUP_FORM_TYPES.FILE,
- };
-
- $scope.onToggleAutoBackups = function onToggleAutoBackups(checked) {
- $scope.$evalAsync(() => {
- $scope.formValues.scheduleAutomaticBackups = checked;
- });
- };
-
- $scope.onBackupOptionsChange = function (type, limited) {
- $scope.formValues.backupFormType = type;
- $scope.state.featureLimited = limited;
};
$scope.removeFilteredContainerLabel = function (index) {
@@ -89,28 +66,6 @@ angular.module('portainer.app').controller('SettingsController', [
updateSettings(filteredSettingsPayload, 'Hidden container settings updated');
};
- $scope.downloadBackup = function () {
- const payload = {};
- if ($scope.formValues.passwordProtect) {
- payload.password = $scope.formValues.password;
- }
-
- $scope.state.backupInProgress = true;
-
- BackupService.downloadBackup(payload)
- .then(function success(data) {
- const downloadData = new Blob([data.file], { type: 'application/gzip' });
- FileSaver.saveAs(downloadData, data.name);
- Notifications.success('Success', 'Backup successfully downloaded');
- })
- .catch(function error(err) {
- Notifications.error('Failure', err, 'Unable to download backup');
- })
- .finally(function final() {
- $scope.state.backupInProgress = false;
- });
- };
-
// only update the values from the kube settings widget. In future separate the api endpoints
$scope.saveKubernetesSettings = function () {
const kubeSettingsPayload = {
diff --git a/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/EnableTelemetryField.tsx b/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/EnableTelemetryField.tsx
index c1d9d2a11..95ec4a7ad 100644
--- a/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/EnableTelemetryField.tsx
+++ b/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/EnableTelemetryField.tsx
@@ -14,7 +14,7 @@ export function EnableTelemetryField() {
setIsEnabled(checked)}
/>
diff --git a/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/ScreenBannerFieldset.tsx b/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/ScreenBannerFieldset.tsx
index 4abef9399..43ea3b010 100644
--- a/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/ScreenBannerFieldset.tsx
+++ b/app/react/portainer/settings/SettingsView/ApplicationSettingsPanel/ScreenBannerFieldset.tsx
@@ -20,7 +20,7 @@ export function ScreenBannerFieldset() {
+ initialValues={settings}
+ validationSchema={validationSchema}
+ onSubmit={onSubmit}
+ validateOnMount
+ >
+ {({ isSubmitting, isValid }) => (
+
+ )}
+
+ );
+
+ async function onSubmit(values: BackupFileSettings) {
+ const payload: DownloadBackupPayload = {
+ password: '',
+ };
+ if (values.passwordProtect) {
+ payload.password = values.password;
+ }
+
+ downloadMutate.mutate(payload, {
+ onSuccess() {
+ notifySuccess('Success', 'Downloaded backup successfully');
+ },
+ });
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupFileForm.validation.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupFileForm.validation.ts
new file mode 100644
index 000000000..5aaa52a0e
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupFileForm.validation.ts
@@ -0,0 +1,15 @@
+import { SchemaOf, object, string, boolean } from 'yup';
+
+import { BackupFileSettings } from './types';
+
+export function validationSchema(): SchemaOf {
+ return object({
+ passwordProtect: boolean().default(false),
+ password: string()
+ .default('')
+ .when('passwordProtect', {
+ is: true,
+ then: (schema) => schema.required('This field is required.'),
+ }),
+ });
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.tsx b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.tsx
new file mode 100644
index 000000000..7ad8b955a
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.tsx
@@ -0,0 +1,251 @@
+import { Formik, Form, Field } from 'formik';
+import { Upload } from 'lucide-react';
+import clsx from 'clsx';
+
+import {
+ isLimitedToBE,
+ isBE,
+} from '@/react/portainer/feature-flags/feature-flags.service';
+import { success as notifySuccess } from '@/portainer/services/notifications';
+import { FeatureId } from '@/react/portainer/feature-flags/enums';
+
+import { FormControl } from '@@/form-components/FormControl';
+import { LoadingButton } from '@@/buttons/LoadingButton';
+import { Input } from '@@/form-components/Input';
+import { SwitchField } from '@@/form-components/SwitchField';
+
+import {
+ useBackupS3Settings,
+ useExportS3BackupMutation,
+ useUpdateBackupS3SettingsMutation,
+} from './queries';
+import { BackupS3Model, BackupS3Settings } from './types';
+import { validationSchema } from './BackupS3Form.validation';
+import { SecurityFieldset } from './SecurityFieldset';
+
+export function BackupS3Form() {
+ const limitedToBE = isLimitedToBE(FeatureId.S3_BACKUP_SETTING);
+
+ const exportS3Mutate = useExportS3BackupMutation();
+
+ const updateS3Mutate = useUpdateBackupS3SettingsMutation();
+
+ const settingsQuery = useBackupS3Settings({ enabled: isBE });
+ if (settingsQuery.isLoading) {
+ return null;
+ }
+
+ const settings = settingsQuery.data;
+
+ const backupS3Settings = {
+ password: settings?.password || '',
+ cronRule: settings?.cronRule || '',
+ accessKeyID: settings?.accessKeyID || '',
+ secretAccessKey: settings?.secretAccessKey || '',
+ region: settings?.region || '',
+ bucketName: settings?.bucketName || '',
+ s3CompatibleHost: settings?.s3CompatibleHost || '',
+ scheduleAutomaticBackup: !!settings?.cronRule,
+ passwordProtect: !!settings?.password,
+ };
+
+ return (
+
+ initialValues={backupS3Settings}
+ validationSchema={validationSchema}
+ onSubmit={onSubmit}
+ validateOnMount
+ >
+ {({ values, errors, isSubmitting, setFieldValue, isValid }) => (
+
+ )}
+
+ );
+
+ function handleExport(values: BackupS3Settings) {
+ const payload: BackupS3Model = {
+ password: values.passwordProtect ? values.password : '',
+ cronRule: values.scheduleAutomaticBackup ? values.cronRule : '',
+ accessKeyID: values.accessKeyID,
+ secretAccessKey: values.secretAccessKey,
+ region: values.region,
+ bucketName: values.bucketName,
+ s3CompatibleHost: values.s3CompatibleHost,
+ };
+ exportS3Mutate.mutate(payload, {
+ onSuccess() {
+ notifySuccess('Success', 'Exported backup to S3 successfully');
+ },
+ });
+ }
+
+ async function onSubmit(values: BackupS3Settings) {
+ const payload: BackupS3Model = {
+ password: values.passwordProtect ? values.password : '',
+ cronRule: values.scheduleAutomaticBackup ? values.cronRule : '',
+ accessKeyID: values.accessKeyID,
+ secretAccessKey: values.secretAccessKey,
+ region: values.region,
+ bucketName: values.bucketName,
+ s3CompatibleHost: values.s3CompatibleHost,
+ };
+
+ updateS3Mutate.mutate(payload, {
+ onSuccess() {
+ notifySuccess('Success', 'S3 backup settings saved successfully');
+ },
+ });
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.validation.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.validation.ts
new file mode 100644
index 000000000..aaed56d17
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.validation.ts
@@ -0,0 +1,59 @@
+import { SchemaOf, object, string, boolean } from 'yup';
+
+import { BackupS3Settings } from './types';
+
+export function validationSchema(): SchemaOf {
+ return object({
+ passwordProtect: boolean().default(false),
+ password: string()
+ .default('')
+ .when('passwordProtect', {
+ is: true,
+ then: (schema) => schema.required('This field is required.'),
+ }),
+ scheduleAutomaticBackup: boolean().default(false),
+ cronRule: string()
+ .default('')
+ .when('scheduleAutomaticBackup', {
+ is: true,
+ then: (schema) =>
+ schema.required('This field is required.').when('cronRule', {
+ is: (val: string) => val !== '',
+ then: (schema) =>
+ schema.matches(
+ /^(\*(\/[1-9][0-9]*)?|([0-5]?[0-9]|6[0-9]|7[0-9])(-[0-5]?[0-9])?)(\s+(\*(\/[1-9][0-9]*)?|([0-5]?[0-9]|6[0-9]|7[0-9])(-[0-5]?[0-9])?)){4}$/,
+ 'Please enter a valid cron rule.'
+ ),
+ }),
+ }),
+ accessKeyID: string()
+ .default('')
+ .when('scheduleAutomaticBackup', {
+ is: true,
+ then: (schema) => schema.required('This field is required.'),
+ }),
+ secretAccessKey: string()
+ .default('')
+ .when('scheduleAutomaticBackup', {
+ is: true,
+ then: (schema) => schema.required('This field is required.'),
+ }),
+ region: string().default('').optional(),
+ bucketName: string()
+ .default('')
+ .when('scheduleAutomaticBackup', {
+ is: true,
+ then: (schema) => schema.required('This field is required.'),
+ }),
+ s3CompatibleHost: string()
+ .default('')
+ .when({
+ is: (val: string) => val !== '',
+ then: (schema) =>
+ schema.matches(
+ /^https?:\/\//,
+ 'S3 host must begin with http:// or https://.'
+ ),
+ }),
+ });
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel.tsx b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel.tsx
new file mode 100644
index 000000000..76b019b2c
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel.tsx
@@ -0,0 +1,47 @@
+import { Download } from 'lucide-react';
+import { useState } from 'react';
+
+import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
+import { FormSection } from '@@/form-components/FormSection';
+import { BoxSelector } from '@@/BoxSelector';
+
+import { BackupFormType, options } from './backup-options';
+import { BackupFileForm } from './BackupFileForm';
+import { BackupS3Form } from './BackupS3Form';
+
+export function BackupSettingsPanel() {
+ const [backupType, setBackupType] = useState(options[0].value);
+
+ return (
+
+
+
+
+
+
+
+
+ This will back up your Portainer server configuration and does
+ not include containers.
+
+ setBackupType(v)}
+ radioName="backup-type"
+ />
+
+ {backupType === BackupFormType.S3 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/SecurityFieldset.tsx b/app/react/portainer/settings/SettingsView/BackupSettingsView/SecurityFieldset.tsx
new file mode 100644
index 000000000..46529fc04
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/SecurityFieldset.tsx
@@ -0,0 +1,60 @@
+import { useField, Field } from 'formik';
+
+import { FormSection } from '@@/form-components/FormSection';
+import { FormControl } from '@@/form-components/FormControl';
+import { Input } from '@@/form-components/Input';
+import { SwitchField } from '@@/form-components/SwitchField';
+
+interface Props {
+ switchDataCy: string;
+ inputDataCy: string;
+ disabled?: boolean;
+}
+
+export function SecurityFieldset({
+ switchDataCy,
+ inputDataCy,
+ disabled,
+}: Props) {
+ const [{ value: passwordProtect }, , { setValue: setPasswordProtect }] =
+ useField('passwordProtect');
+
+ const [{ name }, { error }] = useField('password');
+
+ return (
+
+
+
+ setPasswordProtect(checked)}
+ disabled={disabled}
+ />
+
+
+
+ {passwordProtect && (
+
+
+
+ )}
+
+ );
+}
diff --git a/app/react/portainer/settings/SettingsView/backup-options.tsx b/app/react/portainer/settings/SettingsView/BackupSettingsView/backup-options.tsx
similarity index 80%
rename from app/react/portainer/settings/SettingsView/backup-options.tsx
rename to app/react/portainer/settings/SettingsView/BackupSettingsView/backup-options.tsx
index 4f86cb0e3..c91e54ef6 100644
--- a/app/react/portainer/settings/SettingsView/backup-options.tsx
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/backup-options.tsx
@@ -4,19 +4,24 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { BadgeIcon } from '@@/BadgeIcon';
+export enum BackupFormType {
+ S3 = 's3',
+ File = 'file',
+}
+
export const options = [
{
id: 'backup_file',
icon: ,
label: 'Download backup file',
- value: 'file',
+ value: BackupFormType.File,
},
{
id: 'backup_s3',
icon: ,
label: 'Store in S3',
description: 'Define a cron schedule',
- value: 's3',
+ value: BackupFormType.S3,
feature: FeatureId.S3_BACKUP_SETTING,
},
];
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/index.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/index.ts
new file mode 100644
index 000000000..ca1db9d5a
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/index.ts
@@ -0,0 +1 @@
+export { BackupSettingsPanel } from './BackupSettingsPanel';
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/backupSettings.service.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/backupSettings.service.ts
new file mode 100644
index 000000000..a1645a7d3
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/backupSettings.service.ts
@@ -0,0 +1,12 @@
+export function buildUrl(subResource?: string, action?: string) {
+ let url = 'backup';
+ if (subResource) {
+ url += `/${subResource}`;
+ }
+
+ if (action) {
+ url += `/${action}`;
+ }
+
+ return url;
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/index.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/index.ts
new file mode 100644
index 000000000..f7ef21536
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/index.ts
@@ -0,0 +1,4 @@
+export { useBackupS3Settings } from './useBackupS3Settings';
+export { useUpdateBackupS3SettingsMutation } from './useUpdateBackupS3SettingsMutation';
+export { useDownloadBackupMutation } from './useDownloadBackupMutation';
+export { useExportS3BackupMutation } from './useExportS3BackupMutation';
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/queryKeys.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/queryKeys.ts
new file mode 100644
index 000000000..125619669
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/queryKeys.ts
@@ -0,0 +1,4 @@
+export const queryKeys = {
+ base: () => ['settings'] as const,
+ backupS3Settings: () => [...queryKeys.base(), 'backupS3Settings'] as const,
+};
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useBackupS3Settings.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useBackupS3Settings.ts
new file mode 100644
index 000000000..048e7717f
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useBackupS3Settings.ts
@@ -0,0 +1,36 @@
+import { useQuery } from 'react-query';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withError } from '@/react-tools/react-query';
+
+import { BackupS3Model } from '../types';
+
+import { buildUrl } from './backupSettings.service';
+import { queryKeys } from './queryKeys';
+
+export function useBackupS3Settings({
+ select,
+ enabled,
+ onSuccess,
+}: {
+ select?: (settings: BackupS3Model) => T;
+ enabled?: boolean;
+ onSuccess?: (data: T) => void;
+} = {}) {
+ return useQuery(queryKeys.backupS3Settings(), getBackupS3Settings, {
+ select,
+ enabled,
+ ...withError('Unable to retrieve s3 backup settings'),
+ onSuccess,
+ });
+}
+
+async function getBackupS3Settings() {
+ try {
+ const { data } = await axios.get(buildUrl('s3', 'settings'));
+
+ return data;
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to retrieve s3 backup settings');
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useDownloadBackupMutation.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useDownloadBackupMutation.ts
new file mode 100644
index 000000000..d305d2b03
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useDownloadBackupMutation.ts
@@ -0,0 +1,35 @@
+import { useMutation } from 'react-query';
+import { saveAs } from 'file-saver';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withGlobalError } from '@/react-tools/react-query';
+
+import { buildUrl } from './backupSettings.service';
+
+export interface DownloadBackupPayload {
+ password: string;
+}
+
+export function useDownloadBackupMutation() {
+ return useMutation(downloadBackup, {
+ ...withGlobalError('Unable to download backup'),
+ });
+}
+
+async function downloadBackup(payload: DownloadBackupPayload) {
+ try {
+ const response = await axios.post(buildUrl(), payload, {
+ responseType: 'arraybuffer',
+ });
+
+ const file = response.data;
+ const filename = response.headers['content-disposition'].replace(
+ 'attachment; filename=',
+ ''
+ );
+ const blob = new Blob([file], { type: 'application/zip' });
+ return saveAs(blob, filename);
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to download backup');
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useExportS3BackupMutation.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useExportS3BackupMutation.ts
new file mode 100644
index 000000000..6c78c14be
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useExportS3BackupMutation.ts
@@ -0,0 +1,24 @@
+import { useMutation } from 'react-query';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withGlobalError } from '@/react-tools/react-query';
+
+import { BackupS3Model } from '../types';
+
+import { buildUrl } from './backupSettings.service';
+
+export function useExportS3BackupMutation() {
+ return useMutation(exportS3Backup, {
+ ...withGlobalError('Unable to export backup to S3'),
+ });
+}
+
+async function exportS3Backup(payload: BackupS3Model) {
+ try {
+ const response = await axios.post(buildUrl('s3', 'execute'), payload, {});
+
+ return response.data;
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to export s3 backup');
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useUpdateBackupS3SettingsMutation.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useUpdateBackupS3SettingsMutation.ts
new file mode 100644
index 000000000..7f9c3ad30
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useUpdateBackupS3SettingsMutation.ts
@@ -0,0 +1,29 @@
+import { useMutation, useQueryClient } from 'react-query';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withGlobalError } from '@/react-tools/react-query';
+
+import { BackupS3Model } from '../types';
+
+import { buildUrl } from './backupSettings.service';
+import { queryKeys } from './queryKeys';
+
+export function useUpdateBackupS3SettingsMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation(updateBackupS3Settings, {
+ onSuccess: () =>
+ queryClient.invalidateQueries(queryKeys.backupS3Settings()),
+ ...withGlobalError('Unable to save s3 backup settings'),
+ });
+}
+
+async function updateBackupS3Settings(payload: BackupS3Model) {
+ try {
+ const response = await axios.post(buildUrl('s3', 'settings'), payload);
+
+ return response.data;
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to save s3 backup settings');
+ }
+}
diff --git a/app/react/portainer/settings/SettingsView/BackupSettingsView/types.ts b/app/react/portainer/settings/SettingsView/BackupSettingsView/types.ts
new file mode 100644
index 000000000..f7394357d
--- /dev/null
+++ b/app/react/portainer/settings/SettingsView/BackupSettingsView/types.ts
@@ -0,0 +1,26 @@
+export interface BackupS3Model {
+ cronRule: string;
+ accessKeyID: string;
+ secretAccessKey: string;
+ region: string;
+ bucketName: string;
+ password: string;
+ s3CompatibleHost: string;
+}
+
+export interface BackupS3Settings {
+ passwordProtect: boolean;
+ password: string;
+ scheduleAutomaticBackup: boolean;
+ cronRule: string;
+ accessKeyID: string;
+ secretAccessKey: string;
+ region: string;
+ bucketName: string;
+ s3CompatibleHost: string;
+}
+
+export interface BackupFileSettings {
+ passwordProtect: boolean;
+ password: string;
+}