@skyhook-io/k8s-ui 1.16.3 → 1.17.0
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 +1 -1
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/primitives.tsx +3 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +5 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesView.tsx +7 -8
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg.ts +0 -4
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +120 -43
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
package/package.json
CHANGED
|
@@ -377,6 +377,21 @@ describe("ApplicationDetail shell", () => {
|
|
|
377
377
|
expect(local).toContain('ring is full, so the oldest activity is not loaded')
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
+
it('shows the host history-limit note beside the history, and nothing without one', () => {
|
|
381
|
+
const base = {
|
|
382
|
+
selectedView: 'history' as const,
|
|
383
|
+
onSelectView: () => {},
|
|
384
|
+
selectedWorkloadKey: null,
|
|
385
|
+
onSelectWorkload: () => {},
|
|
386
|
+
historyMode: 'local' as const,
|
|
387
|
+
historyRange: 'all' as const,
|
|
388
|
+
historyRangeOptions: [{ value: 'all' as const, label: 'All observed' }],
|
|
389
|
+
historyItems: [],
|
|
390
|
+
}
|
|
391
|
+
expect(renderDetail({ ...base, historyRetentionNote: <span>Host limit note</span> })).toContain('<span>Host limit note</span>')
|
|
392
|
+
expect(renderDetail(base)).not.toContain('Host limit note')
|
|
393
|
+
})
|
|
394
|
+
|
|
380
395
|
it('shows a single honest error state when runtime history fails without source history', () => {
|
|
381
396
|
const html = renderDetail({
|
|
382
397
|
selectedView: 'history',
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
overlayProvenance,
|
|
90
90
|
} from "../../utils/applications";
|
|
91
91
|
import { PaneLoader } from "../ui/PaneLoader";
|
|
92
|
-
import { midTruncate } from "../../utils/format";
|
|
92
|
+
import { formatRelativeAgeTime, midTruncate } from "../../utils/format";
|
|
93
93
|
import { VersionTooltip, AppIdentityTooltip, RolloutSummaryTooltip } from "./AppTooltips";
|
|
94
94
|
import {
|
|
95
95
|
ProvenanceBadge,
|
|
@@ -253,6 +253,9 @@ export type ApplicationDetailProps = {
|
|
|
253
253
|
// The retained ring itself was row-capped: the OLDEST part of the retention
|
|
254
254
|
// window is not loaded, independent of the per-query event limit.
|
|
255
255
|
historyRingTruncated?: boolean;
|
|
256
|
+
// The host's note about how far back history goes, or the stretch before
|
|
257
|
+
// the limit reads as "no history".
|
|
258
|
+
historyRetentionNote?: ReactNode;
|
|
256
259
|
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
257
260
|
onOpenTimeline?: (timestamp?: string) => void;
|
|
258
261
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
@@ -346,6 +349,7 @@ export function ApplicationDetail({
|
|
|
346
349
|
historyCoverageRecordCount,
|
|
347
350
|
historyRuntimeLimited,
|
|
348
351
|
historyRingTruncated,
|
|
352
|
+
historyRetentionNote,
|
|
349
353
|
onHistoryRangeChange,
|
|
350
354
|
onOpenTimeline,
|
|
351
355
|
onOpenSource,
|
|
@@ -784,6 +788,7 @@ export function ApplicationDetail({
|
|
|
784
788
|
historyCoverageRecordCount={historyCoverageRecordCount}
|
|
785
789
|
historyRuntimeLimited={historyRuntimeLimited}
|
|
786
790
|
historyRingTruncated={historyRingTruncated}
|
|
791
|
+
historyRetentionNote={historyRetentionNote}
|
|
787
792
|
onHistoryRangeChange={onHistoryRangeChange}
|
|
788
793
|
onOpenTimeline={onOpenTimeline}
|
|
789
794
|
onOpenSource={onOpenSource}
|
|
@@ -832,6 +837,7 @@ function ApplicationWorkspace({
|
|
|
832
837
|
historyCoverageRecordCount,
|
|
833
838
|
historyRuntimeLimited,
|
|
834
839
|
historyRingTruncated,
|
|
840
|
+
historyRetentionNote,
|
|
835
841
|
onHistoryRangeChange,
|
|
836
842
|
onOpenTimeline,
|
|
837
843
|
onOpenSource,
|
|
@@ -884,6 +890,9 @@ function ApplicationWorkspace({
|
|
|
884
890
|
historyCoverageRecordCount?: number;
|
|
885
891
|
historyRuntimeLimited?: boolean;
|
|
886
892
|
historyRingTruncated?: boolean;
|
|
893
|
+
// The host's note about how far back history goes, or the stretch before
|
|
894
|
+
// the limit reads as "no history".
|
|
895
|
+
historyRetentionNote?: ReactNode;
|
|
887
896
|
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
888
897
|
onOpenTimeline?: (timestamp?: string) => void;
|
|
889
898
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
@@ -953,6 +962,7 @@ function ApplicationWorkspace({
|
|
|
953
962
|
coverageRecordCount={historyCoverageRecordCount}
|
|
954
963
|
runtimeLimited={historyRuntimeLimited}
|
|
955
964
|
ringTruncated={historyRingTruncated}
|
|
965
|
+
retentionNote={historyRetentionNote}
|
|
956
966
|
workloads={workloads}
|
|
957
967
|
onSelectWorkload={onSelectWorkload}
|
|
958
968
|
onNavigateToResource={onNavigateToResource}
|
|
@@ -1827,7 +1837,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1827
1837
|
return {
|
|
1828
1838
|
latestValue: latest?.latestRunPhase || latest?.label || "None",
|
|
1829
1839
|
latestDetail: latest
|
|
1830
|
-
? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${
|
|
1840
|
+
? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${formatRelativeAgeTime(latestRunTimestamp(latest)!)} ` : ""}`.trim()
|
|
1831
1841
|
: "No retained runs",
|
|
1832
1842
|
retainedValue:
|
|
1833
1843
|
retained > 0 ? `${succeeded} succeeded / ${failed} failed` : "0 retained",
|
|
@@ -1842,7 +1852,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1842
1852
|
: `${schedules.length} schedules`,
|
|
1843
1853
|
scheduleDetail:
|
|
1844
1854
|
schedules.length === 1 && latest?.lastSuccessfulAt
|
|
1845
|
-
? `last success ${
|
|
1855
|
+
? `last success ${formatRelativeAgeTime(latest.lastSuccessfulAt)}`
|
|
1846
1856
|
: undefined,
|
|
1847
1857
|
scheduleMono: schedules.length === 1,
|
|
1848
1858
|
};
|
|
@@ -1856,17 +1866,6 @@ function activeWorkloadNames(activity: AppBatchActivity[]): string {
|
|
|
1856
1866
|
return `${active.slice(0, 2).join(", ")} +${active.length - 2} more`;
|
|
1857
1867
|
}
|
|
1858
1868
|
|
|
1859
|
-
function relativeTime(value: string): string {
|
|
1860
|
-
const t = Date.parse(value);
|
|
1861
|
-
if (Number.isNaN(t)) return value;
|
|
1862
|
-
const diff = Date.now() - t;
|
|
1863
|
-
if (diff < 60_000) return "just now";
|
|
1864
|
-
const minutes = Math.floor(diff / 60_000);
|
|
1865
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
1866
|
-
const hours = Math.floor(minutes / 60);
|
|
1867
|
-
if (hours < 48) return `${hours}h ago`;
|
|
1868
|
-
return `${Math.floor(hours / 24)}d ago`;
|
|
1869
|
-
}
|
|
1870
1869
|
|
|
1871
1870
|
function ApplicationLatestHistory({
|
|
1872
1871
|
history,
|
|
@@ -2093,7 +2092,7 @@ function workloadRuntimeDetail(workload: AppWorkload): string {
|
|
|
2093
2092
|
const at =
|
|
2094
2093
|
batch.latestStartedAt || batch.latestFinishedAt || batch.lastScheduledAt;
|
|
2095
2094
|
if (batch.latestRunName)
|
|
2096
|
-
return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${
|
|
2095
|
+
return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${formatRelativeAgeTime(at)}` : ""}`;
|
|
2097
2096
|
if (batch.schedule) return `Schedule ${batch.schedule}`;
|
|
2098
2097
|
return "No retained runs";
|
|
2099
2098
|
}
|
|
@@ -2201,6 +2200,7 @@ function ApplicationHistoryView({
|
|
|
2201
2200
|
coverageRecordCount,
|
|
2202
2201
|
runtimeLimited,
|
|
2203
2202
|
ringTruncated,
|
|
2203
|
+
retentionNote,
|
|
2204
2204
|
workloads,
|
|
2205
2205
|
onSelectWorkload,
|
|
2206
2206
|
onNavigateToResource,
|
|
@@ -2219,6 +2219,7 @@ function ApplicationHistoryView({
|
|
|
2219
2219
|
coverageRecordCount?: number;
|
|
2220
2220
|
runtimeLimited?: boolean;
|
|
2221
2221
|
ringTruncated?: boolean;
|
|
2222
|
+
retentionNote?: ReactNode;
|
|
2222
2223
|
workloads: AppWorkload[];
|
|
2223
2224
|
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2224
2225
|
onNavigateToResource?: (resource: ResourceRef) => void;
|
|
@@ -2428,6 +2429,7 @@ function ApplicationHistoryView({
|
|
|
2428
2429
|
application activity in this range may not be included.
|
|
2429
2430
|
</div>
|
|
2430
2431
|
)}
|
|
2432
|
+
{retentionNote && <div>{retentionNote}</div>}
|
|
2431
2433
|
{ringTruncated && (
|
|
2432
2434
|
<div>
|
|
2433
2435
|
{mode === "retained"
|
|
@@ -61,19 +61,8 @@ export interface CheckResourceRef {
|
|
|
61
61
|
/** Built-in (Radar-detected) finding source. The only V1 source. */
|
|
62
62
|
export const SOURCE_RADAR_BUILTIN = 'radar_builtin';
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* `group|Kind|namespace|name`. Group first because group and namespace can each
|
|
67
|
-
* independently be empty; `|` is delimiter-safe (K8s API groups follow
|
|
68
|
-
* DNS-subdomain rules and can't contain it).
|
|
69
|
-
*/
|
|
70
|
-
export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
|
|
71
|
-
return `${group}|${kind}|${namespace}|${name}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function resourceRefKey(ref: CheckResourceRef): string {
|
|
75
|
-
return resourceKey(ref.group, ref.kind, ref.namespace, ref.name);
|
|
76
|
-
}
|
|
64
|
+
// Preserve the Checks import path while sharing exact resource identity.
|
|
65
|
+
export { resourceKey, resourceRefKey } from '../../utils/resource-refs';
|
|
77
66
|
|
|
78
67
|
/**
|
|
79
68
|
* checkFindingKey is the canonical built-in finding key:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import type { HealthLevel } from '../resources/resource-utils'
|
|
4
|
-
import { formatAge } from '../resources/resource-utils'
|
|
5
4
|
import { StatusDot } from '../ui/status-tone'
|
|
6
5
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
6
|
import { AlertBanner } from '../ui/drawer-components'
|
|
8
7
|
import { TONE_TEXT_CLASS } from '../ui/severity-tone'
|
|
9
8
|
import type { CNPGFact, CNPGProblem } from './workspace'
|
|
9
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
10
10
|
|
|
11
11
|
export interface CNPGRef {
|
|
12
12
|
kind: string
|
|
@@ -35,11 +35,11 @@ export function toneTextClass(tone: HealthLevel): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function FactValue({ fact, className }: { fact: CNPGFact; className?: string }) {
|
|
38
|
-
const
|
|
38
|
+
const when = fact.at ? formatRelativeAgeTime(fact.at) : null
|
|
39
39
|
const body = (
|
|
40
40
|
<span className={clsx(toneTextClass(fact.tone), className)}>
|
|
41
41
|
{fact.text}
|
|
42
|
-
{
|
|
42
|
+
{when && <span className="text-theme-text-secondary">{fact.text ? ' · ' : ''}{when}</span>}
|
|
43
43
|
</span>
|
|
44
44
|
)
|
|
45
45
|
if (!fact.source && !fact.at) return body
|
|
@@ -40,6 +40,7 @@ import { isArgoOperationInProgress, isArgoSuspendedByRadar } from '../resources/
|
|
|
40
40
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
41
41
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
42
42
|
import { parseContextName } from '../../utils/context-name'
|
|
43
|
+
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format'
|
|
43
44
|
|
|
44
45
|
// =============================================================================
|
|
45
46
|
// GitOpsTableView — the canonical GitOps fleet list (Argo + Flux).
|
|
@@ -1373,8 +1374,8 @@ function GitOpsTable({
|
|
|
1373
1374
|
)}
|
|
1374
1375
|
<TableCell>
|
|
1375
1376
|
{row.terminating
|
|
1376
|
-
? <span className="text-orange-400/80">
|
|
1377
|
-
:
|
|
1377
|
+
? <span className="text-orange-400/80">{pendingFor(row.terminationStartedAt)}</span>
|
|
1378
|
+
: row.lastSync ? formatRelativeAgeTime(row.lastSync, '') : row.createdAt ? `Created ${formatRelativeAgeTime(row.createdAt, '')}` : ''}
|
|
1378
1379
|
</TableCell>
|
|
1379
1380
|
{showActions && onRowAction && (
|
|
1380
1381
|
<td
|
|
@@ -1603,9 +1604,9 @@ function GitOpsTile({
|
|
|
1603
1604
|
<div className="truncate font-mono text-[11px] text-theme-text-tertiary">{shortRevision(revision)}</div>
|
|
1604
1605
|
)}
|
|
1605
1606
|
{row.terminating ? (
|
|
1606
|
-
<div className="font-medium text-orange-400/80">
|
|
1607
|
+
<div className="font-medium text-orange-400/80">{pendingFor(row.terminationStartedAt)}</div>
|
|
1607
1608
|
) : (
|
|
1608
|
-
lastSyncRaw && <div className={clsx('font-medium', recencyClass)}>{
|
|
1609
|
+
lastSyncRaw && <div className={clsx('font-medium', recencyClass)}>{formatRelativeAgeTime(lastSyncRaw, '')}</div>
|
|
1609
1610
|
)}
|
|
1610
1611
|
</div>
|
|
1611
1612
|
{(dest || ns || row.project) && (
|
|
@@ -1926,26 +1927,10 @@ function compactClusterURL(dest: string): string {
|
|
|
1926
1927
|
.replace(/^kubernetes\.default\.svc(:\d+)?\/?$/, 'in-cluster')
|
|
1927
1928
|
}
|
|
1928
1929
|
|
|
1929
|
-
//
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
if (!rfc3339) return ''
|
|
1934
|
-
const time = Date.parse(rfc3339)
|
|
1935
|
-
if (!Number.isFinite(time)) return ''
|
|
1936
|
-
const diffMs = Date.now() - time
|
|
1937
|
-
const s = Math.max(0, Math.floor(diffMs / 1000))
|
|
1938
|
-
if (s < 60) return `${s}s ago`
|
|
1939
|
-
const m = Math.floor(s / 60)
|
|
1940
|
-
if (m < 60) return `${m}m ago`
|
|
1941
|
-
const h = Math.floor(m / 60)
|
|
1942
|
-
if (h < 24) return `${h}h ago`
|
|
1943
|
-
const d = Math.floor(h / 24)
|
|
1944
|
-
if (d < 30) return `${d}d ago`
|
|
1945
|
-
const mo = Math.floor(d / 30)
|
|
1946
|
-
if (mo < 12) return `${mo}mo ago`
|
|
1947
|
-
const y = Math.floor(d / 365)
|
|
1948
|
-
return `${y}y ago`
|
|
1930
|
+
// How long deletion has been pending, not when it started.
|
|
1931
|
+
function pendingFor(startedAt?: string): string {
|
|
1932
|
+
const age = formatCompactAge(startedAt)
|
|
1933
|
+
return age ? `Pending for ${age}` : 'Pending'
|
|
1949
1934
|
}
|
|
1950
1935
|
|
|
1951
1936
|
// =============================================================================
|
|
@@ -82,7 +82,7 @@ export function describeGitOpsTerminating(summary?: {
|
|
|
82
82
|
finalizers?: string[]
|
|
83
83
|
}): { chipTooltip: string; actionDisabledTooltip: string } {
|
|
84
84
|
const ageText = formatCompactAge(summary?.terminationStartedAt)
|
|
85
|
-
const ageSuffix = ageText ? ` ${ageText}
|
|
85
|
+
const ageSuffix = ageText ? ` for ${ageText}` : ''
|
|
86
86
|
const finalizers = summary?.finalizers ?? []
|
|
87
87
|
const finSuffix = finalizers.length > 0 ? ` Finalizers: ${finalizers.join(', ')}.` : ''
|
|
88
88
|
const ageInline = ageText ? ` (${ageText})` : ''
|
|
@@ -72,7 +72,7 @@ export function GitOpsStatusStrip({ insight, loading, renderRevisionMeta }: GitO
|
|
|
72
72
|
const revision = summary.lastRevision || summary.targetRevision || ''
|
|
73
73
|
const shortRev = shortRevisionForCommit(revision)
|
|
74
74
|
const commitUrl = revision ? commitURLForRepo(summary.source, revision) : null
|
|
75
|
-
const reconcileAge =
|
|
75
|
+
const reconcileAge = formatRelativeAgeTime(summary.lastReconcile)
|
|
76
76
|
const healthSummary = buildHealthSummary(insight.changes ?? [])
|
|
77
77
|
// STUCK belongs with the operation chip — it's a property of the same
|
|
78
78
|
// state ("failed AND won't self-recover"). Co-locating FAILED + STUCK ·
|
|
@@ -257,7 +257,7 @@ function buildHealthSummary(changes: GitOpsChange[]): { text: string; tone: stri
|
|
|
257
257
|
// disclosure — still available for forensics but not noise during deletion.
|
|
258
258
|
function TerminatingStatusStrip({ summary }: { summary: NonNullable<GitOpsInsight['summary']> }) {
|
|
259
259
|
const [showHistorical, setShowHistorical] = useState(false)
|
|
260
|
-
const pending =
|
|
260
|
+
const pending = formatRelativeAgeTime(summary.terminationStartedAt) || 'recently'
|
|
261
261
|
const finalizers = summary.finalizers ?? []
|
|
262
262
|
return (
|
|
263
263
|
<div className="border-b border-orange-500/20 bg-orange-500/[0.04] px-4 py-2">
|
|
@@ -282,7 +282,7 @@ function TerminatingStatusStrip({ summary }: { summary: NonNullable<GitOpsInsigh
|
|
|
282
282
|
{(summary.lastRevision || summary.targetRevision) && (
|
|
283
283
|
<MetaFact label="Revision" value={summary.lastRevision || summary.targetRevision || '-'} mono />
|
|
284
284
|
)}
|
|
285
|
-
{summary.lastReconcile && <MetaFact label="Last reconcile" value={
|
|
285
|
+
{summary.lastReconcile && <MetaFact label="Last reconcile" value={formatRelativeAgeTime(summary.lastReconcile)} />}
|
|
286
286
|
{summary.autoSyncMode && <MetaFact label="Sync mode" value={summary.autoSyncMode} />}
|
|
287
287
|
</div>
|
|
288
288
|
</Collapse>
|
|
@@ -1628,7 +1628,7 @@ function RecentEventsPanel({ events }: { events: NonNullable<GitOpsChange['recen
|
|
|
1628
1628
|
{e.count && e.count > 1 && (
|
|
1629
1629
|
<span className="text-[10px] text-theme-text-tertiary">×{e.count}</span>
|
|
1630
1630
|
)}
|
|
1631
|
-
<span className="ml-auto text-[10px] text-theme-text-tertiary">{
|
|
1631
|
+
<span className="ml-auto text-[10px] text-theme-text-tertiary">{formatRelativeAgeTime(e.lastTimestamp, '')}</span>
|
|
1632
1632
|
</div>
|
|
1633
1633
|
<p className="mt-0.5 text-theme-text-secondary">{e.message}</p>
|
|
1634
1634
|
</div>
|
|
@@ -1639,10 +1639,6 @@ function RecentEventsPanel({ events }: { events: NonNullable<GitOpsChange['recen
|
|
|
1639
1639
|
)
|
|
1640
1640
|
}
|
|
1641
1641
|
|
|
1642
|
-
function formatRelativeTime(value: string): string {
|
|
1643
|
-
return formatRelativeAgeTime(value, '')
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
1642
|
interface GitOpsActivityInsightViewProps {
|
|
1647
1643
|
insight?: GitOpsInsight | null
|
|
1648
1644
|
error?: Error | null
|
|
@@ -1729,7 +1725,7 @@ function HistoryRows({
|
|
|
1729
1725
|
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
1730
1726
|
<span className="font-mono text-xs text-theme-text-primary">{item.revision || item.phase || '-'}</span>
|
|
1731
1727
|
<PhaseChip phase={item.phase} message={item.message} />
|
|
1732
|
-
<span className="text-[11px] text-theme-text-tertiary">{
|
|
1728
|
+
<span className="text-[11px] text-theme-text-tertiary">{formatRelativeAgeTime(item.deployedAt)}</span>
|
|
1733
1729
|
{item.initiatedBy && (
|
|
1734
1730
|
<span className="text-[11px] text-theme-text-tertiary">by {item.initiatedBy}</span>
|
|
1735
1731
|
)}
|
|
@@ -1831,6 +1827,3 @@ function InsightErrorState({ error }: { error: Error }) {
|
|
|
1831
1827
|
)
|
|
1832
1828
|
}
|
|
1833
1829
|
|
|
1834
|
-
function formatRelative(value?: string) {
|
|
1835
|
-
return formatRelativeAgeTime(value)
|
|
1836
|
-
}
|
|
@@ -752,10 +752,10 @@ function buildChips(node: GitOpsTreeNode): Array<{ label?: string; value: string
|
|
|
752
752
|
// detail title row and fleet rows.
|
|
753
753
|
if (isNodeTerminating(node)) {
|
|
754
754
|
chips.push({ value: 'Terminating', tone: 'warning' })
|
|
755
|
-
if (typeof data.createdAt === 'string') chips.push({ label: 'age', value:
|
|
755
|
+
if (typeof data.createdAt === 'string') chips.push({ label: 'age', value: formatCompactAge(data.createdAt) })
|
|
756
756
|
return chips
|
|
757
757
|
}
|
|
758
|
-
if (typeof data.createdAt === 'string') chips.push({ label: 'age', value:
|
|
758
|
+
if (typeof data.createdAt === 'string') chips.push({ label: 'age', value: formatCompactAge(data.createdAt) })
|
|
759
759
|
const revision = stringData(data.revision) || stringData(data.lastSyncRevision)
|
|
760
760
|
if (revision) chips.push({ label: 'rev', value: revision })
|
|
761
761
|
const attempted = stringData(data.attemptedRevision)
|
|
@@ -886,6 +886,3 @@ function pluralize(kind: string): string {
|
|
|
886
886
|
return `${kind}s`
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
-
function formatAge(timestamp: string): string {
|
|
890
|
-
return formatCompactAge(timestamp)
|
|
891
|
-
}
|
|
@@ -113,9 +113,9 @@ export function issueTiming(issue: Issue): IssueTimingDisplay | null {
|
|
|
113
113
|
}
|
|
114
114
|
return {
|
|
115
115
|
kind: 'regression',
|
|
116
|
-
chip: 'after healthy',
|
|
116
|
+
chip: issue.kind === 'Node' && issue.reason === 'NotReady' ? 'NotReady' : 'after healthy',
|
|
117
117
|
meta: 'failing evidence followed a healthy period',
|
|
118
|
-
tooltip: 'A healthy period preceded this failing evidence.',
|
|
118
|
+
tooltip: issue.kind === 'Node' && issue.reason === 'NotReady' ? 'The Ready condition stopped reporting healthy at this time.' : 'A healthy period preceded this failing evidence.',
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
case 'started_at_resource_creation': {
|
|
@@ -139,7 +139,7 @@ describe('IssueRow', () => {
|
|
|
139
139
|
|
|
140
140
|
it('uses the category-group chip class in the queue row', () => {
|
|
141
141
|
const html = renderToString(createElement(IssueRow, {
|
|
142
|
-
issue: mk({ category_group: 'control_plane' }),
|
|
142
|
+
issue: mk({ category_group: 'control_plane', kind: 'Node' }),
|
|
143
143
|
open: false,
|
|
144
144
|
onToggle: () => undefined,
|
|
145
145
|
}))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ToastProvider } from '../ui/Toast'
|
|
6
|
+
import { WorkloadLogsViewer } from './WorkloadLogsViewer'
|
|
7
|
+
|
|
8
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
9
|
+
|
|
10
|
+
class FakeEventSource extends EventTarget {
|
|
11
|
+
static last: FakeEventSource | undefined
|
|
12
|
+
constructor(readonly url: string) {
|
|
13
|
+
super()
|
|
14
|
+
FakeEventSource.last = this
|
|
15
|
+
}
|
|
16
|
+
close() {}
|
|
17
|
+
emit(type: string, data: unknown) {
|
|
18
|
+
this.dispatchEvent(new MessageEvent(type, { data: JSON.stringify(data) }))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let root: Root
|
|
23
|
+
let element: HTMLDivElement
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.stubGlobal('EventSource', FakeEventSource)
|
|
26
|
+
element = document.createElement('div')
|
|
27
|
+
document.body.appendChild(element)
|
|
28
|
+
root = createRoot(element)
|
|
29
|
+
})
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await act(async () => root.unmount())
|
|
32
|
+
element.remove()
|
|
33
|
+
vi.unstubAllGlobals()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('clears the unreadable-sources notice when the stream ends', async () => {
|
|
37
|
+
const notice = '1 source could not be read: web-0/app: Kubernetes did not return this container\'s logs'
|
|
38
|
+
await act(async () =>
|
|
39
|
+
root.render(
|
|
40
|
+
<ToastProvider>
|
|
41
|
+
<WorkloadLogsViewer
|
|
42
|
+
name="web"
|
|
43
|
+
fetchAll={async () => ({ pods: [], logs: [] })}
|
|
44
|
+
createStream={() => new EventSource('/stream')}
|
|
45
|
+
autoStream
|
|
46
|
+
/>
|
|
47
|
+
</ToastProvider>,
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
const source = FakeEventSource.last
|
|
51
|
+
if (!source) throw new Error('stream was not opened')
|
|
52
|
+
|
|
53
|
+
await act(async () => source.emit('connected', { pods: [{ name: 'web-0', containers: ['app'] }] }))
|
|
54
|
+
await act(async () => source.emit('notice', { notice }))
|
|
55
|
+
expect(element.textContent).toContain(notice)
|
|
56
|
+
|
|
57
|
+
await act(async () => source.emit('end', { reason: 'no pods found' }))
|
|
58
|
+
expect(element.textContent).not.toContain(notice)
|
|
59
|
+
})
|
|
@@ -246,7 +246,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
246
246
|
// while new pod logs start flowing in
|
|
247
247
|
}
|
|
248
248
|
},
|
|
249
|
+
onNotice: (data: unknown) => {
|
|
250
|
+
const notice = typeof data === 'object' && data !== null && 'notice' in data ? data.notice : ''
|
|
251
|
+
setNotice(typeof notice === 'string' ? notice : '')
|
|
252
|
+
},
|
|
249
253
|
onEnd: (data: any) => {
|
|
254
|
+
setNotice('')
|
|
250
255
|
if (data?.emptyMessage) setEmptyMessage(data.emptyMessage)
|
|
251
256
|
if (data?.command) setEmptyCommand(data.command)
|
|
252
257
|
},
|
|
@@ -10,6 +10,8 @@ export interface LogStreamHandlers {
|
|
|
10
10
|
onPodAdded?: (data: unknown) => void
|
|
11
11
|
/** Called when pods are terminated during streaming (workload logs only) */
|
|
12
12
|
onPodRemoved?: (data: unknown) => void
|
|
13
|
+
/** Called when the set of sources the stream cannot read changes (workload logs only) */
|
|
14
|
+
onNotice?: (data: unknown) => void
|
|
13
15
|
/** Called when the server ends the stream cleanly */
|
|
14
16
|
onEnd?: (data: unknown) => void
|
|
15
17
|
}
|
|
@@ -92,6 +94,15 @@ export function useLogStream() {
|
|
|
92
94
|
}
|
|
93
95
|
})
|
|
94
96
|
|
|
97
|
+
es.addEventListener('notice', (event) => {
|
|
98
|
+
if (!isCurrent()) return
|
|
99
|
+
if (handlers.onNotice) {
|
|
100
|
+
try { handlers.onNotice(JSON.parse((event as MessageEvent).data)) } catch (e) {
|
|
101
|
+
console.error('Failed to parse notice event:', e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
95
106
|
es.addEventListener('end', (event) => {
|
|
96
107
|
if (!isCurrent()) return
|
|
97
108
|
endedRef.current = true
|
|
@@ -147,10 +147,10 @@ import {
|
|
|
147
147
|
type PrinterColumnDef, type PrinterTable, PRINTER_COLUMN_PREFIX, formatPrinterCell, printerCellSortValue,
|
|
148
148
|
printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
|
|
149
149
|
} from './printer-columns'
|
|
150
|
-
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
150
|
+
import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
151
151
|
import { pluralize } from '../../utils/pluralize'
|
|
152
152
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
153
|
-
import { parseQuantityToNumber } from '../../utils/format'
|
|
153
|
+
import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
|
|
154
154
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
155
155
|
import { isRolloutActivityVisible } from '../../utils/workload-rollout'
|
|
156
156
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
@@ -6128,7 +6128,7 @@ export function ResourcesView({
|
|
|
6128
6128
|
)}
|
|
6129
6129
|
|
|
6130
6130
|
{/* Column picker */}
|
|
6131
|
-
<div className="relative" ref={columnPickerRef}>
|
|
6131
|
+
<div className="relative flex" ref={columnPickerRef}>
|
|
6132
6132
|
<Tooltip content="Configure columns">
|
|
6133
6133
|
<button
|
|
6134
6134
|
onClick={() => setShowColumnPicker(prev => !prev)}
|
|
@@ -8722,7 +8722,7 @@ function NodeCell({ resource, column, majorityNodeMinorVersion }: { resource: an
|
|
|
8722
8722
|
</span>
|
|
8723
8723
|
{problems.length > 0 && (
|
|
8724
8724
|
<Tooltip content={problems.join(', ')}>
|
|
8725
|
-
<span className=
|
|
8725
|
+
<span className={SEVERITY_TEXT[healthToSeverity(status.level ?? 'unknown')]}>
|
|
8726
8726
|
<AlertTriangle className="w-3.5 h-3.5" />
|
|
8727
8727
|
</span>
|
|
8728
8728
|
</Tooltip>
|
|
@@ -8735,7 +8735,8 @@ function NodeCell({ resource, column, majorityNodeMinorVersion }: { resource: an
|
|
|
8735
8735
|
return <span className="text-sm text-theme-text-secondary">{roles}</span>
|
|
8736
8736
|
}
|
|
8737
8737
|
case 'conditions': {
|
|
8738
|
-
const { problems, healthy } = getNodeConditions(resource)
|
|
8738
|
+
const { problems, healthy, readinessLabel } = getNodeConditions(resource)
|
|
8739
|
+
if (problems.length === 0 && !healthy) return <span className="text-theme-text-secondary">{readinessLabel}</span>
|
|
8739
8740
|
if (healthy) {
|
|
8740
8741
|
return <span className="text-sm text-green-400">Healthy</span>
|
|
8741
8742
|
}
|
|
@@ -9318,9 +9319,7 @@ function LeaseCell({ resource, column }: { resource: any; column: string }) {
|
|
|
9318
9319
|
const elapsed = (Date.now() - new Date(renewTime).getTime()) / 1000
|
|
9319
9320
|
const duration = resource.spec?.leaseDurationSeconds || 0
|
|
9320
9321
|
const isStale = duration > 0 && elapsed > duration
|
|
9321
|
-
const
|
|
9322
|
-
const seconds = Math.floor(diff / 1000)
|
|
9323
|
-
const label = seconds < 60 ? `${seconds}s ago` : seconds < 3600 ? `${Math.floor(seconds / 60)}m ago` : seconds < 86400 ? `${Math.floor(seconds / 3600)}h ago` : `${Math.floor(seconds / 86400)}d ago`
|
|
9322
|
+
const label = formatRelativeAgeTime(renewTime)
|
|
9324
9323
|
return <span className={clsx('text-sm', isStale ? 'text-red-400' : 'text-green-400')} title={new Date(renewTime).toLocaleString()}>{label}</span>
|
|
9325
9324
|
}
|
|
9326
9325
|
default:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Activity, BarChart3, Server } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
3
3
|
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
4
|
-
import {
|
|
4
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
5
5
|
|
|
6
6
|
interface AnalysisRunRendererProps {
|
|
7
7
|
data: any
|
|
@@ -60,8 +60,8 @@ export function AnalysisRunRenderer({ data, onNavigate }: AnalysisRunRendererPro
|
|
|
60
60
|
<Property label="Phase" value={<Badge severity={phaseSeverity(phase)}>{phase}</Badge>} />
|
|
61
61
|
{labels['rollout-type'] && <Property label="Triggered as" value={labels['rollout-type']} />}
|
|
62
62
|
{labels['step-index'] && <Property label="Canary step" value={labels['step-index']} />}
|
|
63
|
-
<Property label="Started" value={status.startedAt ?
|
|
64
|
-
<Property label="Completed" value={status.completedAt ?
|
|
63
|
+
<Property label="Started" value={status.startedAt ? <TimeValue timestamp={status.startedAt} /> : undefined} />
|
|
64
|
+
<Property label="Completed" value={status.completedAt ? <TimeValue timestamp={status.completedAt} /> : undefined} />
|
|
65
65
|
<Property label="Message" value={status.message} />
|
|
66
66
|
{ownerRollout && (
|
|
67
67
|
<Property
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { GitBranch, FolderTree, Settings, Target, XCircle, History, ListChecks, ExternalLink } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { GitOpsStatusBadge, ManagedResourcesList, SyncCountdown } from '../../gitops'
|
|
6
5
|
import {
|
|
7
6
|
argoStatusToGitOpsStatus,
|
|
@@ -11,6 +10,8 @@ import {
|
|
|
11
10
|
} from '../../../types/gitops'
|
|
12
11
|
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
13
12
|
import { buildRepoBrowseUrl, buildPathBrowseUrl } from '../../../utils/git-provider-urls'
|
|
13
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
14
|
+
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
14
15
|
|
|
15
16
|
const REPO_LINK_CLASS = 'text-blue-400 hover:text-blue-300 hover:underline break-all'
|
|
16
17
|
|
|
@@ -297,10 +298,10 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
297
298
|
<Property label="Message" value={operationState.message} />
|
|
298
299
|
)}
|
|
299
300
|
{operationState.startedAt && (
|
|
300
|
-
<Property label="Started" value={
|
|
301
|
+
<Property label="Started" value={<TimeValue timestamp={operationState.startedAt} />} />
|
|
301
302
|
)}
|
|
302
303
|
{operationState.finishedAt && (
|
|
303
|
-
<Property label="Finished" value={
|
|
304
|
+
<Property label="Finished" value={<TimeValue timestamp={operationState.finishedAt} />} />
|
|
304
305
|
)}
|
|
305
306
|
{operationState.retryCount != null && (
|
|
306
307
|
<Property
|
|
@@ -402,7 +403,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
402
403
|
</span>
|
|
403
404
|
{entry.deployedAt && (
|
|
404
405
|
<span className="text-xs text-theme-text-tertiary">
|
|
405
|
-
{
|
|
406
|
+
{formatRelativeAgeTime(entry.deployedAt)}
|
|
406
407
|
</span>
|
|
407
408
|
)}
|
|
408
409
|
</div>
|
|
@@ -425,7 +426,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
425
426
|
<Property label="Current Revision" value={status.sync.revision} />
|
|
426
427
|
)}
|
|
427
428
|
{status.reconciledAt && (
|
|
428
|
-
<Property label="Last Reconciled" value={
|
|
429
|
+
<Property label="Last Reconciled" value={<TimeValue timestamp={status.reconciledAt} />} />
|
|
429
430
|
)}
|
|
430
431
|
</PropertyList>
|
|
431
432
|
</Section>
|
|
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Server, Globe, Network, Layers, Download, CheckCircle, AlertCircle } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
4
4
|
import { kindToPlural } from '../../../utils/navigation'
|
|
5
|
-
import { formatAge } from '../resource-utils'
|
|
6
5
|
import { getClusterStatus, getClusterClass, getClusterVersion, getClusterEndpoint, getProviderFromInfraKind, parseCAPIConditionMessage } from '../resource-utils-capi'
|
|
6
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
data: any
|
|
@@ -119,7 +119,7 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
|
|
|
119
119
|
{className !== '-' && <Property label="Cluster Class" value={className} />}
|
|
120
120
|
{endpoint !== '-' && <Property label="Control Plane Endpoint" value={endpoint} />}
|
|
121
121
|
{readyCond?.lastTransitionTime && (
|
|
122
|
-
<Property label="Since" value={
|
|
122
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
123
123
|
)}
|
|
124
124
|
</PropertyList>
|
|
125
125
|
</Section>
|