@skyhook-io/k8s-ui 1.16.3 → 1.17.1
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/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +57 -17
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -24
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +99 -39
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +8 -74
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +21 -2
- package/src/types/core.ts +60 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ActiveFilterBar, type ActiveFilterBarProps } from './ActiveFilterBar'
|
|
6
|
+
|
|
7
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
8
|
+
|
|
9
|
+
let root: Root | null = null
|
|
10
|
+
let element: HTMLDivElement
|
|
11
|
+
|
|
12
|
+
async function render(overrides: Partial<ActiveFilterBarProps> = {}) {
|
|
13
|
+
const props: ActiveFilterBarProps = {
|
|
14
|
+
columnFilters: {},
|
|
15
|
+
columnFilterExcludes: {},
|
|
16
|
+
problemFilters: [],
|
|
17
|
+
labelSelector: '',
|
|
18
|
+
onClearColumn: vi.fn(),
|
|
19
|
+
onClearProblems: vi.fn(),
|
|
20
|
+
onClearLabels: vi.fn(),
|
|
21
|
+
...overrides,
|
|
22
|
+
}
|
|
23
|
+
element = document.createElement('div')
|
|
24
|
+
root = createRoot(element)
|
|
25
|
+
await act(async () => root!.render(<ActiveFilterBar {...props} />))
|
|
26
|
+
return props
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buttonWithText(text: string): HTMLButtonElement {
|
|
30
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent?.includes(text))
|
|
31
|
+
if (!button) throw new Error(`no button containing "${text}"`)
|
|
32
|
+
return button
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await act(async () => root?.unmount())
|
|
37
|
+
root = null
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('ActiveFilterBar', () => {
|
|
41
|
+
it('renders nothing without active filters or a restore', async () => {
|
|
42
|
+
await render({ columnFilters: { status: [] } })
|
|
43
|
+
expect(element.innerHTML).toBe('')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders one chip per active filter, marking excluded columns', async () => {
|
|
47
|
+
await render({
|
|
48
|
+
columnFilters: { status: ['Running', 'Pending'], node: ['n1'], empty: [] },
|
|
49
|
+
columnFilterExcludes: { node: true },
|
|
50
|
+
problemFilters: ['crashloop'],
|
|
51
|
+
labelSelector: 'app=nginx',
|
|
52
|
+
})
|
|
53
|
+
expect(element.querySelectorAll('button')).toHaveLength(4)
|
|
54
|
+
expect(element.textContent).toContain('status: Running, Pending')
|
|
55
|
+
expect(element.textContent).toContain('node: not n1')
|
|
56
|
+
expect(element.textContent).toContain('Problems: crashloop')
|
|
57
|
+
expect(element.textContent).toContain('app=nginx')
|
|
58
|
+
expect(element.textContent).not.toContain('restored')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('calls the matching clear handler when a chip is clicked', async () => {
|
|
62
|
+
const props = await render({ columnFilters: { status: ['Running'] }, problemFilters: ['oom'], labelSelector: 'tier=web' })
|
|
63
|
+
await act(async () => buttonWithText('status:').click())
|
|
64
|
+
await act(async () => buttonWithText('Problems:').click())
|
|
65
|
+
await act(async () => buttonWithText('tier=web').click())
|
|
66
|
+
expect(props.onClearColumn).toHaveBeenCalledWith('status')
|
|
67
|
+
expect(props.onClearProblems).toHaveBeenCalledOnce()
|
|
68
|
+
expect(props.onClearLabels).toHaveBeenCalledOnce()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('shows the restored line only when restored, with Clear calling onClearAll', async () => {
|
|
72
|
+
const onClearAll = vi.fn()
|
|
73
|
+
await render({ restored: true, onClearAll, columnFilters: { status: ['Running'] } })
|
|
74
|
+
expect(element.textContent).toContain('Filters restored from your last visit')
|
|
75
|
+
await act(async () => buttonWithText('Clear').click())
|
|
76
|
+
expect(onClearAll).toHaveBeenCalledOnce()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { AlertTriangle, History, ListFilter, Tag, X } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export interface ActiveFilterBarProps {
|
|
5
|
+
columnFilters: Record<string, string[]>
|
|
6
|
+
columnFilterExcludes: Record<string, boolean>
|
|
7
|
+
problemFilters: string[]
|
|
8
|
+
labelSelector: string
|
|
9
|
+
onClearColumn: (key: string) => void
|
|
10
|
+
onClearProblems: () => void
|
|
11
|
+
onClearLabels: () => void
|
|
12
|
+
restored?: boolean
|
|
13
|
+
onClearAll?: () => void
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ActiveFilterBar({
|
|
18
|
+
columnFilters,
|
|
19
|
+
columnFilterExcludes,
|
|
20
|
+
problemFilters,
|
|
21
|
+
labelSelector,
|
|
22
|
+
onClearColumn,
|
|
23
|
+
onClearProblems,
|
|
24
|
+
onClearLabels,
|
|
25
|
+
restored = false,
|
|
26
|
+
onClearAll,
|
|
27
|
+
className,
|
|
28
|
+
}: ActiveFilterBarProps) {
|
|
29
|
+
const activeColEntries = Object.entries(columnFilters).filter(([, vals]) => vals.length > 0)
|
|
30
|
+
const hasChips = activeColEntries.length > 0 || problemFilters.length > 0 || !!labelSelector
|
|
31
|
+
if (!hasChips && !restored) return null
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={clsx('flex flex-wrap items-center gap-1.5', className)}>
|
|
35
|
+
{restored && (
|
|
36
|
+
<span className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
37
|
+
<History className="w-3 h-3" />
|
|
38
|
+
<span>Filters restored from your last visit.</span>
|
|
39
|
+
{onClearAll && (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={onClearAll}
|
|
43
|
+
className="text-theme-text-secondary hover:text-theme-text-primary underline underline-offset-2"
|
|
44
|
+
>
|
|
45
|
+
Clear
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
{activeColEntries.map(([key, vals]) => (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
key={key}
|
|
54
|
+
onClick={() => onClearColumn(key)}
|
|
55
|
+
className="flex items-center gap-1 px-2 py-1 text-xs selection selection-text rounded-md hover:selection-strong transition-colors"
|
|
56
|
+
>
|
|
57
|
+
<ListFilter className="w-3 h-3" />
|
|
58
|
+
<span>{key}: {columnFilterExcludes[key] ? 'not ' : ''}{vals.join(', ')}</span>
|
|
59
|
+
<X className="w-3 h-3" />
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
{problemFilters.length > 0 && (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={onClearProblems}
|
|
66
|
+
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-500/15 text-red-700 dark:text-red-300 rounded-md hover:bg-red-500/25 transition-colors"
|
|
67
|
+
>
|
|
68
|
+
<AlertTriangle className="w-3 h-3" />
|
|
69
|
+
<span>Problems: {problemFilters.join(', ')}</span>
|
|
70
|
+
<X className="w-3 h-3" />
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
{labelSelector && (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={onClearLabels}
|
|
77
|
+
className="flex items-center gap-1 px-2 py-1 text-xs bg-green-500/15 text-green-700 dark:text-green-300 rounded-md hover:bg-green-500/25 transition-colors"
|
|
78
|
+
>
|
|
79
|
+
<Tag className="w-3 h-3" />
|
|
80
|
+
<span>{labelSelector}</span>
|
|
81
|
+
<X className="w-3 h-3" />
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
6
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
7
|
+
window.matchMedia = vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })
|
|
8
|
+
let root: ReturnType<typeof createRoot>
|
|
9
|
+
let host: HTMLDivElement
|
|
10
|
+
afterEach(() => { act(() => root?.unmount()); host?.remove() })
|
|
11
|
+
it('navigates Favorites, Views and grouped kinds in their visible order', () => {
|
|
12
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
13
|
+
const view = vi.fn(), kind = vi.fn()
|
|
14
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
15
|
+
const cluster = { group: 'postgresql.cnpg.io', version: 'v1', kind: 'Cluster', name: 'clusters', namespaced: true, isCrd: true, verbs: ['list'] }
|
|
16
|
+
act(() => root.render(<ResourcesSidebar selectedKind={null} onSelectedKindChange={kind} apiResources={[cluster]} resourceCounts={{ 'postgresql.cnpg.io/Cluster': 1 }} pinned={[{ name: 'pods', kind: 'Pod', group: '' }]} categoryWorkspaces={{ CloudNativePG: { destinations: [{ id: 'overview', label: 'Clusters view', onSelect: view }], defaultKindsCollapsed: true } }} />))
|
|
17
|
+
const category = [...host.querySelectorAll('button')].find((b) => b.textContent?.includes('CloudNativePG'))!
|
|
18
|
+
if (category.getAttribute('aria-expanded') !== 'true') act(() => category.click())
|
|
19
|
+
const input = host.querySelector('input')!
|
|
20
|
+
const key = (name: string) => act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: name, bubbles: true })))
|
|
21
|
+
key('ArrowDown'); key('ArrowDown'); key('Enter')
|
|
22
|
+
expect(view).toHaveBeenCalledTimes(1)
|
|
23
|
+
expect(kind).not.toHaveBeenCalled()
|
|
24
|
+
act(() => [...host.querySelectorAll('button')].find((b) => b.textContent === 'Resource kinds')!.click())
|
|
25
|
+
key('ArrowDown'); key('ArrowDown'); key('ArrowDown'); key('Enter')
|
|
26
|
+
expect(kind).toHaveBeenCalledWith(expect.objectContaining({ name: 'clusters', group: 'postgresql.cnpg.io' }))
|
|
27
|
+
})
|
|
28
|
+
it('opens a matching View with Enter when search finds no kind', () => {
|
|
29
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
30
|
+
const view = vi.fn()
|
|
31
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
32
|
+
act(() => root.render(<ResourcesSidebar selectedKind={null} onSelectedKindChange={() => {}} apiResources={[{ group: 'postgresql.cnpg.io', version: 'v1', kind: 'Cluster', name: 'clusters', namespaced: true, isCrd: true, verbs: ['list'] }]} resourceCounts={{ 'postgresql.cnpg.io/Cluster': 1 }} categoryWorkspaces={{ CloudNativePG: { destinations: [{ id: 'operator', label: 'Operator', onSelect: view }], defaultKindsCollapsed: true } }} />))
|
|
33
|
+
const input = host.querySelector('input')!
|
|
34
|
+
act(() => {
|
|
35
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(input, 'operator')
|
|
36
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
37
|
+
})
|
|
38
|
+
expect(host.textContent).toContain('Operator')
|
|
39
|
+
expect(host.textContent).not.toContain('>Cluster<')
|
|
40
|
+
act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
|
|
41
|
+
expect(view).toHaveBeenCalledTimes(1)
|
|
42
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
3
|
import type { APIResource } from '../../types'
|
|
4
|
-
import { rawCRDGroupTitle, resourceMatchesSidebarFilter, ResourcesSidebar } from './ResourcesSidebar'
|
|
4
|
+
import { rawCRDGroupTitle, resourceMatchesSidebarFilter, ResourcesSidebar, sidebarDestinationsMatching } from './ResourcesSidebar'
|
|
5
5
|
|
|
6
6
|
const sqlInstance: APIResource = {
|
|
7
7
|
group: 'sql.cnrm.cloud.google.com',
|
|
@@ -144,7 +144,7 @@ describe('ResourcesSidebar category workspaces', () => {
|
|
|
144
144
|
}}
|
|
145
145
|
/>
|
|
146
146
|
)
|
|
147
|
-
expect(html).toContain('
|
|
147
|
+
expect(html).toContain('Views')
|
|
148
148
|
expect(html).toContain('Overview')
|
|
149
149
|
expect(html).toContain('pg-orders')
|
|
150
150
|
expect(html).toContain('Counts for namespace payments')
|
|
@@ -153,6 +153,21 @@ describe('ResourcesSidebar category workspaces', () => {
|
|
|
153
153
|
expect(html).not.toContain('selection-strong selection-text">Pod')
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
+
it('marks a count taken over partly readable data as a lower bound, and never shows its zero as none', () => {
|
|
157
|
+
const render = (count: number) =>
|
|
158
|
+
renderToString(
|
|
159
|
+
<ResourcesSidebar
|
|
160
|
+
selectedKind={null}
|
|
161
|
+
onSelectedKindChange={() => {}}
|
|
162
|
+
apiResources={[cnpgCluster]}
|
|
163
|
+
resourceCounts={{ 'postgresql.cnpg.io/Cluster': 1 }}
|
|
164
|
+
categoryWorkspaces={{ CloudNativePG: { destinations: [{ id: 'overview', label: 'Overview', count, countLowerBound: true, onSelect: () => {} }] } }}
|
|
165
|
+
/>,
|
|
166
|
+
)
|
|
167
|
+
expect(render(2)).toMatch(/≥(<!-- -->)?2/)
|
|
168
|
+
expect(render(0)).toContain('–')
|
|
169
|
+
})
|
|
170
|
+
|
|
156
171
|
it('keeps a workspace category visible when it has no resources', () => {
|
|
157
172
|
const html = renderToString(
|
|
158
173
|
<ResourcesSidebar
|
|
@@ -181,3 +196,22 @@ describe('ResourcesSidebar category workspaces', () => {
|
|
|
181
196
|
expect(html).toContain('barmancloud.cnpg.io')
|
|
182
197
|
})
|
|
183
198
|
})
|
|
199
|
+
|
|
200
|
+
describe('sidebarDestinationsMatching', () => {
|
|
201
|
+
const views = ['Clusters', 'Backups', 'Declarations', 'Pooling', 'Operator'].map((label) => ({ id: label, label, onSelect: () => {} }))
|
|
202
|
+
const resources = [{ group: 'postgresql.cnpg.io' }, { group: 'barmancloud.cnpg.io' }]
|
|
203
|
+
const labels = (term: string) => sidebarDestinationsMatching('CloudNativePG', resources, views, term).map((d) => d.label)
|
|
204
|
+
|
|
205
|
+
it('keeps every view when the term names the category or one of its API groups', () => {
|
|
206
|
+
expect(labels('cloud')).toEqual(['Clusters', 'Backups', 'Declarations', 'Pooling', 'Operator'])
|
|
207
|
+
expect(labels('cnpg')).toEqual(['Clusters', 'Backups', 'Declarations', 'Pooling', 'Operator'])
|
|
208
|
+
})
|
|
209
|
+
it('keeps the views whose label matches otherwise', () => {
|
|
210
|
+
expect(labels('backup')).toEqual(['Backups'])
|
|
211
|
+
expect(labels('declar')).toEqual(['Declarations'])
|
|
212
|
+
expect(labels('deployment')).toEqual([])
|
|
213
|
+
})
|
|
214
|
+
it('keeps every view without a term', () => {
|
|
215
|
+
expect(labels(' ')).toHaveLength(5)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -13,6 +13,7 @@ import type { APIResource } from '../../types'
|
|
|
13
13
|
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
14
14
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
15
15
|
import { Tooltip } from '../ui/Tooltip'
|
|
16
|
+
import { certaintyGlyph } from '../facts'
|
|
16
17
|
import { Input } from '../ui/Input'
|
|
17
18
|
|
|
18
19
|
// Selected resource type info (need both name for API and kind for display)
|
|
@@ -36,6 +37,8 @@ export interface SidebarCategoryDestination {
|
|
|
36
37
|
icon?: ComponentType<{ className?: string }>
|
|
37
38
|
/** Problem count. `undefined` renders no badge; `null` renders the unknown dash. */
|
|
38
39
|
count?: number | null
|
|
40
|
+
/** The count was taken over data read only in part: rendered "≥N", and a zero is unknown rather than none. */
|
|
41
|
+
countLowerBound?: boolean
|
|
39
42
|
countTitle?: string
|
|
40
43
|
active?: boolean
|
|
41
44
|
/** The object currently open under this destination, nested beneath it. */
|
|
@@ -97,6 +100,24 @@ const CORE_RESOURCE_TYPES = [
|
|
|
97
100
|
{ kind: 'hpas', label: 'HPAs' },
|
|
98
101
|
] as const
|
|
99
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The workspace views a filter keeps: all of them when the term names the
|
|
105
|
+
* category or one of its API groups ("cloud", "cnpg"), otherwise the views
|
|
106
|
+
* whose label matches. The views are the way into a workspace, so a filter
|
|
107
|
+
* that finds the category's kinds must not hide them.
|
|
108
|
+
*/
|
|
109
|
+
export function sidebarDestinationsMatching(
|
|
110
|
+
categoryName: string,
|
|
111
|
+
resources: Pick<APIResource, 'group'>[],
|
|
112
|
+
destinations: SidebarCategoryDestination[],
|
|
113
|
+
term: string,
|
|
114
|
+
): SidebarCategoryDestination[] {
|
|
115
|
+
const normalized = term.trim().toLowerCase()
|
|
116
|
+
if (!normalized) return destinations
|
|
117
|
+
if (categoryName.toLowerCase().includes(normalized) || resources.some((r) => r.group.toLowerCase().includes(normalized))) return destinations
|
|
118
|
+
return destinations.filter((d) => d.label.toLowerCase().includes(normalized))
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
export function resourceMatchesSidebarFilter(resource: Pick<APIResource, 'kind' | 'name' | 'group'>, term: string): boolean {
|
|
101
122
|
const normalized = term.trim().toLowerCase()
|
|
102
123
|
if (!normalized) return true
|
|
@@ -427,14 +448,16 @@ export function ResourcesSidebar({
|
|
|
427
448
|
// If the group name matches, show all its resources
|
|
428
449
|
if (categoryMatches || rawGroupMatches) return category
|
|
429
450
|
const matchingResources = category.visibleResources.filter((resource: APIResource) => resourceMatchesSidebarFilter(resource, term))
|
|
430
|
-
|
|
451
|
+
const ws = categoryWorkspaces?.[category.name]
|
|
452
|
+
const matchingViews = ws ? sidebarDestinationsMatching(category.name, category.resources, ws.destinations, term) : []
|
|
453
|
+
if (matchingResources.length === 0 && matchingViews.length === 0) return null
|
|
431
454
|
return {
|
|
432
455
|
...category,
|
|
433
456
|
visibleResources: matchingResources,
|
|
434
457
|
}
|
|
435
458
|
})
|
|
436
459
|
.filter(Boolean) as typeof sortedCategories
|
|
437
|
-
}, [sortedCategories, kindFilter])
|
|
460
|
+
}, [sortedCategories, kindFilter, categoryWorkspaces])
|
|
438
461
|
|
|
439
462
|
// Auto-expand all categories when filtering
|
|
440
463
|
const isKindFiltering = kindFilter.trim().length > 0
|
|
@@ -455,26 +478,23 @@ export function ResourcesSidebar({
|
|
|
455
478
|
})
|
|
456
479
|
}
|
|
457
480
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const flatVisibleKinds = useMemo<SelectedKindInfo[]>(() => {
|
|
461
|
-
const kinds: SelectedKindInfo[] = []
|
|
481
|
+
const visibleResults = useMemo(() => {
|
|
482
|
+
const results: ({ type: 'kind'; kind: SelectedKindInfo } | { type: 'view'; category: string; destination: SidebarCategoryDestination })[] = []
|
|
462
483
|
if (favoritesExpanded) {
|
|
463
|
-
for (const
|
|
464
|
-
kinds.push({ name: p.name, kind: p.kind, group: p.group })
|
|
465
|
-
}
|
|
484
|
+
for (const kind of pinned) results.push({ type: 'kind', kind })
|
|
466
485
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
486
|
+
for (const cat of filteredCategories ?? []) {
|
|
487
|
+
if (!effectiveExpandedCategories.has(cat.name)) continue
|
|
488
|
+
const workspace = categoryWorkspaces?.[cat.name]
|
|
489
|
+
for (const destination of workspace ? sidebarDestinationsMatching(cat.name, cat.resources, workspace.destinations, kindFilter) : []) {
|
|
490
|
+
results.push({ type: 'view', category: cat.name, destination })
|
|
491
|
+
}
|
|
492
|
+
if (isKindFiltering || kindsOpen(cat.name)) {
|
|
493
|
+
for (const kind of workspace ? sortByGroup(cat.visibleResources) : cat.visibleResources) results.push({ type: 'kind', kind })
|
|
474
494
|
}
|
|
475
495
|
}
|
|
476
|
-
return
|
|
477
|
-
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories, kindsOpenOverrides, categoryWorkspaces, activeDestinationCategory]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
496
|
+
return results
|
|
497
|
+
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories, kindsOpenOverrides, categoryWorkspaces, activeDestinationCategory, isKindFiltering, kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
478
498
|
|
|
479
499
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
480
500
|
// Reset highlight when the filter or kind list changes
|
|
@@ -482,10 +502,12 @@ export function ResourcesSidebar({
|
|
|
482
502
|
setHighlightedIndex(kindFilter ? 0 : -1)
|
|
483
503
|
}, [kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
484
504
|
|
|
485
|
-
const
|
|
486
|
-
?
|
|
505
|
+
const highlightedResult = highlightedIndex >= 0 && highlightedIndex < visibleResults.length
|
|
506
|
+
? visibleResults[highlightedIndex]
|
|
487
507
|
: null
|
|
488
508
|
|
|
509
|
+
const highlightedKind = highlightedResult?.type === 'kind' ? highlightedResult.kind : null
|
|
510
|
+
|
|
489
511
|
// Scroll the highlighted kind button into view
|
|
490
512
|
const highlightedRef = useRef<HTMLButtonElement>(null)
|
|
491
513
|
useEffect(() => {
|
|
@@ -501,18 +523,19 @@ export function ResourcesSidebar({
|
|
|
501
523
|
;(e.target as HTMLInputElement).blur()
|
|
502
524
|
} else if (e.key === 'ArrowDown') {
|
|
503
525
|
e.preventDefault()
|
|
504
|
-
setHighlightedIndex(prev => Math.min(prev + 1,
|
|
526
|
+
setHighlightedIndex(prev => Math.min(prev + 1, visibleResults.length - 1))
|
|
505
527
|
} else if (e.key === 'ArrowUp') {
|
|
506
528
|
e.preventDefault()
|
|
507
529
|
setHighlightedIndex(prev => Math.max(prev - 1, 0))
|
|
508
|
-
} else if (e.key === 'Enter' &&
|
|
530
|
+
} else if (e.key === 'Enter' && highlightedResult) {
|
|
509
531
|
e.preventDefault()
|
|
510
|
-
|
|
532
|
+
if (highlightedResult.type === 'view') highlightedResult.destination.onSelect()
|
|
533
|
+
else selectKind(highlightedResult.kind)
|
|
511
534
|
setHighlightedIndex(-1)
|
|
512
535
|
setKindFilter('')
|
|
513
536
|
onKindNavigated?.()
|
|
514
537
|
}
|
|
515
|
-
}, [
|
|
538
|
+
}, [visibleResults.length, highlightedResult, selectKind, onKindNavigated]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
516
539
|
|
|
517
540
|
const isKindHighlighted = useCallback((name: string, group: string) => {
|
|
518
541
|
return highlightedKind?.name === name && highlightedKind?.group === group
|
|
@@ -618,6 +641,7 @@ export function ResourcesSidebar({
|
|
|
618
641
|
const rawGroupTitle = rawCRDGroupTitle(category.resources)
|
|
619
642
|
const workspace = categoryWorkspaces?.[category.name]
|
|
620
643
|
const showKinds = isKindFiltering || kindsOpen(category.name)
|
|
644
|
+
const views = workspace ? sidebarDestinationsMatching(category.name, category.resources, workspace.destinations, kindFilter) : []
|
|
621
645
|
return (
|
|
622
646
|
<div key={category.name} className="mb-2">
|
|
623
647
|
<button
|
|
@@ -635,9 +659,14 @@ export function ResourcesSidebar({
|
|
|
635
659
|
)}
|
|
636
660
|
</button>
|
|
637
661
|
<Collapse open={isExpanded} id={disclosurePanelId(categoryPanelBase, category.name)}>
|
|
638
|
-
{workspace &&
|
|
662
|
+
{workspace && views.length > 0 && (
|
|
639
663
|
<WorkspaceDestinations
|
|
640
664
|
workspace={workspace}
|
|
665
|
+
highlightedId={highlightedResult?.type === 'view' && highlightedResult.category === category.name ? highlightedResult.destination.id : undefined}
|
|
666
|
+
highlightedRef={highlightedRef}
|
|
667
|
+
destinations={views}
|
|
668
|
+
filtering={isKindFiltering}
|
|
669
|
+
hasKinds={category.visibleResources.length > 0}
|
|
641
670
|
kindsOpen={showKinds}
|
|
642
671
|
onToggleKinds={() => toggleKinds(category.name)}
|
|
643
672
|
kindsPanelId={disclosurePanelId(categoryPanelBase, `${category.name}-kinds`)}
|
|
@@ -776,45 +805,63 @@ function groupHeaderBefore(resources: APIResource[], index: number): string | nu
|
|
|
776
805
|
return group || 'core'
|
|
777
806
|
}
|
|
778
807
|
|
|
808
|
+
// While the sidebar is filtered, `destinations` are the views that match and
|
|
809
|
+
// the kinds below are forced open, so they get a label rather than a toggle.
|
|
779
810
|
function WorkspaceDestinations({
|
|
780
811
|
workspace,
|
|
812
|
+
destinations,
|
|
813
|
+
filtering,
|
|
814
|
+
hasKinds,
|
|
781
815
|
kindsOpen,
|
|
782
816
|
onToggleKinds,
|
|
783
817
|
kindsPanelId,
|
|
818
|
+
highlightedId,
|
|
819
|
+
highlightedRef,
|
|
784
820
|
}: {
|
|
785
821
|
workspace: SidebarCategoryWorkspace
|
|
822
|
+
highlightedId?: string
|
|
823
|
+
highlightedRef: React.RefObject<HTMLButtonElement | null>
|
|
824
|
+
destinations: SidebarCategoryDestination[]
|
|
825
|
+
filtering: boolean
|
|
826
|
+
hasKinds: boolean
|
|
786
827
|
kindsOpen: boolean
|
|
787
828
|
onToggleKinds: () => void
|
|
788
829
|
kindsPanelId: string
|
|
789
830
|
}) {
|
|
790
831
|
return (
|
|
791
832
|
<div className="space-y-0.5">
|
|
792
|
-
<div className="pl-5 pr-2 pt-0.5 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
793
|
-
{
|
|
833
|
+
<div className="pl-5 pr-2 pt-0.5 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Views</div>
|
|
834
|
+
{destinations.map((d) => {
|
|
794
835
|
const Icon = d.icon
|
|
795
836
|
return (
|
|
796
837
|
<div key={d.id}>
|
|
797
838
|
<button
|
|
839
|
+
ref={highlightedId === d.id ? highlightedRef : undefined}
|
|
798
840
|
onClick={d.onSelect}
|
|
799
841
|
aria-current={d.active && !d.child ? 'page' : undefined}
|
|
800
842
|
className={clsx(
|
|
801
843
|
'w-full flex items-center gap-2 pl-5 xl:pl-6 pr-2 xl:pr-3 py-1.5 rounded-lg text-sm transition-colors min-w-0',
|
|
802
844
|
d.active && !d.child
|
|
803
845
|
? 'selection-strong selection-text'
|
|
804
|
-
: d.
|
|
805
|
-
? 'text-theme-text-primary
|
|
806
|
-
:
|
|
846
|
+
: highlightedId === d.id
|
|
847
|
+
? 'bg-theme-hover text-theme-text-primary'
|
|
848
|
+
: d.active
|
|
849
|
+
? 'text-theme-text-primary font-medium hover:bg-theme-elevated'
|
|
850
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary',
|
|
807
851
|
)}
|
|
808
852
|
>
|
|
809
853
|
{Icon && <Icon className="w-4 h-4 shrink-0" />}
|
|
810
854
|
<span className="flex-1 text-left truncate">{d.label}</span>
|
|
811
|
-
{d.count === null ? (
|
|
855
|
+
{d.count === null || (d.count === 0 && d.countLowerBound) ? (
|
|
812
856
|
<Tooltip content={d.countTitle ?? COUNT_UNAVAILABLE_MESSAGE} position="left">
|
|
813
857
|
<span className="text-xs py-0.5 rounded text-center font-mono w-8 bg-theme-elevated text-theme-text-disabled">–</span>
|
|
814
858
|
</Tooltip>
|
|
815
859
|
) : d.count !== undefined && d.count > 0 ? (
|
|
816
860
|
<Tooltip content={d.countTitle ?? `${d.count}`} position="left">
|
|
817
|
-
<span className=
|
|
861
|
+
<span className={clsx('text-xs py-0.5 rounded text-center font-mono status-degraded', d.countLowerBound ? 'min-w-8 px-1' : 'w-8')}>
|
|
862
|
+
{d.countLowerBound && certaintyGlyph('lower_bound')}
|
|
863
|
+
{d.count}
|
|
864
|
+
</span>
|
|
818
865
|
</Tooltip>
|
|
819
866
|
) : null}
|
|
820
867
|
</button>
|
|
@@ -833,15 +880,19 @@ function WorkspaceDestinations({
|
|
|
833
880
|
{workspace.scopeNote && (
|
|
834
881
|
<div className="pl-6 pr-2 pt-0.5 text-[11px] text-theme-text-tertiary">{workspace.scopeNote}</div>
|
|
835
882
|
)}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
883
|
+
{filtering ? (
|
|
884
|
+
hasKinds && <div className="pl-5 pr-2 pt-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Resource kinds</div>
|
|
885
|
+
) : (
|
|
886
|
+
<button
|
|
887
|
+
aria-expanded={kindsOpen}
|
|
888
|
+
aria-controls={kindsPanelId}
|
|
889
|
+
onClick={onToggleKinds}
|
|
890
|
+
className="w-full flex items-center gap-1.5 pl-5 pr-2 pt-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
891
|
+
>
|
|
892
|
+
<CollapseChevron open={kindsOpen} className="w-3 h-3" />
|
|
893
|
+
<span>Resource kinds</span>
|
|
894
|
+
</button>
|
|
895
|
+
)}
|
|
845
896
|
</div>
|
|
846
897
|
)
|
|
847
898
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { hasFilterIntent } from './ResourcesView'
|
|
3
|
+
import { kindFiltersFromSearch, kindFiltersToSearch, serializeColumnFilters } from './resource-utils'
|
|
4
|
+
|
|
5
|
+
describe('kindFiltersFromSearch', () => {
|
|
6
|
+
it('restores column filters, their exclude operator and problem filters', () => {
|
|
7
|
+
const filters = serializeColumnFilters({ namespace: ['a', 'b'], status: ['Running'] }, { status: true })
|
|
8
|
+
const search = `?${new URLSearchParams({ filters, problems: 'failed,pending', search: 'web' })}`
|
|
9
|
+
|
|
10
|
+
expect(kindFiltersFromSearch(search)).toEqual({
|
|
11
|
+
columnFilters: { namespace: ['a', 'b'], status: ['Running'] },
|
|
12
|
+
columnFilterExcludes: { status: true },
|
|
13
|
+
problemFilters: ['failed', 'pending'],
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('starts empty for a kind with nothing remembered', () => {
|
|
18
|
+
expect(kindFiltersFromSearch(undefined)).toEqual({ columnFilters: {}, columnFilterExcludes: {}, problemFilters: [] })
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('kindFiltersToSearch', () => {
|
|
23
|
+
it('round-trips through kindFiltersFromSearch and keeps only the per-kind filters', () => {
|
|
24
|
+
const filters = serializeColumnFilters({ namespace: ['a'], status: ['Running'] }, { status: true })
|
|
25
|
+
const saved = kindFiltersToSearch(kindFiltersFromSearch(`?${new URLSearchParams({ filters, problems: 'failed', search: 'web', namespaces: 'x' })}`))
|
|
26
|
+
|
|
27
|
+
expect(new URLSearchParams(saved).get('search')).toBeNull()
|
|
28
|
+
expect(kindFiltersFromSearch(saved)).toEqual({
|
|
29
|
+
columnFilters: { namespace: ['a'], status: ['Running'] },
|
|
30
|
+
columnFilterExcludes: { status: true },
|
|
31
|
+
problemFilters: ['failed'],
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('is empty when there is nothing to save, ignoring an exclude flag with no values', () => {
|
|
36
|
+
expect(kindFiltersToSearch({ columnFilters: { status: [] }, columnFilterExcludes: { status: true }, problemFilters: [] })).toBe('')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('hasFilterIntent', () => {
|
|
41
|
+
it('is false for a bare list URL, even with the namespace pick or a kind group', () => {
|
|
42
|
+
expect(hasFilterIntent('')).toBe(false)
|
|
43
|
+
expect(hasFilterIntent('?namespaces=a,b&apiGroup=serving.knative.dev&ai-run=r1')).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it.each(['filters=status:Running', 'problems=failed', 'search=web', 'regex=true', 'labels=app%3Dweb', 'ownerKind=DaemonSet', 'ownerName=x', 'resource=a/web', 'showInactive=true'])(
|
|
47
|
+
'is true when the URL carries %s',
|
|
48
|
+
(param) => {
|
|
49
|
+
expect(hasFilterIntent(`?namespaces=a&${param}`)).toBe(true)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
})
|