Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 388 additions & 68 deletions app/(dashboard)/site-replication/page.tsx

Large diffs are not rendered by default.

15 changes: 1 addition & 14 deletions app/(dashboard)/status/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isScannerCycleActive,
resolveUsageFreshness,
} from "@/lib/performance-data"
import { formatDuration } from "@/lib/functions"
import { cn } from "@/lib/utils"
import {
RiArchiveDrawerFill,
Expand All @@ -32,20 +33,6 @@ import { PerformanceStatusSources } from "../_components/performance-status-sour
import { PerformanceStatusSummary } from "../_components/performance-status-summary"
import { PerformanceUsageCard } from "../_components/performance-usage-card"

function formatDuration(seconds: number | undefined, t: (key: string) => string) {
if (seconds === undefined || !Number.isFinite(seconds) || seconds < 0) return t("Unknown")
const totalMinutes = Math.floor(seconds / 60)
const days = Math.floor(totalMinutes / 1440)
const hours = Math.floor((totalMinutes % 1440) / 60)
const minutes = totalMinutes % 60
const parts = [
days ? `${days} ${t("Days")}` : "",
hours ? `${hours} ${t("Hours")}` : "",
!days && minutes ? `${minutes} ${t("Minutes")}` : "",
].filter(Boolean)
return parts.slice(0, 2).join(" ") || `0 ${t("Minutes")}`
}

export default function PerformancePage() {
const { t, i18n } = useTranslation()
const { canAccessPath } = usePermissions()
Expand Down
132 changes: 132 additions & 0 deletions hooks/use-site-replication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@ import { useApi } from "@/contexts/api-context"
import type {
SiteReplicationAddResponse,
SiteReplicationAddSiteInput,
SiteReplicationBucketStats,
SiteReplicationEditResponse,
SiteReplicationEntityStats,
SiteReplicationInfo,
SiteReplicationMetric,
SiteReplicationMetricsSummary,
SiteReplicationPeerError,
SiteReplicationPeerInfo,
SiteReplicationPendingOperation,
SiteReplicationPolicyStats,
SiteReplicationQueueStat,
SiteReplicationRemoveInput,
SiteReplicationRemoveResponse,
SiteReplicationResyncOperation,
SiteReplicationResyncResponse,
SiteReplicationRetryStats,
SiteReplicationSiteSummary,
SiteReplicationStateInfo,
SiteReplicationStatus,
SiteReplicationSyncState,
SiteReplicationTimedErrors,
SiteReplicationVolume,
SiteReplicationWorkerStat,
} from "@/types/site-replication"

Expand All @@ -39,6 +48,14 @@ function asBoolean(value: unknown): boolean {
return typeof value === "boolean" ? value : false
}

function asStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []
}

function mapRecord<T>(value: unknown, normalize: (item: unknown) => T): Record<string, T> {
return Object.fromEntries(Object.entries(asRecord(value)).map(([key, item]) => [key, normalize(item)]))
}

function normalizeSyncState(value: unknown): SiteReplicationSyncState {
const normalized = asString(value).toLowerCase()
if (normalized === "enable" || normalized === "disable") {
Expand All @@ -57,6 +74,102 @@ function normalizeWorkerStat(value: unknown): SiteReplicationWorkerStat {
}
}

function normalizeVolume(value: unknown): SiteReplicationVolume {
const record = asRecord(value)

return { count: asNumber(record.count), bytes: asNumber(record.bytes) }
}

function normalizeTimedErrors(value: unknown): SiteReplicationTimedErrors {
const record = asRecord(value)

return {
lastMinute: normalizeVolume(record.lastMinute),
lastHour: normalizeVolume(record.lastHour),
totals: normalizeVolume(record.totals),
}
}

function normalizeQueueStat(value: unknown): SiteReplicationQueueStat {
const record = asRecord(value)

return {
curr: normalizeVolume(record.curr),
avg: normalizeVolume(record.avg),
max: normalizeVolume(record.max),
}
}

// Both fields are omitted by the server while nothing is outstanding, so an
// absent value means "healthy", not "unknown".
function normalizePendingOperation(value: unknown): SiteReplicationPendingOperation | undefined {
const record = asRecord(value)
const operation = asString(record.operation)
if (!operation) {
return undefined
}

return {
operation,
id: asString(record.id),
pendingPeers: asStringArray(record.pendingPeers),
ackedPeers: asStringArray(record.ackedPeers),
updatedAt: asString(record.updatedAt) || undefined,
}
}

function normalizeRetryStats(value: unknown): SiteReplicationRetryStats | undefined {
if (!value || typeof value !== "object") {
return undefined
}
const record = asRecord(value)

return {
pending: asNumber(record.pending),
failed: asNumber(record.failed),
lastError: asString(record.lastError),
}
}

function normalizePeerError(value: unknown): SiteReplicationPeerError {
const record = asRecord(value)

return { name: asString(record.name), endpoint: asString(record.endpoint), error: asString(record.error) }
}

function normalizeBucketStats(value: unknown): SiteReplicationBucketStats {
const record = asRecord(value)

return {
hasBucket: asBoolean(record.HasBucket),
bucketMarkedDeleted: asBoolean(record.BucketMarkedDeleted),
tagMismatch: asBoolean(record.TagMismatch),
versioningConfigMismatch: asBoolean(record.VersioningConfigMismatch),
objectLockConfigMismatch: asBoolean(record.OLockConfigMismatch),
policyMismatch: asBoolean(record.PolicyMismatch),
sseConfigMismatch: asBoolean(record.SSEConfigMismatch),
replicationCfgMismatch: asBoolean(record.ReplicationCfgMismatch),
quotaCfgMismatch: asBoolean(record.QuotaCfgMismatch),
corsCfgMismatch: asBoolean(record.CorsCfgMismatch),
}
}

function normalizePolicyStats(value: unknown): SiteReplicationPolicyStats {
const record = asRecord(value)

return { hasPolicy: asBoolean(record.HasPolicy), policyMismatch: asBoolean(record.PolicyMismatch) }
}

function normalizeEntityStats<T>(value: unknown, normalize: (item: unknown) => T): SiteReplicationEntityStats<T> {
return mapRecord(value, (entity) => mapRecord(entity, normalize))
}

// User, group, and ILM expiry rule entries only exist on the sites that hold
// the entity, so the inner keys are the holding deployment IDs.
function normalizeEntitySites(value: unknown): Record<string, string[]> {
return mapRecord(value, (entity) => Object.keys(asRecord(entity)))
}

function normalizePeerInfo(value: unknown): SiteReplicationPeerInfo {
const record = asRecord(value)
const bandwidth = asRecord(record.defaultbandwidth)
Expand Down Expand Up @@ -174,6 +287,7 @@ function normalizeMetric(value: unknown): SiteReplicationMetric {
online: asBoolean(record.isOnline),
replicatedSize: asNumber(record.replicatedSize),
replicatedCount: asNumber(record.replicatedCount),
failed: normalizeTimedErrors(record.failed),
}
}

Expand All @@ -190,6 +304,7 @@ function normalizeMetricsSummary(value: unknown): SiteReplicationMetricsSummary
activeWorkers: normalizeWorkerStat(record.activeWorkers),
replicaSize: asNumber(record.replicaSize),
replicaCount: asNumber(record.replicaCount),
queued: normalizeQueueStat(record.queued),
metrics: normalizeMetricMap(record.replMetrics),
}
}
Expand All @@ -203,6 +318,8 @@ function normalizeInfo(value: unknown): SiteReplicationInfo {
name: asString(record.name),
sites,
serviceAccountAccessKey: asString(record.serviceAccountAccessKey),
pendingOperation: normalizePendingOperation(record.pendingOperation),
retryStats: normalizeRetryStats(record.retryStats),
apiVersion: asString(record.apiVersion) || undefined,
}
}
Expand All @@ -220,7 +337,15 @@ function normalizeStatus(value: unknown): SiteReplicationStatus {
sites: normalizePeerMap(record.Sites),
statsSummary: normalizeSiteSummaryMap(record.StatsSummary),
peerStates: normalizeStateInfoMap(record.PeerStates),
peerErrors: mapRecord(record.PeerErrors, normalizePeerError),
pendingOperation: normalizePendingOperation(record.PendingOperation),
retryStats: normalizeRetryStats(record.RetryStats),
metrics: normalizeMetricsSummary(record.Metrics),
bucketStats: normalizeEntityStats(record.BucketStats, normalizeBucketStats),
policyStats: normalizeEntityStats(record.PolicyStats, normalizePolicyStats),
userSites: normalizeEntitySites(record.UserStats),
groupSites: normalizeEntitySites(record.GroupStats),
ilmExpiryRuleSites: normalizeEntitySites(record.ILMExpiryStats),
apiVersion: asString(record.apiVersion) || undefined,
}
}
Expand Down Expand Up @@ -275,7 +400,14 @@ function normalizeResyncResponse(value: unknown): SiteReplicationResyncResponse
opType: asString(record.op),
resyncId: asString(record.id),
status: asString(record.status),
state: asString(record.state),
buckets,
totalBuckets: asNumber(record.totalBuckets),
completedBuckets: asNumber(record.completedBuckets),
failedBuckets: asNumber(record.failedBuckets),
replicatedObjects: asNumber(record.replicatedObjects),
failedObjects: asNumber(record.failedObjects),
updatedAt: asString(record.updatedAt) || undefined,
errorDetail: asString(record.errorDetail),
}
}
Expand Down
42 changes: 41 additions & 1 deletion i18n/locales/ar-MA.json
Original file line number Diff line number Diff line change
Expand Up @@ -2241,5 +2241,45 @@
"Operations": "العمليات",
"Resources": "الموارد",
"Bucket size distribution": "توزيع أحجام الحاويات",
"Other buckets": "حاويات أخرى"
"Other buckets": "حاويات أخرى",
"Active workers": "العمال النشطون",
"Configuration deliveries": "تسليم الإعدادات",
"Connection": "الاتصال",
"Consistency": "الاتساق",
"Consistency cannot be checked until every peer site is reachable.": "لا يمكن التحقق من الاتساق حتى يصبح كل موقع نظير قابلاً للوصول.",
"Differs: {settings}": "مختلف: {settings}",
"Downtime": "مدة الانقطاع",
"Every peer site is reachable and nothing is waiting to be replicated or repaired.": "كل المواقع النظيرة قابلة للوصول ولا يوجد ما ينتظر النسخ أو الإصلاح.",
"Every site holds the same buckets, IAM entities, and expiry rules.": "تحتوي كل المواقع على نفس الحاويات وكيانات IAM وقواعد الانتهاء.",
"Failed in the last hour": "فشل خلال الساعة الأخيرة",
"Failed since start": "فشل منذ البدء",
"Failures and Backlog": "الإخفاقات والتراكم",
"Missing on {sites}": "مفقود في {sites}",
"No resync has been started for this site.": "لم تبدأ أي إعادة مزامنة لهذا الموقع.",
"Object counters come from the node that answered this request and reset when it restarts.": "تأتي عدادات الكائنات من العقدة التي أجابت على هذا الطلب وتُعاد إلى الصفر عند إعادة تشغيلها.",
"Peer reachability is not reported by this server, so replication health cannot be confirmed.": "لا يبلّغ هذا الخادم عن إمكانية الوصول إلى النظراء، لذا لا يمكن تأكيد سلامة النسخ.",
"Queued now": "في الانتظار الآن",
"Queued peak": "ذروة الانتظار",
"Refreshes every 30 seconds": "يتحدث كل 30 ثانية",
"Replication Health": "سلامة النسخ",
"Resync Status": "حالة إعادة المزامنة",
"Site {site} is unreachable, so nothing replicates to it.": "الموقع {site} غير قابل للوصول، لذا لا يتم نسخ أي شيء إليه.",
"Sync Setting": "إعداد المزامنة",
"and {count} more": "و{count} أخرى",
"{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} من الحاويات أو كيانات IAM أو قواعد الانتهاء تختلف بين المواقع.",
"{count} object replication failures in the last hour.": "{count} من حالات فشل نسخ الكائنات خلال الساعة الأخيرة.",
"{operation} has not finished.": "{operation} لم تكتمل.",
"{operation} has not finished; waiting for {sites}.": "{operation} لم تكتمل؛ في انتظار {sites}.",
"{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} من تغييرات الإعدادات بانتظار التسليم إلى المواقع النظيرة، وفشل {failed}.",
"{pending} waiting, {failed} failed": "{pending} قيد الانتظار، {failed} فشل",
"{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: اكتملت {completed} من {total} حاوية، وفشلت {failed}. تم نسخ {objects} كائن، وفشل {failedObjects}.",
"Site removal": "إزالة الموقع",
"Endpoint update": "تحديث نقطة النهاية",
"Group": "المجموعة",
"ILM Expiry Rule": "قاعدة انتهاء ILM",
"Marked for deletion": "محدد للحذف",
"Quota": "الحصة",
"CORS": "CORS",
"Policy document": "مستند السياسة",
"Service account rotation": "تدوير حساب الخدمة"
}
42 changes: 41 additions & 1 deletion i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -2241,5 +2241,45 @@
"Operations": "Betrieb",
"Resources": "Ressourcen",
"Bucket size distribution": "Verteilung der Bucket-Größen",
"Other buckets": "Weitere Buckets"
"Other buckets": "Weitere Buckets",
"Active workers": "Aktive Worker",
"Configuration deliveries": "Konfigurationszustellungen",
"Connection": "Verbindung",
"Consistency": "Konsistenz",
"Consistency cannot be checked until every peer site is reachable.": "Die Konsistenz kann erst geprüft werden, wenn alle Peer-Standorte erreichbar sind.",
"Differs: {settings}": "Abweichend: {settings}",
"Downtime": "Ausfallzeit",
"Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Alle Peer-Standorte sind erreichbar, und nichts wartet auf Replikation oder Reparatur.",
"Every site holds the same buckets, IAM entities, and expiry rules.": "Alle Standorte enthalten dieselben Buckets, IAM-Entitäten und Ablaufregeln.",
"Failed in the last hour": "Fehlgeschlagen in der letzten Stunde",
"Failed since start": "Fehlgeschlagen seit Start",
"Failures and Backlog": "Fehler und Rückstand",
"Missing on {sites}": "Fehlt auf {sites}",
"No resync has been started for this site.": "Für diesen Standort wurde noch keine Neusynchronisierung gestartet.",
"Object counters come from the node that answered this request and reset when it restarts.": "Die Objektzähler stammen vom Knoten, der diese Anfrage beantwortet hat, und werden bei dessen Neustart zurückgesetzt.",
"Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Dieser Server meldet die Erreichbarkeit der Peers nicht, daher kann der Replikationszustand nicht bestätigt werden.",
"Queued now": "Aktuell in Warteschlange",
"Queued peak": "Höchststand Warteschlange",
"Refreshes every 30 seconds": "Aktualisierung alle 30 Sekunden",
"Replication Health": "Replikationszustand",
"Resync Status": "Status der Neusynchronisierung",
"Site {site} is unreachable, so nothing replicates to it.": "Standort {site} ist nicht erreichbar, daher wird nichts dorthin repliziert.",
"Sync Setting": "Sync-Einstellung",
"and {count} more": "und {count} weitere",
"{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} Buckets, IAM-Entitäten oder Ablaufregeln unterscheiden sich zwischen den Standorten.",
"{count} object replication failures in the last hour.": "{count} fehlgeschlagene Objektreplikationen in der letzten Stunde.",
"{operation} has not finished.": "{operation} ist nicht abgeschlossen.",
"{operation} has not finished; waiting for {sites}.": "{operation} ist nicht abgeschlossen; wartet auf {sites}.",
"{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} Konfigurationsänderungen warten auf Zustellung an Peer-Standorte, {failed} fehlgeschlagen.",
"{pending} waiting, {failed} failed": "{pending} wartend, {failed} fehlgeschlagen",
"{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} von {total} Buckets abgeschlossen, {failed} fehlgeschlagen. {objects} Objekte repliziert, {failedObjects} fehlgeschlagen.",
"Site removal": "Standortentfernung",
"Endpoint update": "Endpunkt-Aktualisierung",
"Group": "Gruppe",
"ILM Expiry Rule": "ILM-Ablaufregel",
"Marked for deletion": "Zum Löschen markiert",
"Quota": "Kontingent",
"CORS": "CORS",
"Policy document": "Richtliniendokument",
"Service account rotation": "Rotation des Dienstkontos"
}
42 changes: 41 additions & 1 deletion i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2241,5 +2241,45 @@
"Operations": "Operations",
"Resources": "Resources",
"Bucket size distribution": "Bucket size distribution",
"Other buckets": "Other buckets"
"Other buckets": "Other buckets",
"Active workers": "Active workers",
"Configuration deliveries": "Configuration deliveries",
"Connection": "Connection",
"Consistency": "Consistency",
"Consistency cannot be checked until every peer site is reachable.": "Consistency cannot be checked until every peer site is reachable.",
"Differs: {settings}": "Differs: {settings}",
"Downtime": "Downtime",
"Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Every peer site is reachable and nothing is waiting to be replicated or repaired.",
"Every site holds the same buckets, IAM entities, and expiry rules.": "Every site holds the same buckets, IAM entities, and expiry rules.",
"Failed in the last hour": "Failed in the last hour",
"Failed since start": "Failed since start",
"Failures and Backlog": "Failures and Backlog",
"Missing on {sites}": "Missing on {sites}",
"No resync has been started for this site.": "No resync has been started for this site.",
"Object counters come from the node that answered this request and reset when it restarts.": "Object counters come from the node that answered this request and reset when it restarts.",
"Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Peer reachability is not reported by this server, so replication health cannot be confirmed.",
"Queued now": "Queued now",
"Queued peak": "Queued peak",
"Refreshes every 30 seconds": "Refreshes every 30 seconds",
"Replication Health": "Replication Health",
"Resync Status": "Resync Status",
"Site {site} is unreachable, so nothing replicates to it.": "Site {site} is unreachable, so nothing replicates to it.",
"Sync Setting": "Sync Setting",
"and {count} more": "and {count} more",
"{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} buckets, IAM entities, or expiry rules differ between sites.",
"{count} object replication failures in the last hour.": "{count} object replication failures in the last hour.",
"{operation} has not finished.": "{operation} has not finished.",
"{operation} has not finished; waiting for {sites}.": "{operation} has not finished; waiting for {sites}.",
"{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.",
"{pending} waiting, {failed} failed": "{pending} waiting, {failed} failed",
"{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.",
"Site removal": "Site removal",
"Endpoint update": "Endpoint update",
"Group": "Group",
"ILM Expiry Rule": "ILM Expiry Rule",
"Marked for deletion": "Marked for deletion",
"Quota": "Quota",
"CORS": "CORS",
"Policy document": "Policy document",
"Service account rotation": "Service account rotation"
}
Loading