mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 08:41:58 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcfe382a81 | ||
|
|
7055b85c34 |
@@ -20,6 +20,7 @@ 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"
|
||||
@@ -180,7 +181,6 @@ func setUpRoutes(r *gin.Engine) {
|
||||
}
|
||||
|
||||
func setUpDependencies() {
|
||||
backups.SetupDependencies()
|
||||
backups.SetupDependencies()
|
||||
restores.SetupDependencies()
|
||||
healthcheck_config.SetupDependencies()
|
||||
@@ -204,12 +204,16 @@ func runBackgroundTasks(log *slog.Logger) {
|
||||
})
|
||||
|
||||
go runWithPanicLogging(log, "healthcheck attempt background service", func() {
|
||||
healthcheck_attempt.GetHealthcheckAttemptBackgroundService().RunBackgroundTasks()
|
||||
healthcheck_attempt.GetHealthcheckAttemptBackgroundService().Run()
|
||||
})
|
||||
|
||||
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()) {
|
||||
|
||||
@@ -13,7 +13,7 @@ type HealthcheckAttemptBackgroundService struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (s *HealthcheckAttemptBackgroundService) RunBackgroundTasks() {
|
||||
func (s *HealthcheckAttemptBackgroundService) Run() {
|
||||
// first healthcheck immediately
|
||||
s.checkDatabases()
|
||||
|
||||
|
||||
@@ -1,3 +1,292 @@
|
||||
package postgres_monitoring_collectors
|
||||
|
||||
type DbMonitoringBackgroundService struct{}
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"postgresus-backend/internal/config"
|
||||
"postgresus-backend/internal/features/databases"
|
||||
"postgresus-backend/internal/features/databases/databases/postgresql"
|
||||
postgres_monitoring_metrics "postgresus-backend/internal/features/monitoring/postgres/metrics"
|
||||
postgres_monitoring_settings "postgresus-backend/internal/features/monitoring/postgres/settings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type DbMonitoringBackgroundService struct {
|
||||
databaseService *databases.DatabaseService
|
||||
monitoringSettingsService *postgres_monitoring_settings.PostgresMonitoringSettingsService
|
||||
metricsService *postgres_monitoring_metrics.PostgresMonitoringMetricService
|
||||
logger *slog.Logger
|
||||
isRunning int32
|
||||
lastRunTimes map[uuid.UUID]time.Time
|
||||
lastRunTimesMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) Run() {
|
||||
for {
|
||||
if config.IsShouldShutdown() {
|
||||
s.logger.Info("stopping background monitoring tasks")
|
||||
return
|
||||
}
|
||||
|
||||
s.processMonitoringTasks()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) processMonitoringTasks() {
|
||||
if !atomic.CompareAndSwapInt32(&s.isRunning, 0, 1) {
|
||||
s.logger.Warn("skipping background task execution, previous task still running")
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&s.isRunning, 0)
|
||||
|
||||
dbsWithEnabledDbMonitoring, err := s.monitoringSettingsService.GetAllDbsWithEnabledDbMonitoring()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get all databases with enabled db monitoring", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, dbSettings := range dbsWithEnabledDbMonitoring {
|
||||
s.processDatabase(&dbSettings)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) processDatabase(
|
||||
settings *postgres_monitoring_settings.PostgresMonitoringSettings,
|
||||
) {
|
||||
db, err := s.databaseService.GetDatabaseByID(settings.DatabaseID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get database by id", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if db.Type != databases.DatabaseTypePostgres {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.isReadyForNextRun(settings) {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.collectAndSaveMetrics(db, settings)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to collect and save metrics", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.updateLastRunTime(db)
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) collectAndSaveMetrics(
|
||||
db *databases.Database,
|
||||
settings *postgres_monitoring_settings.PostgresMonitoringSettings,
|
||||
) error {
|
||||
if db.Postgresql == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Debug("collecting metrics for database", "database_id", db.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := s.connectToDatabase(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := conn.Close(ctx); closeErr != nil {
|
||||
s.logger.Error("Failed to close connection", "error", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
var metrics []postgres_monitoring_metrics.PostgresMonitoringMetric
|
||||
now := time.Now().UTC()
|
||||
|
||||
if settings.IsDbResourcesMonitoringEnabled {
|
||||
dbMetrics, err := s.collectDatabaseResourceMetrics(ctx, conn, db.ID, now)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to collect database resource metrics", "error", err)
|
||||
} else {
|
||||
metrics = append(metrics, dbMetrics...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(metrics) > 0 {
|
||||
if err := s.metricsService.Insert(metrics); err != nil {
|
||||
return fmt.Errorf("failed to insert metrics: %w", err)
|
||||
}
|
||||
s.logger.Debug(
|
||||
"successfully collected and saved metrics",
|
||||
"count",
|
||||
len(metrics),
|
||||
"database_id",
|
||||
db.ID,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) isReadyForNextRun(
|
||||
settings *postgres_monitoring_settings.PostgresMonitoringSettings,
|
||||
) bool {
|
||||
s.lastRunTimesMutex.RLock()
|
||||
defer s.lastRunTimesMutex.RUnlock()
|
||||
|
||||
if s.lastRunTimes == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
lastRun, exists := s.lastRunTimes[settings.DatabaseID]
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
|
||||
return time.Since(lastRun) >= time.Duration(settings.MonitoringIntervalSeconds)*time.Second
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) updateLastRunTime(db *databases.Database) {
|
||||
s.lastRunTimesMutex.Lock()
|
||||
defer s.lastRunTimesMutex.Unlock()
|
||||
|
||||
if s.lastRunTimes == nil {
|
||||
s.lastRunTimes = make(map[uuid.UUID]time.Time)
|
||||
}
|
||||
s.lastRunTimes[db.ID] = time.Now().UTC()
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) connectToDatabase(
|
||||
ctx context.Context,
|
||||
db *databases.Database,
|
||||
) (*pgx.Conn, error) {
|
||||
if db.Postgresql == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if db.Postgresql.Database == nil || *db.Postgresql.Database == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
connStr := s.buildConnectionString(db.Postgresql)
|
||||
return pgx.Connect(ctx, connStr)
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) buildConnectionString(
|
||||
pg *postgresql.PostgresqlDatabase,
|
||||
) string {
|
||||
sslMode := "disable"
|
||||
if pg.IsHttps {
|
||||
sslMode = "require"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
pg.Host,
|
||||
pg.Port,
|
||||
pg.Username,
|
||||
pg.Password,
|
||||
*pg.Database,
|
||||
sslMode,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) collectDatabaseResourceMetrics(
|
||||
ctx context.Context,
|
||||
conn *pgx.Conn,
|
||||
databaseID uuid.UUID,
|
||||
timestamp time.Time,
|
||||
) ([]postgres_monitoring_metrics.PostgresMonitoringMetric, error) {
|
||||
var metrics []postgres_monitoring_metrics.PostgresMonitoringMetric
|
||||
|
||||
// Collect I/O statistics
|
||||
ioMetrics, err := s.collectIOMetrics(ctx, conn, databaseID, timestamp)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed to collect I/O metrics", "error", err)
|
||||
} else {
|
||||
metrics = append(metrics, ioMetrics...)
|
||||
}
|
||||
|
||||
// Collect memory usage (approximation based on buffer usage)
|
||||
ramMetric, err := s.collectRAMUsageMetric(ctx, conn, databaseID, timestamp)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed to collect RAM usage metric", "error", err)
|
||||
} else {
|
||||
metrics = append(metrics, ramMetric)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) collectIOMetrics(
|
||||
ctx context.Context,
|
||||
conn *pgx.Conn,
|
||||
databaseID uuid.UUID,
|
||||
timestamp time.Time,
|
||||
) ([]postgres_monitoring_metrics.PostgresMonitoringMetric, error) {
|
||||
var blocksRead, blocksHit int64
|
||||
query := `
|
||||
SELECT
|
||||
COALESCE(SUM(blks_read), 0) as total_reads,
|
||||
COALESCE(SUM(blks_hit), 0) as total_hits
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database()
|
||||
`
|
||||
|
||||
err := conn.QueryRow(ctx, query).Scan(&blocksRead, &blocksHit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate I/O activity as total blocks accessed (PostgreSQL block size is typically 8KB)
|
||||
const pgBlockSize = 8192 // 8KB
|
||||
totalIOBytes := float64((blocksRead + blocksHit) * pgBlockSize)
|
||||
|
||||
return []postgres_monitoring_metrics.PostgresMonitoringMetric{
|
||||
{
|
||||
DatabaseID: databaseID,
|
||||
Metric: postgres_monitoring_metrics.MetricsTypeDbIO,
|
||||
ValueType: postgres_monitoring_metrics.MetricsValueTypeByte,
|
||||
Value: totalIOBytes,
|
||||
CreatedAt: timestamp,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DbMonitoringBackgroundService) collectRAMUsageMetric(
|
||||
ctx context.Context,
|
||||
conn *pgx.Conn,
|
||||
databaseID uuid.UUID,
|
||||
timestamp time.Time,
|
||||
) (postgres_monitoring_metrics.PostgresMonitoringMetric, error) {
|
||||
var sharedBuffers int64
|
||||
query := `
|
||||
SELECT
|
||||
COALESCE(SUM(blks_hit), 0) * 8192 as buffer_usage
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database()
|
||||
`
|
||||
|
||||
err := conn.QueryRow(ctx, query).Scan(&sharedBuffers)
|
||||
if err != nil {
|
||||
return postgres_monitoring_metrics.PostgresMonitoringMetric{}, err
|
||||
}
|
||||
|
||||
return postgres_monitoring_metrics.PostgresMonitoringMetric{
|
||||
DatabaseID: databaseID,
|
||||
Metric: postgres_monitoring_metrics.MetricsTypeDbRAM,
|
||||
ValueType: postgres_monitoring_metrics.MetricsValueTypeByte,
|
||||
Value: float64(sharedBuffers),
|
||||
CreatedAt: timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package postgres_monitoring_collectors
|
||||
|
||||
import (
|
||||
"postgresus-backend/internal/features/databases"
|
||||
postgres_monitoring_metrics "postgresus-backend/internal/features/monitoring/postgres/metrics"
|
||||
postgres_monitoring_settings "postgresus-backend/internal/features/monitoring/postgres/settings"
|
||||
"postgresus-backend/internal/util/logger"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var dbMonitoringBackgroundService = &DbMonitoringBackgroundService{
|
||||
databases.GetDatabaseService(),
|
||||
postgres_monitoring_settings.GetPostgresMonitoringSettingsService(),
|
||||
postgres_monitoring_metrics.GetPostgresMonitoringMetricsService(),
|
||||
logger.GetLogger(),
|
||||
0,
|
||||
nil,
|
||||
sync.RWMutex{},
|
||||
}
|
||||
|
||||
func GetDbMonitoringBackgroundService() *DbMonitoringBackgroundService {
|
||||
return dbMonitoringBackgroundService
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package postgres_monitoring_collectors
|
||||
|
||||
type SystemMonitoringBackgroundService struct{}
|
||||
@@ -3,14 +3,8 @@ package postgres_monitoring_metrics
|
||||
type PostgresMonitoringMetricType string
|
||||
|
||||
const (
|
||||
// system resources (need extensions)
|
||||
MetricsTypeSystemCPU PostgresMonitoringMetricType = "SYSTEM_CPU_USAGE"
|
||||
MetricsTypeSystemRAM PostgresMonitoringMetricType = "SYSTEM_RAM_USAGE"
|
||||
MetricsTypeSystemROM PostgresMonitoringMetricType = "SYSTEM_ROM_USAGE"
|
||||
MetricsTypeSystemIO PostgresMonitoringMetricType = "SYSTEM_IO_USAGE"
|
||||
// db resources (don't need extensions)
|
||||
MetricsTypeDbRAM PostgresMonitoringMetricType = "DB_RAM_USAGE"
|
||||
MetricsTypeDbROM PostgresMonitoringMetricType = "DB_ROM_USAGE"
|
||||
MetricsTypeDbIO PostgresMonitoringMetricType = "DB_IO_USAGE"
|
||||
)
|
||||
|
||||
|
||||
@@ -65,13 +65,6 @@ func Test_GetMetrics_MetricsReturned(t *testing.T) {
|
||||
Value: 2048000,
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
},
|
||||
{
|
||||
DatabaseID: database.ID,
|
||||
Metric: MetricsTypeSystemCPU,
|
||||
ValueType: MetricsValueTypePercent,
|
||||
Value: 75.5,
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
// Insert test metrics
|
||||
@@ -93,14 +86,6 @@ func Test_GetMetrics_MetricsReturned(t *testing.T) {
|
||||
assert.Equal(t, MetricsTypeDbRAM, metrics[0].Metric)
|
||||
assert.Equal(t, MetricsValueTypeByte, metrics[0].ValueType)
|
||||
|
||||
// Test getting CPU metrics
|
||||
cpuMetrics, err := service.GetMetrics(testUser, database.ID, MetricsTypeSystemCPU, from, to)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cpuMetrics, 1)
|
||||
assert.Equal(t, float64(75.5), cpuMetrics[0].Value)
|
||||
assert.Equal(t, MetricsTypeSystemCPU, cpuMetrics[0].Metric)
|
||||
assert.Equal(t, MetricsValueTypePercent, cpuMetrics[0].ValueType)
|
||||
|
||||
// Test access control - create another user and test they can't access this database
|
||||
anotherUser := &users_models.User{
|
||||
ID: uuid.New(),
|
||||
@@ -204,37 +189,6 @@ func Test_GetMetricsWithFilterByType_FilterWorks(t *testing.T) {
|
||||
Value: 2048000,
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
},
|
||||
// DB ROM metrics
|
||||
{
|
||||
DatabaseID: database.ID,
|
||||
Metric: MetricsTypeDbROM,
|
||||
ValueType: MetricsValueTypeByte,
|
||||
Value: 5000000,
|
||||
CreatedAt: now.Add(-90 * time.Minute),
|
||||
},
|
||||
{
|
||||
DatabaseID: database.ID,
|
||||
Metric: MetricsTypeDbROM,
|
||||
ValueType: MetricsValueTypeByte,
|
||||
Value: 5500000,
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
},
|
||||
// System CPU metrics
|
||||
{
|
||||
DatabaseID: database.ID,
|
||||
Metric: MetricsTypeSystemCPU,
|
||||
ValueType: MetricsValueTypePercent,
|
||||
Value: 75.5,
|
||||
CreatedAt: now.Add(-45 * time.Minute),
|
||||
},
|
||||
// System RAM metrics
|
||||
{
|
||||
DatabaseID: database.ID,
|
||||
Metric: MetricsTypeSystemRAM,
|
||||
ValueType: MetricsValueTypePercent,
|
||||
Value: 65.2,
|
||||
CreatedAt: now.Add(-25 * time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
// Insert test metrics
|
||||
@@ -253,39 +207,6 @@ func Test_GetMetricsWithFilterByType_FilterWorks(t *testing.T) {
|
||||
assert.Equal(t, MetricsValueTypeByte, metric.ValueType)
|
||||
}
|
||||
|
||||
// Test filtering by DB ROM type
|
||||
romMetrics, err := service.GetMetrics(testUser, database.ID, MetricsTypeDbROM, from, to)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, romMetrics, 2)
|
||||
for _, metric := range romMetrics {
|
||||
assert.Equal(t, MetricsTypeDbROM, metric.Metric)
|
||||
assert.Equal(t, MetricsValueTypeByte, metric.ValueType)
|
||||
}
|
||||
|
||||
// Test filtering by System CPU type
|
||||
cpuMetrics, err := service.GetMetrics(testUser, database.ID, MetricsTypeSystemCPU, from, to)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cpuMetrics, 1)
|
||||
for _, metric := range cpuMetrics {
|
||||
assert.Equal(t, MetricsTypeSystemCPU, metric.Metric)
|
||||
assert.Equal(t, MetricsValueTypePercent, metric.ValueType)
|
||||
}
|
||||
|
||||
// Test filtering by System RAM type
|
||||
systemRamMetrics, err := service.GetMetrics(
|
||||
testUser,
|
||||
database.ID,
|
||||
MetricsTypeSystemRAM,
|
||||
from,
|
||||
to,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, systemRamMetrics, 1)
|
||||
for _, metric := range systemRamMetrics {
|
||||
assert.Equal(t, MetricsTypeSystemRAM, metric.Metric)
|
||||
assert.Equal(t, MetricsValueTypePercent, metric.ValueType)
|
||||
}
|
||||
|
||||
// Test filtering by non-existent metric type (should return empty)
|
||||
ioMetrics, err := service.GetMetrics(testUser, database.ID, MetricsTypeDbIO, from, to)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -13,10 +13,8 @@ type PostgresMonitoringSettings struct {
|
||||
DatabaseID uuid.UUID `json:"databaseId" gorm:"primaryKey;column:database_id;not null"`
|
||||
Database *databases.Database `json:"database" gorm:"foreignKey:DatabaseID"`
|
||||
|
||||
IsSystemResourcesMonitoringEnabled bool `json:"isSystemResourcesMonitoringEnabled" gorm:"column:is_system_resources_monitoring_enabled;not null"`
|
||||
IsDbResourcesMonitoringEnabled bool `json:"isDbResourcesMonitoringEnabled" gorm:"column:is_db_resources_monitoring_enabled;not null"`
|
||||
IsQueriesMonitoringEnabled bool `json:"isQueriesMonitoringEnabled" gorm:"column:is_queries_monitoring_enabled;not null"`
|
||||
MonitoringIntervalSeconds int64 `json:"monitoringIntervalSeconds" gorm:"column:monitoring_interval_seconds;not null"`
|
||||
IsDbResourcesMonitoringEnabled bool `json:"isDbResourcesMonitoringEnabled" gorm:"column:is_db_resources_monitoring_enabled;not null"`
|
||||
MonitoringIntervalSeconds int64 `json:"monitoringIntervalSeconds" gorm:"column:monitoring_interval_seconds;not null"`
|
||||
|
||||
InstalledExtensions []tools.PostgresqlExtension `json:"installedExtensions" gorm:"-"`
|
||||
InstalledExtensionsRaw string `json:"-" gorm:"column:installed_extensions_raw"`
|
||||
|
||||
@@ -48,3 +48,18 @@ func (r *PostgresMonitoringSettingsRepository) GetByDbIDWithRelations(
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
func (r *PostgresMonitoringSettingsRepository) GetAllDbsWithEnabledDbMonitoring() (
|
||||
[]PostgresMonitoringSettings, error,
|
||||
) {
|
||||
var settings []PostgresMonitoringSettings
|
||||
|
||||
if err := storage.
|
||||
GetDb().
|
||||
Where("is_db_resources_monitoring_enabled = ?", true).
|
||||
Find(&settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"postgresus-backend/internal/features/databases"
|
||||
users_models "postgresus-backend/internal/features/users/models"
|
||||
"postgresus-backend/internal/util/logger"
|
||||
"postgresus-backend/internal/util/tools"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -28,31 +27,9 @@ func (s *PostgresMonitoringSettingsService) OnDatabaseCreated(dbID uuid.UUID) {
|
||||
}
|
||||
|
||||
settings := &PostgresMonitoringSettings{
|
||||
DatabaseID: dbID,
|
||||
IsSystemResourcesMonitoringEnabled: true,
|
||||
IsDbResourcesMonitoringEnabled: true,
|
||||
IsQueriesMonitoringEnabled: true,
|
||||
MonitoringIntervalSeconds: 15,
|
||||
}
|
||||
|
||||
err = s.ensureExtensionsInstalled(
|
||||
dbID,
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgProctab},
|
||||
)
|
||||
if err != nil {
|
||||
settings.IsSystemResourcesMonitoringEnabled = false
|
||||
} else {
|
||||
settings.AddInstalledExtensions([]tools.PostgresqlExtension{tools.PostgresqlExtensionPgProctab})
|
||||
}
|
||||
|
||||
err = s.ensureExtensionsInstalled(
|
||||
dbID,
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgStatMonitor},
|
||||
)
|
||||
if err != nil {
|
||||
settings.IsQueriesMonitoringEnabled = false
|
||||
} else {
|
||||
settings.AddInstalledExtensions([]tools.PostgresqlExtension{tools.PostgresqlExtensionPgStatMonitor})
|
||||
DatabaseID: dbID,
|
||||
IsDbResourcesMonitoringEnabled: true,
|
||||
MonitoringIntervalSeconds: 60,
|
||||
}
|
||||
|
||||
err = s.postgresMonitoringSettingsRepository.Save(settings)
|
||||
@@ -74,47 +51,6 @@ func (s *PostgresMonitoringSettingsService) Save(
|
||||
return errors.New("user does not have access to this database")
|
||||
}
|
||||
|
||||
existingSettings, err := s.postgresMonitoringSettingsRepository.GetByDbID(settings.DatabaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingSettings != nil &&
|
||||
settings.IsSystemResourcesMonitoringEnabled &&
|
||||
!existingSettings.IsSystemResourcesMonitoringEnabled {
|
||||
err := s.ensureExtensionsInstalled(
|
||||
settings.DatabaseID,
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgProctab},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.New(
|
||||
"failed to install pg_proctab extension, system resources is not possible (please, disable it)",
|
||||
)
|
||||
}
|
||||
|
||||
settings.AddInstalledExtensions(
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgProctab},
|
||||
)
|
||||
}
|
||||
|
||||
if existingSettings != nil &&
|
||||
settings.IsQueriesMonitoringEnabled &&
|
||||
!existingSettings.IsQueriesMonitoringEnabled {
|
||||
err := s.ensureExtensionsInstalled(
|
||||
settings.DatabaseID,
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgStatMonitor},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.New(
|
||||
"failed to install pg_stat_monitor extension, queries monitoring is not possible (please, disable it)",
|
||||
)
|
||||
}
|
||||
|
||||
settings.AddInstalledExtensions(
|
||||
[]tools.PostgresqlExtension{tools.PostgresqlExtensionPgStatMonitor},
|
||||
)
|
||||
}
|
||||
|
||||
return s.postgresMonitoringSettingsRepository.Save(settings)
|
||||
}
|
||||
|
||||
@@ -149,31 +85,8 @@ func (s *PostgresMonitoringSettingsService) GetByDbID(
|
||||
return dbSettings, nil
|
||||
}
|
||||
|
||||
func (s *PostgresMonitoringSettingsService) ensureExtensionsInstalled(
|
||||
dbID uuid.UUID,
|
||||
extensions []tools.PostgresqlExtension,
|
||||
) error {
|
||||
database, err := s.databaseService.GetDatabaseByID(dbID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if database.Type != databases.DatabaseTypePostgres {
|
||||
return errors.New("database is not a postgres database")
|
||||
}
|
||||
|
||||
if database.Postgresql == nil {
|
||||
return errors.New("database is not a postgres database")
|
||||
}
|
||||
|
||||
if database.Postgresql.Version < tools.PostgresqlVersion16 {
|
||||
return errors.New("system monitoring extensions supported for postgres 16+")
|
||||
}
|
||||
|
||||
err = database.Postgresql.InstallExtensions(extensions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *PostgresMonitoringSettingsService) GetAllDbsWithEnabledDbMonitoring() (
|
||||
[]PostgresMonitoringSettings, error,
|
||||
) {
|
||||
return s.postgresMonitoringSettingsRepository.GetAllDbsWithEnabledDbMonitoring()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ package postgres_monitoring_settings
|
||||
|
||||
import (
|
||||
"postgresus-backend/internal/features/databases"
|
||||
"postgresus-backend/internal/features/databases/databases/postgresql"
|
||||
"postgresus-backend/internal/features/notifiers"
|
||||
"postgresus-backend/internal/features/storages"
|
||||
"postgresus-backend/internal/features/users"
|
||||
users_models "postgresus-backend/internal/features/users/models"
|
||||
"postgresus-backend/internal/util/tools"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -33,7 +31,7 @@ func getTestUserModel() *users_models.User {
|
||||
return user
|
||||
}
|
||||
|
||||
func Test_DatabaseCreated_SettingsCreatedAndExtensionsInstalled(t *testing.T) {
|
||||
func Test_DatabaseCreated_SettingsCreated(t *testing.T) {
|
||||
// Get or create a test user
|
||||
testUserResponse := users.GetTestUser()
|
||||
storage := storages.CreateTestStorage(testUserResponse.UserID)
|
||||
@@ -60,161 +58,8 @@ func Test_DatabaseCreated_SettingsCreatedAndExtensionsInstalled(t *testing.T) {
|
||||
|
||||
// Verify default settings values
|
||||
assert.Equal(t, database.ID, settings.DatabaseID)
|
||||
assert.Equal(t, int64(15), settings.MonitoringIntervalSeconds)
|
||||
assert.Equal(t, int64(60), settings.MonitoringIntervalSeconds)
|
||||
assert.True(t, settings.IsDbResourcesMonitoringEnabled) // Always enabled
|
||||
|
||||
// System and queries monitoring may be disabled if extension installation fails
|
||||
// in the test environment, but the service should handle this gracefully
|
||||
// We test the logic by checking the installed extensions field
|
||||
t.Logf("System monitoring enabled: %v", settings.IsSystemResourcesMonitoringEnabled)
|
||||
t.Logf("Queries monitoring enabled: %v", settings.IsQueriesMonitoringEnabled)
|
||||
t.Logf("Installed extensions: %v", settings.InstalledExtensions)
|
||||
|
||||
// If system monitoring is enabled, pg_proctab should be in installed extensions
|
||||
if settings.IsSystemResourcesMonitoringEnabled {
|
||||
assert.Contains(t, settings.InstalledExtensions, tools.PostgresqlExtensionPgProctab,
|
||||
"If system monitoring is enabled, pg_proctab extension should be tracked")
|
||||
}
|
||||
|
||||
// If queries monitoring is enabled, pg_stat_monitor should be in installed extensions
|
||||
if settings.IsQueriesMonitoringEnabled {
|
||||
assert.Contains(t, settings.InstalledExtensions, tools.PostgresqlExtensionPgStatMonitor,
|
||||
"If queries monitoring is enabled, pg_stat_monitor extension should be tracked")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DatabaseCreated_PrePostgres16_ExtensionsNotSupported(t *testing.T) {
|
||||
// Test that extension-based monitoring is disabled for older PostgreSQL versions
|
||||
testUserResponse := users.GetTestUser()
|
||||
storage := storages.CreateTestStorage(testUserResponse.UserID)
|
||||
notifier := notifiers.CreateTestNotifier(testUserResponse.UserID)
|
||||
|
||||
// Note: We manually create the database here because CreateTestDatabase always uses PostgreSQL 16,
|
||||
// but this test specifically needs PostgreSQL 14 to verify older version behavior
|
||||
testDatabase := &databases.Database{
|
||||
UserID: testUserResponse.UserID,
|
||||
Name: "Old PostgreSQL Database " + uuid.New().String(),
|
||||
Type: databases.DatabaseTypePostgres,
|
||||
Postgresql: &postgresql.PostgresqlDatabase{
|
||||
Version: tools.PostgresqlVersion14, // Older version
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
Database: func() *string { s := "test_db"; return &s }(),
|
||||
},
|
||||
Notifiers: []notifiers.Notifier{*notifier},
|
||||
}
|
||||
|
||||
// Save the test database
|
||||
repo := &databases.DatabaseRepository{}
|
||||
database, err := repo.Save(testDatabase)
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer storages.RemoveTestStorage(storage.ID)
|
||||
defer notifiers.RemoveTestNotifier(notifier)
|
||||
defer repo.Delete(database.ID)
|
||||
|
||||
// Get the monitoring settings service
|
||||
service := GetPostgresMonitoringSettingsService()
|
||||
|
||||
// Execute - trigger the database creation event
|
||||
service.OnDatabaseCreated(database.ID)
|
||||
|
||||
// Verify settings were created
|
||||
settingsRepo := GetPostgresMonitoringSettingsRepository()
|
||||
settings, err := settingsRepo.GetByDbID(database.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, settings)
|
||||
|
||||
// For pre-16 versions, extension-based monitoring should be disabled
|
||||
// because ensureExtensionsInstalled should return an error for versions < 16
|
||||
assert.False(t, settings.IsSystemResourcesMonitoringEnabled,
|
||||
"System monitoring should be disabled for PostgreSQL versions < 16")
|
||||
assert.False(t, settings.IsQueriesMonitoringEnabled,
|
||||
"Queries monitoring should be disabled for PostgreSQL versions < 16")
|
||||
|
||||
// DB resources monitoring should still be enabled (doesn't require extensions)
|
||||
assert.True(t, settings.IsDbResourcesMonitoringEnabled)
|
||||
|
||||
// No extensions should be installed for older versions
|
||||
assert.Empty(t, settings.InstalledExtensions,
|
||||
"No extensions should be installed for PostgreSQL versions < 16")
|
||||
}
|
||||
|
||||
func Test_MonitoringEnabled_ExtensionsInstalled(t *testing.T) {
|
||||
// Get or create a test user
|
||||
testUser := getTestUserModel()
|
||||
testUserResponse := users.GetTestUser()
|
||||
storage := storages.CreateTestStorage(testUserResponse.UserID)
|
||||
notifier := notifiers.CreateTestNotifier(testUserResponse.UserID)
|
||||
database := databases.CreateTestDatabase(testUserResponse.UserID, storage, notifier)
|
||||
|
||||
defer storages.RemoveTestStorage(storage.ID)
|
||||
defer notifiers.RemoveTestNotifier(notifier)
|
||||
defer databases.RemoveTestDatabase(database)
|
||||
|
||||
// Create initial settings with monitoring disabled
|
||||
service := GetPostgresMonitoringSettingsService()
|
||||
settingsRepo := GetPostgresMonitoringSettingsRepository()
|
||||
|
||||
initialSettings := &PostgresMonitoringSettings{
|
||||
DatabaseID: database.ID,
|
||||
IsSystemResourcesMonitoringEnabled: false,
|
||||
IsDbResourcesMonitoringEnabled: true,
|
||||
IsQueriesMonitoringEnabled: false,
|
||||
MonitoringIntervalSeconds: 15,
|
||||
}
|
||||
|
||||
err := settingsRepo.Save(initialSettings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test enabling system monitoring - extension installation might fail in test environment
|
||||
systemSettings := &PostgresMonitoringSettings{
|
||||
DatabaseID: database.ID,
|
||||
IsSystemResourcesMonitoringEnabled: true,
|
||||
IsDbResourcesMonitoringEnabled: true,
|
||||
IsQueriesMonitoringEnabled: false,
|
||||
MonitoringIntervalSeconds: 15,
|
||||
}
|
||||
|
||||
err = service.Save(testUser, systemSettings)
|
||||
// In test environment, extension installation might fail - this is expected behavior
|
||||
if err != nil {
|
||||
t.Logf("Extension installation failed as expected in test environment: %v", err)
|
||||
assert.Contains(t, err.Error(), "failed to install pg_proctab extension")
|
||||
return // Test passed - service correctly handles extension installation failures
|
||||
}
|
||||
|
||||
// If extension installation succeeded, verify the settings
|
||||
updatedSettings, err := settingsRepo.GetByDbID(database.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updatedSettings.IsSystemResourcesMonitoringEnabled)
|
||||
assert.Contains(t, updatedSettings.InstalledExtensions, tools.PostgresqlExtensionPgProctab)
|
||||
|
||||
// Test enabling queries monitoring - should install pg_stat_monitor extension
|
||||
queriesSettings := &PostgresMonitoringSettings{
|
||||
DatabaseID: database.ID,
|
||||
IsSystemResourcesMonitoringEnabled: true,
|
||||
IsDbResourcesMonitoringEnabled: true,
|
||||
IsQueriesMonitoringEnabled: true,
|
||||
MonitoringIntervalSeconds: 15,
|
||||
}
|
||||
|
||||
err = service.Save(testUser, queriesSettings)
|
||||
if err != nil {
|
||||
t.Logf("Queries monitoring extension installation failed: %v", err)
|
||||
assert.Contains(t, err.Error(), "failed to install pg_stat_monitor extension")
|
||||
return // Test passed - service correctly handles extension installation failures
|
||||
}
|
||||
|
||||
// If both extensions installed successfully, verify final state
|
||||
finalSettings, err := settingsRepo.GetByDbID(database.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, finalSettings.IsSystemResourcesMonitoringEnabled)
|
||||
assert.True(t, finalSettings.IsQueriesMonitoringEnabled)
|
||||
assert.Contains(t, finalSettings.InstalledExtensions, tools.PostgresqlExtensionPgProctab)
|
||||
assert.Contains(t, finalSettings.InstalledExtensions, tools.PostgresqlExtensionPgStatMonitor)
|
||||
}
|
||||
|
||||
func Test_GetSettingsByDbID_SettingsReturned(t *testing.T) {
|
||||
@@ -236,7 +81,7 @@ func Test_GetSettingsByDbID_SettingsReturned(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, settings)
|
||||
assert.Equal(t, database.ID, settings.DatabaseID)
|
||||
assert.Equal(t, int64(15), settings.MonitoringIntervalSeconds)
|
||||
assert.Equal(t, int64(60), settings.MonitoringIntervalSeconds)
|
||||
assert.True(t, settings.IsDbResourcesMonitoringEnabled) // Always enabled
|
||||
|
||||
// Test 2: Get settings that already exist
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
type PostgresqlExtension string
|
||||
|
||||
const (
|
||||
// needed for system monitoring (CPU, RAM)
|
||||
PostgresqlExtensionPgProctab PostgresqlExtension = "pg_proctab"
|
||||
// needed for queries monitoring
|
||||
PostgresqlExtensionPgStatMonitor PostgresqlExtension = "pg_stat_statements"
|
||||
)
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
-- Create postgres_monitoring_settings table
|
||||
CREATE TABLE postgres_monitoring_settings (
|
||||
database_id UUID PRIMARY KEY,
|
||||
is_system_resources_monitoring_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_db_resources_monitoring_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_queries_monitoring_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
monitoring_interval_seconds BIGINT NOT NULL DEFAULT 60,
|
||||
installed_extensions_raw TEXT
|
||||
);
|
||||
|
||||
@@ -71,6 +71,12 @@ 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:
|
||||
|
||||
- do not remove old backups on backups disable
|
||||
@@ -96,12 +102,11 @@ Extra:
|
||||
|
||||
Monitoring flow:
|
||||
|
||||
- add system metrics (CPU, RAM, disk, IO) (in progress by Rostislav Dugin)
|
||||
|
||||
- add queries stats (slowest, most frequent, etc. via pg_stat_statements)
|
||||
- add triggering backups and restores via API
|
||||
- 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 DB size distribution chart (tables, indexes, etc.)
|
||||
- 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.)
|
||||
|
||||
390
frontend/package-lock.json
generated
390
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-router": "^7.6.0",
|
||||
"recharts": "^3.2.0",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1315,6 +1316,32 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
|
||||
@@ -1575,6 +1602,18 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
|
||||
@@ -1917,6 +1956,69 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@@ -1934,7 +2036,7 @@
|
||||
"version": "19.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
|
||||
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1950,6 +2052,12 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
@@ -2666,6 +2774,15 @@
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2745,6 +2862,127 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@@ -2823,6 +3061,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -3097,6 +3341,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.10",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||
@@ -3371,6 +3625,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -3813,6 +4073,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -3855,6 +4125,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -5879,9 +6158,31 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -5914,6 +6215,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz",
|
||||
"integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -5958,6 +6301,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
@@ -6508,6 +6857,12 @@
|
||||
"node": ">=12.22"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
@@ -6770,6 +7125,37 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-router": "^7.6.0",
|
||||
"recharts": "^3.2.0",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
2
frontend/src/entity/monitoring/index.ts
Normal file
2
frontend/src/entity/monitoring/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './metrics';
|
||||
export * from './settings';
|
||||
16
frontend/src/entity/monitoring/metrics/api/metricsApi.ts
Normal file
16
frontend/src/entity/monitoring/metrics/api/metricsApi.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getApplicationServer } from '../../../../constants';
|
||||
import RequestOptions from '../../../../shared/api/RequestOptions';
|
||||
import { apiHelper } from '../../../../shared/api/apiHelper';
|
||||
import type { GetMetricsRequest } from '../model/GetMetricsRequest';
|
||||
import type { PostgresMonitoringMetric } from '../model/PostgresMonitoringMetric';
|
||||
|
||||
export const metricsApi = {
|
||||
async getMetrics(request: GetMetricsRequest): Promise<PostgresMonitoringMetric[]> {
|
||||
const requestOptions: RequestOptions = new RequestOptions();
|
||||
requestOptions.setBody(JSON.stringify(request));
|
||||
return apiHelper.fetchPostJson<PostgresMonitoringMetric[]>(
|
||||
`${getApplicationServer()}/api/v1/postgres-monitoring-metrics/get`,
|
||||
requestOptions,
|
||||
);
|
||||
},
|
||||
};
|
||||
5
frontend/src/entity/monitoring/metrics/index.ts
Normal file
5
frontend/src/entity/monitoring/metrics/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { metricsApi } from './api/metricsApi';
|
||||
export type { PostgresMonitoringMetric } from './model/PostgresMonitoringMetric';
|
||||
export type { GetMetricsRequest } from './model/GetMetricsRequest';
|
||||
export { PostgresMonitoringMetricType } from './model/PostgresMonitoringMetricType';
|
||||
export { PostgresMonitoringMetricValueType } from './model/PostgresMonitoringMetricValueType';
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { PostgresMonitoringMetricType } from './PostgresMonitoringMetricType';
|
||||
|
||||
export interface GetMetricsRequest {
|
||||
databaseId: string;
|
||||
metricType: PostgresMonitoringMetricType;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { PostgresMonitoringMetricType } from './PostgresMonitoringMetricType';
|
||||
import type { PostgresMonitoringMetricValueType } from './PostgresMonitoringMetricValueType';
|
||||
|
||||
export interface PostgresMonitoringMetric {
|
||||
id: string;
|
||||
databaseId: string;
|
||||
metric: PostgresMonitoringMetricType;
|
||||
valueType: PostgresMonitoringMetricValueType;
|
||||
value: number;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum PostgresMonitoringMetricType {
|
||||
DB_RAM_USAGE = 'DB_RAM_USAGE',
|
||||
DB_IO_USAGE = 'DB_IO_USAGE',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum PostgresMonitoringMetricValueType {
|
||||
BYTE = 'BYTE',
|
||||
PERCENT = 'PERCENT',
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getApplicationServer } from '../../../../constants';
|
||||
import RequestOptions from '../../../../shared/api/RequestOptions';
|
||||
import { apiHelper } from '../../../../shared/api/apiHelper';
|
||||
import type { PostgresMonitoringSettings } from '../model/PostgresMonitoringSettings';
|
||||
|
||||
export const monitoringSettingsApi = {
|
||||
async saveSettings(settings: PostgresMonitoringSettings) {
|
||||
const requestOptions: RequestOptions = new RequestOptions();
|
||||
requestOptions.setBody(JSON.stringify(settings));
|
||||
return apiHelper.fetchPostJson<PostgresMonitoringSettings>(
|
||||
`${getApplicationServer()}/api/v1/postgres-monitoring-settings/save`,
|
||||
requestOptions,
|
||||
);
|
||||
},
|
||||
|
||||
async getSettingsByDbID(databaseId: string) {
|
||||
const requestOptions: RequestOptions = new RequestOptions();
|
||||
return apiHelper.fetchGetJson<PostgresMonitoringSettings>(
|
||||
`${getApplicationServer()}/api/v1/postgres-monitoring-settings/database/${databaseId}`,
|
||||
requestOptions,
|
||||
true,
|
||||
);
|
||||
},
|
||||
};
|
||||
3
frontend/src/entity/monitoring/settings/index.ts
Normal file
3
frontend/src/entity/monitoring/settings/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { monitoringSettingsApi } from './api/monitoringSettingsApi';
|
||||
export type { PostgresMonitoringSettings } from './model/PostgresMonitoringSettings';
|
||||
export { PostgresqlExtension } from './model/PostgresqlExtension';
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Database } from '../../../databases';
|
||||
import { PostgresqlExtension } from './PostgresqlExtension';
|
||||
|
||||
export interface PostgresMonitoringSettings {
|
||||
databaseId: string;
|
||||
database?: Database;
|
||||
|
||||
isDbResourcesMonitoringEnabled: boolean;
|
||||
monitoringIntervalSeconds: number;
|
||||
|
||||
installedExtensions: PostgresqlExtension[];
|
||||
installedExtensionsRaw?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum PostgresqlExtension {
|
||||
PG_PROCTAB = 'pg_proctab',
|
||||
PG_STAT_STATEMENTS = 'pg_stat_statements',
|
||||
}
|
||||
@@ -350,7 +350,7 @@ export const BackupsComponent = ({ database }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5 w-full rounded bg-white p-5 shadow">
|
||||
<div className="mt-5 w-full rounded-md bg-white p-5 shadow">
|
||||
<h2 className="text-xl font-bold">Backups</h2>
|
||||
|
||||
<div className="mt-5" />
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import { CloseOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import { Spin } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { type Database, databaseApi } from '../../../entity/databases';
|
||||
import { ToastHelper } from '../../../shared/toast';
|
||||
import { ConfirmationComponent } from '../../../shared/ui';
|
||||
import {
|
||||
BackupsComponent,
|
||||
EditBackupConfigComponent,
|
||||
ShowBackupConfigComponent,
|
||||
} from '../../backups';
|
||||
import {
|
||||
EditHealthcheckConfigComponent,
|
||||
HealthckeckAttemptsComponent,
|
||||
ShowHealthcheckConfigComponent,
|
||||
} from '../../healthcheck';
|
||||
import { EditDatabaseNotifiersComponent } from './edit/EditDatabaseNotifiersComponent';
|
||||
import { EditDatabaseSpecificDataComponent } from './edit/EditDatabaseSpecificDataComponent';
|
||||
import { ShowDatabaseNotifiersComponent } from './show/ShowDatabaseNotifiersComponent';
|
||||
import { ShowDatabaseSpecificDataComponent } from './show/ShowDatabaseSpecificDataComponent';
|
||||
import { BackupsComponent } from '../../backups';
|
||||
import { HealthckeckAttemptsComponent } from '../../healthcheck';
|
||||
import { MetricsComponent } from '../../monitoring/metrics';
|
||||
import { DatabaseConfigComponent } from './DatabaseConfigComponent';
|
||||
|
||||
interface Props {
|
||||
contentHeight: number;
|
||||
@@ -34,94 +21,10 @@ export const DatabaseComponent = ({
|
||||
onDatabaseChanged,
|
||||
onDatabaseDeleted,
|
||||
}: Props) => {
|
||||
const [currentTab, setCurrentTab] = useState<'config' | 'backups' | 'metrics'>('backups');
|
||||
|
||||
const [database, setDatabase] = useState<Database | undefined>();
|
||||
|
||||
const [isEditName, setIsEditName] = useState(false);
|
||||
const [isEditDatabaseSpecificDataSettings, setIsEditDatabaseSpecificDataSettings] =
|
||||
useState(false);
|
||||
const [isEditBackupConfig, setIsEditBackupConfig] = useState(false);
|
||||
const [isEditNotifiersSettings, setIsEditNotifiersSettings] = useState(false);
|
||||
const [isEditHealthcheckSettings, setIsEditHealthcheckSettings] = useState(false);
|
||||
|
||||
const [editDatabase, setEditDatabase] = useState<Database | undefined>();
|
||||
const [isNameUnsaved, setIsNameUnsaved] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||
|
||||
const [isShowRemoveConfirm, setIsShowRemoveConfirm] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
const testConnection = () => {
|
||||
if (!database) return;
|
||||
|
||||
setIsTestingConnection(true);
|
||||
databaseApi
|
||||
.testDatabaseConnection(database.id)
|
||||
.then(() => {
|
||||
ToastHelper.showToast({
|
||||
title: 'Connection test successful!',
|
||||
description: 'Database connection tested successfully',
|
||||
});
|
||||
|
||||
if (database.lastBackupErrorMessage) {
|
||||
setDatabase({ ...database, lastBackupErrorMessage: undefined });
|
||||
onDatabaseChanged(database);
|
||||
}
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsTestingConnection(false);
|
||||
});
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
if (!database) return;
|
||||
|
||||
setIsRemoving(true);
|
||||
databaseApi
|
||||
.deleteDatabase(database.id)
|
||||
.then(() => {
|
||||
onDatabaseDeleted();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRemoving(false);
|
||||
});
|
||||
};
|
||||
|
||||
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');
|
||||
setIsNameUnsaved(false);
|
||||
};
|
||||
|
||||
const saveName = () => {
|
||||
if (!editDatabase) return;
|
||||
|
||||
setIsSaving(true);
|
||||
databaseApi
|
||||
.updateDatabase(editDatabase)
|
||||
.then(() => {
|
||||
setDatabase(editDatabase);
|
||||
setIsSaving(false);
|
||||
setIsNameUnsaved(false);
|
||||
setIsEditName(false);
|
||||
onDatabaseChanged(editDatabase);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
setIsSaving(false);
|
||||
});
|
||||
};
|
||||
|
||||
const loadSettings = () => {
|
||||
setDatabase(undefined);
|
||||
@@ -133,278 +36,52 @@ export const DatabaseComponent = ({
|
||||
loadSettings();
|
||||
}, [databaseId]);
|
||||
|
||||
if (!database) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto" style={{ maxHeight: contentHeight }}>
|
||||
<div className="w-full rounded bg-white p-5 shadow">
|
||||
{!database ? (
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{!isEditName ? (
|
||||
<div className="mb-5 flex items-center text-2xl font-bold">
|
||||
{database.name}
|
||||
<div className="ml-2 cursor-pointer" onClick={() => startEdit('name')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="max-w-[250px]"
|
||||
value={editDatabase?.name}
|
||||
onChange={(e) => {
|
||||
if (!editDatabase) return;
|
||||
<div className="flex">
|
||||
<div
|
||||
className={`mr-2 cursor-pointer rounded-tl-md rounded-tr-md px-6 py-2 ${currentTab === 'config' ? 'bg-white' : 'bg-gray-200'}`}
|
||||
onClick={() => setCurrentTab('config')}
|
||||
>
|
||||
Config
|
||||
</div>
|
||||
|
||||
setEditDatabase({ ...editDatabase, name: e.target.value });
|
||||
setIsNameUnsaved(true);
|
||||
}}
|
||||
placeholder="Enter name..."
|
||||
size="large"
|
||||
/>
|
||||
<div
|
||||
className={`mr-2 cursor-pointer rounded-tl-md rounded-tr-md px-6 py-2 ${currentTab === 'backups' ? 'bg-white' : 'bg-gray-200'}`}
|
||||
onClick={() => setCurrentTab('backups')}
|
||||
>
|
||||
Backups
|
||||
</div>
|
||||
|
||||
<div className="ml-1 flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
className="flex h-6 w-6 items-center justify-center p-0"
|
||||
onClick={() => {
|
||||
setIsEditName(false);
|
||||
setIsNameUnsaved(false);
|
||||
setEditDatabase(undefined);
|
||||
}}
|
||||
>
|
||||
<CloseOutlined className="text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isNameUnsaved && (
|
||||
<Button
|
||||
className="mt-1"
|
||||
type="primary"
|
||||
onClick={() => saveName()}
|
||||
loading={isSaving}
|
||||
disabled={!editDatabase?.name}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{database.lastBackupErrorMessage && (
|
||||
<div className="max-w-[400px] rounded border border-red-600 px-3 py-3">
|
||||
<div className="mt-1 flex items-center text-sm font-bold text-red-600">
|
||||
<InfoCircleOutlined className="mr-2" style={{ color: 'red' }} />
|
||||
Last backup error
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-sm">
|
||||
The error:
|
||||
<br />
|
||||
{database.lastBackupErrorMessage}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-sm text-gray-500">
|
||||
To clean this error (choose any):
|
||||
<ul>
|
||||
<li>- test connection via button below (even if you updated settings);</li>
|
||||
<li>- wait until the next backup is done without errors;</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-10">
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Database settings</div>
|
||||
|
||||
{!isEditDatabaseSpecificDataSettings ? (
|
||||
<div
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => startEdit('database')}
|
||||
>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditDatabaseSpecificDataSettings ? (
|
||||
<EditDatabaseSpecificDataComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
isShowBackButton={false}
|
||||
onBack={() => {}}
|
||||
onCancel={() => {
|
||||
setIsEditDatabaseSpecificDataSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
onSaved={onDatabaseChanged}
|
||||
/>
|
||||
) : (
|
||||
<ShowDatabaseSpecificDataComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Backup config</div>
|
||||
|
||||
{!isEditBackupConfig ? (
|
||||
<div
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => startEdit('backup-config')}
|
||||
>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditBackupConfig ? (
|
||||
<EditBackupConfigComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
onCancel={() => {
|
||||
setIsEditBackupConfig(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
onSaved={() => onDatabaseChanged(database)}
|
||||
isShowBackButton={false}
|
||||
onBack={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<ShowBackupConfigComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-10">
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Healthcheck settings</div>
|
||||
|
||||
{!isEditHealthcheckSettings ? (
|
||||
<div
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => startEdit('healthcheck')}
|
||||
>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditHealthcheckSettings ? (
|
||||
<EditHealthcheckConfigComponent
|
||||
databaseId={database.id}
|
||||
onClose={() => {
|
||||
setIsEditHealthcheckSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ShowHealthcheckConfigComponent databaseId={database.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Notifiers settings</div>
|
||||
|
||||
{!isEditNotifiersSettings ? (
|
||||
<div
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => startEdit('notifiers')}
|
||||
>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditNotifiersSettings ? (
|
||||
<EditDatabaseNotifiersComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
isShowBackButton={false}
|
||||
isShowSaveOnlyForUnsaved={true}
|
||||
onBack={() => {}}
|
||||
onCancel={() => {
|
||||
setIsEditNotifiersSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
saveButtonText="Save"
|
||||
onSaved={onDatabaseChanged}
|
||||
/>
|
||||
) : (
|
||||
<ShowDatabaseNotifiersComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isEditDatabaseSpecificDataSettings && (
|
||||
<div className="mt-10">
|
||||
<Button
|
||||
type="primary"
|
||||
className="mr-1"
|
||||
ghost
|
||||
onClick={testConnection}
|
||||
loading={isTestingConnection}
|
||||
disabled={isTestingConnection}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => setIsShowRemoveConfirm(true)}
|
||||
ghost
|
||||
loading={isRemoving}
|
||||
disabled={isRemoving}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShowRemoveConfirm && (
|
||||
<ConfirmationComponent
|
||||
onConfirm={remove}
|
||||
onDecline={() => setIsShowRemoveConfirm(false)}
|
||||
description="Are you sure you want to remove this database? This action cannot be undone."
|
||||
actionText="Remove"
|
||||
actionButtonColor="red"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{database && <HealthckeckAttemptsComponent database={database} />}
|
||||
{database && <BackupsComponent database={database} />}
|
||||
{currentTab === 'config' && (
|
||||
<DatabaseConfigComponent
|
||||
database={database}
|
||||
setDatabase={setDatabase}
|
||||
onDatabaseChanged={onDatabaseChanged}
|
||||
onDatabaseDeleted={onDatabaseDeleted}
|
||||
editDatabase={editDatabase}
|
||||
setEditDatabase={setEditDatabase}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'backups' && (
|
||||
<>
|
||||
<HealthckeckAttemptsComponent database={database} />
|
||||
<BackupsComponent database={database} />
|
||||
</>
|
||||
)}
|
||||
{currentTab === 'metrics' && <MetricsComponent databaseId={database.id} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
418
frontend/src/features/databases/ui/DatabaseConfigComponent.tsx
Normal file
418
frontend/src/features/databases/ui/DatabaseConfigComponent.tsx
Normal file
@@ -0,0 +1,418 @@
|
||||
import { CloseOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Input } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type Database, databaseApi } from '../../../entity/databases';
|
||||
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';
|
||||
import { ShowDatabaseSpecificDataComponent } from './show/ShowDatabaseSpecificDataComponent';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
setDatabase: (database?: Database | undefined) => void;
|
||||
onDatabaseChanged: (database: Database) => void;
|
||||
onDatabaseDeleted: () => void;
|
||||
editDatabase: Database | undefined;
|
||||
setEditDatabase: (database: Database | undefined) => void;
|
||||
}
|
||||
|
||||
export const DatabaseConfigComponent = ({
|
||||
database,
|
||||
setDatabase,
|
||||
onDatabaseChanged,
|
||||
onDatabaseDeleted,
|
||||
editDatabase,
|
||||
setEditDatabase,
|
||||
}: Props) => {
|
||||
const [isEditName, setIsEditName] = useState(false);
|
||||
const [isEditDatabaseSpecificDataSettings, setIsEditDatabaseSpecificDataSettings] =
|
||||
useState(false);
|
||||
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 [isShowRemoveConfirm, setIsShowRemoveConfirm] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
const loadSettings = () => {
|
||||
setDatabase(undefined);
|
||||
setEditDatabase(undefined);
|
||||
databaseApi.getDatabase(database.id).then(setDatabase);
|
||||
};
|
||||
|
||||
const testConnection = () => {
|
||||
if (!database) return;
|
||||
|
||||
setIsTestingConnection(true);
|
||||
databaseApi
|
||||
.testDatabaseConnection(database.id)
|
||||
.then(() => {
|
||||
ToastHelper.showToast({
|
||||
title: 'Connection test successful!',
|
||||
description: 'Database connection tested successfully',
|
||||
});
|
||||
|
||||
if (database.lastBackupErrorMessage) {
|
||||
setDatabase({ ...database, lastBackupErrorMessage: undefined });
|
||||
onDatabaseChanged(database);
|
||||
}
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsTestingConnection(false);
|
||||
});
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
if (!database) return;
|
||||
|
||||
setIsRemoving(true);
|
||||
databaseApi
|
||||
.deleteDatabase(database.id)
|
||||
.then(() => {
|
||||
onDatabaseDeleted();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRemoving(false);
|
||||
});
|
||||
};
|
||||
|
||||
const startEdit = (
|
||||
type: 'name' | 'database' | 'backup-config' | 'notifiers' | 'healthcheck' | 'monitoring',
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
||||
const saveName = () => {
|
||||
if (!editDatabase) return;
|
||||
|
||||
setIsSaving(true);
|
||||
databaseApi
|
||||
.updateDatabase(editDatabase)
|
||||
.then(() => {
|
||||
setDatabase(editDatabase);
|
||||
setIsSaving(false);
|
||||
setIsNameUnsaved(false);
|
||||
setIsEditName(false);
|
||||
onDatabaseChanged(editDatabase);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
setIsSaving(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-tr-md rounded-br-md rounded-bl-md bg-white p-5 shadow">
|
||||
{!isEditName ? (
|
||||
<div className="mb-5 flex items-center text-2xl font-bold">
|
||||
{database.name}
|
||||
<div className="ml-2 cursor-pointer" onClick={() => startEdit('name')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="max-w-[250px]"
|
||||
value={editDatabase?.name}
|
||||
onChange={(e) => {
|
||||
if (!editDatabase) return;
|
||||
|
||||
setEditDatabase({ ...editDatabase, name: e.target.value });
|
||||
setIsNameUnsaved(true);
|
||||
}}
|
||||
placeholder="Enter name..."
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<div className="ml-1 flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
className="flex h-6 w-6 items-center justify-center p-0"
|
||||
onClick={() => {
|
||||
setIsEditName(false);
|
||||
setIsNameUnsaved(false);
|
||||
setEditDatabase(undefined);
|
||||
}}
|
||||
>
|
||||
<CloseOutlined className="text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isNameUnsaved && (
|
||||
<Button
|
||||
className="mt-1"
|
||||
type="primary"
|
||||
onClick={() => saveName()}
|
||||
loading={isSaving}
|
||||
disabled={!editDatabase?.name}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{database.lastBackupErrorMessage && (
|
||||
<div className="max-w-[400px] rounded border border-red-600 px-3 py-3">
|
||||
<div className="mt-1 flex items-center text-sm font-bold text-red-600">
|
||||
<InfoCircleOutlined className="mr-2" style={{ color: 'red' }} />
|
||||
Last backup error
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-sm">
|
||||
The error:
|
||||
<br />
|
||||
{database.lastBackupErrorMessage}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-sm text-gray-500">
|
||||
To clean this error (choose any):
|
||||
<ul>
|
||||
<li>- test connection via button below (even if you updated settings);</li>
|
||||
<li>- wait until the next backup is done without errors;</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-10">
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Database settings</div>
|
||||
|
||||
{!isEditDatabaseSpecificDataSettings ? (
|
||||
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('database')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditDatabaseSpecificDataSettings ? (
|
||||
<EditDatabaseSpecificDataComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
isShowBackButton={false}
|
||||
onBack={() => {}}
|
||||
onCancel={() => {
|
||||
setIsEditDatabaseSpecificDataSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
onSaved={onDatabaseChanged}
|
||||
/>
|
||||
) : (
|
||||
<ShowDatabaseSpecificDataComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Backup config</div>
|
||||
|
||||
{!isEditBackupConfig ? (
|
||||
<div
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => startEdit('backup-config')}
|
||||
>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditBackupConfig ? (
|
||||
<EditBackupConfigComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
onCancel={() => {
|
||||
setIsEditBackupConfig(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
onSaved={() => onDatabaseChanged(database)}
|
||||
isShowBackButton={false}
|
||||
onBack={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<ShowBackupConfigComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-10">
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Healthcheck settings</div>
|
||||
|
||||
{!isEditHealthcheckSettings ? (
|
||||
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('healthcheck')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditHealthcheckSettings ? (
|
||||
<EditHealthcheckConfigComponent
|
||||
databaseId={database.id}
|
||||
onClose={() => {
|
||||
setIsEditHealthcheckSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ShowHealthcheckConfigComponent databaseId={database.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Notifiers settings</div>
|
||||
|
||||
{!isEditNotifiersSettings ? (
|
||||
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('notifiers')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditNotifiersSettings ? (
|
||||
<EditDatabaseNotifiersComponent
|
||||
database={database}
|
||||
isShowCancelButton
|
||||
isShowBackButton={false}
|
||||
isShowSaveOnlyForUnsaved={true}
|
||||
onBack={() => {}}
|
||||
onCancel={() => {
|
||||
setIsEditNotifiersSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
isSaveToApi={true}
|
||||
saveButtonText="Save"
|
||||
onSaved={onDatabaseChanged}
|
||||
/>
|
||||
) : (
|
||||
<ShowDatabaseNotifiersComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</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
|
||||
type="primary"
|
||||
className="mr-1"
|
||||
ghost
|
||||
onClick={testConnection}
|
||||
loading={isTestingConnection}
|
||||
disabled={isTestingConnection}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => setIsShowRemoveConfirm(true)}
|
||||
ghost
|
||||
loading={isRemoving}
|
||||
disabled={isRemoving}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShowRemoveConfirm && (
|
||||
<ConfirmationComponent
|
||||
onConfirm={remove}
|
||||
onDecline={() => setIsShowRemoveConfirm(false)}
|
||||
description="Are you sure you want to remove this database? This action cannot be undone."
|
||||
actionText="Remove"
|
||||
actionButtonColor="red"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -118,7 +118,7 @@ export const HealthckeckAttemptsComponent = ({ database }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5 w-full rounded bg-white p-5 shadow">
|
||||
<div className="w-full rounded-tr-md rounded-br-md rounded-bl-md bg-white p-5 shadow">
|
||||
<h2 className="text-xl font-bold">Healthcheck attempts</h2>
|
||||
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
|
||||
1
frontend/src/features/monitoring/metrics/index.ts
Normal file
1
frontend/src/features/monitoring/metrics/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MetricsComponent } from './ui/MetricsComponent';
|
||||
245
frontend/src/features/monitoring/metrics/ui/MetricsComponent.tsx
Normal file
245
frontend/src/features/monitoring/metrics/ui/MetricsComponent.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { GetMetricsRequest, PostgresMonitoringMetric } from '../../../../entity/monitoring';
|
||||
import { PostgresMonitoringMetricType, metricsApi } from '../../../../entity/monitoring';
|
||||
|
||||
interface Props {
|
||||
databaseId: string;
|
||||
}
|
||||
|
||||
type Period = '1H' | '24H' | '7D' | '1M';
|
||||
|
||||
interface ChartDataPoint {
|
||||
timestamp: string;
|
||||
displayTime: string;
|
||||
ramUsage?: number;
|
||||
ioUsage?: number;
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const getDateRange = (period: Period) => {
|
||||
const now = dayjs();
|
||||
let from: dayjs.Dayjs;
|
||||
|
||||
switch (period) {
|
||||
case '1H':
|
||||
from = now.subtract(1, 'hour');
|
||||
break;
|
||||
case '24H':
|
||||
from = now.subtract(24, 'hours');
|
||||
break;
|
||||
case '7D':
|
||||
from = now.subtract(7, 'days');
|
||||
break;
|
||||
case '1M':
|
||||
from = now.subtract(1, 'month');
|
||||
break;
|
||||
default:
|
||||
from = now.subtract(24, 'hours');
|
||||
}
|
||||
|
||||
return {
|
||||
from: from.toISOString(),
|
||||
to: now.toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
const getDisplayTime = (timestamp: string, period: Period): string => {
|
||||
const date = dayjs(timestamp);
|
||||
|
||||
switch (period) {
|
||||
case '1H':
|
||||
return date.format('HH:mm');
|
||||
case '24H':
|
||||
return date.format('HH:mm');
|
||||
case '7D':
|
||||
return date.format('MM/DD HH:mm');
|
||||
case '1M':
|
||||
return date.format('MM/DD');
|
||||
default:
|
||||
return date.format('HH:mm');
|
||||
}
|
||||
};
|
||||
|
||||
export const MetricsComponent = ({ databaseId }: Props) => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<Period>('24H');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [ramData, setRamData] = useState<PostgresMonitoringMetric[]>([]);
|
||||
const [ioData, setIoData] = useState<PostgresMonitoringMetric[]>([]);
|
||||
|
||||
const loadMetrics = async (period: Period) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { from, to } = getDateRange(period);
|
||||
|
||||
const [ramMetrics, ioMetrics] = await Promise.all([
|
||||
metricsApi.getMetrics({
|
||||
databaseId,
|
||||
metricType: PostgresMonitoringMetricType.DB_RAM_USAGE,
|
||||
from,
|
||||
to,
|
||||
} as GetMetricsRequest),
|
||||
metricsApi.getMetrics({
|
||||
databaseId,
|
||||
metricType: PostgresMonitoringMetricType.DB_IO_USAGE,
|
||||
from,
|
||||
to,
|
||||
} as GetMetricsRequest),
|
||||
]);
|
||||
|
||||
setRamData(ramMetrics);
|
||||
setIoData(ioMetrics);
|
||||
} catch (error) {
|
||||
alert((error as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMetrics(selectedPeriod);
|
||||
}, [databaseId, selectedPeriod]);
|
||||
|
||||
const prepareChartData = (): ChartDataPoint[] => {
|
||||
// Create a map for easier lookup
|
||||
const ramMap = new Map(ramData.map((item) => [item.createdAt, item.value]));
|
||||
const ioMap = new Map(ioData.map((item) => [item.createdAt, item.value]));
|
||||
|
||||
// Get all unique timestamps and sort them
|
||||
const allTimestamps = Array.from(
|
||||
new Set([...ramData.map((d) => d.createdAt), ...ioData.map((d) => d.createdAt)]),
|
||||
).sort();
|
||||
|
||||
return allTimestamps.map((timestamp) => ({
|
||||
timestamp,
|
||||
displayTime: getDisplayTime(timestamp, selectedPeriod),
|
||||
ramUsage: ramMap.get(timestamp),
|
||||
ioUsage: ioMap.get(timestamp),
|
||||
}));
|
||||
};
|
||||
|
||||
const chartData = prepareChartData();
|
||||
|
||||
const periodButtons: Period[] = ['1H', '24H', '7D', '1M'];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center p-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-tr-md rounded-br-md rounded-bl-md bg-white p-5 shadow">
|
||||
<div className="mb-6">
|
||||
<h3 className="mb-4 text-lg font-bold">Database Metrics</h3>
|
||||
|
||||
<div className="mb-4 rounded-md border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-800">
|
||||
<div className="flex items-center">
|
||||
<InfoCircleOutlined className="mr-2 text-yellow-600" />
|
||||
This feature is in development. Do not consider it as production ready.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{periodButtons.map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
type={selectedPeriod === period ? 'primary' : 'default'}
|
||||
onClick={() => setSelectedPeriod(period)}
|
||||
size="small"
|
||||
>
|
||||
{period}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* RAM Usage Chart */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-base font-semibold">RAM Usage (cumulative)</h4>
|
||||
<div style={{ width: '100%', height: '300px' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData} margin={{ top: 10, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis dataKey="displayTime" tick={{ fontSize: 12 }} stroke="#666" />
|
||||
<YAxis tickFormatter={formatBytes} tick={{ fontSize: 12 }} stroke="#666" />
|
||||
<Tooltip
|
||||
formatter={(value: number) => [formatBytes(value), 'RAM Usage']}
|
||||
labelStyle={{ color: '#666' }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="ramUsage"
|
||||
stroke="#1890ff"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#1890ff', strokeWidth: 2, r: 3 }}
|
||||
connectNulls={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IO Usage Chart */}
|
||||
<div>
|
||||
<h4 className="mb-3 text-base font-semibold">IO Usage (cumulative)</h4>
|
||||
<div style={{ width: '100%', height: '300px' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData} margin={{ top: 10, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis dataKey="displayTime" tick={{ fontSize: 12 }} stroke="#666" />
|
||||
<YAxis tickFormatter={formatBytes} tick={{ fontSize: 12 }} stroke="#666" />
|
||||
<Tooltip
|
||||
formatter={(value: number) => [formatBytes(value), 'IO Usage']}
|
||||
labelStyle={{ color: '#666' }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="ioUsage"
|
||||
stroke="#52c41a"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#52c41a', strokeWidth: 2, r: 3 }}
|
||||
connectNulls={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{chartData.length === 0 && (
|
||||
<div className="mt-6 text-center text-gray-500">
|
||||
No metrics data available for the selected period
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
frontend/src/features/monitoring/settings/index.ts
Normal file
2
frontend/src/features/monitoring/settings/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ShowMonitoringSettingsComponent } from './ui/ShowMonitoringSettingsComponent';
|
||||
export { EditMonitoringSettingsComponent } from './ui/EditMonitoringSettingsComponent';
|
||||
@@ -0,0 +1,122 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Select, Spin, Switch, Tooltip } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { Database } from '../../../../entity/databases';
|
||||
import type { PostgresMonitoringSettings } from '../../../../entity/monitoring/settings';
|
||||
import { monitoringSettingsApi } from '../../../../entity/monitoring/settings';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
|
||||
onCancel: () => void;
|
||||
onSaved: (monitoringSettings: PostgresMonitoringSettings) => void;
|
||||
}
|
||||
|
||||
const intervalOptions = [
|
||||
{ label: '15 seconds', value: 15 },
|
||||
{ label: '30 seconds', value: 30 },
|
||||
{ label: '1 minute', value: 60 },
|
||||
{ label: '2 minutes', value: 120 },
|
||||
{ label: '5 minutes', value: 300 },
|
||||
{ label: '10 minutes', value: 600 },
|
||||
{ label: '15 minutes', value: 900 },
|
||||
{ label: '30 minutes', value: 1800 },
|
||||
{ label: '1 hour', value: 3600 },
|
||||
];
|
||||
|
||||
export const EditMonitoringSettingsComponent = ({ database, onCancel, onSaved }: Props) => {
|
||||
const [monitoringSettings, setMonitoringSettings] = useState<PostgresMonitoringSettings>();
|
||||
const [isUnsaved, setIsUnsaved] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const updateSettings = (patch: Partial<PostgresMonitoringSettings>) => {
|
||||
setMonitoringSettings((prev) => (prev ? { ...prev, ...patch } : prev));
|
||||
setIsUnsaved(true);
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!monitoringSettings) return;
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
await monitoringSettingsApi.saveSettings(monitoringSettings);
|
||||
setIsUnsaved(false);
|
||||
onSaved(monitoringSettings);
|
||||
} catch (e) {
|
||||
alert((e as Error).message);
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
monitoringSettingsApi
|
||||
.getSettingsByDbID(database.id)
|
||||
.then((res) => {
|
||||
setMonitoringSettings(res);
|
||||
setIsUnsaved(false);
|
||||
setIsSaving(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
alert((e as Error).message);
|
||||
});
|
||||
}, [database]);
|
||||
|
||||
if (!monitoringSettings) return <Spin size="small" />;
|
||||
|
||||
const isAllFieldsValid = true; // All fields have defaults, so always valid
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[200px]">Database resources monitoring</div>
|
||||
<Switch
|
||||
checked={monitoringSettings.isDbResourcesMonitoringEnabled}
|
||||
onChange={(checked) => updateSettings({ isDbResourcesMonitoringEnabled: checked })}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Monitor database-specific metrics like connections, locks, buffer cache hit ratio, and transaction statistics."
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-1 flex w-full items-center">
|
||||
<div className="min-w-[200px]">Monitoring interval</div>
|
||||
<Select
|
||||
value={monitoringSettings.monitoringIntervalSeconds}
|
||||
onChange={(v) => updateSettings({ monitoringIntervalSeconds: v })}
|
||||
size="small"
|
||||
className="max-w-[200px] grow"
|
||||
options={intervalOptions}
|
||||
/>
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="How often to collect monitoring metrics. Lower intervals provide more detailed data but use more resources."
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex">
|
||||
<Button danger ghost className="mr-1" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="mr-5 ml-auto"
|
||||
onClick={saveSettings}
|
||||
loading={isSaving}
|
||||
disabled={!isUnsaved || !isAllFieldsValid}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Switch } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { Database } from '../../../../entity/databases';
|
||||
import type { PostgresMonitoringSettings } from '../../../../entity/monitoring/settings';
|
||||
import { monitoringSettingsApi } from '../../../../entity/monitoring/settings';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
}
|
||||
|
||||
const intervalLabels = {
|
||||
15: '15 seconds',
|
||||
30: '30 seconds',
|
||||
60: '1 minute',
|
||||
120: '2 minutes',
|
||||
300: '5 minutes',
|
||||
600: '10 minutes',
|
||||
900: '15 minutes',
|
||||
1800: '30 minutes',
|
||||
3600: '1 hour',
|
||||
};
|
||||
|
||||
export const ShowMonitoringSettingsComponent = ({ database }: Props) => {
|
||||
const [monitoringSettings, setMonitoringSettings] = useState<PostgresMonitoringSettings>();
|
||||
|
||||
useEffect(() => {
|
||||
if (database.id) {
|
||||
monitoringSettingsApi.getSettingsByDbID(database.id).then((res) => {
|
||||
setMonitoringSettings(res);
|
||||
});
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
if (!monitoringSettings) return <div />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[200px]">Database resources monitoring</div>
|
||||
<Switch checked={monitoringSettings.isDbResourcesMonitoringEnabled} disabled size="small" />
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[200px]">Monitoring interval</div>
|
||||
<div>
|
||||
{intervalLabels[
|
||||
monitoringSettings.monitoringIntervalSeconds as keyof typeof intervalLabels
|
||||
] || `${monitoringSettings.monitoringIntervalSeconds} seconds`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
470
package-lock.json
generated
Normal file
470
package-lock.json
generated
Normal file
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"name": "postgresus",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@types/recharts": "^1.8.29",
|
||||
"recharts": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
|
||||
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
|
||||
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
|
||||
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts": {
|
||||
"version": "1.8.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
|
||||
"integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-shape": "^1",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.10",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
|
||||
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz",
|
||||
"integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor/node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/recharts": "^1.8.29",
|
||||
"recharts": "^3.2.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user