@skyhook-io/k8s-ui 1.13.0 → 1.13.4
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 +2 -2
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +41 -9
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +35 -7
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +973 -100
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +198 -0
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/trace/TracePanel.tsx +1 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +146 -83
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +62 -4
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
3
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
4
|
+
import { useProgressiveRefresh } from '../../hooks/useProgressiveRefresh'
|
|
4
5
|
import { startViewTransitionSafe } from '../../utils/view-transition'
|
|
5
6
|
import { FetchResult } from '../ui/FetchResult'
|
|
6
7
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
@@ -30,12 +31,12 @@ import {
|
|
|
30
31
|
BarChart3,
|
|
31
32
|
Network,
|
|
32
33
|
Stethoscope,
|
|
33
|
-
|
|
34
|
+
Coins,
|
|
34
35
|
} from 'lucide-react'
|
|
35
36
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
36
37
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
37
38
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
38
|
-
import { refToSelectedResource, pluralToKind, kindToPlural, apiVersionToGroup } from '../../utils/navigation'
|
|
39
|
+
import { refToSelectedResource, pluralToKind, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
|
|
39
40
|
import { neighborhoodFor, seedNodeIds } from '../../utils/topology-neighborhood'
|
|
40
41
|
import { TopologyGraph } from '../topology/TopologyGraph'
|
|
41
42
|
import { gitOpsOwnerFromRelationships, type GitOpsOwnerRef } from '../../utils/gitops-owner'
|
|
@@ -67,8 +68,12 @@ import {
|
|
|
67
68
|
getIngressAddress,
|
|
68
69
|
getIngressHosts,
|
|
69
70
|
getServiceExternalIP,
|
|
71
|
+
getWorkloadDisplayStatus,
|
|
70
72
|
} from '../resources/resource-utils'
|
|
71
73
|
import { ServicePortCards, type ServicePortRenderProps } from '../resources/renderers/ServiceRenderer'
|
|
74
|
+
import { rolloutMayAdvanceAutomatically, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
|
|
75
|
+
import { WorkloadRolloutNotice } from './WorkloadRolloutNotice'
|
|
76
|
+
import { isCoreBatchJob } from '../../utils/api-resources'
|
|
72
77
|
|
|
73
78
|
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
|
|
74
79
|
type TabType = WorkloadTabType
|
|
@@ -465,6 +470,23 @@ export function WorkloadView({
|
|
|
465
470
|
const [initialContainer, setInitialContainer] = useState<string | null>(null)
|
|
466
471
|
const [copied, setCopied] = useState<string | null>(null)
|
|
467
472
|
const [saveSuccess, setSaveSuccess] = useState(false)
|
|
473
|
+
const [recentImageSave, setRecentImageSave] = useState(false)
|
|
474
|
+
const sawImageRollout = useRef(false)
|
|
475
|
+
|
|
476
|
+
const effectiveActionsBarProps = useMemo(() => {
|
|
477
|
+
const onSetImages = actionsBarProps?.onSetImages as ((params: unknown) => Promise<unknown>) | undefined
|
|
478
|
+
if (!onSetImages) return actionsBarProps
|
|
479
|
+
return {
|
|
480
|
+
...actionsBarProps,
|
|
481
|
+
onSetImages: async (params: unknown) => {
|
|
482
|
+
const result = await onSetImages(params)
|
|
483
|
+
sawImageRollout.current = false
|
|
484
|
+
setRecentImageSave(true)
|
|
485
|
+
refetchProp?.()
|
|
486
|
+
return result
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
}, [actionsBarProps, refetchProp])
|
|
468
490
|
|
|
469
491
|
// Refresh animation
|
|
470
492
|
const [refetch, isRefreshAnimating, refreshPhase] = useRefreshAnimation(refetchProp ?? (() => {}))
|
|
@@ -534,11 +556,12 @@ export function WorkloadView({
|
|
|
534
556
|
const handleTopologyNodeClick = useCallback(
|
|
535
557
|
(node: TopologyNode) => {
|
|
536
558
|
if (!onNavigateToResource || !node.kind || !node.name) return
|
|
559
|
+
const group = apiVersionToGroup(node.data?.apiVersion as string | undefined)
|
|
537
560
|
onNavigateToResource({
|
|
538
|
-
kind:
|
|
561
|
+
kind: kindToPluralWithGroup(node.kind, group),
|
|
539
562
|
namespace: (node.data?.namespace as string) || '',
|
|
540
563
|
name: node.name,
|
|
541
|
-
group
|
|
564
|
+
group,
|
|
542
565
|
})
|
|
543
566
|
},
|
|
544
567
|
[onNavigateToResource],
|
|
@@ -678,7 +701,24 @@ export function WorkloadView({
|
|
|
678
701
|
},
|
|
679
702
|
], [active, expanded, onBack, onClose, switchView]))
|
|
680
703
|
|
|
681
|
-
const
|
|
704
|
+
const rolloutDisplay = resource && isRolloutStatusKind(apiKind)
|
|
705
|
+
? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
|
|
706
|
+
: null
|
|
707
|
+
const rolloutActivity = rolloutDisplay?.activity ?? null
|
|
708
|
+
const status = rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
|
|
709
|
+
const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
|
|
710
|
+
useEffect(() => {
|
|
711
|
+
if (!recentImageSave) return
|
|
712
|
+
if (rolloutActivity?.phase !== 'idle') sawImageRollout.current = true
|
|
713
|
+
if (sawImageRollout.current && rolloutActivity?.phase === 'idle') {
|
|
714
|
+
setRecentImageSave(false)
|
|
715
|
+
sawImageRollout.current = false
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
|
|
719
|
+
return () => window.clearTimeout(timeout)
|
|
720
|
+
}, [recentImageSave, rolloutActivity?.phase])
|
|
721
|
+
useProgressiveRefresh(rolloutMayAutoAdvance || recentImageSave, refetchProp)
|
|
682
722
|
const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
|
|
683
723
|
const headerImage = metadata.find(m => m.label === 'Image')?.value
|
|
684
724
|
|
|
@@ -699,7 +739,10 @@ export function WorkloadView({
|
|
|
699
739
|
|
|
700
740
|
const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
|
|
701
741
|
const showCostTab = isCostAvailable ? isCostAvailable(kind, resource) : false
|
|
702
|
-
const
|
|
742
|
+
const normalizedKind = kindToPlural(kind).toLowerCase()
|
|
743
|
+
const logsWithoutPods = LOGS_TAB_WITHOUT_PODS_KINDS.has(normalizedKind) &&
|
|
744
|
+
(normalizedKind !== 'jobs' || isCoreBatchJob(kind, group))
|
|
745
|
+
const logsTabVisible = Boolean(renderLogsTab) && (allPods.length > 0 || logsWithoutPods)
|
|
703
746
|
const metricsTabVisible = Boolean(showMetricsTab && renderMetricsTab)
|
|
704
747
|
const costTabVisible = Boolean(showCostTab && renderCostTab)
|
|
705
748
|
const podEvidenceLoading = resourceLoading || workloadPodsLoading || eventsLoading
|
|
@@ -722,7 +765,7 @@ export function WorkloadView({
|
|
|
722
765
|
icon: <Stethoscope className="w-4 h-4" />,
|
|
723
766
|
hidden: !(renderDiagnoseTab && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
|
|
724
767
|
},
|
|
725
|
-
{ id: 'cost', label: 'Cost', icon: <
|
|
768
|
+
{ id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
|
|
726
769
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
727
770
|
]
|
|
728
771
|
const requestedTabAvailable = tabs.some((tab) => tab.id === requestedTab && !tab.hidden)
|
|
@@ -831,7 +874,7 @@ export function WorkloadView({
|
|
|
831
874
|
</div>
|
|
832
875
|
|
|
833
876
|
{/* Actions bar */}
|
|
834
|
-
<ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...
|
|
877
|
+
<ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...effectiveActionsBarProps} />
|
|
835
878
|
</div>
|
|
836
879
|
|
|
837
880
|
{/* Success animation overlay */}
|
|
@@ -875,6 +918,7 @@ export function WorkloadView({
|
|
|
875
918
|
certificateInfo={certificateInfo}
|
|
876
919
|
hpaDiagnosis={hpaDiagnosis}
|
|
877
920
|
scalerDiagnostics={scalerDiagnostics}
|
|
921
|
+
workloadPods={workloadPods}
|
|
878
922
|
onCopy={copyToClipboard}
|
|
879
923
|
copied={copied}
|
|
880
924
|
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
@@ -1010,10 +1054,17 @@ export function WorkloadView({
|
|
|
1010
1054
|
activeTab={effectiveTab}
|
|
1011
1055
|
onTabChange={handleSetTab}
|
|
1012
1056
|
scopeControls={scopeControls}
|
|
1013
|
-
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...
|
|
1057
|
+
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...effectiveActionsBarProps} />}
|
|
1014
1058
|
overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
|
|
1015
1059
|
compactHeader={compactHeader}
|
|
1016
1060
|
>
|
|
1061
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
1062
|
+
{effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
|
|
1063
|
+
<div className="shrink-0 px-4 pt-4">
|
|
1064
|
+
<WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
<div className="min-h-0 flex-1">
|
|
1017
1068
|
{effectiveTab === 'overview' && expandedOverview ? (
|
|
1018
1069
|
<div className="h-full min-h-0">
|
|
1019
1070
|
{hasOperationalIssues && renderOverviewLead && (
|
|
@@ -1059,6 +1110,7 @@ export function WorkloadView({
|
|
|
1059
1110
|
introContent={overviewIntro}
|
|
1060
1111
|
leadContent={hasOperationalIssues && renderOverviewLead ? renderOverviewLead({ kind, namespace, name }) : undefined}
|
|
1061
1112
|
onEvaluateCapacity={onEvaluateCapacity}
|
|
1113
|
+
recentImageSave={recentImageSave}
|
|
1062
1114
|
/>
|
|
1063
1115
|
)}
|
|
1064
1116
|
{effectiveTab === 'topology' && (
|
|
@@ -1168,6 +1220,8 @@ export function WorkloadView({
|
|
|
1168
1220
|
</div>
|
|
1169
1221
|
</div>
|
|
1170
1222
|
)}
|
|
1223
|
+
</div>
|
|
1224
|
+
</div>
|
|
1171
1225
|
</DetailShell>
|
|
1172
1226
|
</OperationalIssuesShownContext.Provider>
|
|
1173
1227
|
)
|
|
@@ -1533,6 +1587,7 @@ function InfoTab({
|
|
|
1533
1587
|
introContent,
|
|
1534
1588
|
leadContent,
|
|
1535
1589
|
onEvaluateCapacity,
|
|
1590
|
+
recentImageSave,
|
|
1536
1591
|
}: {
|
|
1537
1592
|
resource: any
|
|
1538
1593
|
selectedResource: SelectedResource
|
|
@@ -1568,12 +1623,13 @@ function InfoTab({
|
|
|
1568
1623
|
extraContent?: ReactNode
|
|
1569
1624
|
introContent?: ReactNode
|
|
1570
1625
|
leadContent?: ReactNode
|
|
1626
|
+
recentImageSave?: boolean
|
|
1571
1627
|
}) {
|
|
1572
1628
|
if (!resource) {
|
|
1573
1629
|
return <FetchResult loading={isLoading} error={error} className="h-full" />
|
|
1574
1630
|
}
|
|
1575
1631
|
|
|
1576
|
-
if (!isRuntimeWorkloadOverviewKind(selectedResource.kind)) {
|
|
1632
|
+
if (!isRuntimeWorkloadOverviewKind(selectedResource.kind, selectedResource.group)) {
|
|
1577
1633
|
return (
|
|
1578
1634
|
<div className="h-full overflow-auto">
|
|
1579
1635
|
{leadContent && (
|
|
@@ -1655,6 +1711,7 @@ function InfoTab({
|
|
|
1655
1711
|
introContent={introContent}
|
|
1656
1712
|
leadContent={leadContent}
|
|
1657
1713
|
onEvaluateCapacity={onEvaluateCapacity}
|
|
1714
|
+
recentImageSave={recentImageSave}
|
|
1658
1715
|
/>
|
|
1659
1716
|
)
|
|
1660
1717
|
}
|
|
@@ -1673,10 +1730,17 @@ const LOGS_TAB_WITHOUT_PODS_KINDS = new Set([
|
|
|
1673
1730
|
'scaledjobs',
|
|
1674
1731
|
])
|
|
1675
1732
|
const RUNTIME_WORKLOAD_OVERVIEW_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs'])
|
|
1733
|
+
const ROLLOUT_STATUS_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'rollouts'])
|
|
1676
1734
|
type RuntimeOverviewShape = 'replicated' | 'job' | 'cronjob'
|
|
1677
1735
|
|
|
1678
|
-
function isRuntimeWorkloadOverviewKind(kind: string) {
|
|
1679
|
-
|
|
1736
|
+
function isRuntimeWorkloadOverviewKind(kind: string, group?: string) {
|
|
1737
|
+
const normalizedKind = kindToPlural(kind).toLowerCase()
|
|
1738
|
+
return RUNTIME_WORKLOAD_OVERVIEW_KINDS.has(normalizedKind) &&
|
|
1739
|
+
(normalizedKind !== 'jobs' || isCoreBatchJob(kind, group))
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function isRolloutStatusKind(kind: string) {
|
|
1743
|
+
return ROLLOUT_STATUS_KINDS.has(kindToPlural(kind).toLowerCase())
|
|
1680
1744
|
}
|
|
1681
1745
|
|
|
1682
1746
|
function runtimeOverviewShape(kind: string): RuntimeOverviewShape {
|
|
@@ -1711,6 +1775,7 @@ function WorkloadOverviewTab({
|
|
|
1711
1775
|
introContent,
|
|
1712
1776
|
leadContent,
|
|
1713
1777
|
onEvaluateCapacity,
|
|
1778
|
+
recentImageSave,
|
|
1714
1779
|
}: {
|
|
1715
1780
|
resource: any
|
|
1716
1781
|
selectedResource: SelectedResource
|
|
@@ -1736,8 +1801,9 @@ function WorkloadOverviewTab({
|
|
|
1736
1801
|
extraContent?: ReactNode
|
|
1737
1802
|
introContent?: ReactNode
|
|
1738
1803
|
leadContent?: ReactNode
|
|
1804
|
+
recentImageSave?: boolean
|
|
1739
1805
|
}) {
|
|
1740
|
-
const apiKind =
|
|
1806
|
+
const apiKind = kindToPluralWithGroup(selectedResource.kind, selectedResource.group ?? '')
|
|
1741
1807
|
const resourceKind = resource?.kind || displayKindName(apiKind, resource?.kind)
|
|
1742
1808
|
const readiness = getReadinessSummary(resource, apiKind)
|
|
1743
1809
|
const podSummary = getPodSummary(workloadPods, relationships, apiKind)
|
|
@@ -1758,7 +1824,7 @@ function WorkloadOverviewTab({
|
|
|
1758
1824
|
: 'Related resources'
|
|
1759
1825
|
|
|
1760
1826
|
if (overviewShape === 'replicated') {
|
|
1761
|
-
const state = getReplicatedWorkloadState(resource, apiKind)
|
|
1827
|
+
const state = getReplicatedWorkloadState(resource, apiKind, workloadPods)
|
|
1762
1828
|
const serviceAccountName = getPodTemplateSpec(resource, apiKind)?.serviceAccountName || 'default'
|
|
1763
1829
|
return (
|
|
1764
1830
|
<div className="h-full overflow-auto bg-theme-base">
|
|
@@ -1771,6 +1837,7 @@ function WorkloadOverviewTab({
|
|
|
1771
1837
|
readiness={readiness}
|
|
1772
1838
|
podSummary={podSummary}
|
|
1773
1839
|
servingSummary={showServingPath ? buildServingStripSummary(servingRelationshipGroups, servingResources) : undefined}
|
|
1840
|
+
recentImageSave={recentImageSave}
|
|
1774
1841
|
/>
|
|
1775
1842
|
|
|
1776
1843
|
{onEvaluateCapacity && (
|
|
@@ -1989,9 +2056,10 @@ interface ReplicatedWorkloadState {
|
|
|
1989
2056
|
label: string
|
|
1990
2057
|
detail: string
|
|
1991
2058
|
badgeClass: string
|
|
1992
|
-
level: 'healthy' | 'neutral' | 'degraded' | 'unhealthy' | 'unknown'
|
|
2059
|
+
level: 'healthy' | 'neutral' | 'degraded' | 'alert' | 'unhealthy' | 'unknown'
|
|
1993
2060
|
healthy: boolean
|
|
1994
2061
|
needsAttention: boolean
|
|
2062
|
+
rollout: WorkloadRolloutActivity
|
|
1995
2063
|
}
|
|
1996
2064
|
|
|
1997
2065
|
function WorkloadStatusStrip({
|
|
@@ -1999,14 +2067,21 @@ function WorkloadStatusStrip({
|
|
|
1999
2067
|
readiness,
|
|
2000
2068
|
podSummary,
|
|
2001
2069
|
servingSummary,
|
|
2070
|
+
recentImageSave,
|
|
2002
2071
|
}: {
|
|
2003
2072
|
state: ReplicatedWorkloadState
|
|
2004
2073
|
readiness: ReadinessSummary
|
|
2005
2074
|
podSummary: ReturnType<typeof getPodSummary>
|
|
2006
2075
|
servingSummary?: string
|
|
2076
|
+
recentImageSave?: boolean
|
|
2007
2077
|
}) {
|
|
2008
2078
|
const servingMode = Boolean(servingSummary)
|
|
2009
|
-
const
|
|
2079
|
+
const baseStateDetail = servingMode && state.healthy ? 'runtime health' : state.detail
|
|
2080
|
+
const stateDetail = recentImageSave
|
|
2081
|
+
? state.rollout.phase === 'idle'
|
|
2082
|
+
? 'Image saved · waiting for controller status'
|
|
2083
|
+
: `Image saved · ${baseStateDetail}`
|
|
2084
|
+
: baseStateDetail
|
|
2010
2085
|
const podItem = servingMode && typeof podSummary.total === 'number'
|
|
2011
2086
|
? {
|
|
2012
2087
|
label: 'Pods',
|
|
@@ -2040,50 +2115,19 @@ function WorkloadStatusStrip({
|
|
|
2040
2115
|
)
|
|
2041
2116
|
}
|
|
2042
2117
|
|
|
2043
|
-
function getReplicatedWorkloadState(resource: any, apiKind: string): ReplicatedWorkloadState {
|
|
2044
|
-
const status = resource
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
const k = apiKind.toLowerCase()
|
|
2048
|
-
const generation = metadata.generation
|
|
2049
|
-
const observedGeneration = status.observedGeneration
|
|
2050
|
-
const controllerBehind =
|
|
2051
|
-
typeof generation === 'number' &&
|
|
2052
|
-
typeof observedGeneration === 'number' &&
|
|
2053
|
-
observedGeneration < generation
|
|
2054
|
-
|
|
2055
|
-
if (k === 'daemonsets') {
|
|
2056
|
-
const desired = status.desiredNumberScheduled ?? 0
|
|
2057
|
-
const ready = status.numberReady ?? 0
|
|
2058
|
-
const updated = status.updatedNumberScheduled ?? 0
|
|
2059
|
-
const unavailable = status.numberUnavailable ?? Math.max(0, desired - ready)
|
|
2060
|
-
if (desired === 0) return stateSummary('No targets', 'selector matches no nodes', 'status-neutral', 'neutral')
|
|
2061
|
-
if (controllerBehind) return stateSummary('Applying', 'controller has not observed latest spec', 'status-neutral', 'neutral')
|
|
2062
|
-
if (updated < desired) return stateSummary('Rolling out', `${updated}/${desired} updated`, 'status-neutral', 'neutral')
|
|
2063
|
-
if (ready === desired && unavailable === 0) return stateSummary('Healthy', `${ready}/${desired} ready`, 'status-healthy', 'healthy')
|
|
2064
|
-
if (ready > 0) return stateSummary('Degraded', `${ready}/${desired} ready`, 'status-degraded', 'degraded')
|
|
2065
|
-
return stateSummary('Unhealthy', 'no scheduled pods are ready', 'status-unhealthy', 'unhealthy')
|
|
2118
|
+
function getReplicatedWorkloadState(resource: any, apiKind: string, workloadPods?: WorkloadPodInfo[]): ReplicatedWorkloadState {
|
|
2119
|
+
const { activity: rollout, status } = getWorkloadDisplayStatus(resource, apiKind, workloadPods)
|
|
2120
|
+
if (rollout.phase !== 'idle' || rollout.label !== 'Stable') {
|
|
2121
|
+
return stateSummary(rollout.label, rollout.detail || '', status.color, status.level, rollout)
|
|
2066
2122
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
const progressCondition = Array.isArray(status.conditions)
|
|
2076
|
-
? status.conditions.find((condition: any) => condition.type === 'Progressing')
|
|
2077
|
-
: undefined
|
|
2078
|
-
if (progressCondition?.status === 'False' || progressCondition?.reason === 'ProgressDeadlineExceeded') {
|
|
2079
|
-
return stateSummary('Stalled', progressCondition.message || `${ready}/${desired} ready`, 'status-unhealthy', 'unhealthy')
|
|
2080
|
-
}
|
|
2081
|
-
if (controllerBehind) return stateSummary('Applying', 'controller has not observed latest spec', 'status-neutral', 'neutral')
|
|
2082
|
-
const statefulSetRolling = k === 'statefulsets' && status.updateRevision && status.currentRevision && status.updateRevision !== status.currentRevision
|
|
2083
|
-
if (updated < desired || statefulSetRolling) return stateSummary('Rolling out', `${updated}/${desired} updated`, 'status-neutral', 'neutral')
|
|
2084
|
-
if (ready === desired && available === desired && unavailable === 0) return stateSummary('Healthy', `${ready}/${desired} ready`, 'status-healthy', 'healthy')
|
|
2085
|
-
if (ready > 0) return stateSummary('Degraded', `${ready}/${desired} ready`, 'status-degraded', 'degraded')
|
|
2086
|
-
return stateSummary('Unhealthy', 'no replicas are ready', 'status-unhealthy', 'unhealthy')
|
|
2123
|
+
const label = status.level === 'healthy'
|
|
2124
|
+
? 'Healthy'
|
|
2125
|
+
: status.level === 'degraded'
|
|
2126
|
+
? 'Degraded'
|
|
2127
|
+
: status.level === 'unhealthy'
|
|
2128
|
+
? 'Unhealthy'
|
|
2129
|
+
: 'Stable'
|
|
2130
|
+
return stateSummary(label, rollout.detail || status.text, status.color, status.level, rollout)
|
|
2087
2131
|
}
|
|
2088
2132
|
|
|
2089
2133
|
function stateSummary(
|
|
@@ -2091,6 +2135,7 @@ function stateSummary(
|
|
|
2091
2135
|
detail: string,
|
|
2092
2136
|
badgeClass: string,
|
|
2093
2137
|
level: ReplicatedWorkloadState['level'],
|
|
2138
|
+
rollout: WorkloadRolloutActivity,
|
|
2094
2139
|
): ReplicatedWorkloadState {
|
|
2095
2140
|
return {
|
|
2096
2141
|
label,
|
|
@@ -2098,7 +2143,8 @@ function stateSummary(
|
|
|
2098
2143
|
badgeClass,
|
|
2099
2144
|
level,
|
|
2100
2145
|
healthy: level === 'healthy',
|
|
2101
|
-
needsAttention: level === 'degraded' || level === 'unhealthy',
|
|
2146
|
+
needsAttention: level === 'degraded' || level === 'alert' || level === 'unhealthy',
|
|
2147
|
+
rollout,
|
|
2102
2148
|
}
|
|
2103
2149
|
}
|
|
2104
2150
|
|
|
@@ -2247,16 +2293,16 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2247
2293
|
const available = status.numberAvailable ?? ready
|
|
2248
2294
|
const unavailable = status.numberUnavailable ?? Math.max(0, desired - available)
|
|
2249
2295
|
const noTargets = desired === 0
|
|
2250
|
-
const healthy = !noTargets && ready
|
|
2296
|
+
const healthy = !noTargets && ready >= desired && unavailable === 0
|
|
2251
2297
|
return {
|
|
2252
|
-
readyLabel: `${ready}/${desired}`,
|
|
2298
|
+
readyLabel: `${Math.min(ready, desired)}/${desired}`,
|
|
2253
2299
|
detail: `${updated}/${desired} up-to-date`,
|
|
2254
2300
|
healthLabel: noTargets ? 'No targets' : healthy ? 'Healthy' : 'Degraded',
|
|
2255
2301
|
healthBadge: noTargets ? 'bg-theme-elevated text-theme-text-secondary' : healthy ? 'status-healthy' : 'status-degraded',
|
|
2256
2302
|
hasIssue: !healthy && !noTargets,
|
|
2257
2303
|
stats: [
|
|
2258
2304
|
{ label: 'Desired', value: desired },
|
|
2259
|
-
{ label: 'Ready', value: ready, tone: ready
|
|
2305
|
+
{ label: 'Ready', value: Math.min(ready, desired), tone: ready >= desired ? 'good' : 'warn' },
|
|
2260
2306
|
{ label: 'Available', value: available },
|
|
2261
2307
|
{ label: 'Unavailable', value: unavailable, tone: unavailable > 0 ? 'warn' : 'default' },
|
|
2262
2308
|
],
|
|
@@ -2307,16 +2353,17 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2307
2353
|
}
|
|
2308
2354
|
}
|
|
2309
2355
|
|
|
2310
|
-
const desired = spec.replicas ??
|
|
2356
|
+
const desired = spec.replicas ?? 1
|
|
2311
2357
|
const current = status.replicas ?? desired
|
|
2312
2358
|
const ready = status.readyReplicas ?? 0
|
|
2359
|
+
const targetReady = Math.min(ready, desired)
|
|
2313
2360
|
const updated = status.updatedReplicas ?? 0
|
|
2314
2361
|
const available = status.availableReplicas ?? ready
|
|
2315
2362
|
const unavailable = status.unavailableReplicas ?? Math.max(0, desired - available)
|
|
2316
2363
|
const scaledToZero = desired === 0
|
|
2317
|
-
const healthy = !scaledToZero && ready
|
|
2364
|
+
const healthy = !scaledToZero && ready >= desired && unavailable === 0
|
|
2318
2365
|
return {
|
|
2319
|
-
readyLabel: `${
|
|
2366
|
+
readyLabel: `${targetReady}/${desired}`,
|
|
2320
2367
|
detail: `${updated}/${desired} updated · ${available} available`,
|
|
2321
2368
|
healthLabel: scaledToZero ? 'Scaled to zero' : healthy ? 'Healthy' : 'Degraded',
|
|
2322
2369
|
healthBadge: scaledToZero ? 'bg-theme-elevated text-theme-text-secondary' : healthy ? 'status-healthy' : 'status-degraded',
|
|
@@ -2324,7 +2371,7 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2324
2371
|
stats: [
|
|
2325
2372
|
{ label: 'Desired', value: desired },
|
|
2326
2373
|
{ label: 'Current', value: current },
|
|
2327
|
-
{ label: 'Ready', value:
|
|
2374
|
+
{ label: 'Ready', value: targetReady, tone: ready >= desired ? 'good' : 'warn' },
|
|
2328
2375
|
{ label: 'Updated', value: updated },
|
|
2329
2376
|
{ label: 'Available', value: available },
|
|
2330
2377
|
{ label: 'Unavailable', value: unavailable, tone: unavailable > 0 ? 'warn' : 'default' },
|
|
@@ -2455,7 +2502,7 @@ function CurrentStateCard({ resource, apiKind, readiness }: { resource: any; api
|
|
|
2455
2502
|
const metadata = resource?.metadata || {}
|
|
2456
2503
|
const templateSpec = getPodTemplateSpec(resource, apiKind)
|
|
2457
2504
|
const containers = templateSpec?.containers ?? []
|
|
2458
|
-
const
|
|
2505
|
+
const images = containers.filter((container: any) => container?.name && container?.image)
|
|
2459
2506
|
const k = apiKind.toLowerCase()
|
|
2460
2507
|
const title = k === 'jobs' ? 'Execution state' : k === 'cronjobs' ? 'Schedule state' : 'Current state'
|
|
2461
2508
|
const generation = metadata.generation
|
|
@@ -2482,7 +2529,21 @@ function CurrentStateCard({ resource, apiKind, readiness }: { resource: any; api
|
|
|
2482
2529
|
<div className="mt-4 grid gap-2 border-t border-theme-border pt-3 text-sm sm:grid-cols-2">
|
|
2483
2530
|
<InlineFact label="Kind" value={displayKindName(apiKind, resource?.kind)} />
|
|
2484
2531
|
{templateSpec && <InlineFact label="Service account" value={templateSpec.serviceAccountName || 'default'} />}
|
|
2485
|
-
{
|
|
2532
|
+
{images.length === 1 && <InlineFact label="Image" value={<span className="break-all font-mono text-xs">{images[0].image}</span>} />}
|
|
2533
|
+
{images.length > 1 && (
|
|
2534
|
+
<InlineFact
|
|
2535
|
+
label="Images"
|
|
2536
|
+
value={(
|
|
2537
|
+
<span className="flex min-w-0 flex-col gap-1">
|
|
2538
|
+
{images.map((container: any) => (
|
|
2539
|
+
<span key={container.name} className="min-w-0 break-all font-mono text-xs">
|
|
2540
|
+
<span className="text-theme-text-tertiary">{container.name}:</span> {container.image}
|
|
2541
|
+
</span>
|
|
2542
|
+
))}
|
|
2543
|
+
</span>
|
|
2544
|
+
)}
|
|
2545
|
+
/>
|
|
2546
|
+
)}
|
|
2486
2547
|
</div>
|
|
2487
2548
|
</OverviewCard>
|
|
2488
2549
|
)
|
|
@@ -2580,7 +2641,9 @@ function RuntimeCard({
|
|
|
2580
2641
|
onSwitchToLogs?: () => void
|
|
2581
2642
|
}) {
|
|
2582
2643
|
const progress = rolloutProgress(resource, apiKind, state)
|
|
2583
|
-
const headline =
|
|
2644
|
+
const headline = state.rollout.phase !== 'idle'
|
|
2645
|
+
? `${state.label}${state.detail ? ` · ${state.detail}` : ''}`
|
|
2646
|
+
: rolloutHeadline(resource, apiKind, readiness, scaling)
|
|
2584
2647
|
const facts = rolloutFacts(resource, apiKind, scaling, Boolean(progress))
|
|
2585
2648
|
const scalerRefs = relationships?.scalers ?? []
|
|
2586
2649
|
const navigateRef = onNavigate ? (ref: ResourceRef) => onNavigate(refToSelectedResource(ref)) : undefined
|
|
@@ -3311,7 +3374,7 @@ function RolloutProgressBar({ progress, state }: { progress: RolloutProgressSumm
|
|
|
3311
3374
|
|
|
3312
3375
|
function rolloutProgressFillClass(level: ReplicatedWorkloadState['level']): string {
|
|
3313
3376
|
if (level === 'unhealthy') return 'bg-red-500'
|
|
3314
|
-
if (level === 'degraded') return 'bg-amber-500'
|
|
3377
|
+
if (level === 'degraded' || level === 'alert') return 'bg-amber-500'
|
|
3315
3378
|
return 'bg-accent'
|
|
3316
3379
|
}
|
|
3317
3380
|
|
|
@@ -3333,7 +3396,7 @@ function RolloutFactChip({ label, value, tone = 'default' }: { label: string; va
|
|
|
3333
3396
|
|
|
3334
3397
|
function rolloutStateDotClass(level: ReplicatedWorkloadState['level']): string {
|
|
3335
3398
|
if (level === 'unhealthy') return 'bg-red-500'
|
|
3336
|
-
if (level === 'degraded') return 'bg-amber-500'
|
|
3399
|
+
if (level === 'degraded' || level === 'alert') return 'bg-amber-500'
|
|
3337
3400
|
if (level === 'neutral') return 'bg-skyhook-500'
|
|
3338
3401
|
if (level === 'unknown') return 'bg-theme-text-tertiary'
|
|
3339
3402
|
return 'bg-emerald-500'
|
|
@@ -3373,7 +3436,7 @@ function rolloutFacts(
|
|
|
3373
3436
|
return facts
|
|
3374
3437
|
}
|
|
3375
3438
|
|
|
3376
|
-
const desired = spec.replicas ??
|
|
3439
|
+
const desired = spec.replicas ?? 1
|
|
3377
3440
|
const current = status.replicas ?? desired
|
|
3378
3441
|
const updated = status.updatedReplicas ?? 0
|
|
3379
3442
|
const unavailable = status.unavailableReplicas ?? Math.max(0, desired - (status.availableReplicas ?? status.readyReplicas ?? 0))
|
|
@@ -3407,13 +3470,14 @@ function rolloutProgress(resource: any, apiKind: string, state: ReplicatedWorklo
|
|
|
3407
3470
|
const spec = resource?.spec || {}
|
|
3408
3471
|
const status = resource?.status || {}
|
|
3409
3472
|
const k = apiKind.toLowerCase()
|
|
3473
|
+
if (state.rollout.phase === 'idle' || state.rollout.phase === 'partition-reached' || state.rollout.phase === 'applying') return null
|
|
3410
3474
|
|
|
3411
3475
|
if (k === 'daemonsets') {
|
|
3412
3476
|
const desired = status.desiredNumberScheduled ?? 0
|
|
3413
3477
|
const updated = status.updatedNumberScheduled ?? 0
|
|
3414
3478
|
const ready = status.numberReady ?? 0
|
|
3415
3479
|
const unavailable = status.numberUnavailable ?? Math.max(0, desired - ready)
|
|
3416
|
-
if (!shouldShowRolloutProgress(desired,
|
|
3480
|
+
if (!shouldShowRolloutProgress(desired, state)) return null
|
|
3417
3481
|
return {
|
|
3418
3482
|
updated,
|
|
3419
3483
|
desired,
|
|
@@ -3424,31 +3488,30 @@ function rolloutProgress(resource: any, apiKind: string, state: ReplicatedWorklo
|
|
|
3424
3488
|
}
|
|
3425
3489
|
}
|
|
3426
3490
|
|
|
3427
|
-
const
|
|
3491
|
+
const configuredDesired = spec.replicas ?? 1
|
|
3492
|
+
const desired = k === 'statefulsets'
|
|
3493
|
+
? Math.max(0, configuredDesired - (spec.updateStrategy?.rollingUpdate?.partition ?? 0))
|
|
3494
|
+
: configuredDesired
|
|
3428
3495
|
const updated = status.updatedReplicas ?? 0
|
|
3429
3496
|
const ready = status.readyReplicas ?? 0
|
|
3430
|
-
const unavailable = status.unavailableReplicas ?? Math.max(0,
|
|
3431
|
-
if (!shouldShowRolloutProgress(desired,
|
|
3497
|
+
const unavailable = status.unavailableReplicas ?? Math.max(0, configuredDesired - (status.availableReplicas ?? ready))
|
|
3498
|
+
if (!shouldShowRolloutProgress(desired, state)) return null
|
|
3432
3499
|
return {
|
|
3433
3500
|
updated,
|
|
3434
3501
|
desired,
|
|
3435
3502
|
ready,
|
|
3436
3503
|
unavailable,
|
|
3437
3504
|
updatedLabel: `${updated}/${desired} updated`,
|
|
3438
|
-
readyLabel: `${ready}/${
|
|
3505
|
+
readyLabel: `${Math.min(ready, configuredDesired)}/${configuredDesired} ready`,
|
|
3439
3506
|
}
|
|
3440
3507
|
}
|
|
3441
3508
|
|
|
3442
3509
|
function shouldShowRolloutProgress(
|
|
3443
3510
|
desired: number,
|
|
3444
|
-
updated: number,
|
|
3445
|
-
ready: number,
|
|
3446
|
-
unavailable: number,
|
|
3447
3511
|
state: ReplicatedWorkloadState,
|
|
3448
3512
|
): boolean {
|
|
3449
3513
|
if (desired <= 0) return false
|
|
3450
|
-
|
|
3451
|
-
return state.label === 'Rolling out' || state.label === 'Applying' || state.label === 'Stalled'
|
|
3514
|
+
return state.rollout.active || state.rollout.phase === 'stalled'
|
|
3452
3515
|
}
|
|
3453
3516
|
|
|
3454
3517
|
function rolloutHeadline(resource: any, apiKind: string, readiness: ReadinessSummary, scaling: ScalingSummary): string {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
const FAST_REFRESH_INTERVAL = 2000
|
|
4
|
+
const SLOW_REFRESH_INTERVAL = 15000
|
|
5
|
+
const FAST_REFRESH_DURATION = 120000
|
|
6
|
+
|
|
7
|
+
export function useProgressiveRefresh(
|
|
8
|
+
enabled: boolean,
|
|
9
|
+
refresh?: () => void | Promise<unknown>,
|
|
10
|
+
) {
|
|
11
|
+
const refreshRef = useRef(refresh)
|
|
12
|
+
refreshRef.current = refresh
|
|
13
|
+
const hasRefresh = Boolean(refresh)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!enabled || !hasRefresh) return
|
|
17
|
+
const startedAt = Date.now()
|
|
18
|
+
let cancelled = false
|
|
19
|
+
let timeout: number | undefined
|
|
20
|
+
const schedule = () => {
|
|
21
|
+
const delay = Date.now() - startedAt < FAST_REFRESH_DURATION ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL
|
|
22
|
+
timeout = window.setTimeout(() => {
|
|
23
|
+
void Promise.resolve()
|
|
24
|
+
.then(() => refreshRef.current?.())
|
|
25
|
+
.catch(() => undefined)
|
|
26
|
+
.finally(() => {
|
|
27
|
+
if (!cancelled) schedule()
|
|
28
|
+
})
|
|
29
|
+
}, delay)
|
|
30
|
+
}
|
|
31
|
+
schedule()
|
|
32
|
+
return () => {
|
|
33
|
+
cancelled = true
|
|
34
|
+
if (timeout !== undefined) window.clearTimeout(timeout)
|
|
35
|
+
}
|
|
36
|
+
}, [enabled, hasRefresh])
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
// A stray NUL in a source file compiles fine and passes every type and unit
|
|
6
|
+
// check, so nothing else here would catch one. What it does break is the
|
|
7
|
+
// tooling around the file: grep treats the file as binary and silently reports
|
|
8
|
+
// no matches, which is a genuinely confusing way to lose an afternoon. This
|
|
9
|
+
// package is also published as source, so the byte would ship to consumers.
|
|
10
|
+
function sourceFiles(dir: string): string[] {
|
|
11
|
+
const out: string[] = []
|
|
12
|
+
for (const entry of readdirSync(dir)) {
|
|
13
|
+
if (entry === 'node_modules' || entry === 'dist') continue
|
|
14
|
+
const full = join(dir, entry)
|
|
15
|
+
if (statSync(full).isDirectory()) out.push(...sourceFiles(full))
|
|
16
|
+
else if (/\.(ts|tsx|css)$/.test(entry)) out.push(full)
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('source hygiene', () => {
|
|
22
|
+
it('has no NUL bytes in any source file', () => {
|
|
23
|
+
const offenders = sourceFiles(join(__dirname))
|
|
24
|
+
.filter(f => readFileSync(f).includes(0x00))
|
|
25
|
+
.map(f => f.slice(f.indexOf('/src/')))
|
|
26
|
+
expect(offenders).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
--color-theme-text-disabled: var(--text-disabled);
|
|
23
23
|
--color-warning-text: var(--warning-text);
|
|
24
24
|
|
|
25
|
+
/* Semantic status */
|
|
26
|
+
--color-semantic-warning: var(--color-warning);
|
|
27
|
+
--color-semantic-error: var(--color-error);
|
|
28
|
+
|
|
25
29
|
/* Borders */
|
|
26
30
|
--color-theme-border: var(--border-default);
|
|
27
31
|
--color-theme-border-light: var(--border-light);
|