@skyhook-io/k8s-ui 1.8.10 → 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/ApplicationsView.tsx +62 -10
- 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 +2 -3
- package/src/components/checks/severity.ts +14 -10
- 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 +3 -3
- 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 +51 -38
- package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
- package/src/components/issues/ResourceIssuesSection.tsx +28 -146
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +2 -0
- 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 +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- 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/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/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/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/severity-tone.ts +11 -4
- 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 +1 -1
- package/src/types/core.ts +57 -4
- 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
|
@@ -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' },
|
|
@@ -1818,6 +1838,8 @@ interface ResourcesViewData {
|
|
|
1818
1838
|
export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
|
|
1819
1839
|
|
|
1820
1840
|
export interface ResourceQueryResult {
|
|
1841
|
+
resourceName?: string
|
|
1842
|
+
group?: string
|
|
1821
1843
|
data?: any[]
|
|
1822
1844
|
isLoading: boolean
|
|
1823
1845
|
error?: any
|
|
@@ -1850,7 +1872,7 @@ interface ResourcesViewProps {
|
|
|
1850
1872
|
/** @deprecated Use resourceCounts + resourceForbidden + selectedKindQuery instead */
|
|
1851
1873
|
resourceQueries?: ResourceQueryResult[]
|
|
1852
1874
|
// Lightweight counts for sidebar badges (from /api/resource-counts)
|
|
1853
|
-
resourceCounts?: Record<string, number>
|
|
1875
|
+
resourceCounts?: Record<string, number | null>
|
|
1854
1876
|
resourceForbidden?: string[]
|
|
1855
1877
|
/** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
|
|
1856
1878
|
* keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
|
|
@@ -1958,6 +1980,26 @@ interface ResourcesViewProps {
|
|
|
1958
1980
|
|
|
1959
1981
|
// Default selected kind
|
|
1960
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
|
+
}
|
|
1961
2003
|
|
|
1962
2004
|
// Read initial state from URL — kind is in the path: {basePath}/{kind}
|
|
1963
2005
|
//
|
|
@@ -2007,6 +2049,53 @@ function getInitialKindFromURL(
|
|
|
2007
2049
|
return defaultKind
|
|
2008
2050
|
}
|
|
2009
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
|
+
|
|
2010
2099
|
// Get initial filters from URL
|
|
2011
2100
|
function getInitialFiltersFromURL() {
|
|
2012
2101
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -2014,6 +2103,7 @@ function getInitialFiltersFromURL() {
|
|
|
2014
2103
|
const columnFilters = parseColumnFilters(params.get('filters'))
|
|
2015
2104
|
const result = {
|
|
2016
2105
|
search: params.get('search') || '',
|
|
2106
|
+
regex: params.get('regex') === 'true',
|
|
2017
2107
|
columnFilters,
|
|
2018
2108
|
problemFilters: params.get('problems')?.split(',').filter(Boolean) || [],
|
|
2019
2109
|
showInactive: params.get('showInactive') === 'true',
|
|
@@ -2093,7 +2183,7 @@ export function ResourcesView({
|
|
|
2093
2183
|
setBulkForceDelete(false)
|
|
2094
2184
|
}, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
2095
2185
|
const [searchTerm, setSearchTerm] = useState(initialFilters.search)
|
|
2096
|
-
const [regexMode, setRegexMode] = useState(
|
|
2186
|
+
const [regexMode, setRegexMode] = useState(initialFilters.regex)
|
|
2097
2187
|
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
2098
2188
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null)
|
|
2099
2189
|
// Filter state
|
|
@@ -2687,11 +2777,11 @@ export function ResourcesView({
|
|
|
2687
2777
|
if (containers.length > 0) {
|
|
2688
2778
|
openLogs?.({ namespace: ns, podName: name, containers })
|
|
2689
2779
|
}
|
|
2690
|
-
} 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)) {
|
|
2691
2781
|
openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
|
|
2692
2782
|
}
|
|
2693
2783
|
},
|
|
2694
|
-
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()),
|
|
2695
2785
|
},
|
|
2696
2786
|
{
|
|
2697
2787
|
id: 'resources-sort-name',
|
|
@@ -2842,6 +2932,11 @@ export function ResourcesView({
|
|
|
2842
2932
|
setSearchTerm(newFilters.search)
|
|
2843
2933
|
}
|
|
2844
2934
|
|
|
2935
|
+
// Update regex mode if it changed
|
|
2936
|
+
if (newFilters.regex !== regexMode) {
|
|
2937
|
+
setRegexMode(newFilters.regex)
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2845
2940
|
// Update column filters if changed
|
|
2846
2941
|
const newFiltersStr = serializeColumnFilters(newFilters.columnFilters)
|
|
2847
2942
|
const currentFiltersStr = serializeColumnFilters(columnFilters)
|
|
@@ -2872,6 +2967,7 @@ export function ResourcesView({
|
|
|
2872
2967
|
const updateURL = useCallback((
|
|
2873
2968
|
kindInfo: SelectedKindInfo,
|
|
2874
2969
|
search: string,
|
|
2970
|
+
regex: boolean,
|
|
2875
2971
|
colFilters: Record<string, string[]>,
|
|
2876
2972
|
problems: string[],
|
|
2877
2973
|
showInactive: boolean,
|
|
@@ -2894,6 +2990,11 @@ export function ResourcesView({
|
|
|
2894
2990
|
} else {
|
|
2895
2991
|
params.delete('search')
|
|
2896
2992
|
}
|
|
2993
|
+
if (regex) {
|
|
2994
|
+
params.set('regex', 'true')
|
|
2995
|
+
} else {
|
|
2996
|
+
params.delete('regex')
|
|
2997
|
+
}
|
|
2897
2998
|
// Write column filters as `filters` param; remove legacy `status` param
|
|
2898
2999
|
const filtersStr = serializeColumnFilters(colFilters)
|
|
2899
3000
|
if (filtersStr) {
|
|
@@ -2955,6 +3056,7 @@ export function ResourcesView({
|
|
|
2955
3056
|
|
|
2956
3057
|
const clearAllFilters = useCallback(() => {
|
|
2957
3058
|
setSearchTerm('')
|
|
3059
|
+
setRegexMode(false)
|
|
2958
3060
|
setColumnFilters({})
|
|
2959
3061
|
setProblemFilters([])
|
|
2960
3062
|
setLabelSelector('')
|
|
@@ -2965,7 +3067,7 @@ export function ResourcesView({
|
|
|
2965
3067
|
// params are out of scope here; the host's onClearNamespaces (and its
|
|
2966
3068
|
// own state→URL sync) owns namespace cleanup.
|
|
2967
3069
|
const params = new URLSearchParams(window.location.search)
|
|
2968
|
-
for (const key of ['search', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
|
|
3070
|
+
for (const key of ['search', 'regex', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
|
|
2969
3071
|
params.delete(key)
|
|
2970
3072
|
}
|
|
2971
3073
|
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
@@ -3022,8 +3124,8 @@ export function ResourcesView({
|
|
|
3022
3124
|
shouldPushHistory.current = false
|
|
3023
3125
|
prevSelectedResourceRef.current = current
|
|
3024
3126
|
|
|
3025
|
-
updateURL(selectedKind, searchTerm, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
3026
|
-
}, [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])
|
|
3027
3129
|
|
|
3028
3130
|
// Handle resource click from URL on mount
|
|
3029
3131
|
useEffect(() => {
|
|
@@ -3144,24 +3246,15 @@ export function ResourcesView({
|
|
|
3144
3246
|
// getInitialKindFromURL can't look up CRDs, so name may be wrong (e.g., 'HTTPRoute' instead of 'httproutes')
|
|
3145
3247
|
useEffect(() => {
|
|
3146
3248
|
if (!apiResources) return
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
)
|
|
3151
|
-
if (alreadyResolved) return
|
|
3152
|
-
|
|
3153
|
-
// Try to match by kind name (URL stores kind=HTTPRoute, API has name=httproutes)
|
|
3154
|
-
const match = apiResources.find(r =>
|
|
3155
|
-
r.kind === selectedKind.kind && r.group === selectedKind.group
|
|
3156
|
-
)
|
|
3157
|
-
if (match) {
|
|
3158
|
-
setSelectedKind({ name: match.name, kind: match.kind, group: match.group })
|
|
3249
|
+
const canonicalKind = canonicalizeSelectedKind(selectedKind, resourcesToCount, apiResources)
|
|
3250
|
+
if (canonicalKind) {
|
|
3251
|
+
setSelectedKind(canonicalKind)
|
|
3159
3252
|
}
|
|
3160
3253
|
}, [apiResources, resourcesToCount, selectedKind.name, selectedKind.kind, selectedKind.group])
|
|
3161
3254
|
|
|
3162
3255
|
// Resource data — prefer new lightweight props over legacy resourceQueries array
|
|
3163
3256
|
const resourceQueries = resourceQueriesProp ?? []
|
|
3164
|
-
const useNewCountsMode =
|
|
3257
|
+
const useNewCountsMode = resourceCountsProp !== undefined || selectedKindQueryProp !== undefined
|
|
3165
3258
|
|
|
3166
3259
|
// Find the selected kind's query
|
|
3167
3260
|
const selectedQueryIndex = useMemo(() => {
|
|
@@ -3171,7 +3264,8 @@ export function ResourcesView({
|
|
|
3171
3264
|
}, [resourcesToCount, selectedKind.name, selectedKind.group])
|
|
3172
3265
|
|
|
3173
3266
|
const selectedQuery = selectedKindQueryProp ?? resourceQueries[selectedQueryIndex]
|
|
3174
|
-
const
|
|
3267
|
+
const selectedQueryMatchesKind = resourceQueryMatchesSelectedKind(selectedQuery, selectedKind)
|
|
3268
|
+
const resources = selectedQueryMatchesKind ? selectedQuery?.data : undefined
|
|
3175
3269
|
|
|
3176
3270
|
// Auto-surface the GPU column the first time GPU-bearing rows load for a kind
|
|
3177
3271
|
// the user has never customized — a static default can't know whether the
|
|
@@ -3212,24 +3306,63 @@ export function ResourcesView({
|
|
|
3212
3306
|
annotation: Array.from(annotations).sort(),
|
|
3213
3307
|
}
|
|
3214
3308
|
}, [resources])
|
|
3215
|
-
const isLoading = selectedQuery?.isLoading ?? true
|
|
3216
|
-
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])
|
|
3217
3359
|
|
|
3218
3360
|
// Derive counts — prefer lightweight resourceCounts prop over full query data
|
|
3219
3361
|
const counts = useMemo(() => {
|
|
3220
3362
|
if (useNewCountsMode) {
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
for (const resource of resourcesToCount) {
|
|
3225
|
-
const key = resource.group ? `${resource.group}/${resource.kind}` : resource.kind
|
|
3226
|
-
if (unavailableKinds.has(key)) {
|
|
3227
|
-
results[key] = null
|
|
3228
|
-
} else if (key in resourceCountsProp!) {
|
|
3229
|
-
results[key] = resourceCountsProp![key]
|
|
3230
|
-
} else {
|
|
3231
|
-
results[key] = 0
|
|
3232
|
-
}
|
|
3363
|
+
const results = deriveSidebarResourceCounts(resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache)
|
|
3364
|
+
if (selectedQueryHasLoadedCount && selectedLoadedResourceCount != null) {
|
|
3365
|
+
results[selectedKindCountKey] = selectedLoadedResourceCount
|
|
3233
3366
|
}
|
|
3234
3367
|
return results
|
|
3235
3368
|
}
|
|
@@ -3241,7 +3374,14 @@ export function ResourcesView({
|
|
|
3241
3374
|
results[key] = Array.isArray(data) ? data.length : 0
|
|
3242
3375
|
})
|
|
3243
3376
|
return results
|
|
3244
|
-
}, [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])
|
|
3245
3385
|
|
|
3246
3386
|
// Track which resource kinds returned 403 Forbidden
|
|
3247
3387
|
const forbiddenKinds = useMemo(() => {
|
|
@@ -3262,9 +3402,6 @@ export function ResourcesView({
|
|
|
3262
3402
|
// denials) OR the selected kind is in the counts `forbidden` set. Denied
|
|
3263
3403
|
// cluster-scoped kinds return 200 with `[]` from the list endpoint, so the
|
|
3264
3404
|
// 403 signal alone misses them — they'd fall through to "No <kind> found".
|
|
3265
|
-
const selectedKindCountKey = selectedKind.group
|
|
3266
|
-
? `${selectedKind.group}/${selectedKind.kind}`
|
|
3267
|
-
: selectedKind.kind
|
|
3268
3405
|
// Actual rows win over a stale counts `forbidden` entry: a kind can be marked
|
|
3269
3406
|
// forbidden/unavailable in counts (e.g. an informer not yet synced at counts
|
|
3270
3407
|
// time) while the list query has since returned data — show the table, not
|
|
@@ -3926,6 +4063,7 @@ export function ResourcesView({
|
|
|
3926
4063
|
// a no-op for that case.
|
|
3927
4064
|
const hasAnyFilter =
|
|
3928
4065
|
!!searchTerm ||
|
|
4066
|
+
regexMode ||
|
|
3929
4067
|
!!labelSelector ||
|
|
3930
4068
|
hasOwnerFilter ||
|
|
3931
4069
|
problemFilters.length > 0 ||
|
|
@@ -3994,7 +4132,7 @@ export function ResourcesView({
|
|
|
3994
4132
|
apiResources={apiResourcesProp}
|
|
3995
4133
|
resourceCounts={counts}
|
|
3996
4134
|
resourceForbidden={Array.from(forbiddenKinds)}
|
|
3997
|
-
resourceUnavailable={
|
|
4135
|
+
resourceUnavailable={sidebarResourceUnavailable}
|
|
3998
4136
|
pinned={pinned}
|
|
3999
4137
|
togglePin={togglePin}
|
|
4000
4138
|
isPinned={isPinned}
|
|
@@ -4013,9 +4151,8 @@ export function ResourcesView({
|
|
|
4013
4151
|
<div className="flex-1 min-w-0">
|
|
4014
4152
|
<div className="relative max-w-md">
|
|
4015
4153
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
4016
|
-
<
|
|
4154
|
+
<Input
|
|
4017
4155
|
ref={searchInputRef}
|
|
4018
|
-
type="text"
|
|
4019
4156
|
placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
|
|
4020
4157
|
value={searchTerm}
|
|
4021
4158
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
@@ -4042,7 +4179,8 @@ export function ResourcesView({
|
|
|
4042
4179
|
}
|
|
4043
4180
|
}}
|
|
4044
4181
|
className={clsx(
|
|
4045
|
-
'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',
|
|
4046
4184
|
searchRegex.error
|
|
4047
4185
|
? 'border-red-500/60 focus:ring-red-500'
|
|
4048
4186
|
: 'border-theme-border-light focus:ring-skyhook-500'
|
|
@@ -4055,7 +4193,8 @@ export function ResourcesView({
|
|
|
4055
4193
|
aria-label={regexMode ? 'Disable regex search' : 'Enable regex search'}
|
|
4056
4194
|
title={regexMode ? 'Regex search enabled — click to disable' : 'Enable regex search'}
|
|
4057
4195
|
className={clsx(
|
|
4058
|
-
'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',
|
|
4059
4198
|
regexMode
|
|
4060
4199
|
? 'bg-skyhook-500/20 text-skyhook-400'
|
|
4061
4200
|
: 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover'
|
|
@@ -4063,6 +4202,19 @@ export function ResourcesView({
|
|
|
4063
4202
|
>
|
|
4064
4203
|
<Regex className="w-3.5 h-3.5" />
|
|
4065
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
|
+
)}
|
|
4066
4218
|
{searchRegex.error && (
|
|
4067
4219
|
<div
|
|
4068
4220
|
title={searchRegex.error}
|
|
@@ -4154,8 +4306,7 @@ export function ResourcesView({
|
|
|
4154
4306
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
4155
4307
|
<div className="relative flex-1">
|
|
4156
4308
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
4157
|
-
<
|
|
4158
|
-
type="text"
|
|
4309
|
+
<Input
|
|
4159
4310
|
placeholder="Search labels..."
|
|
4160
4311
|
value={labelSearch}
|
|
4161
4312
|
onChange={(e) => setLabelSearch(e.target.value)}
|
|
@@ -4333,8 +4484,7 @@ export function ResourcesView({
|
|
|
4333
4484
|
setCustomColumnDraft(d => ({ ...d, path: '' }))
|
|
4334
4485
|
}}
|
|
4335
4486
|
>
|
|
4336
|
-
<
|
|
4337
|
-
type="text"
|
|
4487
|
+
<Input
|
|
4338
4488
|
list={customColKeysListId}
|
|
4339
4489
|
value={customColumnDraft.path}
|
|
4340
4490
|
onChange={e => setCustomColumnDraft(d => ({ ...d, path: e.target.value }))}
|
|
@@ -4519,7 +4669,10 @@ export function ResourcesView({
|
|
|
4519
4669
|
<p>No {selectedKind.kind} found</p>
|
|
4520
4670
|
{searchTerm && (
|
|
4521
4671
|
<button
|
|
4522
|
-
onClick={() =>
|
|
4672
|
+
onClick={() => {
|
|
4673
|
+
setSearchTerm('')
|
|
4674
|
+
setRegexMode(false)
|
|
4675
|
+
}}
|
|
4523
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"
|
|
4524
4677
|
>
|
|
4525
4678
|
No results for "{searchTerm}"
|
|
@@ -4716,8 +4869,7 @@ export function ResourcesView({
|
|
|
4716
4869
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
4717
4870
|
<div className="relative flex-1">
|
|
4718
4871
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
4719
|
-
<
|
|
4720
|
-
type="text"
|
|
4872
|
+
<Input
|
|
4721
4873
|
placeholder="Search..."
|
|
4722
4874
|
value={columnFilterSearch}
|
|
4723
4875
|
onChange={(e) => setColumnFilterSearch(e.target.value)}
|
|
@@ -5218,6 +5370,8 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5218
5370
|
return <RolloutCell resource={resource} column={column} />
|
|
5219
5371
|
case 'workflows':
|
|
5220
5372
|
return <WorkflowCell resource={resource} column={column} />
|
|
5373
|
+
case 'cronworkflows':
|
|
5374
|
+
return <CronWorkflowCell resource={resource} column={column} />
|
|
5221
5375
|
case 'certificates':
|
|
5222
5376
|
return <CertificateCell resource={resource} column={column} />
|
|
5223
5377
|
case 'persistentvolumes':
|
|
@@ -5250,6 +5404,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5250
5404
|
case 'sealedsecrets':
|
|
5251
5405
|
return <SealedSecretCell resource={resource} column={column} />
|
|
5252
5406
|
case 'workflowtemplates':
|
|
5407
|
+
case 'clusterworkflowtemplates':
|
|
5253
5408
|
return <WorkflowTemplateCell resource={resource} column={column} />
|
|
5254
5409
|
case 'networkpolicies':
|
|
5255
5410
|
return <NetworkPolicyCell resource={resource} column={column} />
|
|
@@ -6584,6 +6739,33 @@ function WorkflowCell({ resource, column }: { resource: any; column: string }) {
|
|
|
6584
6739
|
}
|
|
6585
6740
|
}
|
|
6586
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
|
+
|
|
6587
6769
|
|
|
6588
6770
|
function PersistentVolumeCell({ resource, column }: { resource: any; column: string }) {
|
|
6589
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
|
+
}
|
|
@@ -48,7 +48,9 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
48
48
|
const conditions = status.conditions || []
|
|
49
49
|
|
|
50
50
|
const startTime = status.startTime ? new Date(status.startTime) : null
|
|
51
|
-
const
|
|
51
|
+
const terminalCondition = conditions.find((c: any) => (c.type === 'Complete' || c.type === 'Failed') && c.status === 'True')
|
|
52
|
+
const finishedAt = status.completionTime || terminalCondition?.lastTransitionTime
|
|
53
|
+
const completionTime = finishedAt ? new Date(finishedAt) : null
|
|
52
54
|
const duration = startTime && completionTime
|
|
53
55
|
? formatDuration(completionTime.getTime() - startTime.getTime(), true)
|
|
54
56
|
: startTime
|