+
+ {isCanManageDBs && (
+
+ )}
+
+
setIsPropagationOpen(false)}
+ onOk={applyPropagation}
+ okText="Apply"
+ confirmLoading={isApplyingPropagation}
+ width={600}
+ >
+ Choose what to apply to existing databases for this cluster and preview the changes before applying.
+
+ refreshPreview({ ...propagationOpts, applyStorage: e.target.checked })}
+ >
+ Apply storage
+
+ refreshPreview({ ...propagationOpts, applySchedule: e.target.checked })}
+ >
+ Apply schedule
+
+ refreshPreview({ ...propagationOpts, applyEnableBackups: e.target.checked })}
+ >
+ Apply enable backups
+
+ refreshPreview({ ...propagationOpts, respectExclusions: e.target.checked })}
+ >
+ Respect exclusions
+
+
+
+ {previewLoading ? (
+
+ ) : preview.length === 0 ? (
+ No databases would be changed with the current options.
+ ) : (
+
+ {preview.map((p) => (
+
+
{p.name}
+
+ {p.changeStorage ? 'Storage' : ''}{p.changeStorage && p.changeSchedule ? ' ยท ' : ''}{p.changeSchedule ? 'Schedule' : ''}
+
+
+ ))}
+
+ )}
+
+
+
+
{clusters.length} clusters
+
+
+
+ {isLoading ? (
+
+
+
+ ) : clusters.length === 0 ? (
+
No clusters yet
+ ) : (
+ clusters.map((c) => (
+
selectCluster(c)}
+ >
+
{c.name}
+
+ {c.postgresql.host}:{c.postgresql.port} ยท {c.postgresql.username}
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ {!editCluster ? (
+
Select a cluster on the left to manage settings, storage, and exclusions.
+ ) : (
+
+
Cluster settings
+
+
+
Name
+
setEditCluster({ ...(editCluster as Cluster), name: e.target.value })}
+ size="small"
+ className="max-w-[280px] grow"
+ placeholder="Cluster name"
+ />
+
+
+
+
PG version
+
+
+
+
Host
+
setEditCluster({ ...(editCluster as Cluster), postgresql: { ...(editCluster!.postgresql), host: e.target.value.trim().replace('https://', '').replace('http://', '') } })}
+ size="small"
+ className="max-w-[280px] grow"
+ placeholder="Enter PG host"
+ />
+
+
+
+
Port
+
typeof v === 'number' && setEditCluster({ ...(editCluster as Cluster), postgresql: { ...(editCluster!.postgresql), port: v } })}
+ size="small"
+ className="max-w-[280px] grow"
+ />
+
+
+
+
Username
+
setEditCluster({ ...(editCluster as Cluster), postgresql: { ...(editCluster!.postgresql), username: e.target.value.trim() } })}
+ size="small"
+ className="max-w-[280px] grow"
+ />
+
+
+
+
Password
+
setEditCluster({ ...(editCluster as Cluster), postgresql: { ...(editCluster!.postgresql), password: e.target.value } })}
+ size="small"
+ className="max-w-[280px] grow"
+ placeholder="Leave empty to keep"
+ />
+
+
+
+
Use HTTPS
+
setEditCluster({ ...(editCluster as Cluster), postgresql: { ...(editCluster!.postgresql), isHttps: checked } })}
+ size="small"
+ />
+
+
+
+
Enable backups
+
setEditCluster({ ...(editCluster as Cluster), isBackupsEnabled: checked })}
+ size="small"
+ />
+
+
+
+
Store period
+
+
+
+
CPU count
+
typeof v === 'number' && setEditCluster({ ...(editCluster as Cluster), cpuCount: v })}
+ size="small"
+ className="max-w-[280px] grow"
+ />
+
+
+
+
Storage
+
+
+ {editCluster.isBackupsEnabled && (
+ <>
+
+
Backup interval
+
+
+ {editCluster.backupInterval?.interval === IntervalType.WEEKLY && (
+
+
Backup weekday
+
+ )}
+
+ {editCluster.backupInterval?.interval === IntervalType.MONTHLY && (
+
+
Backup day of month
+
{
+ if (!localDom) return;
+ const ref = dayjs(editCluster.backupInterval?.timeOfDay || '04:00', 'HH:mm');
+ setEditCluster({
+ ...(editCluster as Cluster),
+ backupInterval: {
+ ...(editCluster.backupInterval || { id: undefined as unknown as string, interval: IntervalType.MONTHLY, timeOfDay: '04:00' }),
+ dayOfMonth: getUtcDayOfMonth(localDom, ref),
+ },
+ });
+ }}
+ size="small"
+ className="max-w-[280px] grow"
+ />
+
+ )}
+
+ {editCluster.backupInterval?.interval !== IntervalType.HOURLY && (
+
+
Backup time of day
+
{
+ if (!t) return;
+ setEditCluster({
+ ...(editCluster as Cluster),
+ backupInterval: {
+ ...(editCluster.backupInterval || { id: undefined as unknown as string, interval: IntervalType.DAILY }),
+ timeOfDay: t.utc().format('HH:mm'),
+ },
+ });
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
Databases in cluster
+ {isLoadingDbs ? (
+
+
+
+ ) : clusterDbs.length === 0 ? (
+
No databases found or cannot connect
+ ) : (
+
+ {clusterDbs.map((name) => (
+
+
{name}
+
{
+ const next = new Set(excluded);
+ if (e.target.checked) next.add(name.toLowerCase());
+ else next.delete(name.toLowerCase());
+ setExcluded(next);
+ }}
+ >
+ Exclude
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+
+ {isShowAddCluster && (
+
}
+ open={isShowAddCluster}
+ onCancel={() => setIsShowAddCluster(false)}
+ width={520}
+ >
+
+
+
Name
+
setNewCluster({ ...newCluster, name: e.target.value })}
+ size="small"
+ className="max-w-[220px] grow"
+ placeholder="Cluster name"
+ />
+
+
+
+
PG version
+
+
+
+
Host
+
setNewCluster({ ...newCluster, postgresql: { ...newCluster.postgresql, host: e.target.value.trim().replace('https://', '').replace('http://', '') } })}
+ size="small"
+ className="max-w-[220px] grow"
+ placeholder="Enter PG host"
+ />
+
+
+
+
Port
+
typeof v === 'number' && setNewCluster({ ...newCluster, postgresql: { ...newCluster.postgresql, port: v } })}
+ size="small"
+ className="max-w-[220px] grow"
+ placeholder="Enter PG port"
+ />
+
+
+
+
Username
+
setNewCluster({ ...newCluster, postgresql: { ...newCluster.postgresql, username: e.target.value.trim() } })}
+ size="small"
+ className="max-w/[220px] grow"
+ placeholder="Enter PG username"
+ />
+
+
+
+
Password
+
setNewCluster({ ...newCluster, postgresql: { ...newCluster.postgresql, password: e.target.value.trim() } })}
+ size="small"
+ className="max-w/[220px] grow"
+ placeholder="Enter PG password"
+ />
+
+
+
+
Use HTTPS
+
setNewCluster({ ...newCluster, postgresql: { ...newCluster.postgresql, isHttps: checked } })}
+ size="small"
+ />
+
+
+
+
Enable backups
+
setNewCluster({ ...newCluster, isBackupsEnabled: checked })}
+ size="small"
+ />
+
+
+ {newCluster.isBackupsEnabled && (
+ <>
+
+
Backup interval
+
+
+ {newCluster.backupInterval?.interval === IntervalType.WEEKLY && (
+
+
Backup weekday
+
+ )}
+
+ {newCluster.backupInterval?.interval === IntervalType.MONTHLY && (
+
+
Backup day of month
+
typeof v === 'number' && setNewCluster({ ...newCluster, backupInterval: { ...(newCluster.backupInterval || { id: undefined as unknown as string, interval: IntervalType.MONTHLY, timeOfDay: '04:00' }), dayOfMonth: v } })}
+ size="small"
+ className="max-w-[220px] grow"
+ />
+
+ )}
+
+ {newCluster.backupInterval?.interval !== IntervalType.HOURLY && (
+
+
Backup time of day
+
t && setNewCluster({ ...newCluster, backupInterval: { ...(newCluster.backupInterval || { id: undefined as unknown as string, interval: IntervalType.DAILY }), timeOfDay: t.utc().format('HH:mm') } })}
+ />
+
+ )}
+ >
+ )}
+
+
+
CPU count
+
typeof v === 'number' && setNewCluster({ ...newCluster, cpuCount: v })}
+ size="small"
+ className="max-w/[220px] grow"
+ placeholder="CPU count for pg_dump"
+ />
+
+
+
+
Store period
+
+
+
+
CPU count
+
typeof v === 'number' && setNewCluster({ ...newCluster, cpuCount: v })}
+ size="small"
+ className="max-w/[220px] grow"
+ placeholder="CPU count for pg_dump"
+ />
+
+
+
+
Storage
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/features/databases/ui/CreateDatabasesFromClusterComponent.tsx b/frontend/src/features/databases/ui/CreateDatabasesFromClusterComponent.tsx
new file mode 100644
index 0000000..06370d4
--- /dev/null
+++ b/frontend/src/features/databases/ui/CreateDatabasesFromClusterComponent.tsx
@@ -0,0 +1,331 @@
+import { Button, Input, InputNumber, Select, Spin, Switch } from 'antd';
+import { useState } from 'react';
+
+import { backupsApi, type BackupConfig, backupConfigApi } from '../../../entity/backups';
+import { type Database, DatabaseType, type PostgresqlDatabase, databaseApi } from '../../../entity/databases';
+import { PostgresqlVersion } from '../../../entity/databases/model/postgresql/PostgresqlVersion';
+import type { Notifier } from '../../../entity/notifiers';
+import { EditBackupConfigComponent } from '../../backups';
+import { EditDatabaseNotifiersComponent } from './edit/EditDatabaseNotifiersComponent';
+
+interface Props {
+ workspaceId: string;
+ onCreated: () => void;
+ onClose: () => void;
+}
+
+export const CreateDatabasesFromClusterComponent = ({ workspaceId, onCreated, onClose }: Props) => {
+ const [step, setStep] = useState<'cluster-settings' | 'select-databases' | 'backup-config' | 'notifiers' | 'creating'>('cluster-settings');
+
+ const [clusterDb, setClusterDb] = useState