From 9f9cdf7d438d93358101841f2650b9b56c41ed62 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Mon, 19 Jun 2023 09:57:33 +1200 Subject: [PATCH] refactor(settings/backup): migrate backup setting module [EE-5508] (#9076) --- app/portainer/react/views/index.ts | 5 + app/portainer/views/settings/settings.html | 240 +---------------- .../views/settings/settingsController.js | 47 +--- .../EnableTelemetryField.tsx | 2 +- .../ApplicationSettingsPanel/LogoFieldset.tsx | 2 +- .../ScreenBannerFieldset.tsx | 2 +- .../BackupSettingsView/BackupFileForm.tsx | 68 +++++ .../BackupFileForm.validation.ts | 15 ++ .../BackupSettingsView/BackupS3Form.tsx | 251 ++++++++++++++++++ .../BackupS3Form.validation.ts | 59 ++++ .../BackupSettingsPanel.tsx | 47 ++++ .../BackupSettingsView/SecurityFieldset.tsx | 60 +++++ .../backup-options.tsx | 9 +- .../SettingsView/BackupSettingsView/index.ts | 1 + .../queries/backupSettings.service.ts | 12 + .../BackupSettingsView/queries/index.ts | 4 + .../BackupSettingsView/queries/queryKeys.ts | 4 + .../queries/useBackupS3Settings.ts | 36 +++ .../queries/useDownloadBackupMutation.ts | 35 +++ .../queries/useExportS3BackupMutation.ts | 24 ++ .../useUpdateBackupS3SettingsMutation.ts | 29 ++ .../SettingsView/BackupSettingsView/types.ts | 26 ++ 22 files changed, 689 insertions(+), 289 deletions(-) create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/BackupFileForm.tsx create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/BackupFileForm.validation.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.tsx create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/BackupS3Form.validation.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel.tsx create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/SecurityFieldset.tsx rename app/react/portainer/settings/SettingsView/{ => BackupSettingsView}/backup-options.tsx (80%) create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/index.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/backupSettings.service.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/index.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/queryKeys.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useBackupS3Settings.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useDownloadBackupMutation.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useExportS3BackupMutation.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/queries/useUpdateBackupS3SettingsMutation.ts create mode 100644 app/react/portainer/settings/SettingsView/BackupSettingsView/types.ts 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 @@ -
-
- - - -
-
Backup configuration
-
This will back up your Portainer server configuration and does not include containers.
- - - -
- -
- -
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- -
Security settings
- -
- -
- -
-
- - -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- -
-
- -
-
-
-
-
- -
-
-
-
-
Security settings
- -
- -
- -
-
- - - -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - - -
-
- -
-
-
- -
-
-
-
-
+ + 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 }) => ( +
+ + +
+
+ + Download backup + +
+
+ + )} + + ); + + 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 }) => ( +
+
+
+ setFieldValue('scheduleAutomaticBackup', e)} + /> +
+
+ + {values.scheduleAutomaticBackup && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + +
+
+ { + handleExport(values); + }} + > + Export backup + +
+
+
+
+
+ + Save backup settings + +
+
+ + )} + + ); + + 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; +}