refactor(web): remove deprecated dashboard components and enhance KPI grid
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s
- Deleted unused components: `DashboardActivityTimeline`, `DashboardFramePanel`, `DashboardModulesGrid`, `DashboardRecentJobsGrid`, and `DashboardRecentRevisionsGrid` to streamline the dashboard. - Updated `DashboardKpiGrid` to improve KPI display logic, including progress indicators and enhanced badge functionality. - Refactored `DashboardNetworkHealth` to provide better status representation based on loading states and network conditions. - Introduced new properties for KPI cards to support progress tracking and improved visual feedback. This cleanup aims to enhance performance and maintainability of the dashboard while providing a better user experience.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
@@ -11,13 +12,26 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// asnCacheRowExistsOnce caches the schema check for the process lifetime (see moduleSnapshotRowTableExists).
|
||||
var (
|
||||
asnCacheRowExistsOnce sync.Once
|
||||
asnCacheRowExistsCached bool
|
||||
)
|
||||
|
||||
func asnCacheRowTableExists(ctx context.Context, q queryRower) bool {
|
||||
var n int
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'asn_prefix_cache_row'
|
||||
LIMIT 1`).Scan(&n)
|
||||
return err == nil
|
||||
asnCacheRowExistsOnce.Do(func() {
|
||||
var n int
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'asn_prefix_cache_row'
|
||||
LIMIT 1`).Scan(&n)
|
||||
if err != nil {
|
||||
asnCacheRowExistsOnce = sync.Once{}
|
||||
return
|
||||
}
|
||||
asnCacheRowExistsCached = true
|
||||
})
|
||||
return asnCacheRowExistsCached
|
||||
}
|
||||
|
||||
func (p *Postgres) GetASNPrefixCache(asn int64) (*store.ASNPrefixCacheEntry, bool, error) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (p *Postgres) GetDomainResolveCache(fqdn string) (*store.DomainResolveCacheEntry, bool, error) {
|
||||
key := strings.ToLower(strings.TrimSpace(fqdn))
|
||||
if key == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
var raw []byte
|
||||
var resolvedAt time.Time
|
||||
err := p.pool.QueryRow(ctx, `
|
||||
SELECT addrs_json, resolved_at FROM domain_resolve_cache WHERE fqdn = $1`, key).
|
||||
Scan(&raw, &resolvedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
var addrs []string
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &addrs)
|
||||
}
|
||||
return &store.DomainResolveCacheEntry{
|
||||
FQDN: key,
|
||||
Addrs: addrs,
|
||||
ResolvedAt: resolvedAt.UTC(),
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) SetDomainResolveCache(fqdn string, addrs []string) error {
|
||||
key := strings.ToLower(strings.TrimSpace(fqdn))
|
||||
if key == "" {
|
||||
return store.ErrInvalidInput
|
||||
}
|
||||
if addrs == nil {
|
||||
addrs = []string{}
|
||||
}
|
||||
raw, err := json.Marshal(addrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
INSERT INTO domain_resolve_cache (fqdn, addrs_json, resolved_at)
|
||||
VALUES ($1, $2::jsonb, now())
|
||||
ON CONFLICT (fqdn) DO UPDATE SET
|
||||
addrs_json = EXCLUDED.addrs_json,
|
||||
resolved_at = EXCLUDED.resolved_at`,
|
||||
key, string(raw))
|
||||
return err
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
@@ -12,13 +13,29 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// moduleSnapshotRowExistsOnce caches the schema check for the process lifetime.
|
||||
// The table is created by migrations and never disappears at runtime; DB errors are
|
||||
// not cached so a transient outage falls back to the JSON path only once.
|
||||
var moduleSnapshotRowExistsOnce sync.Once
|
||||
|
||||
var moduleSnapshotRowExistsCached bool
|
||||
|
||||
func moduleSnapshotRowTableExists(ctx context.Context, q queryRower) bool {
|
||||
var n int
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'module_prefix_snapshot_row'
|
||||
LIMIT 1`).Scan(&n)
|
||||
return err == nil
|
||||
moduleSnapshotRowExistsOnce.Do(func() {
|
||||
var n int
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'module_prefix_snapshot_row'
|
||||
LIMIT 1`).Scan(&n)
|
||||
// A query error means the backend is unreachable or information_schema is hidden;
|
||||
// treat as "absent" so callers fall back to prefixes_json, but retry next call.
|
||||
if err != nil {
|
||||
moduleSnapshotRowExistsOnce = sync.Once{}
|
||||
return
|
||||
}
|
||||
moduleSnapshotRowExistsCached = true
|
||||
})
|
||||
return moduleSnapshotRowExistsCached
|
||||
}
|
||||
|
||||
func (p *Postgres) GetModulePrefixSnapshot(tenantID, moduleID string) (*store.ModulePrefixSnapshot, bool, error) {
|
||||
@@ -97,19 +114,25 @@ func (p *Postgres) SetModulePrefixSnapshot(tenantID, moduleID, inputHash string,
|
||||
WHERE tenant_id = $1::uuid AND module_id = $2::uuid`, tenantID, moduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, pr := range prefixes {
|
||||
var comm any
|
||||
if pr.CommunityID != nil && strings.TrimSpace(*pr.CommunityID) != "" {
|
||||
comm = strings.TrimSpace(*pr.CommunityID)
|
||||
}
|
||||
src := pr.Source
|
||||
if strings.TrimSpace(src) == "" {
|
||||
src = "render"
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO module_prefix_snapshot_row (tenant_id, module_id, ord, prefix, community_id, source)
|
||||
VALUES ($1::uuid, $2::uuid, $3, $4, $5::uuid, $6)`,
|
||||
tenantID, moduleID, i, strings.TrimSpace(pr.Prefix), comm, src); err != nil {
|
||||
if len(prefixes) > 0 {
|
||||
_, err = tx.CopyFrom(
|
||||
ctx,
|
||||
pgx.Identifier{"module_prefix_snapshot_row"},
|
||||
[]string{"tenant_id", "module_id", "ord", "prefix", "community_id", "source"},
|
||||
pgx.CopyFromSlice(len(prefixes), func(i int) ([]any, error) {
|
||||
pr := prefixes[i]
|
||||
var comm any
|
||||
if pr.CommunityID != nil && strings.TrimSpace(*pr.CommunityID) != "" {
|
||||
comm = strings.TrimSpace(*pr.CommunityID)
|
||||
}
|
||||
src := pr.Source
|
||||
if strings.TrimSpace(src) == "" {
|
||||
src = "render"
|
||||
}
|
||||
return []any{tenantID, moduleID, i, strings.TrimSpace(pr.Prefix), comm, src}, nil
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -135,3 +158,35 @@ func (p *Postgres) DeleteModulePrefixSnapshot(tenantID, moduleID string) error {
|
||||
tenantID, moduleID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Postgres) SetModuleInputHash(tenantID, moduleID, hash string) error {
|
||||
ctx := context.Background()
|
||||
tag, err := p.pool.Exec(ctx, `
|
||||
UPDATE module SET input_hash = $3
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`,
|
||||
moduleID, tenantID, hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) LockModuleSnapshot(tenantID, moduleID string) func() {
|
||||
ctx := context.Background()
|
||||
conn, err := p.pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
key := strings.TrimSpace(tenantID) + "\x00" + strings.TrimSpace(moduleID)
|
||||
if _, err := conn.Exec(ctx, `SELECT pg_advisory_lock(hashtext($1))`, key); err != nil {
|
||||
conn.Release()
|
||||
return func() {}
|
||||
}
|
||||
return func() {
|
||||
_, _ = conn.Exec(ctx, `SELECT pg_advisory_unlock(hashtext($1))`, key)
|
||||
conn.Release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||
ctx := context.Background()
|
||||
rows, err := p.pool.Query(ctx, `
|
||||
SELECT id, type, name, enabled, priority, doh_profile_id::text, doh_resolver_policy,
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id,
|
||||
COALESCE(input_hash, '')
|
||||
FROM module WHERE tenant_id = $1 AND deleted_at IS NULL ORDER BY priority, name`, tenantID)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -134,7 +135,7 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||
var refresh *int32
|
||||
var last *time.Time
|
||||
var createdBy *string
|
||||
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy); err != nil {
|
||||
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy, &m.InputHash); err != nil {
|
||||
continue
|
||||
}
|
||||
m.DohResolverPolicy = store.NormalizeDohResolverPolicy(m.DohResolverPolicy)
|
||||
@@ -179,7 +180,8 @@ func (p *Postgres) ListModulesPage(tenantID, cursor string, limit int) ([]*store
|
||||
ctx := context.Background()
|
||||
rows, err := p.pool.Query(ctx, `
|
||||
SELECT id, type, name, enabled, priority, doh_profile_id::text, doh_resolver_policy,
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id,
|
||||
COALESCE(input_hash, '')
|
||||
FROM module WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
ORDER BY priority, name
|
||||
LIMIT $2 OFFSET $3`, tenantID, limit+1, off)
|
||||
@@ -196,7 +198,7 @@ func (p *Postgres) ListModulesPage(tenantID, cursor string, limit int) ([]*store
|
||||
var refresh *int32
|
||||
var last *time.Time
|
||||
var createdBy *string
|
||||
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy); err != nil {
|
||||
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy, &m.InputHash); err != nil {
|
||||
continue
|
||||
}
|
||||
m.DohResolverPolicy = store.NormalizeDohResolverPolicy(m.DohResolverPolicy)
|
||||
@@ -249,9 +251,10 @@ func (p *Postgres) GetModule(tenantID, moduleID string) (*store.Module, error) {
|
||||
var createdBy *string
|
||||
err := p.pool.QueryRow(ctx, `
|
||||
SELECT id, type, name, enabled, priority, doh_profile_id::text, doh_resolver_policy,
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id
|
||||
refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at, created_by_user_id,
|
||||
COALESCE(input_hash, '')
|
||||
FROM module WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, moduleID, tenantID).Scan(
|
||||
&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy)
|
||||
&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &m.DohResolverPolicy, &refresh, &cron, &dc, &last, &createdBy, &m.InputHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, store.ErrNotFound
|
||||
@@ -325,9 +328,19 @@ func (p *Postgres) CreateModule(tenantID string, in *store.Module) (*store.Modul
|
||||
if err := p.setModuleDohProfiles(ctx, id, in.DohProfileIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store.TouchModuleInputHash(p, tenantID, id)
|
||||
return p.GetModule(tenantID, id)
|
||||
}
|
||||
|
||||
func (p *Postgres) invalidateModuleInputHash(tenantID, moduleID string) {
|
||||
_ = p.SetModuleInputHash(tenantID, moduleID, "")
|
||||
}
|
||||
|
||||
func (p *Postgres) invalidateTenantModuleHashes(tenantID string) {
|
||||
ctx := context.Background()
|
||||
_, _ = p.pool.Exec(ctx, `UPDATE module SET input_hash = '' WHERE tenant_id = $1 AND deleted_at IS NULL`, tenantID)
|
||||
}
|
||||
|
||||
func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePatch) (*store.Module, error) {
|
||||
if patch == nil {
|
||||
return nil, store.ErrInvalidInput
|
||||
@@ -399,6 +412,7 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.GetModule(tenantID, moduleID)
|
||||
}
|
||||
|
||||
@@ -1191,6 +1205,7 @@ func (p *Postgres) UpdateDohProfile(tenantID, id string, patch *store.DohProfile
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateTenantModuleHashes(tenantID)
|
||||
return p.GetDohProfile(tenantID, id)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSourc
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getCDNSource(ctx, moduleID, id)
|
||||
}
|
||||
|
||||
@@ -154,6 +155,9 @@ func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store.CDNPatchAffectsInputHash(patch) {
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
}
|
||||
return p.getCDNSource(ctx, moduleID, sourceID)
|
||||
}
|
||||
|
||||
@@ -169,6 +173,7 @@ func (p *Postgres) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
|
||||
if tag.RowsAffected() == 0 {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -229,6 +234,7 @@ func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getASEntry(ctx, moduleID, id)
|
||||
}
|
||||
|
||||
@@ -297,6 +303,7 @@ func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *stor
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getASEntry(ctx, moduleID, entryID)
|
||||
}
|
||||
|
||||
@@ -369,6 +376,7 @@ func (p *Postgres) DeleteASEntry(tenantID, moduleID, entryID string) error {
|
||||
if tag.RowsAffected() == 0 {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -417,6 +425,7 @@ func (p *Postgres) CreateDomainEntry(tenantID, moduleID string, in *store.Domain
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getDomainEntry(ctx, moduleID, id)
|
||||
}
|
||||
|
||||
@@ -462,6 +471,7 @@ func (p *Postgres) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getDomainEntry(ctx, moduleID, entryID)
|
||||
}
|
||||
|
||||
@@ -477,6 +487,7 @@ func (p *Postgres) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
|
||||
if tag.RowsAffected() == 0 {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -525,6 +536,7 @@ func (p *Postgres) CreateIPRangeEntry(tenantID, moduleID string, in *store.IPRan
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getIPRangeEntry(ctx, moduleID, id)
|
||||
}
|
||||
|
||||
@@ -570,6 +582,7 @@ func (p *Postgres) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return p.getIPRangeEntry(ctx, moduleID, entryID)
|
||||
}
|
||||
|
||||
@@ -585,6 +598,7 @@ func (p *Postgres) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error
|
||||
if tag.RowsAffected() == 0 {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
p.invalidateModuleInputHash(tenantID, moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user