Compare commits

..

7 Commits

Author SHA1 Message Date
Rostislav Dugin
7fb59bb5d0 FEATURE (search): Add search for databases when there are more than 5 2025-09-30 12:27:17 +03:00
Rostislav Dugin
dc9ddae42e FIX (copying): Fix persisting same interval over copying 2025-09-30 12:26:57 +03:00
Rostislav Dugin
a409c8ccb3 FIX (docker): Add PostgreSQL 18 to Docker 2025-09-27 14:39:51 +03:00
Rostislav Dugin
a018b0c62f FEATURE (postgres): Add PostgreSQL 18 support 2025-09-27 10:36:36 +03:00
Rostislav Dugin
97d7253dda FEATURE (databases): Add DB copying 2025-09-27 10:15:18 +03:00
Rostislav Dugin
81aadd19e1 FEATURE (docs): Update contribution priorities 2025-09-21 11:30:25 +03:00
Rostislav Dugin
432bdced3e FEATURE (readme): Update readme description 2025-09-21 11:26:53 +03:00
31 changed files with 279 additions and 112 deletions

View File

@@ -132,11 +132,12 @@ jobs:
TEST_POSTGRES_15_PORT=5003
TEST_POSTGRES_16_PORT=5004
TEST_POSTGRES_17_PORT=5005
TEST_POSTGRES_18_PORT=5006
# testing S3
TEST_MINIO_PORT=9000
TEST_MINIO_CONSOLE_PORT=9001
# testing NAS
TEST_NAS_PORT=5006
TEST_NAS_PORT=7006
EOF
- name: Start test containers

View File

@@ -85,8 +85,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
> /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
postgresql-17 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-client-16 postgresql-client-17 && \
postgresql-17 postgresql-18 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-client-16 postgresql-client-17 postgresql-client-18 && \
rm -rf /var/lib/apt/lists/*
# Create postgres user and set up directories

View File

@@ -1,8 +1,8 @@
<div align="center">
<img src="assets/logo.svg" style="margin-bottom: 20px;" alt="Postgresus Logo" width="250"/>
<h3>PostgreSQL monitoring and backup</h3>
<p>Free, open source and self-hosted solution for automated PostgreSQL monitoring and backups. With multiple storage options and notifications</p>
<h3>PostgreSQL backup</h3>
<p>Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options and notifications</p>
<!-- Badges -->
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -54,7 +54,7 @@
### 🐘 **PostgreSQL Support**
- **Multiple versions**: PostgreSQL 13, 14, 15, 16 and 17
- **Multiple versions**: PostgreSQL 13, 14, 15, 16, 17 and 18
- **SSL support**: Secure connections available
- **Easy restoration**: One-click restore from any backup
@@ -64,12 +64,6 @@
- **Privacy-first**: All your data stays on your infrastructure
- **Open source**: MIT licensed, inspect every line of code
### 📊 **Monitoring & Insights**
- **Real-time metrics**: Track database health
- **Historical data**: View trends and patterns over time
- **Alert system**: Get notified when issues are detected
### 📦 Installation
You have three ways to install Postgresus:

View File

@@ -22,8 +22,9 @@ TEST_POSTGRES_14_PORT=5002
TEST_POSTGRES_15_PORT=5003
TEST_POSTGRES_16_PORT=5004
TEST_POSTGRES_17_PORT=5005
TEST_POSTGRES_18_PORT=5006
# testing S3
TEST_MINIO_PORT=9000
TEST_MINIO_CONSOLE_PORT=9001
# testing NAS
TEST_NAS_PORT=5006
TEST_NAS_PORT=7006

View File

@@ -20,7 +20,6 @@ import (
"postgresus-backend/internal/features/disk"
healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt"
healthcheck_config "postgresus-backend/internal/features/healthcheck/config"
postgres_monitoring_collectors "postgresus-backend/internal/features/monitoring/postgres/collectors"
postgres_monitoring_metrics "postgresus-backend/internal/features/monitoring/postgres/metrics"
postgres_monitoring_settings "postgresus-backend/internal/features/monitoring/postgres/settings"
"postgresus-backend/internal/features/notifiers"
@@ -210,10 +209,6 @@ func runBackgroundTasks(log *slog.Logger) {
go runWithPanicLogging(log, "postgres monitoring metrics background service", func() {
postgres_monitoring_metrics.GetPostgresMonitoringMetricsBackgroundService().Run()
})
go runWithPanicLogging(log, "postgres monitoring collectors background service", func() {
postgres_monitoring_collectors.GetDbMonitoringBackgroundService().Run()
})
}
func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) {

View File

@@ -87,6 +87,17 @@ services:
container_name: test-postgres-17
shm_size: 1gb
test-postgres-18:
image: postgres:18
ports:
- "${TEST_POSTGRES_18_PORT}:5432"
environment:
- POSTGRES_DB=testdb
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpassword
container_name: test-postgres-18
shm_size: 1gb
# Test NAS server (Samba)
test-nas:
image: dperson/samba:latest

View File

@@ -38,6 +38,7 @@ type EnvVariables struct {
TestPostgres15Port string `env:"TEST_POSTGRES_15_PORT"`
TestPostgres16Port string `env:"TEST_POSTGRES_16_PORT"`
TestPostgres17Port string `env:"TEST_POSTGRES_17_PORT"`
TestPostgres18Port string `env:"TEST_POSTGRES_18_PORT"`
TestMinioPort string `env:"TEST_MINIO_PORT"`
TestMinioConsolePort string `env:"TEST_MINIO_CONSOLE_PORT"`
@@ -154,6 +155,10 @@ func loadEnvVariables() {
log.Error("TEST_POSTGRES_17_PORT is empty")
os.Exit(1)
}
if env.TestPostgres18Port == "" {
log.Error("TEST_POSTGRES_18_PORT is empty")
os.Exit(1)
}
if env.TestMinioPort == "" {
log.Error("TEST_MINIO_PORT is empty")

View File

@@ -44,6 +44,7 @@ func SetupDependencies() {
SetDatabaseStorageChangeListener(backupService)
databases.GetDatabaseService().AddDbRemoveListener(backupService)
databases.GetDatabaseService().AddDbCopyListener(backups_config.GetBackupConfigService())
}
func GetBackupService() *BackupService {

View File

@@ -90,3 +90,18 @@ func (b *BackupConfig) Validate() error {
return nil
}
func (b *BackupConfig) Copy(newDatabaseID uuid.UUID) *BackupConfig {
return &BackupConfig{
DatabaseID: newDatabaseID,
IsBackupsEnabled: b.IsBackupsEnabled,
StorePeriod: b.StorePeriod,
BackupIntervalID: uuid.Nil,
BackupInterval: b.BackupInterval.Copy(),
StorageID: b.StorageID,
SendNotificationsOn: b.SendNotificationsOn,
IsRetryIfFailed: b.IsRetryIfFailed,
MaxFailedTriesCount: b.MaxFailedTriesCount,
CpuCount: b.CpuCount,
}
}

View File

@@ -130,6 +130,20 @@ func (s *BackupConfigService) GetBackupConfigsWithEnabledBackups() ([]*BackupCon
return s.backupConfigRepository.GetWithEnabledBackups()
}
func (s *BackupConfigService) OnDatabaseCopied(originalDatabaseID, newDatabaseID uuid.UUID) {
originalConfig, err := s.GetBackupConfigByDbId(originalDatabaseID)
if err != nil {
return
}
newConfig := originalConfig.Copy(newDatabaseID)
_, err = s.SaveBackupConfig(newConfig)
if err != nil {
return
}
}
func (s *BackupConfigService) initializeDefaultConfig(
databaseID uuid.UUID,
) error {

View File

@@ -21,6 +21,7 @@ func (c *DatabaseController) RegisterRoutes(router *gin.RouterGroup) {
router.GET("/databases", c.GetDatabases)
router.POST("/databases/:id/test-connection", c.TestDatabaseConnection)
router.POST("/databases/test-connection-direct", c.TestDatabaseConnectionDirect)
router.POST("/databases/:id/copy", c.CopyDatabase)
router.GET("/databases/notifier/:id/is-using", c.IsNotifierUsing)
}
@@ -325,3 +326,42 @@ func (c *DatabaseController) IsNotifierUsing(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"isUsing": isUsing})
}
// CopyDatabase
// @Summary Copy a database
// @Description Copy an existing database configuration
// @Tags databases
// @Produce json
// @Param id path string true "Database ID"
// @Success 201 {object} Database
// @Failure 400
// @Failure 401
// @Failure 500
// @Router /databases/{id}/copy [post]
func (c *DatabaseController) CopyDatabase(ctx *gin.Context) {
id, err := uuid.Parse(ctx.Param("id"))
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid database ID"})
return
}
authorizationHeader := ctx.GetHeader("Authorization")
if authorizationHeader == "" {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"})
return
}
user, err := c.userService.GetUserFromToken(authorizationHeader)
if err != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
copiedDatabase, err := c.databaseService.CopyDatabase(user, id)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusCreated, copiedDatabase)
}

View File

@@ -14,6 +14,7 @@ var databaseService = &DatabaseService{
logger.GetLogger(),
[]DatabaseCreationListener{},
[]DatabaseRemoveListener{},
[]DatabaseCopyListener{},
}
var databaseController = &DatabaseController{

View File

@@ -21,3 +21,7 @@ type DatabaseCreationListener interface {
type DatabaseRemoveListener interface {
OnBeforeDatabaseRemove(databaseID uuid.UUID) error
}
type DatabaseCopyListener interface {
OnDatabaseCopied(originalDatabaseID, newDatabaseID uuid.UUID)
}

View File

@@ -35,6 +35,10 @@ func (d *Database) Validate() error {
switch d.Type {
case DatabaseTypePostgres:
if d.Postgresql == nil {
return errors.New("postgresql database is required")
}
return d.Postgresql.Validate()
default:
return errors.New("invalid database type: " + string(d.Type))

View File

@@ -3,6 +3,7 @@ package databases
import (
"errors"
"log/slog"
"postgresus-backend/internal/features/databases/databases/postgresql"
"postgresus-backend/internal/features/notifiers"
users_models "postgresus-backend/internal/features/users/models"
"time"
@@ -17,6 +18,7 @@ type DatabaseService struct {
dbCreationListener []DatabaseCreationListener
dbRemoveListener []DatabaseRemoveListener
dbCopyListener []DatabaseCopyListener
}
func (s *DatabaseService) AddDbCreationListener(
@@ -31,6 +33,12 @@ func (s *DatabaseService) AddDbRemoveListener(
s.dbRemoveListener = append(s.dbRemoveListener, dbRemoveListener)
}
func (s *DatabaseService) AddDbCopyListener(
dbCopyListener DatabaseCopyListener,
) {
s.dbCopyListener = append(s.dbCopyListener, dbCopyListener)
}
func (s *DatabaseService) CreateDatabase(
user *users_models.User,
database *Database,
@@ -220,6 +228,67 @@ func (s *DatabaseService) SetLastBackupTime(databaseID uuid.UUID, backupTime tim
return nil
}
func (s *DatabaseService) CopyDatabase(
user *users_models.User,
databaseID uuid.UUID,
) (*Database, error) {
existingDatabase, err := s.dbRepository.FindByID(databaseID)
if err != nil {
return nil, err
}
if existingDatabase.UserID != user.ID {
return nil, errors.New("you have not access to this database")
}
newDatabase := &Database{
ID: uuid.Nil,
UserID: user.ID,
Name: existingDatabase.Name + " (Copy)",
Type: existingDatabase.Type,
Notifiers: existingDatabase.Notifiers,
LastBackupTime: nil,
LastBackupErrorMessage: nil,
HealthStatus: existingDatabase.HealthStatus,
}
switch existingDatabase.Type {
case DatabaseTypePostgres:
if existingDatabase.Postgresql != nil {
newDatabase.Postgresql = &postgresql.PostgresqlDatabase{
ID: uuid.Nil,
DatabaseID: nil,
Version: existingDatabase.Postgresql.Version,
Host: existingDatabase.Postgresql.Host,
Port: existingDatabase.Postgresql.Port,
Username: existingDatabase.Postgresql.Username,
Password: existingDatabase.Postgresql.Password,
Database: existingDatabase.Postgresql.Database,
IsHttps: existingDatabase.Postgresql.IsHttps,
}
}
}
if err := newDatabase.Validate(); err != nil {
return nil, err
}
copiedDatabase, err := s.dbRepository.Save(newDatabase)
if err != nil {
return nil, err
}
for _, listener := range s.dbCreationListener {
listener.OnDatabaseCreated(copiedDatabase.ID)
}
for _, listener := range s.dbCopyListener {
listener.OnDatabaseCopied(databaseID, copiedDatabase.ID)
}
return copiedDatabase, nil
}
func (s *DatabaseService) SetHealthStatus(
databaseID uuid.UUID,
healthStatus *HealthStatus,

View File

@@ -64,6 +64,16 @@ func (i *Interval) ShouldTriggerBackup(now time.Time, lastBackupTime *time.Time)
}
}
func (i *Interval) Copy() *Interval {
return &Interval{
ID: uuid.Nil,
Interval: i.Interval,
TimeOfDay: i.TimeOfDay,
Weekday: i.Weekday,
DayOfMonth: i.DayOfMonth,
}
}
// daily trigger: honour the TimeOfDay slot and catch up the previous one
func (i *Interval) shouldTriggerDaily(now, lastBackup time.Time) bool {
if i.TimeOfDay == nil {

View File

@@ -73,6 +73,7 @@ func Test_BackupAndRestorePostgresql_RestoreIsSuccesful(t *testing.T) {
{"PostgreSQL 15", "15", env.TestPostgres15Port},
{"PostgreSQL 16", "16", env.TestPostgres16Port},
{"PostgreSQL 17", "17", env.TestPostgres17Port},
{"PostgreSQL 18", "18", env.TestPostgres18Port},
}
for _, tc := range cases {

View File

@@ -20,6 +20,7 @@ const (
PostgresqlVersion15 PostgresqlVersion = "15"
PostgresqlVersion16 PostgresqlVersion = "16"
PostgresqlVersion17 PostgresqlVersion = "17"
PostgresqlVersion18 PostgresqlVersion = "18"
)
type PostgresqlExecutable string
@@ -41,6 +42,8 @@ func GetPostgresqlVersionEnum(version string) PostgresqlVersion {
return PostgresqlVersion16
case "17":
return PostgresqlVersion17
case "18":
return PostgresqlVersion18
default:
panic(fmt.Sprintf("invalid postgresql version: %s", version))
}

View File

@@ -46,6 +46,7 @@ func VerifyPostgresesInstallation(
PostgresqlVersion15,
PostgresqlVersion16,
PostgresqlVersion17,
PostgresqlVersion18,
}
requiredCommands := []PostgresqlExecutable{

View File

@@ -5,7 +5,7 @@ set -e # Exit on any error
# Ensure non-interactive mode for apt
export DEBIAN_FRONTEND=noninteractive
echo "Installing PostgreSQL client tools versions 13-17 for Linux (Debian/Ubuntu)..."
echo "Installing PostgreSQL client tools versions 13-18 for Linux (Debian/Ubuntu)..."
echo
# Check if running on supported system
@@ -47,7 +47,7 @@ echo "Updating package list..."
$SUDO apt-get update -qq -y
# Install client tools for each version
versions="13 14 15 16 17"
versions="13 14 15 16 17 18"
for version in $versions; do
echo "Installing PostgreSQL $version client tools..."

View File

@@ -2,7 +2,7 @@
set -e # Exit on any error
echo "Installing PostgreSQL client tools versions 13-17 for MacOS..."
echo "Installing PostgreSQL client tools versions 13-18 for MacOS..."
echo
# Check if Homebrew is installed
@@ -36,6 +36,7 @@ declare -A PG_URLS=(
["15"]="https://ftp.postgresql.org/pub/source/v15.8/postgresql-15.8.tar.gz"
["16"]="https://ftp.postgresql.org/pub/source/v16.4/postgresql-16.4.tar.gz"
["17"]="https://ftp.postgresql.org/pub/source/v17.0/postgresql-17.0.tar.gz"
["18"]="https://ftp.postgresql.org/pub/source/v18.0/postgresql-18.0.tar.gz"
)
# Create temporary build directory
@@ -106,7 +107,7 @@ build_postgresql_client() {
}
# Build each version
versions="13 14 15 16 17"
versions="13 14 15 16 17 18"
for version in $versions; do
url=${PG_URLS[$version]}

View File

@@ -1,7 +1,7 @@
@echo off
setlocal enabledelayedexpansion
echo Downloading and installing PostgreSQL versions 13-17 for Windows...
echo Downloading and installing PostgreSQL versions 13-18 for Windows...
echo.
:: Create downloads and postgresql directories if they don't exist
@@ -22,9 +22,10 @@ set "PG14_URL=%BASE_URL%/postgresql-14.13-1-windows-x64.exe"
set "PG15_URL=%BASE_URL%/postgresql-15.8-1-windows-x64.exe"
set "PG16_URL=%BASE_URL%/postgresql-16.4-1-windows-x64.exe"
set "PG17_URL=%BASE_URL%/postgresql-17.0-1-windows-x64.exe"
set "PG18_URL=%BASE_URL%/postgresql-18.0-1-windows-x64.exe"
:: Array of versions
set "versions=13 14 15 16 17"
set "versions=13 14 15 16 17 18"
:: Download and install each version
for %%v in (%versions%) do (

View File

@@ -1,6 +1,6 @@
This directory is needed only for development and CI\CD.
We have to download and install all the PostgreSQL versions from 13 to 17 locally.
We have to download and install all the PostgreSQL versions from 13 to 18 locally.
This is needed so we can call pg_dump, pg_dumpall, etc. on each version of the PostgreSQL database.
You do not need to install PostgreSQL fully with all the components.
@@ -13,6 +13,7 @@ We have to install the following:
- PostgreSQL 15
- PostgreSQL 16
- PostgreSQL 17
- PostgreSQL 18
## Installation
@@ -76,6 +77,7 @@ For example:
- `./tools/postgresql/postgresql-15/bin/pg_dump`
- `./tools/postgresql/postgresql-16/bin/pg_dump`
- `./tools/postgresql/postgresql-17/bin/pg_dump`
- `./tools/postgresql/postgresql-18/bin/pg_dump`
## Usage

View File

@@ -72,10 +72,7 @@ If you need to add some explanation, do it in appropriate place in the code. Or
Before taking anything more than a couple of lines of code, please write Rostislav via Telegram (@rostislav_dugin) and confirm priority. It is possible that we already have something in the works, it is not needed or it's not project priority.
Nearsest features:
- add system metrics (CPU, RAM, disk, IO) (in progress by Rostislav Dugin)
- add copying of databases
- add API keys and API actions
- add UI component of backups lazy loaded
Backups flow:
@@ -102,29 +99,6 @@ Extra:
Monitoring flow:
- add queries stats (slowest, most frequent, etc. via pg_stat_statements)
- add DB size distribution chart (tables, indexes, etc.)
- add alerting for slow queries (listen for slow query and if they reach >100ms - send message)
- add alerting for high resource usage (listen for high resource usage and if they reach >90% - send message)
- add performance test for DB (to compare DBs on different clouds and VPS)
- add DB metrics (pg_stat_activity, pg_locks, pg_stat_database)
- add chart of connections (from IPs, apps names, etc.)
- add chart of transactions (TPS)
- deadlocks chart
- chart of connection attempts (to see crash loops)
- add chart of IDLE transactions VS executing transactions
- show queries that take the most IO time (suboptimal indexes)
- show chart by top IO / CPU queries usage (see page 90 of the PostgreSQL monitoring book)
```
exec_time | IO | CPU | query
105 hrs | 73% | 27% | SELECT * FROM users;
```
- chart of read / update / delete / insert queries
- chart with deadlocks, conflicts, rollbacks (see page 115 of the PostgreSQL monitoring book)
- stats of buffer usage
- status of IO (DB, indexes, sequences)
- % of cache hit
- replication stats

View File

@@ -48,6 +48,14 @@ export const databaseApi = {
);
},
async copyDatabase(id: string) {
const requestOptions: RequestOptions = new RequestOptions();
return apiHelper.fetchPostJson<Database>(
`${getApplicationServer()}/api/v1/databases/${id}/copy`,
requestOptions,
);
},
async testDatabaseConnection(id: string) {
const requestOptions: RequestOptions = new RequestOptions();
return apiHelper.fetchPostJson(

View File

@@ -4,4 +4,5 @@ export enum PostgresqlVersion {
PostgresqlVersion15 = '15',
PostgresqlVersion16 = '16',
PostgresqlVersion17 = '17',
PostgresqlVersion18 = '18',
}

View File

@@ -56,13 +56,6 @@ export const DatabaseComponent = ({
>
Backups
</div>
<div
className={`cursor-pointer rounded-tl-md rounded-tr-md px-6 py-2 ${currentTab === 'metrics' ? 'bg-white' : 'bg-gray-200'}`}
onClick={() => setCurrentTab('metrics')}
>
Metrics
</div>
</div>
{currentTab === 'config' && (

View File

@@ -7,10 +7,6 @@ import { ToastHelper } from '../../../shared/toast';
import { ConfirmationComponent } from '../../../shared/ui';
import { EditBackupConfigComponent, ShowBackupConfigComponent } from '../../backups';
import { EditHealthcheckConfigComponent, ShowHealthcheckConfigComponent } from '../../healthcheck';
import {
EditMonitoringSettingsComponent,
ShowMonitoringSettingsComponent,
} from '../../monitoring/settings';
import { EditDatabaseNotifiersComponent } from './edit/EditDatabaseNotifiersComponent';
import { EditDatabaseSpecificDataComponent } from './edit/EditDatabaseSpecificDataComponent';
import { ShowDatabaseNotifiersComponent } from './show/ShowDatabaseNotifiersComponent';
@@ -39,13 +35,12 @@ export const DatabaseConfigComponent = ({
const [isEditBackupConfig, setIsEditBackupConfig] = useState(false);
const [isEditNotifiersSettings, setIsEditNotifiersSettings] = useState(false);
const [isEditHealthcheckSettings, setIsEditHealthcheckSettings] = useState(false);
const [isEditMonitoringSettings, setIsEditMonitoringSettings] = useState(false);
const [isNameUnsaved, setIsNameUnsaved] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [isCopying, setIsCopying] = useState(false);
const [isShowRemoveConfirm, setIsShowRemoveConfirm] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
@@ -55,6 +50,28 @@ export const DatabaseConfigComponent = ({
databaseApi.getDatabase(database.id).then(setDatabase);
};
const copyDatabase = () => {
if (!database) return;
setIsCopying(true);
databaseApi
.copyDatabase(database.id)
.then((copiedDatabase) => {
ToastHelper.showToast({
title: 'Database copied successfully!',
description: `"${copiedDatabase.name}" has been created successfully`,
});
window.location.reload();
})
.catch((e: Error) => {
alert(e.message);
})
.finally(() => {
setIsCopying(false);
});
};
const testConnection = () => {
if (!database) return;
@@ -97,16 +114,13 @@ export const DatabaseConfigComponent = ({
});
};
const startEdit = (
type: 'name' | 'database' | 'backup-config' | 'notifiers' | 'healthcheck' | 'monitoring',
) => {
const startEdit = (type: 'name' | 'database' | 'backup-config' | 'notifiers' | 'healthcheck') => {
setEditDatabase(JSON.parse(JSON.stringify(database)));
setIsEditName(type === 'name');
setIsEditDatabaseSpecificDataSettings(type === 'database');
setIsEditBackupConfig(type === 'backup-config');
setIsEditNotifiersSettings(type === 'notifiers');
setIsEditHealthcheckSettings(type === 'healthcheck');
setIsEditMonitoringSettings(type === 'monitoring');
setIsNameUnsaved(false);
};
@@ -344,40 +358,6 @@ export const DatabaseConfigComponent = ({
</div>
</div>
<div className="flex flex-wrap gap-10">
<div className="w-[400px]">
<div className="mt-5 flex items-center font-bold">
<div>Monitoring settings</div>
{!isEditMonitoringSettings ? (
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('monitoring')}>
<img src="/icons/pen-gray.svg" />
</div>
) : (
<div />
)}
</div>
<div className="mt-1 text-sm">
{isEditMonitoringSettings ? (
<EditMonitoringSettingsComponent
database={database}
onCancel={() => {
setIsEditMonitoringSettings(false);
loadSettings();
}}
onSaved={() => {
setIsEditMonitoringSettings(false);
loadSettings();
}}
/>
) : (
<ShowMonitoringSettingsComponent database={database} />
)}
</div>
</div>
</div>
{!isEditDatabaseSpecificDataSettings && (
<div className="mt-10">
<Button
@@ -391,6 +371,17 @@ export const DatabaseConfigComponent = ({
Test connection
</Button>
<Button
type="primary"
className="mr-1"
ghost
onClick={copyDatabase}
loading={isCopying}
disabled={isCopying}
>
Copy
</Button>
<Button
type="primary"
danger

View File

@@ -13,6 +13,7 @@ interface Props {
export const DatabasesComponent = ({ contentHeight }: Props) => {
const [isLoading, setIsLoading] = useState(true);
const [databases, setDatabases] = useState<Database[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [isShowAddDatabase, setIsShowAddDatabase] = useState(false);
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | undefined>(undefined);
@@ -58,23 +59,46 @@ export const DatabasesComponent = ({ contentHeight }: Props) => {
</Button>
);
const filteredDatabases = databases.filter((database) =>
database.name.toLowerCase().includes(searchQuery.toLowerCase()),
);
return (
<>
<div className="flex grow">
<div
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto"
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto pr-2"
style={{ height: contentHeight }}
>
{databases.length >= 5 && addDatabaseButton}
{databases.length >= 5 && (
<>
{addDatabaseButton}
{databases.map((database) => (
<DatabaseCardComponent
key={database.id}
database={database}
selectedDatabaseId={selectedDatabaseId}
setSelectedDatabaseId={setSelectedDatabaseId}
/>
))}
<div className="mb-2">
<input
placeholder="Search database"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full border-b border-gray-300 p-1 text-gray-500 outline-none"
/>
</div>
</>
)}
{filteredDatabases.length > 0
? filteredDatabases.map((database) => (
<DatabaseCardComponent
key={database.id}
database={database}
selectedDatabaseId={selectedDatabaseId}
setSelectedDatabaseId={setSelectedDatabaseId}
/>
))
: searchQuery && (
<div className="mb-4 text-center text-sm text-gray-500">
No databases found matching &quot;{searchQuery}&quot;
</div>
)}
{databases.length < 5 && addDatabaseButton}

View File

@@ -155,6 +155,7 @@ export const EditDatabaseSpecificDataComponent = ({
{ label: '15', value: PostgresqlVersion.PostgresqlVersion15 },
{ label: '16', value: PostgresqlVersion.PostgresqlVersion16 },
{ label: '17', value: PostgresqlVersion.PostgresqlVersion17 },
{ label: '18', value: PostgresqlVersion.PostgresqlVersion18 },
]}
/>

View File

@@ -14,6 +14,7 @@ const postgresqlVersionLabels = {
[PostgresqlVersion.PostgresqlVersion15]: '15',
[PostgresqlVersion.PostgresqlVersion16]: '16',
[PostgresqlVersion.PostgresqlVersion17]: '17',
[PostgresqlVersion.PostgresqlVersion18]: '18',
};
export const ShowDatabaseSpecificDataComponent = ({ database }: Props) => {