@skyhook-io/k8s-ui 1.8.7 → 1.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/applications/ApplicationsList.tsx +5 -2
- package/src/components/applications/ApplicationsView.tsx +4 -1
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +27 -9
- package/src/components/gitops/GitOpsTableView.tsx +46 -45
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +12 -5
- package/src/components/issues/IssuesView.tsx +41 -5
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/diagnostic.ts +22 -0
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +21 -0
- package/src/components/issues/types.ts +18 -0
- package/src/components/namespace-switcher/NamespacePicker.tsx +381 -0
- package/src/components/namespace-switcher/index.ts +6 -0
- package/src/components/resources/ResourcesView.tsx +20 -81
- package/src/components/scope-pill/ScopePill.tsx +35 -0
- package/src/components/scope-pill/index.ts +2 -0
- package/src/components/timeline/TimelineList.tsx +27 -1
- package/src/components/topology/TopologyControls.tsx +90 -14
- package/src/components/ui/FreshnessControl.tsx +153 -0
- package/src/components/ui/SortableTh.tsx +16 -10
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +215 -32
- package/src/components/workload/WorkloadView.tsx +26 -8
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useKeyboardShortcuts.tsx +23 -2
- package/src/hooks/useRefreshAnimation.ts +15 -2
- package/src/index.ts +8 -0
- package/src/types/core.ts +42 -0
- package/src/types/gitops-insights.ts +4 -0
- package/src/utils/animation.ts +10 -0
- package/src/utils/format-freshness.test.ts +34 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/resource-hierarchy.test.ts +51 -0
- package/src/utils/resource-hierarchy.ts +7 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { formatUpdatedAgo, formatLastUpdatedBucket, msToNextBucket } from './format'
|
|
3
|
+
|
|
4
|
+
describe('formatUpdatedAgo', () => {
|
|
5
|
+
it('collapses the first minute to "just now"', () => {
|
|
6
|
+
expect(formatUpdatedAgo(0)).toBe('just now')
|
|
7
|
+
expect(formatUpdatedAgo(59_000)).toBe('just now')
|
|
8
|
+
})
|
|
9
|
+
it('renders minutes and hours with an "ago" suffix', () => {
|
|
10
|
+
expect(formatUpdatedAgo(60_000)).toBe('1m ago')
|
|
11
|
+
expect(formatUpdatedAgo(3_600_000)).toBe('1h ago')
|
|
12
|
+
})
|
|
13
|
+
it('collapses anything past a day to "over a day ago"', () => {
|
|
14
|
+
expect(formatUpdatedAgo(25 * 3_600_000)).toBe('over a day ago')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('formatLastUpdatedBucket', () => {
|
|
19
|
+
it('buckets by the coarsest unit', () => {
|
|
20
|
+
expect(formatLastUpdatedBucket(0)).toBe('just now')
|
|
21
|
+
expect(formatLastUpdatedBucket(60_000)).toBe('1m')
|
|
22
|
+
expect(formatLastUpdatedBucket(3_600_000)).toBe('1h')
|
|
23
|
+
expect(formatLastUpdatedBucket(86_400_000)).toBe('1d')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('msToNextBucket', () => {
|
|
28
|
+
it('schedules the next re-render exactly on the bucket boundary', () => {
|
|
29
|
+
expect(msToNextBucket(0)).toBe(60_000)
|
|
30
|
+
expect(msToNextBucket(30_000)).toBe(30_000)
|
|
31
|
+
// 90s elapsed → 30s until the "2m" bucket flips.
|
|
32
|
+
expect(msToNextBucket(90_000)).toBe(30_000)
|
|
33
|
+
})
|
|
34
|
+
})
|
package/src/utils/format.ts
CHANGED
|
@@ -192,6 +192,38 @@ export function formatCompactAge(value?: string): string {
|
|
|
192
192
|
return `${Math.floor(hours / 24)}d`
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// Coarse "just now / Xm / Xh / Xd" buckets for freshness labels — finer-grained
|
|
196
|
+
// updates add motion in the periphery without aiding any user decision.
|
|
197
|
+
export function formatLastUpdatedBucket(elapsedMs: number): string {
|
|
198
|
+
const elapsedSec = Math.max(0, Math.floor(elapsedMs / 1000))
|
|
199
|
+
if (elapsedSec < 60) return 'just now'
|
|
200
|
+
const minutes = Math.floor(elapsedSec / 60)
|
|
201
|
+
if (minutes < 60) return `${minutes}m`
|
|
202
|
+
const hours = Math.floor(minutes / 60)
|
|
203
|
+
if (hours < 24) return `${hours}h`
|
|
204
|
+
return `${Math.floor(hours / 24)}d`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ms until the bucket produced by formatLastUpdatedBucket would change — lets a
|
|
208
|
+
// ticker re-render exactly on the boundary instead of polling every second.
|
|
209
|
+
export function msToNextBucket(elapsedMs: number): number {
|
|
210
|
+
const elapsed = Math.max(0, elapsedMs)
|
|
211
|
+
if (elapsed < 60_000) return 60_000 - elapsed
|
|
212
|
+
if (elapsed < 3_600_000) return 60_000 - (elapsed % 60_000)
|
|
213
|
+
if (elapsed < 86_400_000) return 3_600_000 - (elapsed % 3_600_000)
|
|
214
|
+
return 86_400_000 - (elapsed % 86_400_000)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Freshness phrasing for "Updated X" indicators: just now / Xm ago / Xh ago /
|
|
218
|
+
// over a day ago. An exact day count is noise for an auto-refresh signal, so
|
|
219
|
+
// anything past 24h collapses to "over a day ago".
|
|
220
|
+
export function formatUpdatedAgo(elapsedMs: number): string {
|
|
221
|
+
const bucket = formatLastUpdatedBucket(elapsedMs)
|
|
222
|
+
if (bucket === 'just now') return 'just now'
|
|
223
|
+
if (bucket.endsWith('d')) return 'over a day ago'
|
|
224
|
+
return `${bucket} ago`
|
|
225
|
+
}
|
|
226
|
+
|
|
195
227
|
export function formatRelativeAgeTime(value?: string, fallback = '-'): string {
|
|
196
228
|
if (!value) return fallback
|
|
197
229
|
const time = Date.parse(value)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { TimelineEvent, Topology } from '../types/core'
|
|
4
|
+
|
|
5
|
+
import { buildResourceHierarchy } from './resource-hierarchy'
|
|
6
|
+
|
|
7
|
+
function svcEvent(namespace: string, name: string): TimelineEvent {
|
|
8
|
+
return {
|
|
9
|
+
id: `${namespace}/${name}`,
|
|
10
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
11
|
+
source: 'informer',
|
|
12
|
+
kind: 'Service',
|
|
13
|
+
namespace,
|
|
14
|
+
name,
|
|
15
|
+
eventType: 'update',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function svcNode(namespace: string, name: string, app: string) {
|
|
20
|
+
return {
|
|
21
|
+
id: `service/${namespace}/${name}`,
|
|
22
|
+
data: { apiVersion: 'v1', labels: { 'app.kubernetes.io/name': app } },
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function topo(nodes: ReturnType<typeof svcNode>[]): Topology {
|
|
27
|
+
return { nodes, edges: [] } as unknown as Topology
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('buildResourceHierarchy app-label grouping', () => {
|
|
31
|
+
it('does not merge the same app label across namespaces', () => {
|
|
32
|
+
const events = [svcEvent('team-a', 'web'), svcEvent('team-b', 'web')]
|
|
33
|
+
const topology = topo([svcNode('team-a', 'web', 'web'), svcNode('team-b', 'web', 'web')])
|
|
34
|
+
|
|
35
|
+
const lanes = buildResourceHierarchy({ events, topology, groupByApp: true })
|
|
36
|
+
|
|
37
|
+
expect(lanes).toHaveLength(2)
|
|
38
|
+
expect(lanes.every((l) => (l.children ?? []).length === 0)).toBe(true)
|
|
39
|
+
expect(new Set(lanes.map((l) => l.namespace))).toEqual(new Set(['team-a', 'team-b']))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('groups distinct resources sharing an app label within one namespace', () => {
|
|
43
|
+
const events = [svcEvent('team-a', 'web'), svcEvent('team-a', 'web-edge')]
|
|
44
|
+
const topology = topo([svcNode('team-a', 'web', 'web'), svcNode('team-a', 'web-edge', 'web')])
|
|
45
|
+
|
|
46
|
+
const lanes = buildResourceHierarchy({ events, topology, groupByApp: true })
|
|
47
|
+
|
|
48
|
+
expect(lanes).toHaveLength(1)
|
|
49
|
+
expect(lanes[0].children).toHaveLength(1)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -465,17 +465,20 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
465
465
|
'HTTPProxy', // Contour
|
|
466
466
|
])
|
|
467
467
|
|
|
468
|
-
// Group lanes by app label
|
|
468
|
+
// Group lanes by app label, scoped to namespace: the same app label in two
|
|
469
|
+
// namespaces (e.g. the same workload deployed to dev and staging) is two
|
|
470
|
+
// distinct apps and must not collapse into one lane.
|
|
469
471
|
const appGroups = new Map<string, string[]>()
|
|
470
472
|
for (const [laneId, lane] of laneMap) {
|
|
471
473
|
if (laneParent.has(laneId)) continue
|
|
472
474
|
if (!appLabelEligibleKinds.has(lane.kind)) continue
|
|
473
475
|
const appLabel = laneAppLabels.get(laneId)
|
|
474
476
|
if (!appLabel) continue
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
const groupKey = `${lane.namespace}/${appLabel}`
|
|
478
|
+
if (!appGroups.has(groupKey)) {
|
|
479
|
+
appGroups.set(groupKey, [])
|
|
477
480
|
}
|
|
478
|
-
appGroups.get(
|
|
481
|
+
appGroups.get(groupKey)!.push(laneId)
|
|
479
482
|
}
|
|
480
483
|
|
|
481
484
|
// For each app group with multiple members, pick the best parent
|