mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
feat(admin): add H2 database backup & restore management to admin UI (#5528)
# Description of Changes This PR introduces comprehensive database backup and restore management for embedded H2 databases directly within the admin UI, along with a corrected backend condition for H2 detection. ### What was changed - Fixed `H2SQLCondition` logic so embedded H2 is correctly detected when custom databases are disabled. - Updated and expanded unit tests to reflect the corrected H2 enablement behavior. - Added a new **Backups & Restore** section to the Admin Database settings UI. - Implemented UI features to: - Create H2 database backups - List available backup files with metadata - Download, import, and delete existing backups - Upload and import `.sql` backup files with a confirmation code safety step - Added a new frontend service (`databaseManagementService`) to encapsulate database management API calls. - Extended English (en-GB) translations for all new database backup and restore UI elements. ### Why the change was made - Embedded H2 is the default database when no custom database is configured, but this was not consistently reflected in backend conditions. - Administrators need a safe, UI-driven way to manage H2 backups without manual filesystem access. - The new UI improves operability, safety (confirmation code on import), and transparency (version and status badges). <img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/15869b9c-fa86-4b31-ac9b-8e6424138b73" /> <img width="1920" height="1032" alt="image" src="https://github.com/user-attachments/assets/8668dc07-0384-40f0-91e1-c6973c1ce535" /> Closes #5515 --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -12,8 +12,9 @@ public class H2SQLCondition implements Condition {
|
||||
boolean enableCustomDatabase =
|
||||
env.getProperty("system.datasource.enableCustomDatabase", Boolean.class, false);
|
||||
|
||||
if (enableCustomDatabase) {
|
||||
return false;
|
||||
// If custom database is not enabled, H2 is used by default
|
||||
if (!enableCustomDatabase) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String dataSourceType = env.getProperty("system.datasource.type", String.class, "");
|
||||
|
||||
@@ -36,12 +36,12 @@ class H2SQLConditionTest {
|
||||
|
||||
@Test
|
||||
void returnsFalse_whenEnableCustomDatabase_true_regardlessOfType() {
|
||||
// Flag=true, Typ=h2 -> false
|
||||
// Flag=true, Typ=h2 -> true
|
||||
MockEnvironment envTrueH2 =
|
||||
new MockEnvironment()
|
||||
.withProperty("system.datasource.enableCustomDatabase", "true")
|
||||
.withProperty("system.datasource.type", "h2");
|
||||
assertFalse(eval(envTrueH2));
|
||||
assertTrue(eval(envTrueH2));
|
||||
|
||||
// Flag=true, Typ=postgres -> false
|
||||
MockEnvironment envTrueOther =
|
||||
@@ -57,20 +57,6 @@ class H2SQLConditionTest {
|
||||
assertFalse(eval(envTrueMissingType));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsFalse_whenTypeNotH2_orMissing_andFlagNotEnabled() {
|
||||
// Flag fehlt, Typ=postgres -> false
|
||||
MockEnvironment envNotH2 =
|
||||
new MockEnvironment().withProperty("system.datasource.type", "postgresql");
|
||||
assertFalse(eval(envNotH2));
|
||||
|
||||
// Flag=false, Typ fehlt -> false (Default: "")
|
||||
MockEnvironment envMissingType =
|
||||
new MockEnvironment()
|
||||
.withProperty("system.datasource.enableCustomDatabase", "false");
|
||||
assertFalse(eval(envMissingType));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsFalse_whenEnabled_but_type_not_h2_or_missing() {
|
||||
MockEnvironment envNotH2 =
|
||||
@@ -84,4 +70,18 @@ class H2SQLConditionTest {
|
||||
.withProperty("system.datasource.enableCustomDatabase", "true");
|
||||
assertFalse(eval(envMissingType));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsTrue_whenTypeNotH2_andCustomDatabaseDisabled() {
|
||||
MockEnvironment envNotH2 =
|
||||
new MockEnvironment()
|
||||
.withProperty("system.datasource.enableCustomDatabase", "false")
|
||||
.withProperty("system.datasource.type", "postgresql");
|
||||
assertTrue(eval(envNotH2));
|
||||
|
||||
MockEnvironment envMissingType =
|
||||
new MockEnvironment()
|
||||
.withProperty("system.datasource.enableCustomDatabase", "false");
|
||||
assertTrue(eval(envMissingType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4759,6 +4759,48 @@ mobileScannerStretchToFitDesc = "Stretch images to fill the entire page. If disa
|
||||
title = "Database"
|
||||
description = "Configure custom database connection settings for enterprise deployments."
|
||||
configuration = "Database Configuration"
|
||||
backupTitle = "Backups & Restore"
|
||||
backupDescription = "Manage H2 backups directly from the admin console."
|
||||
loadError = "Failed to load database backups"
|
||||
backupCreated = "Backup created successfully"
|
||||
backupFailed = "Failed to create backup"
|
||||
deleteTitle = "Delete backup"
|
||||
deleteConfirm = "Delete this backup? This cannot be undone."
|
||||
deleteSuccess = "Backup deleted"
|
||||
deleteFailed = "Failed to delete backup"
|
||||
deleteConfirmAction = "Delete backup"
|
||||
downloadFailed = "Failed to download backup"
|
||||
version = "H2 Version"
|
||||
embedded = "Embedded H2"
|
||||
external = "External DB"
|
||||
h2Only = "Backups are available only for the embedded H2 database."
|
||||
h2Hint = "Set the database type to H2 and disable custom database to enable backup and restore."
|
||||
manageBackups = "Manage backups"
|
||||
refresh = "Refresh"
|
||||
createBackup = "Create backup"
|
||||
uploadTitle = "Upload & import"
|
||||
chooseFile = "Choose a .sql backup file"
|
||||
importFromUpload = "Import upload"
|
||||
confirmImportTitle = "Confirm database import"
|
||||
overwriteWarning = "Warning: This will overwrite the current database."
|
||||
overwriteWarningBody = "All existing data will be replaced by the uploaded backup. This action cannot be undone."
|
||||
confirmCodeLabel = "Enter the confirmation code to proceed"
|
||||
enterCode = "Enter the code shown above"
|
||||
confirmImport = "Confirm import"
|
||||
codeMismatch = "Confirmation code does not match"
|
||||
codeMismatchBody = "Please enter the code exactly as shown to proceed."
|
||||
selectFile = "Please select a .sql file to import"
|
||||
importSuccess = "Backup imported successfully"
|
||||
importFailed = "Failed to import backup"
|
||||
noBackups = "No backups found yet."
|
||||
unavailable = "Backup list unavailable for the current database configuration."
|
||||
fileName = "File"
|
||||
created = "Created"
|
||||
size = "Size"
|
||||
actions = "Actions"
|
||||
download = "Download"
|
||||
import = "Import"
|
||||
delete = "Delete"
|
||||
|
||||
[admin.settings.database.enableCustom]
|
||||
label = "Enable Custom Database"
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, TextInput, PasswordInput, Select, Badge } from '@mantine/core';
|
||||
import { alert } from '@app/components/toast';
|
||||
import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal';
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
NumberInput,
|
||||
Switch,
|
||||
Button,
|
||||
Stack,
|
||||
Paper,
|
||||
Text,
|
||||
Loader,
|
||||
Group,
|
||||
TextInput,
|
||||
PasswordInput,
|
||||
Select,
|
||||
Badge,
|
||||
Table,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
FileInput,
|
||||
Alert,
|
||||
Divider,
|
||||
Box,
|
||||
Modal,
|
||||
} from "@mantine/core";
|
||||
import { alert } from "@app/components/toast";
|
||||
import RestartConfirmationModal from "@app/components/shared/config/RestartConfirmationModal";
|
||||
import { useRestartServer } from "@app/components/shared/config/useRestartServer";
|
||||
import { useAdminSettings } from "@app/hooks/useAdminSettings";
|
||||
import PendingBadge from "@app/components/shared/config/PendingBadge";
|
||||
import { useLoginRequired } from "@app/hooks/useLoginRequired";
|
||||
import LoginRequiredBanner from "@app/components/shared/config/LoginRequiredBanner";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import databaseManagementService, { DatabaseBackupFile } from "@app/services/databaseManagementService";
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
|
||||
|
||||
interface DatabaseSettingsData {
|
||||
enableCustomDatabase?: boolean;
|
||||
@@ -25,60 +49,66 @@ export default function AdminDatabaseSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
const [backupFiles, setBackupFiles] = useState<DatabaseBackupFile[]>([]);
|
||||
const [databaseVersion, setDatabaseVersion] = useState<string | null>(null);
|
||||
const [backupsLoading, setBackupsLoading] = useState(false);
|
||||
const [creatingBackup, setCreatingBackup] = useState(false);
|
||||
const [importingUpload, setImportingUpload] = useState(false);
|
||||
const [importingBackupFile, setImportingBackupFile] = useState<string | null>(null);
|
||||
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [confirmImportOpen, setConfirmImportOpen] = useState(false);
|
||||
const [deleteConfirmFile, setDeleteConfirmFile] = useState<string | null>(null);
|
||||
const [confirmCode, setConfirmCode] = useState("");
|
||||
const [confirmInput, setConfirmInput] = useState("");
|
||||
|
||||
const {
|
||||
settings,
|
||||
setSettings,
|
||||
loading,
|
||||
saving,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
isFieldPending,
|
||||
} = useAdminSettings<DatabaseSettingsData>({
|
||||
sectionName: 'database',
|
||||
fetchTransformer: async () => {
|
||||
const response = await apiClient.get('/api/v1/admin/settings/section/system');
|
||||
const systemData = response.data || {};
|
||||
const { settings, setSettings, loading, saving, fetchSettings, saveSettings, isFieldPending } =
|
||||
useAdminSettings<DatabaseSettingsData>({
|
||||
sectionName: "database",
|
||||
fetchTransformer: async () => {
|
||||
const response = await apiClient.get("/api/v1/admin/settings/section/system");
|
||||
const systemData = response.data || {};
|
||||
|
||||
// Extract datasource from system response and handle pending
|
||||
const datasource = systemData.datasource || {
|
||||
enableCustomDatabase: false,
|
||||
customDatabaseUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
type: 'postgresql',
|
||||
hostName: 'localhost',
|
||||
port: 5432,
|
||||
name: 'postgres'
|
||||
};
|
||||
// Extract datasource from system response and handle pending
|
||||
const datasource = systemData.datasource || {
|
||||
enableCustomDatabase: false,
|
||||
customDatabaseUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
type: "postgresql",
|
||||
hostName: "localhost",
|
||||
port: 5432,
|
||||
name: "postgres",
|
||||
};
|
||||
|
||||
// Map pending changes from system._pending.datasource to root level
|
||||
const result: any = { ...datasource };
|
||||
if (systemData._pending?.datasource) {
|
||||
result._pending = systemData._pending.datasource;
|
||||
}
|
||||
// Map pending changes from system._pending.datasource to root level
|
||||
const result: any = { ...datasource };
|
||||
if (systemData._pending?.datasource) {
|
||||
result._pending = systemData._pending.datasource;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
saveTransformer: (settings) => {
|
||||
// Convert flat settings to dot-notation for delta endpoint
|
||||
const deltaSettings: Record<string, any> = {
|
||||
'system.datasource.enableCustomDatabase': settings.enableCustomDatabase,
|
||||
'system.datasource.customDatabaseUrl': settings.customDatabaseUrl,
|
||||
'system.datasource.username': settings.username,
|
||||
'system.datasource.password': settings.password,
|
||||
'system.datasource.type': settings.type,
|
||||
'system.datasource.hostName': settings.hostName,
|
||||
'system.datasource.port': settings.port,
|
||||
'system.datasource.name': settings.name
|
||||
};
|
||||
return result;
|
||||
},
|
||||
saveTransformer: (settings) => {
|
||||
// Convert flat settings to dot-notation for delta endpoint
|
||||
const deltaSettings: Record<string, any> = {
|
||||
"system.datasource.enableCustomDatabase": settings.enableCustomDatabase,
|
||||
"system.datasource.customDatabaseUrl": settings.customDatabaseUrl,
|
||||
"system.datasource.username": settings.username,
|
||||
"system.datasource.password": settings.password,
|
||||
"system.datasource.type": settings.type,
|
||||
"system.datasource.hostName": settings.hostName,
|
||||
"system.datasource.port": settings.port,
|
||||
"system.datasource.name": settings.name,
|
||||
};
|
||||
|
||||
return {
|
||||
sectionData: {},
|
||||
deltaSettings
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
sectionData: {},
|
||||
deltaSettings,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loginEnabled) {
|
||||
@@ -86,6 +116,45 @@ export default function AdminDatabaseSection() {
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
const datasourceType = (settings?.type || "").toLowerCase();
|
||||
const isCustomDatabase = settings?.enableCustomDatabase === true;
|
||||
const isEmbeddedH2 = useMemo(() => {
|
||||
if (isCustomDatabase === false) {
|
||||
return true;
|
||||
}
|
||||
if (datasourceType === "h2") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [isCustomDatabase, datasourceType]);
|
||||
|
||||
const loadBackupData = async () => {
|
||||
if (!loginEnabled || !isEmbeddedH2) {
|
||||
setBackupFiles([]);
|
||||
setDatabaseVersion(null);
|
||||
return;
|
||||
}
|
||||
setBackupsLoading(true);
|
||||
try {
|
||||
const data = await databaseManagementService.getDatabaseData();
|
||||
setBackupFiles(data.backupFiles || []);
|
||||
setDatabaseVersion(data.databaseVersion || null);
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.loadError", "Failed to load database backups"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
setBackupsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBackupData();
|
||||
}, [loginEnabled, isEmbeddedH2, isCustomDatabase, datasourceType]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
@@ -96,13 +165,169 @@ export default function AdminDatabaseSection() {
|
||||
showRestartModal();
|
||||
} catch (_error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
alertType: "error",
|
||||
title: t("admin.error", "Error"),
|
||||
body: t("admin.settings.saveError", "Failed to save settings"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
setCreatingBackup(true);
|
||||
try {
|
||||
await databaseManagementService.createBackup();
|
||||
alert({ alertType: "success", title: t("admin.settings.database.backupCreated", "Backup created successfully") });
|
||||
await loadBackupData();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.backupFailed", "Failed to create backup"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
setCreatingBackup(false);
|
||||
}
|
||||
};
|
||||
|
||||
const performUploadImport = async () => {
|
||||
if (!uploadFile) return;
|
||||
setImportingUpload(true);
|
||||
try {
|
||||
await databaseManagementService.uploadAndImport(uploadFile);
|
||||
alert({ alertType: "success", title: t("admin.settings.database.importSuccess", "Backup imported successfully") });
|
||||
setUploadFile(null);
|
||||
await loadBackupData();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.importFailed", "Failed to import backup"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
setImportingUpload(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateConfirmationCode = () => {
|
||||
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
||||
const array = new Uint32Array(1);
|
||||
crypto.getRandomValues(array);
|
||||
const randomNumber = array[0] % 10000; // 0-9999
|
||||
return randomNumber.toString().padStart(4, "0");
|
||||
}
|
||||
// Fallback: non-cryptographic but avoids Math.random(); this is only a UX safeguard.
|
||||
const fallbackNumber = Date.now() % 10000;
|
||||
return fallbackNumber.toString().padStart(4, "0");
|
||||
};
|
||||
|
||||
const handleUploadImport = () => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
if (!uploadFile) {
|
||||
alert({ alertType: "warning", title: t("admin.settings.database.selectFile", "Please select a .sql file to import") });
|
||||
return;
|
||||
}
|
||||
const code = generateConfirmationCode();
|
||||
setConfirmCode(code);
|
||||
setConfirmInput("");
|
||||
setConfirmImportOpen(true);
|
||||
};
|
||||
|
||||
const closeConfirmImportModal = () => {
|
||||
setConfirmImportOpen(false);
|
||||
setConfirmInput("");
|
||||
};
|
||||
|
||||
const handleConfirmImport = async () => {
|
||||
if (confirmInput !== confirmCode) {
|
||||
alert({
|
||||
alertType: "warning",
|
||||
title: t("admin.settings.database.codeMismatch", "Confirmation code does not match"),
|
||||
body: t("admin.settings.database.codeMismatchBody", "Please enter the code exactly as shown to proceed."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
closeConfirmImportModal();
|
||||
await performUploadImport();
|
||||
};
|
||||
|
||||
const handleImportExisting = async (fileName: string) => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
setImportingBackupFile(fileName);
|
||||
try {
|
||||
await databaseManagementService.importFromFileName(fileName);
|
||||
alert({ alertType: "success", title: t("admin.settings.database.importSuccess", "Backup imported successfully") });
|
||||
await loadBackupData();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.importFailed", "Failed to import backup"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
setImportingBackupFile(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (fileName: string) => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
setDeletingFile(fileName);
|
||||
try {
|
||||
await databaseManagementService.deleteBackup(fileName);
|
||||
alert({ alertType: "success", title: t("admin.settings.database.deleteSuccess", "Backup deleted") });
|
||||
await loadBackupData();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.deleteFailed", "Failed to delete backup"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
setDeletingFile(null);
|
||||
setDeleteConfirmFile(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (fileName: string) => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
setDeleteConfirmFile(fileName);
|
||||
};
|
||||
|
||||
const handleDownload = async (fileName: string) => {
|
||||
if (!validateLoginEnabled()) return;
|
||||
setDownloadingFile(fileName);
|
||||
let url: string | null = null;
|
||||
|
||||
const link = document.createElement("a");
|
||||
try {
|
||||
const blob = await databaseManagementService.downloadBackup(fileName);
|
||||
url = window.URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.settings.database.downloadFailed", "Failed to download backup"),
|
||||
body: message,
|
||||
});
|
||||
} finally {
|
||||
if (link.isConnected) {
|
||||
link.remove();
|
||||
}
|
||||
if (url) {
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
setDownloadingFile(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
@@ -121,25 +346,39 @@ export default function AdminDatabaseSection() {
|
||||
<div>
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.database.title', 'Database')}</Text>
|
||||
<Text fw={600} size="lg">
|
||||
{t("admin.settings.database.title", "Database")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.database.description', 'Configure custom database connection settings for enterprise deployments.')}
|
||||
{t(
|
||||
"admin.settings.database.description",
|
||||
"Configure custom database connection settings for enterprise deployments.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Badge color="grape" size="lg">ENTERPRISE</Badge>
|
||||
<Badge color="grape" size="lg">
|
||||
ENTERPRISE
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{/* Database Configuration */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.database.configuration', 'Database Configuration')}</Text>
|
||||
<Text fw={600} size="sm" mb="xs">
|
||||
{t("admin.settings.database.configuration", "Database Configuration")}
|
||||
</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.database.enableCustom.label', 'Enable Custom Database')}</Text>
|
||||
<Text fw={500} size="sm">
|
||||
{t("admin.settings.database.enableCustom.label", "Enable Custom Database")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.database.enableCustom.description', 'Use your own custom database configuration instead of the default embedded database')}
|
||||
{t(
|
||||
"admin.settings.database.enableCustom.description",
|
||||
"Use your own custom database configuration instead of the default embedded database",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
@@ -152,7 +391,7 @@ export default function AdminDatabaseSection() {
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('enableCustomDatabase')} />
|
||||
<PendingBadge show={isFieldPending("enableCustomDatabase")} />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
@@ -162,12 +401,15 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.customUrl.label', 'Custom Database URL')}</span>
|
||||
<PendingBadge show={isFieldPending('customDatabaseUrl')} />
|
||||
<span>{t("admin.settings.database.customUrl.label", "Custom Database URL")}</span>
|
||||
<PendingBadge show={isFieldPending("customDatabaseUrl")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.customUrl.description', 'Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.')}
|
||||
value={settings?.customDatabaseUrl || ''}
|
||||
description={t(
|
||||
"admin.settings.database.customUrl.description",
|
||||
"Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.",
|
||||
)}
|
||||
value={settings?.customDatabaseUrl || ""}
|
||||
onChange={(e) => setSettings({ ...settings, customDatabaseUrl: e.target.value })}
|
||||
placeholder="jdbc:postgresql://localhost:5432/postgres"
|
||||
disabled={!loginEnabled}
|
||||
@@ -178,18 +420,21 @@ export default function AdminDatabaseSection() {
|
||||
<Select
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.type.label', 'Database Type')}</span>
|
||||
<PendingBadge show={isFieldPending('type')} />
|
||||
<span>{t("admin.settings.database.type.label", "Database Type")}</span>
|
||||
<PendingBadge show={isFieldPending("type")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.type.description', 'Type of database (not used if custom URL is provided)')}
|
||||
value={settings?.type || 'postgresql'}
|
||||
onChange={(value) => setSettings({ ...settings, type: value || 'postgresql' })}
|
||||
description={t(
|
||||
"admin.settings.database.type.description",
|
||||
"Type of database (not used if custom URL is provided)",
|
||||
)}
|
||||
value={settings?.type || "postgresql"}
|
||||
onChange={(value) => setSettings({ ...settings, type: value || "postgresql" })}
|
||||
data={[
|
||||
{ value: 'postgresql', label: 'PostgreSQL' },
|
||||
{ value: 'h2', label: 'H2' },
|
||||
{ value: 'mysql', label: 'MySQL' },
|
||||
{ value: 'mariadb', label: 'MariaDB' }
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "h2", label: "H2" },
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "mariadb", label: "MariaDB" },
|
||||
]}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
@@ -199,12 +444,15 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.hostName.label', 'Host Name')}</span>
|
||||
<PendingBadge show={isFieldPending('hostName')} />
|
||||
<span>{t("admin.settings.database.hostName.label", "Host Name")}</span>
|
||||
<PendingBadge show={isFieldPending("hostName")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.hostName.description', 'Database server hostname (not used if custom URL is provided)')}
|
||||
value={settings?.hostName || ''}
|
||||
description={t(
|
||||
"admin.settings.database.hostName.description",
|
||||
"Database server hostname (not used if custom URL is provided)",
|
||||
)}
|
||||
value={settings?.hostName || ""}
|
||||
onChange={(e) => setSettings({ ...settings, hostName: e.target.value })}
|
||||
placeholder="localhost"
|
||||
disabled={!loginEnabled}
|
||||
@@ -215,11 +463,14 @@ export default function AdminDatabaseSection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.port.label', 'Port')}</span>
|
||||
<PendingBadge show={isFieldPending('port')} />
|
||||
<span>{t("admin.settings.database.port.label", "Port")}</span>
|
||||
<PendingBadge show={isFieldPending("port")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.port.description', 'Database server port (not used if custom URL is provided)')}
|
||||
description={t(
|
||||
"admin.settings.database.port.description",
|
||||
"Database server port (not used if custom URL is provided)",
|
||||
)}
|
||||
value={settings?.port || 5432}
|
||||
onChange={(value) => setSettings({ ...settings, port: Number(value) })}
|
||||
min={1}
|
||||
@@ -232,12 +483,15 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.name.label', 'Database Name')}</span>
|
||||
<PendingBadge show={isFieldPending('name')} />
|
||||
<span>{t("admin.settings.database.name.label", "Database Name")}</span>
|
||||
<PendingBadge show={isFieldPending("name")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.name.description', 'Name of the database (not used if custom URL is provided)')}
|
||||
value={settings?.name || ''}
|
||||
description={t(
|
||||
"admin.settings.database.name.description",
|
||||
"Name of the database (not used if custom URL is provided)",
|
||||
)}
|
||||
value={settings?.name || ""}
|
||||
onChange={(e) => setSettings({ ...settings, name: e.target.value })}
|
||||
placeholder="postgres"
|
||||
disabled={!loginEnabled}
|
||||
@@ -248,12 +502,12 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.username.label', 'Username')}</span>
|
||||
<PendingBadge show={isFieldPending('username')} />
|
||||
<span>{t("admin.settings.database.username.label", "Username")}</span>
|
||||
<PendingBadge show={isFieldPending("username")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.username.description', 'Database authentication username')}
|
||||
value={settings?.username || ''}
|
||||
description={t("admin.settings.database.username.description", "Database authentication username")}
|
||||
value={settings?.username || ""}
|
||||
onChange={(e) => setSettings({ ...settings, username: e.target.value })}
|
||||
placeholder="postgres"
|
||||
disabled={!loginEnabled}
|
||||
@@ -264,12 +518,12 @@ export default function AdminDatabaseSection() {
|
||||
<PasswordInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.password.label', 'Password')}</span>
|
||||
<PendingBadge show={isFieldPending('password')} />
|
||||
<span>{t("admin.settings.database.password.label", "Password")}</span>
|
||||
<PendingBadge show={isFieldPending("password")} />
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.password.description', 'Database authentication password')}
|
||||
value={settings?.password || ''}
|
||||
description={t("admin.settings.database.password.description", "Database authentication password")}
|
||||
value={settings?.password || ""}
|
||||
onChange={(e) => setSettings({ ...settings, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
disabled={!loginEnabled}
|
||||
@@ -283,16 +537,266 @@ export default function AdminDatabaseSection() {
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
{t("admin.settings.save", "Save Changes")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
{t("admin.settings.database.backupTitle", "Backups & Restore")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("admin.settings.database.backupDescription", "Manage H2 backups directly from the admin console.")}
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
{databaseVersion && (
|
||||
<Badge color="blue" variant="light">
|
||||
{t("admin.settings.database.version", "H2 Version")}: {databaseVersion}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge color={isEmbeddedH2 ? "green" : "red"} variant="light">
|
||||
{isEmbeddedH2
|
||||
? t("admin.settings.database.embedded", "Embedded H2")
|
||||
: t("admin.settings.database.external", "External DB")}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{!isEmbeddedH2 && (
|
||||
<Alert icon={<LocalIcon icon="info" width="1.2rem" height="1.2rem" />} color="yellow" radius="md">
|
||||
<Text fw={600} size="sm">
|
||||
{t("admin.settings.database.h2Only", "Backups are available only for the embedded H2 database.")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"admin.settings.database.h2Hint",
|
||||
"Set the database type to H2 and disable custom database to enable backup and restore.",
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{isEmbeddedH2 && (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="backup" width="1.4rem" height="1.4rem" />
|
||||
<Text fw={600}>{t("admin.settings.database.manageBackups", "Manage backups")}</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<LocalIcon icon="refresh" width="1rem" height="1rem" />}
|
||||
onClick={loadBackupData}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
>
|
||||
{t("admin.settings.database.refresh", "Refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<LocalIcon icon="cloud-upload" width="1rem" height="1rem" />}
|
||||
onClick={handleCreateBackup}
|
||||
loading={creatingBackup}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
>
|
||||
{t("admin.settings.database.createBackup", "Create backup")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Box>
|
||||
<Text fw={500} size="sm" mb={6}>
|
||||
{t("admin.settings.database.uploadTitle", "Upload & import")}
|
||||
</Text>
|
||||
<Group gap="sm" align="flex-end" wrap="wrap">
|
||||
<FileInput
|
||||
value={uploadFile}
|
||||
onChange={setUploadFile}
|
||||
placeholder={t("admin.settings.database.chooseFile", "Choose a .sql backup file")}
|
||||
accept=".sql"
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
styles={{ input: { minWidth: 280 } }}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUploadImport}
|
||||
loading={importingUpload}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
leftSection={<LocalIcon icon="play-circle" width="1rem" height="1rem" />}
|
||||
>
|
||||
{t("admin.settings.database.importFromUpload", "Import upload")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{backupsLoading ? (
|
||||
<Group justify="center" py="md">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : backupFiles.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
{isEmbeddedH2
|
||||
? t("admin.settings.database.noBackups", "No backups found yet.")
|
||||
: t(
|
||||
"admin.settings.database.unavailable",
|
||||
"Backup list unavailable for the current database configuration.",
|
||||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<Table highlightOnHover withColumnBorders verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("admin.settings.database.fileName", "File")}</Table.Th>
|
||||
<Table.Th>{t("admin.settings.database.created", "Created")}</Table.Th>
|
||||
<Table.Th>{t("admin.settings.database.size", "Size")}</Table.Th>
|
||||
<Table.Th w={150}>{t("admin.settings.database.actions", "Actions")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{backupFiles.map((backup) => (
|
||||
<Table.Tr key={backup.fileName}>
|
||||
<Table.Td>{backup.fileName}</Table.Td>
|
||||
<Table.Td>{backup.formattedCreationDate || backup.creationDate || "-"}</Table.Td>
|
||||
<Table.Td>{backup.formattedFileSize || "-"}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-start">
|
||||
<Tooltip label={t("admin.settings.database.download", "Download")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => handleDownload(backup.fileName)}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
>
|
||||
{downloadingFile === backup.fileName ? (
|
||||
<Loader size="xs" />
|
||||
) : (
|
||||
<LocalIcon icon="download" width="1rem" height="1rem" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("admin.settings.database.import", "Import")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => handleImportExisting(backup.fileName)}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
>
|
||||
{importingBackupFile === backup.fileName ? (
|
||||
<Loader size="xs" />
|
||||
) : (
|
||||
<LocalIcon icon="backup" width="1rem" height="1rem" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("admin.settings.database.delete", "Delete")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => handleDeleteClick(backup.fileName)}
|
||||
disabled={!loginEnabled || !isEmbeddedH2}
|
||||
>
|
||||
{deletingFile === backup.fileName ? (
|
||||
<Loader size="xs" />
|
||||
) : (
|
||||
<LocalIcon icon="delete" width="1rem" height="1rem" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
<RestartConfirmationModal opened={restartModalOpened} onClose={closeRestartModal} onRestart={restartServer} />
|
||||
|
||||
<Modal
|
||||
opened={confirmImportOpen}
|
||||
onClose={closeConfirmImportModal}
|
||||
title={t("admin.settings.database.confirmImportTitle", "Confirm database import")}
|
||||
centered
|
||||
withinPortal
|
||||
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Alert color="red" variant="light" icon={<LocalIcon icon="warning" width="1.2rem" height="1.2rem" />}>
|
||||
<Text fw={600}>
|
||||
{t("admin.settings.database.overwriteWarning", "Warning: This will overwrite the current database.")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"admin.settings.database.overwriteWarningBody",
|
||||
"All existing data will be replaced by the uploaded backup. This action cannot be undone.",
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
<Stack gap={6}>
|
||||
<Text size="sm" fw={600}>
|
||||
{t("admin.settings.database.confirmCodeLabel", "Enter the confirmation code to proceed")}
|
||||
</Text>
|
||||
<Text size="lg" fw={700}>
|
||||
{confirmCode}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={confirmInput}
|
||||
onChange={(e) => setConfirmInput(e.currentTarget.value)}
|
||||
placeholder={t("admin.settings.database.enterCode", "Enter the code shown above")}
|
||||
minLength={4}
|
||||
maxLength={4}
|
||||
disabled={importingUpload}
|
||||
/>
|
||||
</Stack>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={closeConfirmImportModal} disabled={importingUpload}>
|
||||
{t("cancel", "Cancel")}
|
||||
</Button>
|
||||
<Button color="red" onClick={handleConfirmImport} loading={importingUpload} disabled={confirmInput.length === 0}>
|
||||
{t("admin.settings.database.confirmImport", "Confirm import")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
opened={deleteConfirmFile !== null}
|
||||
onClose={() => setDeleteConfirmFile(null)}
|
||||
title={t("admin.settings.database.deleteTitle", "Delete backup")}
|
||||
centered
|
||||
withinPortal
|
||||
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Alert color="red" variant="light" icon={<LocalIcon icon="warning" width="1.2rem" height="1.2rem" />}>
|
||||
<Text fw={600}>
|
||||
{t("admin.settings.database.deleteConfirm", "Delete this backup? This cannot be undone.")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{deleteConfirmFile}
|
||||
</Text>
|
||||
</Alert>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={() => setDeleteConfirmFile(null)} disabled={deletingFile !== null}>
|
||||
{t("cancel", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => deleteConfirmFile && handleDelete(deleteConfirmFile)}
|
||||
loading={deletingFile === deleteConfirmFile}
|
||||
>
|
||||
{t("admin.settings.database.deleteConfirmAction", "Delete backup")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import apiClient from "@app/services/apiClient";
|
||||
|
||||
export interface DatabaseBackupFile {
|
||||
fileName: string;
|
||||
filePath?: string;
|
||||
formattedCreationDate?: string;
|
||||
formattedFileSize?: string;
|
||||
creationDate?: string;
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
export interface DatabaseData {
|
||||
backupFiles: DatabaseBackupFile[];
|
||||
databaseVersion: string;
|
||||
versionUnknown: boolean;
|
||||
}
|
||||
|
||||
const databaseManagementService = {
|
||||
async getDatabaseData(): Promise<DatabaseData> {
|
||||
const response = await apiClient.get<DatabaseData>("/api/v1/proprietary/ui-data/database", {
|
||||
suppressErrorToast: true,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createBackup(): Promise<void> {
|
||||
await apiClient.get("/api/v1/database/createDatabaseBackup");
|
||||
},
|
||||
|
||||
async importFromFileName(fileName: string): Promise<void> {
|
||||
await apiClient.get(`/api/v1/database/import-database-file/${encodeURIComponent(fileName)}`);
|
||||
},
|
||||
|
||||
async uploadAndImport(file: File): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
|
||||
await apiClient.post("/api/v1/database/import-database", formData);
|
||||
},
|
||||
|
||||
async deleteBackup(fileName: string): Promise<void> {
|
||||
await apiClient.get(`/api/v1/database/delete/${encodeURIComponent(fileName)}`);
|
||||
},
|
||||
|
||||
async downloadBackup(fileName: string): Promise<Blob> {
|
||||
const response = await apiClient.get(`/api/v1/database/download/${encodeURIComponent(fileName)}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default databaseManagementService;
|
||||
Reference in New Issue
Block a user