@skyhook-io/k8s-ui 1.8.9 → 1.8.11
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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useEffect, useCallback, useRef, useContext, u
|
|
|
2
2
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
3
3
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
4
4
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
5
|
+
import { Input } from '../ui/Input'
|
|
5
6
|
import type { TopPodMetrics, TopNodeMetrics } from '../../types'
|
|
6
7
|
import {
|
|
7
8
|
Search,
|
|
@@ -84,6 +85,10 @@ import {
|
|
|
84
85
|
getWorkflowDuration,
|
|
85
86
|
getWorkflowProgress,
|
|
86
87
|
getWorkflowTemplate,
|
|
88
|
+
getCronWorkflowStatus,
|
|
89
|
+
getCronWorkflowSchedule,
|
|
90
|
+
getCronWorkflowLastRun,
|
|
91
|
+
getCronWorkflowTemplate,
|
|
87
92
|
getPVStatus,
|
|
88
93
|
getPVAccessModes,
|
|
89
94
|
getPVClaim,
|
|
@@ -460,6 +465,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
460
465
|
{ key: 'template', label: 'Template', width: 'w-40', hideOnMobile: true },
|
|
461
466
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
462
467
|
],
|
|
468
|
+
cronworkflows: [
|
|
469
|
+
{ key: 'name', label: 'Name' },
|
|
470
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
471
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-40' },
|
|
472
|
+
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
473
|
+
{ key: 'lastRun', label: 'Last Run', width: 'w-28', hideOnMobile: true },
|
|
474
|
+
{ key: 'template', label: 'Template', width: 'w-40', hideOnMobile: true },
|
|
475
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
476
|
+
],
|
|
463
477
|
certificates: [
|
|
464
478
|
{ key: 'name', label: 'Name' },
|
|
465
479
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -762,6 +776,12 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
762
776
|
{ key: 'templates', label: 'Templates', width: 'w-24' },
|
|
763
777
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
764
778
|
],
|
|
779
|
+
clusterworkflowtemplates: [
|
|
780
|
+
{ key: 'name', label: 'Name' },
|
|
781
|
+
{ key: 'entrypoint', label: 'Entrypoint', width: 'w-36' },
|
|
782
|
+
{ key: 'templates', label: 'Templates', width: 'w-24' },
|
|
783
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
784
|
+
],
|
|
765
785
|
networkpolicies: [
|
|
766
786
|
{ key: 'name', label: 'Name' },
|
|
767
787
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -1709,8 +1729,16 @@ function isLikelyCrossplaneMRGroup(kind: string, group: string): boolean {
|
|
|
1709
1729
|
if (k === 'providerconfig' || k === 'providerconfigs') return false
|
|
1710
1730
|
if (group.endsWith('.upbound.io')) return true
|
|
1711
1731
|
if (group === 'kubernetes.crossplane.io' || group === 'helm.crossplane.io') return true
|
|
1712
|
-
// Reserved Crossplane core groups — never MRs.
|
|
1713
|
-
|
|
1732
|
+
// Reserved Crossplane core groups — never MRs. protection.crossplane.io
|
|
1733
|
+
// (Usage/ClusterUsage) and ops.crossplane.io (Operations) are Crossplane v2's
|
|
1734
|
+
// new core groups: lifecycle/orchestration objects, not provider resources.
|
|
1735
|
+
if (
|
|
1736
|
+
group === 'crossplane.io' ||
|
|
1737
|
+
group === 'pkg.crossplane.io' ||
|
|
1738
|
+
group === 'apiextensions.crossplane.io' ||
|
|
1739
|
+
group === 'protection.crossplane.io' ||
|
|
1740
|
+
group === 'ops.crossplane.io'
|
|
1741
|
+
) {
|
|
1714
1742
|
return false
|
|
1715
1743
|
}
|
|
1716
1744
|
// Any other *.crossplane.io subgroup is presumed to be a provider's MR group.
|
|
@@ -1810,6 +1838,8 @@ interface ResourcesViewData {
|
|
|
1810
1838
|
export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
|
|
1811
1839
|
|
|
1812
1840
|
export interface ResourceQueryResult {
|
|
1841
|
+
resourceName?: string
|
|
1842
|
+
group?: string
|
|
1813
1843
|
data?: any[]
|
|
1814
1844
|
isLoading: boolean
|
|
1815
1845
|
error?: any
|
|
@@ -1842,7 +1872,7 @@ interface ResourcesViewProps {
|
|
|
1842
1872
|
/** @deprecated Use resourceCounts + resourceForbidden + selectedKindQuery instead */
|
|
1843
1873
|
resourceQueries?: ResourceQueryResult[]
|
|
1844
1874
|
// Lightweight counts for sidebar badges (from /api/resource-counts)
|
|
1845
|
-
resourceCounts?: Record<string, number>
|
|
1875
|
+
resourceCounts?: Record<string, number | null>
|
|
1846
1876
|
resourceForbidden?: string[]
|
|
1847
1877
|
/** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
|
|
1848
1878
|
* keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
|
|
@@ -1950,6 +1980,26 @@ interface ResourcesViewProps {
|
|
|
1950
1980
|
|
|
1951
1981
|
// Default selected kind
|
|
1952
1982
|
const DEFAULT_KIND_INFO: SelectedKindInfo = { name: 'pods', kind: 'Pod', group: '' }
|
|
1983
|
+
export const LOADED_RESOURCE_COUNT_TTL_MS = 5 * 60 * 1000
|
|
1984
|
+
|
|
1985
|
+
interface LoadedResourceCount {
|
|
1986
|
+
count: number
|
|
1987
|
+
expiresAt: number
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
type LoadedResourceCountCache = Record<string, LoadedResourceCount>
|
|
1991
|
+
|
|
1992
|
+
function resourceCountKey(resource: Pick<APIResource, 'group' | 'kind'>): string {
|
|
1993
|
+
return resource.group ? `${resource.group}/${resource.kind}` : resource.kind
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
export function resourceQueryMatchesSelectedKind(
|
|
1997
|
+
query: Pick<ResourceQueryResult, 'resourceName' | 'group'> | undefined,
|
|
1998
|
+
selectedKind: SelectedKindInfo,
|
|
1999
|
+
): boolean {
|
|
2000
|
+
if (!query?.resourceName) return true
|
|
2001
|
+
return query.resourceName === selectedKind.name && (query.group ?? '') === selectedKind.group
|
|
2002
|
+
}
|
|
1953
2003
|
|
|
1954
2004
|
// Read initial state from URL — kind is in the path: {basePath}/{kind}
|
|
1955
2005
|
//
|
|
@@ -1999,6 +2049,53 @@ function getInitialKindFromURL(
|
|
|
1999
2049
|
return defaultKind
|
|
2000
2050
|
}
|
|
2001
2051
|
|
|
2052
|
+
export function canonicalizeSelectedKind(
|
|
2053
|
+
selectedKind: SelectedKindInfo,
|
|
2054
|
+
resourcesToCount: Array<Pick<APIResource, 'name' | 'kind' | 'group'>>,
|
|
2055
|
+
apiResources?: APIResource[],
|
|
2056
|
+
): SelectedKindInfo | null {
|
|
2057
|
+
const nameMatch = resourcesToCount.find(r =>
|
|
2058
|
+
r.name === selectedKind.name && r.group === selectedKind.group
|
|
2059
|
+
)
|
|
2060
|
+
if (nameMatch) {
|
|
2061
|
+
return selectedKind.kind === nameMatch.kind
|
|
2062
|
+
? null
|
|
2063
|
+
: { name: nameMatch.name, kind: nameMatch.kind, group: nameMatch.group }
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
const match = apiResources?.find(r =>
|
|
2067
|
+
r.kind === selectedKind.kind && r.group === selectedKind.group
|
|
2068
|
+
)
|
|
2069
|
+
return match ? { name: match.name, kind: match.kind, group: match.group } : null
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
export function deriveSidebarResourceCounts(
|
|
2073
|
+
resourcesToCount: Array<Pick<APIResource, 'kind' | 'group'>>,
|
|
2074
|
+
resourceCounts: Record<string, number | null> | undefined,
|
|
2075
|
+
resourceUnavailable: string[] | undefined,
|
|
2076
|
+
loadedCountCache: LoadedResourceCountCache,
|
|
2077
|
+
now: number = Date.now(),
|
|
2078
|
+
): Record<string, number | null> {
|
|
2079
|
+
const unavailableKinds = new Set(resourceUnavailable ?? [])
|
|
2080
|
+
const results: Record<string, number | null> = {}
|
|
2081
|
+
|
|
2082
|
+
for (const resource of resourcesToCount) {
|
|
2083
|
+
const key = resourceCountKey(resource)
|
|
2084
|
+
const cached = loadedCountCache[key]
|
|
2085
|
+
if (resourceCounts && key in resourceCounts) {
|
|
2086
|
+
results[key] = resourceCounts[key] ?? null
|
|
2087
|
+
} else if (cached && cached.expiresAt > now) {
|
|
2088
|
+
results[key] = cached.count
|
|
2089
|
+
} else if (unavailableKinds.has(key)) {
|
|
2090
|
+
results[key] = null
|
|
2091
|
+
} else {
|
|
2092
|
+
results[key] = null
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
return results
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2002
2099
|
// Get initial filters from URL
|
|
2003
2100
|
function getInitialFiltersFromURL() {
|
|
2004
2101
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -2006,6 +2103,7 @@ function getInitialFiltersFromURL() {
|
|
|
2006
2103
|
const columnFilters = parseColumnFilters(params.get('filters'))
|
|
2007
2104
|
const result = {
|
|
2008
2105
|
search: params.get('search') || '',
|
|
2106
|
+
regex: params.get('regex') === 'true',
|
|
2009
2107
|
columnFilters,
|
|
2010
2108
|
problemFilters: params.get('problems')?.split(',').filter(Boolean) || [],
|
|
2011
2109
|
showInactive: params.get('showInactive') === 'true',
|
|
@@ -2085,7 +2183,7 @@ export function ResourcesView({
|
|
|
2085
2183
|
setBulkForceDelete(false)
|
|
2086
2184
|
}, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
2087
2185
|
const [searchTerm, setSearchTerm] = useState(initialFilters.search)
|
|
2088
|
-
const [regexMode, setRegexMode] = useState(
|
|
2186
|
+
const [regexMode, setRegexMode] = useState(initialFilters.regex)
|
|
2089
2187
|
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
2090
2188
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null)
|
|
2091
2189
|
// Filter state
|
|
@@ -2679,11 +2777,11 @@ export function ResourcesView({
|
|
|
2679
2777
|
if (containers.length > 0) {
|
|
2680
2778
|
openLogs?.({ namespace: ns, podName: name, containers })
|
|
2681
2779
|
}
|
|
2682
|
-
} else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(kindLower)) {
|
|
2780
|
+
} else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(kindLower)) {
|
|
2683
2781
|
openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
|
|
2684
2782
|
}
|
|
2685
2783
|
},
|
|
2686
|
-
enabled: highlightedIndex >= 0 && !compareMode && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(selectedKind.name.toLowerCase()),
|
|
2784
|
+
enabled: highlightedIndex >= 0 && !compareMode && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(selectedKind.name.toLowerCase()),
|
|
2687
2785
|
},
|
|
2688
2786
|
{
|
|
2689
2787
|
id: 'resources-sort-name',
|
|
@@ -2834,6 +2932,11 @@ export function ResourcesView({
|
|
|
2834
2932
|
setSearchTerm(newFilters.search)
|
|
2835
2933
|
}
|
|
2836
2934
|
|
|
2935
|
+
// Update regex mode if it changed
|
|
2936
|
+
if (newFilters.regex !== regexMode) {
|
|
2937
|
+
setRegexMode(newFilters.regex)
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2837
2940
|
// Update column filters if changed
|
|
2838
2941
|
const newFiltersStr = serializeColumnFilters(newFilters.columnFilters)
|
|
2839
2942
|
const currentFiltersStr = serializeColumnFilters(columnFilters)
|
|
@@ -2864,6 +2967,7 @@ export function ResourcesView({
|
|
|
2864
2967
|
const updateURL = useCallback((
|
|
2865
2968
|
kindInfo: SelectedKindInfo,
|
|
2866
2969
|
search: string,
|
|
2970
|
+
regex: boolean,
|
|
2867
2971
|
colFilters: Record<string, string[]>,
|
|
2868
2972
|
problems: string[],
|
|
2869
2973
|
showInactive: boolean,
|
|
@@ -2886,6 +2990,11 @@ export function ResourcesView({
|
|
|
2886
2990
|
} else {
|
|
2887
2991
|
params.delete('search')
|
|
2888
2992
|
}
|
|
2993
|
+
if (regex) {
|
|
2994
|
+
params.set('regex', 'true')
|
|
2995
|
+
} else {
|
|
2996
|
+
params.delete('regex')
|
|
2997
|
+
}
|
|
2889
2998
|
// Write column filters as `filters` param; remove legacy `status` param
|
|
2890
2999
|
const filtersStr = serializeColumnFilters(colFilters)
|
|
2891
3000
|
if (filtersStr) {
|
|
@@ -2947,6 +3056,7 @@ export function ResourcesView({
|
|
|
2947
3056
|
|
|
2948
3057
|
const clearAllFilters = useCallback(() => {
|
|
2949
3058
|
setSearchTerm('')
|
|
3059
|
+
setRegexMode(false)
|
|
2950
3060
|
setColumnFilters({})
|
|
2951
3061
|
setProblemFilters([])
|
|
2952
3062
|
setLabelSelector('')
|
|
@@ -2957,7 +3067,7 @@ export function ResourcesView({
|
|
|
2957
3067
|
// params are out of scope here; the host's onClearNamespaces (and its
|
|
2958
3068
|
// own state→URL sync) owns namespace cleanup.
|
|
2959
3069
|
const params = new URLSearchParams(window.location.search)
|
|
2960
|
-
for (const key of ['search', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
|
|
3070
|
+
for (const key of ['search', 'regex', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
|
|
2961
3071
|
params.delete(key)
|
|
2962
3072
|
}
|
|
2963
3073
|
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
@@ -3014,8 +3124,8 @@ export function ResourcesView({
|
|
|
3014
3124
|
shouldPushHistory.current = false
|
|
3015
3125
|
prevSelectedResourceRef.current = current
|
|
3016
3126
|
|
|
3017
|
-
updateURL(selectedKind, searchTerm, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
3018
|
-
}, [selectedKind, searchTerm, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
3127
|
+
updateURL(selectedKind, searchTerm, regexMode, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
3128
|
+
}, [selectedKind, searchTerm, regexMode, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
3019
3129
|
|
|
3020
3130
|
// Handle resource click from URL on mount
|
|
3021
3131
|
useEffect(() => {
|
|
@@ -3136,24 +3246,15 @@ export function ResourcesView({
|
|
|
3136
3246
|
// getInitialKindFromURL can't look up CRDs, so name may be wrong (e.g., 'HTTPRoute' instead of 'httproutes')
|
|
3137
3247
|
useEffect(() => {
|
|
3138
3248
|
if (!apiResources) return
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
)
|
|
3143
|
-
if (alreadyResolved) return
|
|
3144
|
-
|
|
3145
|
-
// Try to match by kind name (URL stores kind=HTTPRoute, API has name=httproutes)
|
|
3146
|
-
const match = apiResources.find(r =>
|
|
3147
|
-
r.kind === selectedKind.kind && r.group === selectedKind.group
|
|
3148
|
-
)
|
|
3149
|
-
if (match) {
|
|
3150
|
-
setSelectedKind({ name: match.name, kind: match.kind, group: match.group })
|
|
3249
|
+
const canonicalKind = canonicalizeSelectedKind(selectedKind, resourcesToCount, apiResources)
|
|
3250
|
+
if (canonicalKind) {
|
|
3251
|
+
setSelectedKind(canonicalKind)
|
|
3151
3252
|
}
|
|
3152
3253
|
}, [apiResources, resourcesToCount, selectedKind.name, selectedKind.kind, selectedKind.group])
|
|
3153
3254
|
|
|
3154
3255
|
// Resource data — prefer new lightweight props over legacy resourceQueries array
|
|
3155
3256
|
const resourceQueries = resourceQueriesProp ?? []
|
|
3156
|
-
const useNewCountsMode =
|
|
3257
|
+
const useNewCountsMode = resourceCountsProp !== undefined || selectedKindQueryProp !== undefined
|
|
3157
3258
|
|
|
3158
3259
|
// Find the selected kind's query
|
|
3159
3260
|
const selectedQueryIndex = useMemo(() => {
|
|
@@ -3163,7 +3264,8 @@ export function ResourcesView({
|
|
|
3163
3264
|
}, [resourcesToCount, selectedKind.name, selectedKind.group])
|
|
3164
3265
|
|
|
3165
3266
|
const selectedQuery = selectedKindQueryProp ?? resourceQueries[selectedQueryIndex]
|
|
3166
|
-
const
|
|
3267
|
+
const selectedQueryMatchesKind = resourceQueryMatchesSelectedKind(selectedQuery, selectedKind)
|
|
3268
|
+
const resources = selectedQueryMatchesKind ? selectedQuery?.data : undefined
|
|
3167
3269
|
|
|
3168
3270
|
// Auto-surface the GPU column the first time GPU-bearing rows load for a kind
|
|
3169
3271
|
// the user has never customized — a static default can't know whether the
|
|
@@ -3204,24 +3306,63 @@ export function ResourcesView({
|
|
|
3204
3306
|
annotation: Array.from(annotations).sort(),
|
|
3205
3307
|
}
|
|
3206
3308
|
}, [resources])
|
|
3207
|
-
const isLoading = selectedQuery?.isLoading ?? true
|
|
3208
|
-
const selectedQueryError = selectedQuery?.error
|
|
3309
|
+
const isLoading = selectedQueryMatchesKind ? (selectedQuery?.isLoading ?? true) : true
|
|
3310
|
+
const selectedQueryError = selectedQueryMatchesKind ? selectedQuery?.error : undefined
|
|
3311
|
+
const selectedKindCountKey = selectedKind.group
|
|
3312
|
+
? `${selectedKind.group}/${selectedKind.kind}`
|
|
3313
|
+
: selectedKind.kind
|
|
3314
|
+
const selectedQueryHasLoadedCount = Array.isArray(resources) && !isLoading && !selectedQueryError && !largeListGuard
|
|
3315
|
+
const selectedLoadedResourceCount = Array.isArray(resources) ? resources.length : undefined
|
|
3316
|
+
const [loadedCountCache, setLoadedCountCache] = useState<LoadedResourceCountCache>({})
|
|
3317
|
+
const namespaceScopeKey = useMemo(
|
|
3318
|
+
() => namespaces.length === 0 ? '' : [...namespaces].sort().join('\0'),
|
|
3319
|
+
[namespaces],
|
|
3320
|
+
)
|
|
3321
|
+
const loadedCountScopeRef = useRef(namespaceScopeKey)
|
|
3322
|
+
|
|
3323
|
+
useEffect(() => {
|
|
3324
|
+
if (loadedCountScopeRef.current === namespaceScopeKey) return
|
|
3325
|
+
loadedCountScopeRef.current = namespaceScopeKey
|
|
3326
|
+
setLoadedCountCache({})
|
|
3327
|
+
}, [namespaceScopeKey])
|
|
3328
|
+
|
|
3329
|
+
useEffect(() => {
|
|
3330
|
+
if (!selectedQueryHasLoadedCount || selectedLoadedResourceCount == null) return
|
|
3331
|
+
const now = Date.now()
|
|
3332
|
+
const loadedAt = selectedQuery?.dataUpdatedAt && selectedQuery.dataUpdatedAt > 0
|
|
3333
|
+
? selectedQuery.dataUpdatedAt
|
|
3334
|
+
: now
|
|
3335
|
+
const expiresAt = loadedAt + LOADED_RESOURCE_COUNT_TTL_MS
|
|
3336
|
+
|
|
3337
|
+
setLoadedCountCache(prev => {
|
|
3338
|
+
const next: LoadedResourceCountCache = {}
|
|
3339
|
+
let changed = false
|
|
3340
|
+
for (const [key, cached] of Object.entries(prev)) {
|
|
3341
|
+
if (cached.expiresAt > now || key === selectedKindCountKey) {
|
|
3342
|
+
next[key] = cached
|
|
3343
|
+
} else {
|
|
3344
|
+
changed = true
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
const existing = next[selectedKindCountKey]
|
|
3349
|
+
if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt) {
|
|
3350
|
+
return changed ? next : prev
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
return {
|
|
3354
|
+
...next,
|
|
3355
|
+
[selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt },
|
|
3356
|
+
}
|
|
3357
|
+
})
|
|
3358
|
+
}, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt])
|
|
3209
3359
|
|
|
3210
3360
|
// Derive counts — prefer lightweight resourceCounts prop over full query data
|
|
3211
3361
|
const counts = useMemo(() => {
|
|
3212
3362
|
if (useNewCountsMode) {
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
for (const resource of resourcesToCount) {
|
|
3217
|
-
const key = resource.group ? `${resource.group}/${resource.kind}` : resource.kind
|
|
3218
|
-
if (unavailableKinds.has(key)) {
|
|
3219
|
-
results[key] = null
|
|
3220
|
-
} else if (key in resourceCountsProp!) {
|
|
3221
|
-
results[key] = resourceCountsProp![key]
|
|
3222
|
-
} else {
|
|
3223
|
-
results[key] = 0
|
|
3224
|
-
}
|
|
3363
|
+
const results = deriveSidebarResourceCounts(resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache)
|
|
3364
|
+
if (selectedQueryHasLoadedCount && selectedLoadedResourceCount != null) {
|
|
3365
|
+
results[selectedKindCountKey] = selectedLoadedResourceCount
|
|
3225
3366
|
}
|
|
3226
3367
|
return results
|
|
3227
3368
|
}
|
|
@@ -3233,7 +3374,14 @@ export function ResourcesView({
|
|
|
3233
3374
|
results[key] = Array.isArray(data) ? data.length : 0
|
|
3234
3375
|
})
|
|
3235
3376
|
return results
|
|
3236
|
-
}, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, resourceQueries])
|
|
3377
|
+
}, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache, selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, resourceQueries])
|
|
3378
|
+
|
|
3379
|
+
const sidebarResourceUnavailable = useMemo(() => {
|
|
3380
|
+
if (!resourceUnavailableProp?.length) {
|
|
3381
|
+
return resourceUnavailableProp
|
|
3382
|
+
}
|
|
3383
|
+
return resourceUnavailableProp.filter(key => counts[key] == null)
|
|
3384
|
+
}, [resourceUnavailableProp, counts])
|
|
3237
3385
|
|
|
3238
3386
|
// Track which resource kinds returned 403 Forbidden
|
|
3239
3387
|
const forbiddenKinds = useMemo(() => {
|
|
@@ -3254,9 +3402,6 @@ export function ResourcesView({
|
|
|
3254
3402
|
// denials) OR the selected kind is in the counts `forbidden` set. Denied
|
|
3255
3403
|
// cluster-scoped kinds return 200 with `[]` from the list endpoint, so the
|
|
3256
3404
|
// 403 signal alone misses them — they'd fall through to "No <kind> found".
|
|
3257
|
-
const selectedKindCountKey = selectedKind.group
|
|
3258
|
-
? `${selectedKind.group}/${selectedKind.kind}`
|
|
3259
|
-
: selectedKind.kind
|
|
3260
3405
|
// Actual rows win over a stale counts `forbidden` entry: a kind can be marked
|
|
3261
3406
|
// forbidden/unavailable in counts (e.g. an informer not yet synced at counts
|
|
3262
3407
|
// time) while the list query has since returned data — show the table, not
|
|
@@ -3918,6 +4063,7 @@ export function ResourcesView({
|
|
|
3918
4063
|
// a no-op for that case.
|
|
3919
4064
|
const hasAnyFilter =
|
|
3920
4065
|
!!searchTerm ||
|
|
4066
|
+
regexMode ||
|
|
3921
4067
|
!!labelSelector ||
|
|
3922
4068
|
hasOwnerFilter ||
|
|
3923
4069
|
problemFilters.length > 0 ||
|
|
@@ -3986,7 +4132,7 @@ export function ResourcesView({
|
|
|
3986
4132
|
apiResources={apiResourcesProp}
|
|
3987
4133
|
resourceCounts={counts}
|
|
3988
4134
|
resourceForbidden={Array.from(forbiddenKinds)}
|
|
3989
|
-
resourceUnavailable={
|
|
4135
|
+
resourceUnavailable={sidebarResourceUnavailable}
|
|
3990
4136
|
pinned={pinned}
|
|
3991
4137
|
togglePin={togglePin}
|
|
3992
4138
|
isPinned={isPinned}
|
|
@@ -4005,9 +4151,8 @@ export function ResourcesView({
|
|
|
4005
4151
|
<div className="flex-1 min-w-0">
|
|
4006
4152
|
<div className="relative max-w-md">
|
|
4007
4153
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
4008
|
-
<
|
|
4154
|
+
<Input
|
|
4009
4155
|
ref={searchInputRef}
|
|
4010
|
-
type="text"
|
|
4011
4156
|
placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
|
|
4012
4157
|
value={searchTerm}
|
|
4013
4158
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
@@ -4034,7 +4179,8 @@ export function ResourcesView({
|
|
|
4034
4179
|
}
|
|
4035
4180
|
}}
|
|
4036
4181
|
className={clsx(
|
|
4037
|
-
'w-full pl-10
|
|
4182
|
+
'w-full pl-10 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
|
|
4183
|
+
searchTerm ? 'pr-16' : 'pr-10',
|
|
4038
4184
|
searchRegex.error
|
|
4039
4185
|
? 'border-red-500/60 focus:ring-red-500'
|
|
4040
4186
|
: 'border-theme-border-light focus:ring-skyhook-500'
|
|
@@ -4047,7 +4193,8 @@ export function ResourcesView({
|
|
|
4047
4193
|
aria-label={regexMode ? 'Disable regex search' : 'Enable regex search'}
|
|
4048
4194
|
title={regexMode ? 'Regex search enabled — click to disable' : 'Enable regex search'}
|
|
4049
4195
|
className={clsx(
|
|
4050
|
-
'absolute
|
|
4196
|
+
'absolute top-1/2 -translate-y-1/2 flex items-center justify-center w-6 h-6 rounded transition-colors',
|
|
4197
|
+
searchTerm ? 'right-9' : 'right-2',
|
|
4051
4198
|
regexMode
|
|
4052
4199
|
? 'bg-skyhook-500/20 text-skyhook-400'
|
|
4053
4200
|
: 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover'
|
|
@@ -4055,6 +4202,19 @@ export function ResourcesView({
|
|
|
4055
4202
|
>
|
|
4056
4203
|
<Regex className="w-3.5 h-3.5" />
|
|
4057
4204
|
</button>
|
|
4205
|
+
{searchTerm && (
|
|
4206
|
+
<button
|
|
4207
|
+
type="button"
|
|
4208
|
+
aria-label="Clear search"
|
|
4209
|
+
onClick={() => {
|
|
4210
|
+
setSearchTerm('')
|
|
4211
|
+
searchInputRef.current?.focus()
|
|
4212
|
+
}}
|
|
4213
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-6 h-6 rounded text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
|
|
4214
|
+
>
|
|
4215
|
+
<X className="w-3.5 h-3.5" />
|
|
4216
|
+
</button>
|
|
4217
|
+
)}
|
|
4058
4218
|
{searchRegex.error && (
|
|
4059
4219
|
<div
|
|
4060
4220
|
title={searchRegex.error}
|
|
@@ -4146,8 +4306,7 @@ export function ResourcesView({
|
|
|
4146
4306
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
4147
4307
|
<div className="relative flex-1">
|
|
4148
4308
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
4149
|
-
<
|
|
4150
|
-
type="text"
|
|
4309
|
+
<Input
|
|
4151
4310
|
placeholder="Search labels..."
|
|
4152
4311
|
value={labelSearch}
|
|
4153
4312
|
onChange={(e) => setLabelSearch(e.target.value)}
|
|
@@ -4325,8 +4484,7 @@ export function ResourcesView({
|
|
|
4325
4484
|
setCustomColumnDraft(d => ({ ...d, path: '' }))
|
|
4326
4485
|
}}
|
|
4327
4486
|
>
|
|
4328
|
-
<
|
|
4329
|
-
type="text"
|
|
4487
|
+
<Input
|
|
4330
4488
|
list={customColKeysListId}
|
|
4331
4489
|
value={customColumnDraft.path}
|
|
4332
4490
|
onChange={e => setCustomColumnDraft(d => ({ ...d, path: e.target.value }))}
|
|
@@ -4511,7 +4669,10 @@ export function ResourcesView({
|
|
|
4511
4669
|
<p>No {selectedKind.kind} found</p>
|
|
4512
4670
|
{searchTerm && (
|
|
4513
4671
|
<button
|
|
4514
|
-
onClick={() =>
|
|
4672
|
+
onClick={() => {
|
|
4673
|
+
setSearchTerm('')
|
|
4674
|
+
setRegexMode(false)
|
|
4675
|
+
}}
|
|
4515
4676
|
className="flex items-center gap-1.5 text-sm mt-2 px-3 py-1.5 rounded-md bg-theme-elevated hover:bg-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
4516
4677
|
>
|
|
4517
4678
|
No results for "{searchTerm}"
|
|
@@ -4708,8 +4869,7 @@ export function ResourcesView({
|
|
|
4708
4869
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
4709
4870
|
<div className="relative flex-1">
|
|
4710
4871
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
4711
|
-
<
|
|
4712
|
-
type="text"
|
|
4872
|
+
<Input
|
|
4713
4873
|
placeholder="Search..."
|
|
4714
4874
|
value={columnFilterSearch}
|
|
4715
4875
|
onChange={(e) => setColumnFilterSearch(e.target.value)}
|
|
@@ -5210,6 +5370,8 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5210
5370
|
return <RolloutCell resource={resource} column={column} />
|
|
5211
5371
|
case 'workflows':
|
|
5212
5372
|
return <WorkflowCell resource={resource} column={column} />
|
|
5373
|
+
case 'cronworkflows':
|
|
5374
|
+
return <CronWorkflowCell resource={resource} column={column} />
|
|
5213
5375
|
case 'certificates':
|
|
5214
5376
|
return <CertificateCell resource={resource} column={column} />
|
|
5215
5377
|
case 'persistentvolumes':
|
|
@@ -5242,6 +5404,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5242
5404
|
case 'sealedsecrets':
|
|
5243
5405
|
return <SealedSecretCell resource={resource} column={column} />
|
|
5244
5406
|
case 'workflowtemplates':
|
|
5407
|
+
case 'clusterworkflowtemplates':
|
|
5245
5408
|
return <WorkflowTemplateCell resource={resource} column={column} />
|
|
5246
5409
|
case 'networkpolicies':
|
|
5247
5410
|
return <NetworkPolicyCell resource={resource} column={column} />
|
|
@@ -6576,6 +6739,33 @@ function WorkflowCell({ resource, column }: { resource: any; column: string }) {
|
|
|
6576
6739
|
}
|
|
6577
6740
|
}
|
|
6578
6741
|
|
|
6742
|
+
function CronWorkflowCell({ resource, column }: { resource: any; column: string }) {
|
|
6743
|
+
switch (column) {
|
|
6744
|
+
case 'status': {
|
|
6745
|
+
const status = getCronWorkflowStatus(resource)
|
|
6746
|
+
return (
|
|
6747
|
+
<span className={clsx('badge', status.color)}>
|
|
6748
|
+
{status.text}
|
|
6749
|
+
</span>
|
|
6750
|
+
)
|
|
6751
|
+
}
|
|
6752
|
+
case 'schedule':
|
|
6753
|
+
return <span className="font-mono text-xs text-theme-text-secondary">{getCronWorkflowSchedule(resource)}</span>
|
|
6754
|
+
case 'lastRun':
|
|
6755
|
+
return <span className="text-sm text-theme-text-secondary">{getCronWorkflowLastRun(resource)}</span>
|
|
6756
|
+
case 'template': {
|
|
6757
|
+
const template = getCronWorkflowTemplate(resource)
|
|
6758
|
+
return (
|
|
6759
|
+
<Tooltip content={template}>
|
|
6760
|
+
<span className="block truncate text-sm text-theme-text-secondary">{template}</span>
|
|
6761
|
+
</Tooltip>
|
|
6762
|
+
)
|
|
6763
|
+
}
|
|
6764
|
+
default:
|
|
6765
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6579
6769
|
|
|
6580
6770
|
function PersistentVolumeCell({ resource, column }: { resource: any; column: string }) {
|
|
6581
6771
|
switch (column) {
|
|
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
|
|
|
2
2
|
import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle, Search } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
5
|
+
import { Input } from '../../ui/Input'
|
|
5
6
|
import { formatAge } from '../resource-utils'
|
|
6
7
|
import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER } from './trivy-shared'
|
|
7
8
|
import { pluralize } from '../../../utils/pluralize'
|
|
@@ -138,8 +139,7 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
138
139
|
<div className="flex items-center gap-2 mb-2">
|
|
139
140
|
<div className="relative flex-1">
|
|
140
141
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
141
|
-
<
|
|
142
|
-
type="text"
|
|
142
|
+
<Input
|
|
143
143
|
placeholder="Filter by control name or ID..."
|
|
144
144
|
value={searchTerm}
|
|
145
145
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Clock, Pause } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink, ConditionsSection } from '../../ui/drawer-components'
|
|
3
|
+
import { formatAge, cronToHuman } from '../resource-utils'
|
|
4
|
+
|
|
5
|
+
interface CronWorkflowRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CronWorkflowRenderer({ data, onNavigate }: CronWorkflowRendererProps) {
|
|
11
|
+
const spec = data.spec || {}
|
|
12
|
+
const status = data.status || {}
|
|
13
|
+
const workflowSpec = spec.workflowSpec || {}
|
|
14
|
+
const schedules = Array.isArray(spec.schedules) ? spec.schedules : spec.schedule ? [spec.schedule] : []
|
|
15
|
+
const isSuspended = spec.suspend === true
|
|
16
|
+
const active = Array.isArray(status.active) ? status.active : []
|
|
17
|
+
const workflowTemplateRef = workflowSpec.workflowTemplateRef || null
|
|
18
|
+
const templateName = workflowTemplateRef?.name || workflowTemplateRef?.template || ''
|
|
19
|
+
const templateClusterScope = workflowTemplateRef?.clusterScope === true
|
|
20
|
+
const hasNeverRun = !status.lastScheduledTime && active.length === 0
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{isSuspended && (
|
|
25
|
+
<AlertBanner
|
|
26
|
+
variant="info"
|
|
27
|
+
icon={Pause}
|
|
28
|
+
title="CronWorkflow Suspended"
|
|
29
|
+
message="No new Workflows will be scheduled until this CronWorkflow is resumed."
|
|
30
|
+
/>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
{hasNeverRun && !isSuspended && (
|
|
34
|
+
<AlertBanner
|
|
35
|
+
variant="info"
|
|
36
|
+
title="Never Scheduled"
|
|
37
|
+
message="This CronWorkflow has no retained active or historical Workflow runs in status."
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
<Section title="Schedule" icon={Clock}>
|
|
42
|
+
<PropertyList>
|
|
43
|
+
<Property label={schedules.length === 1 ? 'Schedule' : 'Schedules'} value={schedules.join(', ')} />
|
|
44
|
+
{schedules.length === 1 && <Property label="Human" value={cronToHuman(schedules[0])} />}
|
|
45
|
+
<Property label="Timezone" value={spec.timezone || 'Cluster default'} />
|
|
46
|
+
<Property label="Suspend" value={isSuspended ? 'Yes' : 'No'} />
|
|
47
|
+
<Property label="Concurrency" value={spec.concurrencyPolicy || 'Allow'} />
|
|
48
|
+
<Property label="Last Scheduled" value={status.lastScheduledTime ? formatAge(status.lastScheduledTime) : 'Never'} />
|
|
49
|
+
<Property label="Active Workflows" value={active.length} />
|
|
50
|
+
</PropertyList>
|
|
51
|
+
</Section>
|
|
52
|
+
|
|
53
|
+
<Section title="Workflow Template">
|
|
54
|
+
<PropertyList>
|
|
55
|
+
{templateName && (
|
|
56
|
+
<Property
|
|
57
|
+
label="Template Ref"
|
|
58
|
+
value={
|
|
59
|
+
<ResourceLink
|
|
60
|
+
name={templateName}
|
|
61
|
+
kind={templateClusterScope ? 'clusterworkflowtemplates' : 'workflowtemplates'}
|
|
62
|
+
namespace={templateClusterScope ? '' : data.metadata?.namespace || ''}
|
|
63
|
+
group="argoproj.io"
|
|
64
|
+
label={templateClusterScope ? `${templateName} (cluster)` : templateName}
|
|
65
|
+
onNavigate={onNavigate}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<Property label="Entrypoint" value={workflowSpec.entrypoint || '-'} />
|
|
71
|
+
<Property label="Arguments" value={(workflowSpec.arguments?.parameters || []).length} />
|
|
72
|
+
<Property label="Templates" value={(workflowSpec.templates || []).length} />
|
|
73
|
+
</PropertyList>
|
|
74
|
+
</Section>
|
|
75
|
+
|
|
76
|
+
<Section title="History">
|
|
77
|
+
<PropertyList>
|
|
78
|
+
<Property label="Success History" value={spec.successfulJobsHistoryLimit ?? 3} />
|
|
79
|
+
<Property label="Failed History" value={spec.failedJobsHistoryLimit ?? 1} />
|
|
80
|
+
<Property label="Starting Deadline" value={spec.startingDeadlineSeconds ? `${spec.startingDeadlineSeconds}s` : 'None'} />
|
|
81
|
+
</PropertyList>
|
|
82
|
+
</Section>
|
|
83
|
+
|
|
84
|
+
{active.length > 0 && (
|
|
85
|
+
<Section title="Active Workflows">
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
{active.map((wf: any) => (
|
|
88
|
+
<div key={`${wf.namespace || data.metadata?.namespace || ''}/${wf.name}`} className="text-sm">
|
|
89
|
+
<ResourceLink name={wf.name} kind="workflows" namespace={wf.namespace || data.metadata?.namespace || ''} group="argoproj.io" onNavigate={onNavigate} />
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</Section>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<ConditionsSection conditions={status.conditions} />
|
|
97
|
+
</>
|
|
98
|
+
)
|
|
99
|
+
}
|