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:
@@ -116,11 +116,19 @@ type Backend interface {
|
||||
GetModulePrefixSnapshot(tenantID, moduleID string) (*ModulePrefixSnapshot, bool, error)
|
||||
SetModulePrefixSnapshot(tenantID, moduleID, inputHash string, prefixes []PrefixRow) error
|
||||
DeleteModulePrefixSnapshot(tenantID, moduleID string) error
|
||||
// SetModuleInputHash stores the ingest fingerprint on the module row (O(1) snapshot check).
|
||||
SetModuleInputHash(tenantID, moduleID, hash string) error
|
||||
// LockModuleSnapshot serializes read-modify-write of one module snapshot; unlock must be called.
|
||||
LockModuleSnapshot(tenantID, moduleID string) (unlock func())
|
||||
|
||||
// ASNPrefixCache stores RIPEstat announced-prefixes per ASN (global TTL cache).
|
||||
GetASNPrefixCache(asn int64) (*ASNPrefixCacheEntry, bool, error)
|
||||
SetASNPrefixCache(asn int64, holder string, prefixes []string) error
|
||||
|
||||
// DomainResolveCache stores DoH results per FQDN (global TTL cache).
|
||||
GetDomainResolveCache(fqdn string) (*DomainResolveCacheEntry, bool, error)
|
||||
SetDomainResolveCache(fqdn string, addrs []string) error
|
||||
|
||||
// Ping verifies backend connectivity (no-op for in-memory).
|
||||
Ping(ctx context.Context) error
|
||||
|
||||
@@ -178,6 +186,13 @@ type ASNPrefixCacheEntry struct {
|
||||
FetchedAt time.Time
|
||||
}
|
||||
|
||||
// DomainResolveCacheEntry is a cached DoH A/AAAA result for one FQDN.
|
||||
type DomainResolveCacheEntry struct {
|
||||
FQDN string
|
||||
Addrs []string
|
||||
ResolvedAt time.Time
|
||||
}
|
||||
|
||||
// ModulePrefixSnapshot is the cached materialization for one module between refreshes.
|
||||
type ModulePrefixSnapshot struct {
|
||||
InputHash string
|
||||
|
||||
@@ -44,7 +44,9 @@ type Memory struct {
|
||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||
revPrefixes map[string][]PrefixRow
|
||||
moduleSnapshots map[string]*moduleSnapshotRec
|
||||
snapshotLocks sync.Map // key -> *sync.Mutex (per-module snapshot RMW)
|
||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||
domainResolveCache map[string]*DomainResolveCacheEntry
|
||||
apiKeys map[string]*apiKeyRec
|
||||
firewallClients map[string]*firewallClientRec
|
||||
firewallRules map[string]*FirewallRule
|
||||
@@ -99,6 +101,7 @@ type Module struct {
|
||||
LastRefreshedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
CreatedByUserID string // portal JWT sub; empty = system / API key
|
||||
InputHash string // ingest fingerprint; empty = not yet computed
|
||||
}
|
||||
|
||||
type Revision struct {
|
||||
@@ -156,6 +159,7 @@ func NewMemory() *Memory {
|
||||
revPrefixes: make(map[string][]PrefixRow),
|
||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||
domainResolveCache: make(map[string]*DomainResolveCacheEntry),
|
||||
apiKeys: make(map[string]*apiKeyRec),
|
||||
firewallClients: make(map[string]*firewallClientRec),
|
||||
firewallRules: make(map[string]*FirewallRule),
|
||||
@@ -696,6 +700,20 @@ func cloneStringPtr(s *string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
func (m *Memory) clearModuleInputHashLocked(moduleID string) {
|
||||
if mod, ok := m.modules[moduleID]; ok && mod != nil {
|
||||
mod.InputHash = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Memory) clearTenantModuleHashesLocked(tenantID string) {
|
||||
for _, mod := range m.modules {
|
||||
if mod != nil && mod.TenantID == tenantID && mod.DeletedAt == nil {
|
||||
mod.InputHash = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cloneModule(m *Module) *Module {
|
||||
if m == nil {
|
||||
return nil
|
||||
|
||||
@@ -81,6 +81,7 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
mod.LastRefreshedAt = &t
|
||||
}
|
||||
mod.InputHash = ""
|
||||
return cloneModule(mod), nil
|
||||
}
|
||||
|
||||
@@ -152,6 +153,7 @@ func (m *Memory) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDN
|
||||
LastRefreshedAt: in.LastRefreshedAt,
|
||||
}
|
||||
m.cdnSources[id] = s
|
||||
mod.InputHash = ""
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -161,7 +163,8 @@ func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDN
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, ok := m.cdnSources[sourceID]
|
||||
@@ -195,6 +198,9 @@ func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDN
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
s.LastRefreshedAt = &t
|
||||
}
|
||||
if CDNPatchAffectsInputHash(patch) {
|
||||
mod.InputHash = ""
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -209,6 +215,7 @@ func (m *Memory) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.cdnSources, sourceID)
|
||||
m.clearModuleInputHashLocked(moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -247,6 +254,7 @@ func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry
|
||||
id := uuid.NewString()
|
||||
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, CommunityID: in.CommunityID}
|
||||
m.asEntries[id] = e
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -256,7 +264,8 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, ok := m.asEntries[entryID]
|
||||
@@ -283,6 +292,7 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
|
||||
e.PrefixCount = nil
|
||||
e.ASNResolvedAt = nil
|
||||
}
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -324,6 +334,7 @@ func (m *Memory) DeleteASEntry(tenantID, moduleID, entryID string) error {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.asEntries, entryID)
|
||||
m.clearModuleInputHashLocked(moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -362,6 +373,7 @@ func (m *Memory) CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (
|
||||
id := uuid.NewString()
|
||||
e := &DomainEntry{ID: id, ModuleID: moduleID, FQDN: strings.TrimSpace(in.FQDN), CommunityID: in.CommunityID}
|
||||
m.domainEnt[id] = e
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -371,7 +383,8 @@ func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *Do
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, ok := m.domainEnt[entryID]
|
||||
@@ -389,6 +402,7 @@ func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *Do
|
||||
e.CommunityID = &v
|
||||
}
|
||||
}
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -403,6 +417,7 @@ func (m *Memory) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.domainEnt, entryID)
|
||||
m.clearModuleInputHashLocked(moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -441,6 +456,7 @@ func (m *Memory) CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry)
|
||||
id := uuid.NewString()
|
||||
e := &IPRangeEntry{ID: id, ModuleID: moduleID, Prefix: strings.TrimSpace(in.Prefix), CommunityID: in.CommunityID}
|
||||
m.ipRanges[id] = e
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -450,7 +466,8 @@ func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *I
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, ok := m.ipRanges[entryID]
|
||||
@@ -468,6 +485,7 @@ func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *I
|
||||
e.CommunityID = &v
|
||||
}
|
||||
}
|
||||
mod.InputHash = ""
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -482,6 +500,7 @@ func (m *Memory) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.ipRanges, entryID)
|
||||
m.clearModuleInputHashLocked(moduleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -551,6 +570,7 @@ func (m *Memory) UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (
|
||||
if patch.SecretRef != nil {
|
||||
p.SecretRef = patch.SecretRef
|
||||
}
|
||||
m.clearTenantModuleHashesLocked(tenantID)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (m *Memory) GetDomainResolveCache(fqdn string) (*DomainResolveCacheEntry, bool, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.domainResolveCache == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
e, ok := m.domainResolveCache[strings.ToLower(strings.TrimSpace(fqdn))]
|
||||
if !ok || e == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &DomainResolveCacheEntry{
|
||||
FQDN: e.FQDN,
|
||||
Addrs: append([]string(nil), e.Addrs...),
|
||||
ResolvedAt: e.ResolvedAt,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (m *Memory) SetDomainResolveCache(fqdn string, addrs []string) error {
|
||||
key := strings.ToLower(strings.TrimSpace(fqdn))
|
||||
if key == "" {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.domainResolveCache == nil {
|
||||
m.domainResolveCache = make(map[string]*DomainResolveCacheEntry)
|
||||
}
|
||||
m.domainResolveCache[key] = &DomainResolveCacheEntry{
|
||||
FQDN: key,
|
||||
Addrs: append([]string(nil), addrs...),
|
||||
ResolvedAt: time.Now().UTC(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,28 @@ func (m *Memory) DeleteModulePrefixSnapshot(tenantID, moduleID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) SetModuleInputHash(tenantID, moduleID, hash string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
mod, ok := m.modules[moduleID]
|
||||
if !ok || mod.DeletedAt != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if mod.TenantID != tenantID {
|
||||
return ErrTenantScope
|
||||
}
|
||||
mod.InputHash = strings.TrimSpace(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) LockModuleSnapshot(tenantID, moduleID string) func() {
|
||||
key := moduleSnapshotKey(tenantID, moduleID)
|
||||
v, _ := m.snapshotLocks.LoadOrStore(key, &sync.Mutex{})
|
||||
mu := v.(*sync.Mutex)
|
||||
mu.Lock()
|
||||
return func() { mu.Unlock() }
|
||||
}
|
||||
|
||||
type moduleSnapshotRec struct {
|
||||
InputHash string
|
||||
CollectedAt time.Time
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ComputeModuleInputHash fingerprints module config and child entries so snapshots
|
||||
// invalidate on CRUD without re-reading every child at render time.
|
||||
func ComputeModuleInputHash(st Backend, tenantID string, mod *Module) (string, error) {
|
||||
if st == nil || mod == nil {
|
||||
return "", fmt.Errorf("store: module hash: missing store or module")
|
||||
}
|
||||
h := sha256.New()
|
||||
_, _ = fmt.Fprintf(h, "type=%s\n", strings.TrimSpace(mod.Type))
|
||||
_, _ = fmt.Fprintf(h, "enabled=%t\n", mod.Enabled)
|
||||
if mod.DefaultCommunityID != nil {
|
||||
_, _ = fmt.Fprintf(h, "default_community=%s\n", strings.TrimSpace(*mod.DefaultCommunityID))
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "doh_policy=%s\n", NormalizeDohResolverPolicy(mod.DohResolverPolicy))
|
||||
for _, pid := range mod.EffectiveDohProfileIDs() {
|
||||
_, _ = fmt.Fprintf(h, "doh_profile=%s\n", pid)
|
||||
if prof, err := st.GetDohProfile(tenantID, pid); err == nil && prof != nil {
|
||||
_, _ = fmt.Fprintf(h, "doh_url=%s\n", strings.TrimSpace(prof.URL))
|
||||
if prof.TimeoutMs != nil {
|
||||
_, _ = fmt.Fprintf(h, "doh_timeout=%d\n", *prof.TimeoutMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch mod.Type {
|
||||
case "IP_RANGES":
|
||||
list, err := st.ListIPRangeEntries(tenantID, mod.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].Prefix < list[j].Prefix })
|
||||
for _, e := range list {
|
||||
comm := ""
|
||||
if e.CommunityID != nil {
|
||||
comm = *e.CommunityID
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "ip=%s|c=%s\n", e.Prefix, comm)
|
||||
}
|
||||
case "AS_PREFIXES":
|
||||
list, err := st.ListASEntries(tenantID, mod.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].ASN < list[j].ASN })
|
||||
for _, e := range list {
|
||||
comm := ""
|
||||
if e.CommunityID != nil {
|
||||
comm = *e.CommunityID
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "as=%d|c=%s\n", e.ASN, comm)
|
||||
}
|
||||
case "CDN_CIDRS":
|
||||
list, err := st.ListCDNSources(tenantID, mod.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
|
||||
for _, s := range list {
|
||||
comm := ""
|
||||
if s.CommunityID != nil {
|
||||
comm = *s.CommunityID
|
||||
}
|
||||
interval := 0
|
||||
if s.RefreshIntervalSec != nil {
|
||||
interval = *s.RefreshIntervalSec
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "cdn=%s|url=%s|kind=%s|path=%s|c=%s|etag=%s|interval=%d\n",
|
||||
s.ID, strings.TrimSpace(s.URL), s.SourceKind, strings.TrimSpace(s.PrefixPath), comm,
|
||||
strings.TrimSpace(s.Etag), interval)
|
||||
}
|
||||
case "DOMAINS":
|
||||
list, err := st.ListDomainEntries(tenantID, mod.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].FQDN < list[j].FQDN })
|
||||
for _, e := range list {
|
||||
comm := ""
|
||||
if e.CommunityID != nil {
|
||||
comm = *e.CommunityID
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "dom=%s|c=%s\n", strings.TrimSpace(e.FQDN), comm)
|
||||
}
|
||||
default:
|
||||
_, _ = fmt.Fprintf(h, "unknown_type=%s\n", mod.Type)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// TouchModuleInputHash recomputes and stores module.input_hash (ARCH-01: hash lives in store).
|
||||
func TouchModuleInputHash(st Backend, tenantID, moduleID string) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
mod, err := st.GetModule(tenantID, moduleID)
|
||||
if err != nil || mod == nil {
|
||||
return
|
||||
}
|
||||
h, err := ComputeModuleInputHash(st, tenantID, mod)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = st.SetModuleInputHash(tenantID, moduleID, h)
|
||||
}
|
||||
|
||||
// CDNPatchAffectsInputHash reports whether a CDN source patch changes ingest fingerprint
|
||||
// fields (URL/kind/path/community/interval). ETag and last_refreshed_at do not.
|
||||
func CDNPatchAffectsInputHash(patch *CDNSourcePatch) bool {
|
||||
if patch == nil {
|
||||
return false
|
||||
}
|
||||
return patch.SourceKind != nil || patch.URL != nil || patch.PrefixPath != nil ||
|
||||
patch.CommunityID != nil || patch.RefreshIntervalSec != nil
|
||||
}
|
||||
Reference in New Issue
Block a user