@skyhook-io/k8s-ui 1.8.7 → 1.8.9
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 +3 -3
- package/src/components/applications/ApplicationsList.tsx +5 -2
- package/src/components/applications/ApplicationsView.tsx +37 -30
- package/src/components/checks/ChecksView.tsx +25 -14
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +27 -9
- package/src/components/gitops/GitOpsTableView.tsx +163 -88
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +12 -5
- package/src/components/issues/IssuesView.tsx +41 -5
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/diagnostic.ts +22 -0
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +21 -0
- package/src/components/issues/types.ts +18 -0
- package/src/components/namespace-switcher/NamespacePicker.tsx +381 -0
- package/src/components/namespace-switcher/index.ts +6 -0
- package/src/components/resources/ResourcesView.tsx +20 -81
- package/src/components/scope-pill/ScopePill.tsx +35 -0
- package/src/components/scope-pill/index.ts +2 -0
- package/src/components/timeline/TimelineList.tsx +27 -1
- package/src/components/topology/TopologyControls.tsx +90 -14
- package/src/components/ui/FreshnessControl.tsx +153 -0
- package/src/components/ui/SortableTh.tsx +16 -10
- package/src/components/ui/SummaryTile.tsx +9 -1
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +215 -32
- package/src/components/workload/WorkloadView.tsx +26 -8
- package/src/filter-state/filter-state-core.test.ts +98 -0
- package/src/filter-state/filter-state-core.ts +138 -0
- package/src/filter-state/filter-state.tsx +127 -0
- package/src/filter-state/index.ts +16 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useKeyboardShortcuts.tsx +23 -2
- package/src/hooks/useRefreshAnimation.ts +15 -2
- package/src/index.ts +12 -0
- package/src/types/core.ts +42 -0
- package/src/types/gitops-insights.ts +4 -0
- package/src/utils/animation.ts +10 -0
- package/src/utils/format-freshness.test.ts +34 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/resource-hierarchy.test.ts +51 -0
- package/src/utils/resource-hierarchy.ts +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"yaml": ">=2.0.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@types/node": "^26.0.
|
|
88
|
+
"@types/node": "^26.0.1",
|
|
89
89
|
"@types/react": "^19.2.17",
|
|
90
90
|
"@types/react-dom": "^19.2.3",
|
|
91
91
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"clsx": "^2.1.1",
|
|
96
96
|
"diff": "^9.0.0",
|
|
97
97
|
"elkjs": "^0.11.1",
|
|
98
|
-
"lucide-react": "^1.
|
|
98
|
+
"lucide-react": "^1.22.0",
|
|
99
99
|
"react": "^19.2.7",
|
|
100
100
|
"react-dom": "^19.2.7",
|
|
101
101
|
"typescript": "^6.0.2",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
1
|
+
import { useMemo, type ReactNode } from 'react'
|
|
2
2
|
import { type AppRow, buildSingleAppEntry } from '../../utils/applications'
|
|
3
3
|
import { ApplicationsView } from './ApplicationsView'
|
|
4
4
|
|
|
@@ -11,9 +11,11 @@ import { ApplicationsView } from './ApplicationsView'
|
|
|
11
11
|
export interface ApplicationsListProps {
|
|
12
12
|
apps: AppRow[]
|
|
13
13
|
onSelect: (key: string) => void
|
|
14
|
+
/** Leading element in the header actions (e.g. a freshness control). */
|
|
15
|
+
headerActions?: ReactNode
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export function ApplicationsList({ apps, onSelect }: ApplicationsListProps) {
|
|
18
|
+
export function ApplicationsList({ apps, onSelect, headerActions }: ApplicationsListProps) {
|
|
17
19
|
// Env tokens this CLUSTER proved (identity classifications on the wire) feed
|
|
18
20
|
// the namespace heuristic, so sibling-less rows in discovered env namespaces
|
|
19
21
|
// still label without any hardcoded vocabulary.
|
|
@@ -25,6 +27,7 @@ export function ApplicationsList({ apps, onSelect }: ApplicationsListProps) {
|
|
|
25
27
|
variant="single"
|
|
26
28
|
entries={entries}
|
|
27
29
|
onSelect={onSelect}
|
|
30
|
+
headerActions={headerActions}
|
|
28
31
|
title="Applications"
|
|
29
32
|
description="Deployable software in this cluster — your services, workers, and jobs, grouped by app/release evidence."
|
|
30
33
|
/>
|
|
@@ -6,6 +6,7 @@ import { StatusDot, mapHealthToTone } from '../ui/status-tone'
|
|
|
6
6
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
7
|
import { EmptyState } from '../ui/EmptyState'
|
|
8
8
|
import { SearchBox } from '../ui/SearchBox'
|
|
9
|
+
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
9
10
|
import { PageHeader } from '../ui/PageHeader'
|
|
10
11
|
import { SummaryTile, type SummaryTone } from '../ui/SummaryTile'
|
|
11
12
|
import { Facet, type FacetTone } from '../ui/Facet'
|
|
@@ -97,16 +98,33 @@ export interface ApplicationsViewProps {
|
|
|
97
98
|
/** Rendered instead of the built-in EmptyState when there are zero entries
|
|
98
99
|
* pre-filter (the fleet host injects a coverage/offline-aware empty). */
|
|
99
100
|
emptySlot?: ReactNode
|
|
101
|
+
/** Leading element in the header actions cluster (e.g. a freshness control). */
|
|
102
|
+
headerActions?: ReactNode
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
const APPS_FILTER_SCHEMA = defineFilterSchema({
|
|
106
|
+
health: { param: 'health', type: 'set' },
|
|
107
|
+
class: { param: 'class', type: 'set' },
|
|
108
|
+
type: { param: 'type', type: 'set' },
|
|
109
|
+
env: { param: 'env', type: 'set' },
|
|
110
|
+
source: { param: 'source', type: 'set' },
|
|
111
|
+
q: { param: 'q', type: 'text' },
|
|
112
|
+
system: { param: 'system', type: 'boolean' },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export function ApplicationsView({ entries: allEntries, variant, onSelect, title = 'Applications', description, emptySlot, headerActions }: ApplicationsViewProps) {
|
|
116
|
+
// Facets + search + show-system live in the URL (shareable, bookmarkable) via
|
|
117
|
+
// the shared filter-state contract. Sort stays local: it's a compound
|
|
118
|
+
// {key, dir} view-preference, not a result-narrowing filter, so it isn't
|
|
119
|
+
// forced into the filter vocabulary.
|
|
120
|
+
const filters = useFilterState(APPS_FILTER_SCHEMA)
|
|
121
|
+
const textFilter = filters.values.q
|
|
122
|
+
const fHealth = filters.values.health as Set<AppHealth>
|
|
123
|
+
const fEnv = filters.values.env
|
|
124
|
+
const fSource = filters.values.source as Set<AppSource>
|
|
125
|
+
const fClass = filters.values.class as Set<AppWorkloadClass>
|
|
126
|
+
const fType = filters.values.type as Set<AppCategory>
|
|
127
|
+
const showSystem = filters.values.system
|
|
110
128
|
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir } | null>(null)
|
|
111
129
|
|
|
112
130
|
// The Show-system toggle keys off per-entry workload namespaces, which only
|
|
@@ -242,12 +260,6 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
242
260
|
return { health, env, source, workloadClass, category }
|
|
243
261
|
}, [all])
|
|
244
262
|
|
|
245
|
-
const toggle = <T,>(set: Set<T>, setter: (s: Set<T>) => void, v: T) => {
|
|
246
|
-
const next = new Set(set)
|
|
247
|
-
next.has(v) ? next.delete(v) : next.add(v)
|
|
248
|
-
setter(next)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
263
|
// asc → desc → off (null = default health-worst-first sort).
|
|
252
264
|
const onSort = (key: SortKey) => {
|
|
253
265
|
setSort((prev) => {
|
|
@@ -265,20 +277,14 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
265
277
|
// Clickable status tile wired to the health facet — tap to filter to that tier.
|
|
266
278
|
const healthTile = (h: AppHealth, tone: SummaryTone) =>
|
|
267
279
|
counts.health[h] ? (
|
|
268
|
-
<SummaryTile key={h} label={HEALTH_META[h].label} value={counts.health[h]} tone={tone} active={fHealth.has(h)} onClick={() => toggle(
|
|
280
|
+
<SummaryTile key={h} label={HEALTH_META[h].label} value={counts.health[h]} tone={tone} active={fHealth.has(h)} onClick={() => filters.toggle('health', h)} />
|
|
269
281
|
) : null
|
|
270
282
|
|
|
271
283
|
// showSystem lives in the Filters rail, so Clear resets it too (and its
|
|
272
284
|
// non-default ON state counts as an active filter that surfaces the button).
|
|
273
|
-
const anyFilterActive =
|
|
285
|
+
const anyFilterActive = filters.isActive
|
|
274
286
|
const clearAllFilters = () => {
|
|
275
|
-
|
|
276
|
-
setFHealth(new Set())
|
|
277
|
-
setFClass(new Set())
|
|
278
|
-
setFType(new Set())
|
|
279
|
-
setFSource(new Set())
|
|
280
|
-
setFEnv(new Set())
|
|
281
|
-
setShowSystem(false)
|
|
287
|
+
filters.clearAll()
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
return (
|
|
@@ -294,6 +300,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
294
300
|
description={description}
|
|
295
301
|
actions={
|
|
296
302
|
<>
|
|
303
|
+
{headerActions}
|
|
297
304
|
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} />
|
|
298
305
|
{healthTile('unhealthy', 'error')}
|
|
299
306
|
{healthTile('degraded', 'warning')}
|
|
@@ -323,14 +330,14 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
323
330
|
)}
|
|
324
331
|
</div>
|
|
325
332
|
<div className="flex-1 overflow-y-auto">
|
|
326
|
-
<Facet icon={HeartPulse} title="Availability" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => toggle(
|
|
327
|
-
<Facet icon={Layers} title="Class" options={CLASS_ORDER.map((c) => ({ value: c, label: CLASS_META[c].label, count: counts.workloadClass[c] ?? 0 }))} selected={fClass} onToggle={(v) => toggle(
|
|
328
|
-
<Facet icon={Shapes} title="Type" options={CATEGORY_ORDER.map((c) => ({ value: c, label: CATEGORY_META[c].label, count: counts.category[c] ?? 0, tooltip: CATEGORY_META[c].tooltip }))} selected={fType} onToggle={(v) => toggle(
|
|
329
|
-
<Facet icon={Globe} title="Environment" info={<EnvHint />} options={envOptions} selected={fEnv} onToggle={(v) => toggle(
|
|
330
|
-
<Facet icon={Tag} title="Source" options={SOURCE_ORDER.map((s) => ({ value: s, label: SOURCE_META[s].label, count: counts.source[s] ?? 0 }))} selected={fSource} onToggle={(v) => toggle(
|
|
333
|
+
<Facet icon={HeartPulse} title="Availability" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => filters.toggle('health', v)} />
|
|
334
|
+
<Facet icon={Layers} title="Class" options={CLASS_ORDER.map((c) => ({ value: c, label: CLASS_META[c].label, count: counts.workloadClass[c] ?? 0 }))} selected={fClass} onToggle={(v) => filters.toggle('class', v)} />
|
|
335
|
+
<Facet icon={Shapes} title="Type" options={CATEGORY_ORDER.map((c) => ({ value: c, label: CATEGORY_META[c].label, count: counts.category[c] ?? 0, tooltip: CATEGORY_META[c].tooltip }))} selected={fType} onToggle={(v) => filters.toggle('type', v)} />
|
|
336
|
+
<Facet icon={Globe} title="Environment" info={<EnvHint />} options={envOptions} selected={fEnv} onToggle={(v) => filters.toggle('env', v)} />
|
|
337
|
+
<Facet icon={Tag} title="Source" options={SOURCE_ORDER.map((s) => ({ value: s, label: SOURCE_META[s].label, count: counts.source[s] ?? 0 }))} selected={fSource} onToggle={(v) => filters.toggle('source', v)} />
|
|
331
338
|
{systemCount > 0 && (
|
|
332
339
|
<label className="flex cursor-pointer items-center gap-2 border-b border-theme-border px-3 py-2 text-[11px] text-theme-text-secondary hover:bg-theme-hover">
|
|
333
|
-
<input type="checkbox" checked={showSystem} onChange={(e) =>
|
|
340
|
+
<input type="checkbox" checked={showSystem} onChange={(e) => filters.setBoolean('system', e.target.checked)} className="accent-skyhook-500" />
|
|
334
341
|
<span>Show system namespaces</span>
|
|
335
342
|
<span className="ml-auto tabular-nums text-theme-text-tertiary">{systemCount}</span>
|
|
336
343
|
</label>
|
|
@@ -343,7 +350,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
343
350
|
<div className="flex shrink-0 items-center gap-3 border-b border-theme-border px-4 py-3">
|
|
344
351
|
<SearchBox
|
|
345
352
|
value={textFilter}
|
|
346
|
-
onChange={
|
|
353
|
+
onChange={(v) => filters.setString('q', v)}
|
|
347
354
|
scope="applications"
|
|
348
355
|
shortcutId="applications-search"
|
|
349
356
|
className="max-w-md flex-1"
|
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { ChevronDown, ChevronRight, ExternalLink, EyeOff, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
4
|
import { ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip } from '../ui'
|
|
5
|
+
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
5
6
|
import type { CheckMeta, CheckReference } from '../audit'
|
|
6
7
|
import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
|
|
7
8
|
import {
|
|
@@ -79,11 +80,24 @@ interface FleetCheck {
|
|
|
79
80
|
clusters: Check[]
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
const CHECKS_FILTER_SCHEMA = defineFilterSchema({
|
|
84
|
+
severity: { param: 'severity', type: 'set' },
|
|
85
|
+
category: { param: 'category', type: 'set' },
|
|
86
|
+
framework: { param: 'framework', type: 'set' },
|
|
87
|
+
q: { param: 'q', type: 'text' },
|
|
88
|
+
})
|
|
89
|
+
|
|
82
90
|
export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceClick, clusterLabel, clusterLabelById, clusterFilter: clusterFilterProp, onClusterFilterChange, emptyAction, onHideCheck, onHideCategory }: ChecksViewProps) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// Severity / category / framework / search live in the URL (shareable,
|
|
92
|
+
// bookmarkable audit links) via the shared filter-state contract. The cluster
|
|
93
|
+
// facet is deliberately NOT here — it's a host-controlled seam (see
|
|
94
|
+
// onClusterFilterChange) for the multi-cluster/fleet case and isn't shown in
|
|
95
|
+
// single-cluster OSS, so it stays on its own controlled/internal path.
|
|
96
|
+
const filters = useFilterState(CHECKS_FILTER_SCHEMA)
|
|
97
|
+
const severityFilter = filters.values.severity as Set<CheckSeverity>
|
|
98
|
+
const categoryFilter = filters.values.category
|
|
99
|
+
const frameworkFilter = filters.values.framework
|
|
100
|
+
const search = filters.values.q
|
|
87
101
|
const [openId, setOpenId] = useState<string | null>(null)
|
|
88
102
|
|
|
89
103
|
// Cluster facet is controlled when the host opts in (onClusterFilterChange);
|
|
@@ -209,13 +223,10 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
|
|
|
209
223
|
return next
|
|
210
224
|
})
|
|
211
225
|
|
|
212
|
-
const hasFilters =
|
|
226
|
+
const hasFilters = filters.isActive || clusterFilter.size > 0
|
|
213
227
|
const clearAll = () => {
|
|
214
|
-
|
|
215
|
-
setCategoryFilter(new Set())
|
|
216
|
-
setFrameworkFilter(new Set())
|
|
228
|
+
filters.clearAll()
|
|
217
229
|
setClusterFilter(new Set())
|
|
218
|
-
setSearch('')
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
return (
|
|
@@ -239,13 +250,13 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
|
|
|
239
250
|
type="text"
|
|
240
251
|
placeholder="Search checks…"
|
|
241
252
|
value={search}
|
|
242
|
-
onChange={(e) =>
|
|
253
|
+
onChange={(e) => filters.setString('q', e.target.value)}
|
|
243
254
|
className="w-64 rounded-lg border border-theme-border-light bg-theme-base py-1.5 pl-9 pr-8 text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-[var(--color-radar-accent)]"
|
|
244
255
|
/>
|
|
245
256
|
{search && (
|
|
246
257
|
<button
|
|
247
258
|
type="button"
|
|
248
|
-
onClick={() =>
|
|
259
|
+
onClick={() => filters.setString('q', '')}
|
|
249
260
|
aria-label="Clear search"
|
|
250
261
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
251
262
|
>
|
|
@@ -259,17 +270,17 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
|
|
|
259
270
|
|
|
260
271
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
261
272
|
{CHECK_SEVERITIES.map((s) => (
|
|
262
|
-
<CheckSeverityChip key={s} severity={s} count={totals[s]} active={severityFilter.has(s)} onClick={() => toggle(
|
|
273
|
+
<CheckSeverityChip key={s} severity={s} count={totals[s]} active={severityFilter.has(s)} onClick={() => filters.toggle('severity', s)} />
|
|
263
274
|
))}
|
|
264
275
|
<span className="mx-1.5 h-5 w-px bg-theme-border" />
|
|
265
276
|
{CATEGORIES.map((c) => (
|
|
266
|
-
<FilterPill key={c} label={c} active={categoryFilter.has(c)} onClick={() => toggle(
|
|
277
|
+
<FilterPill key={c} label={c} active={categoryFilter.has(c)} onClick={() => filters.toggle('category', c)} />
|
|
267
278
|
))}
|
|
268
279
|
{frameworks.length > 0 && (
|
|
269
280
|
<>
|
|
270
281
|
<span className="mx-1.5 h-5 w-px bg-theme-border" />
|
|
271
282
|
{frameworks.map((fw) => (
|
|
272
|
-
<FilterPill key={fw} label={fw} active={frameworkFilter.has(fw)} onClick={() => toggle(
|
|
283
|
+
<FilterPill key={fw} label={fw} active={frameworkFilter.has(fw)} onClick={() => filters.toggle('framework', fw)} />
|
|
273
284
|
))}
|
|
274
285
|
</>
|
|
275
286
|
)}
|
|
@@ -58,6 +58,15 @@ export interface ClusterSwitcherProps {
|
|
|
58
58
|
errorSlot?: ReactNode
|
|
59
59
|
className?: string
|
|
60
60
|
align?: 'left' | 'right'
|
|
61
|
+
/**
|
|
62
|
+
* 'chip' (default) renders a self-contained bordered pill. 'segment' renders
|
|
63
|
+
* a borderless label+value cell for embedding in a shared bordered container
|
|
64
|
+
* (the unified cluster+namespace scope control) — no border/background/min-w
|
|
65
|
+
* of its own, with an optional muted {@link label} before the value.
|
|
66
|
+
*/
|
|
67
|
+
variant?: 'chip' | 'segment'
|
|
68
|
+
/** Muted label shown before the value in the 'segment' variant (e.g. "Cluster"). */
|
|
69
|
+
label?: string
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
// Trigger width cap. With middle-truncation kicking in, this is a
|
|
@@ -83,6 +92,8 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
83
92
|
errorSlot,
|
|
84
93
|
className = '',
|
|
85
94
|
align = 'left',
|
|
95
|
+
variant = 'chip',
|
|
96
|
+
label,
|
|
86
97
|
}, ref) => {
|
|
87
98
|
const [isOpen, setIsOpen] = useState(false)
|
|
88
99
|
const [search, setSearch] = useState('')
|
|
@@ -195,14 +206,21 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
195
206
|
type="button"
|
|
196
207
|
onClick={() => setIsOpen(v => !v)}
|
|
197
208
|
disabled={disabled || loading}
|
|
198
|
-
className={
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
className={
|
|
210
|
+
variant === 'segment'
|
|
211
|
+
? `flex items-center gap-1.5 px-3 py-1.5 h-full min-w-[150px] max-w-[264px] text-[13px] font-medium
|
|
212
|
+
text-theme-text-primary hover:bg-theme-hover transition-colors cursor-pointer
|
|
213
|
+
disabled:opacity-50 disabled:cursor-not-allowed`
|
|
214
|
+
: `flex items-center gap-1.5 px-2.5 py-1.5 min-w-[140px]
|
|
215
|
+
bg-theme-elevated border border-theme-border rounded text-sm font-medium
|
|
216
|
+
text-theme-text-primary hover:bg-theme-hover hover:border-theme-border-light
|
|
217
|
+
transition-colors cursor-pointer
|
|
218
|
+
disabled:opacity-50 disabled:cursor-not-allowed`
|
|
219
|
+
}
|
|
205
220
|
>
|
|
221
|
+
{label && (
|
|
222
|
+
<span className="shrink-0 font-normal text-theme-text-tertiary">{label}</span>
|
|
223
|
+
)}
|
|
206
224
|
{loading ? (
|
|
207
225
|
<>
|
|
208
226
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -220,7 +238,7 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
220
238
|
<ClusterName
|
|
221
239
|
name={currentName}
|
|
222
240
|
fallbackBadge={<Server className="w-3.5 h-3.5 text-theme-text-secondary" />}
|
|
223
|
-
className={TRIGGER_NAME_MAX_WIDTH}
|
|
241
|
+
className={variant === 'segment' ? 'min-w-0 max-w-[214px]' : TRIGGER_NAME_MAX_WIDTH}
|
|
224
242
|
noTooltip={isOpen}
|
|
225
243
|
/>
|
|
226
244
|
{currentSourceLabel && (
|
|
@@ -240,7 +258,7 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
240
258
|
)}
|
|
241
259
|
</>
|
|
242
260
|
)}
|
|
243
|
-
<ChevronDown className={`w-3 h-3
|
|
261
|
+
<ChevronDown className={`w-3 h-3 shrink-0 transition-transform ${variant === 'segment' ? '' : 'ml-auto'} ${isOpen ? 'rotate-180' : ''}`} />
|
|
244
262
|
</button>
|
|
245
263
|
|
|
246
264
|
{isOpen && (
|