@skyhook-io/k8s-ui 1.8.9 → 1.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -1,21 +1,40 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { ChevronDown, ChevronRight, ExternalLink, EyeOff, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
|
-
import { ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip } from '../ui'
|
|
3
|
+
import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown, ChevronRight, ExternalLink, EyeOff, Info, Layers, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
|
+
import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, Input, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
|
|
5
5
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
6
6
|
import type { CheckMeta, CheckReference } from '../audit'
|
|
7
7
|
import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
|
|
8
8
|
import {
|
|
9
9
|
SEVERITY_BADGE_CLASS,
|
|
10
10
|
SEVERITY_FILL_CLASS,
|
|
11
|
+
SEVERITY_HEADER_BAND_CLASS,
|
|
11
12
|
SEVERITY_LABEL,
|
|
12
13
|
SEVERITY_RAIL_CLASS,
|
|
14
|
+
SEVERITY_SOLID_CLASS,
|
|
13
15
|
SEVERITY_TEXT_CLASS,
|
|
14
|
-
categoryBadgeClass,
|
|
15
16
|
} from './severity'
|
|
16
17
|
|
|
17
18
|
const CATEGORIES: readonly string[] = ['Security', 'Reliability', 'Efficiency']
|
|
18
19
|
|
|
20
|
+
// Leading severity glyph, one per tier of the 4-tier ladder: critical = octagon,
|
|
21
|
+
// high = triangle, medium = circle, low = info.
|
|
22
|
+
const CHECK_SEVERITY_ICON: Record<CheckSeverity, ComponentType<{ className?: string }>> = {
|
|
23
|
+
critical: AlertOctagon,
|
|
24
|
+
high: AlertTriangle,
|
|
25
|
+
medium: AlertCircle,
|
|
26
|
+
low: Info,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// An out-of-contract severity the backend might emit is coerced to this tier
|
|
30
|
+
// once, up front (normalizeCheckSeverity), so the icon AND every color map
|
|
31
|
+
// (text/rail/band/pill) resolve together — a raw miss would crash the icon and
|
|
32
|
+
// silently drop the tint everywhere else.
|
|
33
|
+
const CHECK_SEVERITY_FALLBACK: CheckSeverity = 'medium'
|
|
34
|
+
// Object.hasOwn (not `in`) so inherited keys like "toString" don't slip past.
|
|
35
|
+
const normalizeCheckSeverity = (s: CheckSeverity): CheckSeverity =>
|
|
36
|
+
Object.hasOwn(CHECK_SEVERITY_ICON, s) ? s : CHECK_SEVERITY_FALLBACK
|
|
37
|
+
|
|
19
38
|
// Affected-resources shown inline before "View all". A check can fail on
|
|
20
39
|
// thousands of resources; the card stays scannable and only the rare big-list
|
|
21
40
|
// case pays the cost of a full expand.
|
|
@@ -246,8 +265,7 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
|
|
|
246
265
|
</div>
|
|
247
266
|
<div className="relative">
|
|
248
267
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
|
|
249
|
-
<
|
|
250
|
-
type="text"
|
|
268
|
+
<Input
|
|
251
269
|
placeholder="Search checks…"
|
|
252
270
|
value={search}
|
|
253
271
|
onChange={(e) => filters.setString('q', e.target.value)}
|
|
@@ -410,17 +428,22 @@ export interface CheckRemediationBlockProps {
|
|
|
410
428
|
layout?: 'columns' | 'stack'
|
|
411
429
|
}
|
|
412
430
|
|
|
431
|
+
// Remediation body: WHY IT MATTERS (info, the description) → HOW TO FIX
|
|
432
|
+
// (wrench/emerald, the remediation + doc links), rendered as icon-led sections
|
|
433
|
+
// for every host. Prose runs through renderProse so `inline-code` spans become
|
|
434
|
+
// mono chips when the catalog copy carries them. The 'stack' layout is the
|
|
435
|
+
// compact single-column variant for narrow hosts.
|
|
413
436
|
export function CheckRemediationBlock({ description, remediation, references, layout = 'columns' }: CheckRemediationBlockProps) {
|
|
414
437
|
if (!description && !remediation && (!references || references.length === 0)) return null
|
|
415
438
|
|
|
416
439
|
if (layout === 'stack') {
|
|
417
440
|
return (
|
|
418
441
|
<div className="flex flex-col gap-2">
|
|
419
|
-
{description && <p className="text-[13px] leading-relaxed text-theme-text-secondary">{description}</p>}
|
|
442
|
+
{description && <p className="text-[13px] leading-relaxed text-theme-text-secondary">{renderProse(description)}</p>}
|
|
420
443
|
{remediation && (
|
|
421
444
|
<div>
|
|
422
445
|
<div className="text-[11px] uppercase tracking-wider text-theme-text-tertiary">How to fix</div>
|
|
423
|
-
<p className="mt-0.5 text-[13px] leading-relaxed text-theme-text-secondary">{remediation}</p>
|
|
446
|
+
<p className="mt-0.5 text-[13px] leading-relaxed text-theme-text-secondary">{renderProse(remediation)}</p>
|
|
424
447
|
</div>
|
|
425
448
|
)}
|
|
426
449
|
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
@@ -429,25 +452,19 @@ export function CheckRemediationBlock({ description, remediation, references, la
|
|
|
429
452
|
}
|
|
430
453
|
|
|
431
454
|
return (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
</
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<p className="text-sm leading-relaxed text-theme-text-secondary">{description}</p>
|
|
446
|
-
</section>
|
|
447
|
-
)}
|
|
448
|
-
</div>
|
|
449
|
-
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
450
|
-
</>
|
|
455
|
+
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
456
|
+
{description && (
|
|
457
|
+
<CardSection icon={Info} label="Why it matters" tone="neutral">
|
|
458
|
+
<CardBody>{renderProse(description)}</CardBody>
|
|
459
|
+
</CardSection>
|
|
460
|
+
)}
|
|
461
|
+
{(remediation || (references && references.length > 0)) && (
|
|
462
|
+
<CardSection icon={Wrench} label="How to fix" tone="fix">
|
|
463
|
+
{remediation && <CardBody>{renderProse(remediation)}</CardBody>}
|
|
464
|
+
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
465
|
+
</CardSection>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
451
468
|
)
|
|
452
469
|
}
|
|
453
470
|
|
|
@@ -500,16 +517,27 @@ export function CheckCardShell({
|
|
|
500
517
|
dimmed,
|
|
501
518
|
}: CheckCardShellProps) {
|
|
502
519
|
const Container = as
|
|
520
|
+
const sev = normalizeCheckSeverity(severity)
|
|
521
|
+
const SeverityIcon = CHECK_SEVERITY_ICON[sev]
|
|
503
522
|
return (
|
|
504
523
|
<Container
|
|
505
524
|
className={[
|
|
506
|
-
'overflow-hidden rounded-xl border
|
|
525
|
+
'overflow-hidden rounded-xl border bg-theme-surface transition-[border-color,box-shadow] duration-200',
|
|
526
|
+
// The open card lifts via elevation — heavier shadow + a bright
|
|
527
|
+
// emphasis edge that clearly separates it from sibling cards. Severity
|
|
528
|
+
// stays rationed to the band + pill; separation is depth, not more color.
|
|
529
|
+
// ring-1 widens the edge to 2px without the layout shift a border-2
|
|
530
|
+
// swap would cause on expand.
|
|
531
|
+
open ? 'border-[var(--border-emphasis)] ring-1 ring-[var(--border-emphasis)] shadow-theme-md' : 'border-theme-border shadow-theme-sm',
|
|
507
532
|
dimmed ? 'opacity-60' : '',
|
|
508
533
|
className ?? '',
|
|
509
534
|
]
|
|
510
535
|
.filter(Boolean)
|
|
511
536
|
.join(' ')}
|
|
512
537
|
>
|
|
538
|
+
{/* Leading severity icon is the at-a-glance cue; a trailing chevron shows
|
|
539
|
+
open/closed. Collapsed: neutral row + rail. Expanded: severity-tinted
|
|
540
|
+
band + solid pill — the tint is a focus signal, not per-row alarm. */}
|
|
513
541
|
<div
|
|
514
542
|
role="button"
|
|
515
543
|
tabIndex={0}
|
|
@@ -522,31 +550,33 @@ export function CheckCardShell({
|
|
|
522
550
|
onToggle()
|
|
523
551
|
}
|
|
524
552
|
}}
|
|
525
|
-
className={`group flex cursor-pointer items-
|
|
553
|
+
className={`group flex cursor-pointer items-center gap-3 border-l-[3px] py-3 pl-3 pr-4 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40 ${open ? SEVERITY_HEADER_BAND_CLASS[sev] : SEVERITY_RAIL_CLASS[sev]}`}
|
|
526
554
|
>
|
|
527
|
-
<
|
|
555
|
+
<SeverityIcon className={`h-[18px] w-[18px] shrink-0 ${SEVERITY_TEXT_CLASS[sev]}`} aria-hidden />
|
|
528
556
|
|
|
529
557
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
530
558
|
<div className="flex flex-wrap items-center gap-2">
|
|
531
559
|
<span className="truncate text-sm font-semibold text-theme-text-primary">{title}</span>
|
|
532
|
-
<span className={`
|
|
560
|
+
<span className={`shrink-0 ${NEUTRAL_CHIP_CLASS}`}>{category}</span>
|
|
533
561
|
</div>
|
|
534
562
|
{description}
|
|
535
563
|
{summary}
|
|
536
564
|
</div>
|
|
537
565
|
|
|
538
|
-
<span className={`badge-sm
|
|
539
|
-
{SEVERITY_LABEL[
|
|
566
|
+
<span className={`badge-sm shrink-0 px-2.5 py-0.5 text-xs font-semibold ${open ? SEVERITY_SOLID_CLASS[sev] : SEVERITY_BADGE_CLASS[sev]}`}>
|
|
567
|
+
{SEVERITY_LABEL[sev]}
|
|
540
568
|
</span>
|
|
541
569
|
{renderActions?.()}
|
|
570
|
+
<ChevronRight className={`h-4 w-4 shrink-0 text-theme-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`} />
|
|
542
571
|
</div>
|
|
543
572
|
|
|
544
573
|
{/* Kept mounted (not `open &&`) so the grid-rows transition animates the
|
|
545
574
|
collapse too, matching IssueRow; inert when closed so SR + tab skip
|
|
546
|
-
the clipped content.
|
|
575
|
+
the clipped content. Body sits on the card surface (not a recessed grey
|
|
576
|
+
panel) so its text keeps enough contrast. */}
|
|
547
577
|
<div className="grid transition-[grid-template-rows] duration-200 ease-out" style={{ gridTemplateRows: open ? '1fr' : '0fr' }}>
|
|
548
578
|
<div className="overflow-hidden" inert={!open || undefined}>
|
|
549
|
-
<div className="flex flex-col
|
|
579
|
+
<div className="flex flex-col divide-y divide-theme-border/70 border-t border-theme-border bg-theme-surface py-4 pl-6 pr-4 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">{children}</div>
|
|
550
580
|
</div>
|
|
551
581
|
</div>
|
|
552
582
|
</Container>
|
|
@@ -561,7 +591,9 @@ export interface CheckClusterBreakdownGroup {
|
|
|
561
591
|
}
|
|
562
592
|
|
|
563
593
|
export interface CheckClusterBreakdownShellProps<T extends CheckClusterBreakdownGroup> {
|
|
564
|
-
heading
|
|
594
|
+
/** Optional section heading. Omit when the caller already provides one (e.g.
|
|
595
|
+
* a wrapping CardSection owns the "Affected resources" eyebrow). */
|
|
596
|
+
heading?: ReactNode
|
|
565
597
|
groups: T[]
|
|
566
598
|
renderGroupBody: (group: T) => ReactNode
|
|
567
599
|
clusterCap?: number
|
|
@@ -584,7 +616,7 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
584
616
|
|
|
585
617
|
return (
|
|
586
618
|
<section className="flex flex-col gap-1.5">
|
|
587
|
-
<h4 className="text-[11px] font-semibold uppercase tracking-
|
|
619
|
+
{heading ? <h4 className="text-[11px] font-semibold uppercase tracking-[0.06em] text-theme-text-tertiary">{heading}</h4> : null}
|
|
588
620
|
<ul className="flex flex-col gap-1">
|
|
589
621
|
{shown.map((group) => {
|
|
590
622
|
const isOpen = openClusters.has(group.id)
|
|
@@ -687,18 +719,17 @@ function FleetCheckRow({
|
|
|
687
719
|
>
|
|
688
720
|
<CheckRemediationBlock description={meta?.description} remediation={meta?.remediation} references={meta?.references} />
|
|
689
721
|
|
|
690
|
-
<
|
|
722
|
+
<CardSection
|
|
723
|
+
icon={Layers}
|
|
724
|
+
label="Affected resources"
|
|
725
|
+
labelExtra={single ? `· ${fc.totalResources}` : `· ${fc.totalResources} · ${clusterCount} clusters`}
|
|
726
|
+
>
|
|
691
727
|
{single ? (
|
|
692
|
-
<ResourceList
|
|
693
|
-
label={`Affected resources (${fc.totalResources})`}
|
|
694
|
-
check={fc.clusters[0]}
|
|
695
|
-
resourceHref={resourceHref}
|
|
696
|
-
onResourceClick={onResourceClick}
|
|
697
|
-
/>
|
|
728
|
+
<ResourceList check={fc.clusters[0]} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
698
729
|
) : (
|
|
699
730
|
<ClusterBreakdown fc={fc} clusterLabel={clusterLabel} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
700
731
|
)}
|
|
701
|
-
</
|
|
732
|
+
</CardSection>
|
|
702
733
|
</CheckCardShell>
|
|
703
734
|
)
|
|
704
735
|
}
|
|
@@ -720,11 +751,8 @@ function ClusterBreakdown({
|
|
|
720
751
|
}) {
|
|
721
752
|
return (
|
|
722
753
|
<CheckClusterBreakdownShell
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
Affected resources <span className="tabular-nums">({fc.totalResources})</span> · {fc.clusters.length} clusters
|
|
726
|
-
</>
|
|
727
|
-
}
|
|
754
|
+
// Heading omitted — the wrapping CardSection (Layers · "Affected
|
|
755
|
+
// resources") already owns the eyebrow.
|
|
728
756
|
groups={fc.clusters.map((c) => ({
|
|
729
757
|
id: c.subject.cluster_id,
|
|
730
758
|
label: <ClusterName name={clusterLabel?.(c) || c.subject.cluster_id} />,
|
|
@@ -822,7 +850,9 @@ function FindingLine({
|
|
|
822
850
|
{showMessage && <span className="ml-1 truncate text-xs text-theme-text-tertiary">{finding.message}</span>}
|
|
823
851
|
</>
|
|
824
852
|
)
|
|
825
|
-
|
|
853
|
+
// items-baseline so the smaller mono kind label shares a baseline with the
|
|
854
|
+
// larger resource name (their line-heights differ).
|
|
855
|
+
const cls = 'group/f flex w-full items-baseline gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors hover:bg-theme-hover/60'
|
|
826
856
|
return (
|
|
827
857
|
<li>
|
|
828
858
|
{onResourceClick ? (
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import type { CheckSeverity } from './types'
|
|
2
2
|
import { BADGE_SEVERITY_COLORS as sev } from '../ui/Badge'
|
|
3
|
+
import {
|
|
4
|
+
TONE_FILL_CLASS,
|
|
5
|
+
TONE_HEADER_BAND_CLASS,
|
|
6
|
+
TONE_RAIL_CLASS,
|
|
7
|
+
TONE_SOLID_CLASS,
|
|
8
|
+
TONE_TEXT_CLASS,
|
|
9
|
+
type SeverityTone,
|
|
10
|
+
} from '../ui/severity-tone'
|
|
3
11
|
|
|
4
12
|
// The visual language for the 4-tier Checks severity ladder. One hue per tier:
|
|
5
|
-
// red=critical, orange=high,
|
|
6
|
-
// rail top-to-bottom and severity is obvious without reading a word.
|
|
13
|
+
// red=critical, orange=high, yellow=medium, neutral=low — read the queue's left
|
|
14
|
+
// rail top-to-bottom and severity is obvious without reading a word. Medium is
|
|
15
|
+
// yellow (not amber) so it sits a clear hue-step below high's orange rather than
|
|
16
|
+
// blurring into it. The color strings are shared with the Issue card via the
|
|
17
|
+
// tone module; here we only map each tier onto its tone.
|
|
18
|
+
const CHECK_SEVERITY_TONE: Record<CheckSeverity, SeverityTone> = {
|
|
19
|
+
critical: 'red',
|
|
20
|
+
high: 'orange',
|
|
21
|
+
medium: 'yellow',
|
|
22
|
+
low: 'slate',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const byTone = <T,>(toneMap: Record<SeverityTone, T>): Record<CheckSeverity, T> => ({
|
|
26
|
+
critical: toneMap[CHECK_SEVERITY_TONE.critical],
|
|
27
|
+
high: toneMap[CHECK_SEVERITY_TONE.high],
|
|
28
|
+
medium: toneMap[CHECK_SEVERITY_TONE.medium],
|
|
29
|
+
low: toneMap[CHECK_SEVERITY_TONE.low],
|
|
30
|
+
})
|
|
7
31
|
|
|
8
32
|
export const SEVERITY_LABEL: Record<CheckSeverity, string> = {
|
|
9
33
|
critical: 'Critical',
|
|
@@ -12,52 +36,22 @@ export const SEVERITY_LABEL: Record<CheckSeverity, string> = {
|
|
|
12
36
|
low: 'Low',
|
|
13
37
|
}
|
|
14
38
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
39
|
+
// Soft pill badge — the loud, explicit severity signal on rows + drawer header.
|
|
40
|
+
// Critical/high/low reuse the canonical Badge tones; medium takes an explicit
|
|
41
|
+
// yellow (matching its tone above) rather than the shared amber `warning` tone,
|
|
42
|
+
// so it reads a clear step below high's orange without shifting the amber
|
|
43
|
+
// `warning` badge used elsewhere (Issues, GitOps). yellow-800 on yellow-100
|
|
44
|
+
// clears WCAG for the pill text.
|
|
19
45
|
export const SEVERITY_BADGE_CLASS: Record<CheckSeverity, string> = {
|
|
20
46
|
critical: sev.error,
|
|
21
47
|
high: sev.alert,
|
|
22
|
-
medium:
|
|
48
|
+
medium:
|
|
49
|
+
'bg-yellow-100 text-yellow-800 border-yellow-300 dark:bg-yellow-950/50 dark:text-yellow-400 dark:border-yellow-700/40',
|
|
23
50
|
low: sev.neutral,
|
|
24
51
|
}
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
low: 'bg-slate-400',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const SEVERITY_TEXT_CLASS: Record<CheckSeverity, string> = {
|
|
35
|
-
critical: 'text-red-600 dark:text-red-400',
|
|
36
|
-
high: 'text-orange-600 dark:text-orange-400',
|
|
37
|
-
medium: 'text-amber-600 dark:text-amber-400',
|
|
38
|
-
low: 'text-slate-500 dark:text-slate-400',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Left accent rail on a queue row — the scan-down severity cue. Pairs a colored
|
|
42
|
-
// 2px border with a faint severity-tinted background that deepens on hover.
|
|
43
|
-
export const SEVERITY_RAIL_CLASS: Record<CheckSeverity, string> = {
|
|
44
|
-
critical: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
|
|
45
|
-
high: 'border-l-orange-500 hover:bg-orange-50/40 dark:hover:bg-orange-950/20',
|
|
46
|
-
medium: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
|
|
47
|
-
low: 'border-l-slate-300 dark:border-l-slate-600 hover:bg-theme-hover/40',
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Category accent — a quiet tag (severity is the loud one). Security is the
|
|
51
|
-
// headline beat, so it gets the most distinct hue.
|
|
52
|
-
const CATEGORY_BADGE_CLASS: Record<string, string> = {
|
|
53
|
-
Security: 'bg-violet-50 text-violet-700 ring-1 ring-violet-200 dark:bg-violet-950/40 dark:text-violet-300 dark:ring-violet-900',
|
|
54
|
-
Reliability: 'bg-sky-50 text-sky-700 ring-1 ring-sky-200 dark:bg-sky-950/40 dark:text-sky-300 dark:ring-sky-900',
|
|
55
|
-
Efficiency: 'bg-teal-50 text-teal-700 ring-1 ring-teal-200 dark:bg-teal-950/40 dark:text-teal-300 dark:ring-teal-900',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function categoryBadgeClass(category: string): string {
|
|
59
|
-
return (
|
|
60
|
-
CATEGORY_BADGE_CLASS[category] ??
|
|
61
|
-
'bg-theme-elevated text-theme-text-secondary ring-1 ring-theme-border'
|
|
62
|
-
)
|
|
63
|
-
}
|
|
53
|
+
export const SEVERITY_FILL_CLASS = byTone(TONE_FILL_CLASS)
|
|
54
|
+
export const SEVERITY_TEXT_CLASS = byTone(TONE_TEXT_CLASS)
|
|
55
|
+
export const SEVERITY_RAIL_CLASS = byTone(TONE_RAIL_CLASS)
|
|
56
|
+
export const SEVERITY_SOLID_CLASS = byTone(TONE_SOLID_CLASS)
|
|
57
|
+
export const SEVERITY_HEADER_BAND_CLASS = byTone(TONE_HEADER_BAND_CLASS)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'react'
|
|
10
10
|
import { ChevronDown, Check, FolderOpen, Loader2, Search, Server, X } from 'lucide-react'
|
|
11
11
|
import { ClusterName } from '../ui/ClusterName'
|
|
12
|
+
import { Input } from '../ui/Input'
|
|
12
13
|
import { MiddleEllipsis } from '../ui/MiddleEllipsis'
|
|
13
14
|
import { StatusDot, type StatusTone } from '../ui/status-tone'
|
|
14
15
|
|
|
@@ -269,9 +270,8 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
269
270
|
<div className="p-2 border-b border-theme-border">
|
|
270
271
|
<div className="relative">
|
|
271
272
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
272
|
-
<
|
|
273
|
+
<Input
|
|
273
274
|
ref={searchInputRef}
|
|
274
|
-
type="text"
|
|
275
275
|
value={search}
|
|
276
276
|
onChange={e => setSearch(e.target.value)}
|
|
277
277
|
onKeyDown={onSearchKeyDown}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { GitCompare, Search, X } from 'lucide-react'
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
5
|
+
import { Input } from '../ui/Input'
|
|
5
6
|
import { pluralToKind } from '../../utils/navigation'
|
|
6
7
|
import type { CompareResourceRef } from './ResourceCompareView'
|
|
7
8
|
import { sortCandidates, filterCandidates } from './sort'
|
|
@@ -120,9 +121,8 @@ export function CompareResourcePicker({
|
|
|
120
121
|
</div>
|
|
121
122
|
<div className="relative">
|
|
122
123
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-theme-text-tertiary pointer-events-none" />
|
|
123
|
-
<
|
|
124
|
+
<Input
|
|
124
125
|
autoFocus
|
|
125
|
-
type="text"
|
|
126
126
|
value={query}
|
|
127
127
|
onChange={e => setQuery(e.target.value)}
|
|
128
128
|
onKeyDown={handleKeyDown}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CheckCircle2, CircleDot, GitBranch, HeartPulse, LayoutGrid, List, Search } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
import { GitOpsFacetButton, GitOpsFilterSection } from './GitOpsTableView'
|
|
4
|
+
import { Input } from '../ui/Input'
|
|
4
5
|
import type { GitOpsResourceTree } from '../../types/gitops-tree'
|
|
5
6
|
import type { GitOpsTreePreset } from './tree'
|
|
6
7
|
|
|
@@ -79,7 +80,7 @@ export function GitOpsGraphFilterRail({
|
|
|
79
80
|
<div className="border-b border-theme-border px-3 py-3">
|
|
80
81
|
<div className="relative">
|
|
81
82
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
|
|
82
|
-
<
|
|
83
|
+
<Input
|
|
83
84
|
value={search}
|
|
84
85
|
onChange={(event) => onSearchChange(event.target.value)}
|
|
85
86
|
placeholder="Filter resources..."
|
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
|
|
27
27
|
import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
|
|
28
28
|
import { Tooltip } from '../ui/Tooltip'
|
|
29
|
+
import { Input } from '../ui/Input'
|
|
29
30
|
import { PageHeader } from '../ui/PageHeader'
|
|
30
31
|
import { SummaryTile, type SummaryTone } from '../ui/SummaryTile'
|
|
31
32
|
import { FacetSection, FacetButton } from '../ui/Facet'
|
|
32
33
|
import { SortableTh, TH_CLASS, type SortDir } from '../ui/SortableTh'
|
|
33
34
|
import { DistributionBar } from '../ui/DistributionBar'
|
|
34
35
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
36
|
+
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
35
37
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
36
38
|
import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
37
39
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
@@ -686,7 +688,7 @@ export function GitOpsTableView({
|
|
|
686
688
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
687
689
|
<div className="relative w-full max-w-md">
|
|
688
690
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
|
|
689
|
-
<
|
|
691
|
+
<Input
|
|
690
692
|
ref={searchInputRef}
|
|
691
693
|
value={search}
|
|
692
694
|
onChange={(e) => filters.setString('q', e.target.value)}
|
|
@@ -766,7 +768,7 @@ export function GitOpsTableView({
|
|
|
766
768
|
{modeLabel(mode)} view is queued behind the application list.
|
|
767
769
|
</div>
|
|
768
770
|
) : initialLoading ? (
|
|
769
|
-
<
|
|
771
|
+
<BoardTableSkeleton />
|
|
770
772
|
) : error ? (
|
|
771
773
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps applications: {error.message}</div>
|
|
772
774
|
) : filteredRows.length === 0 ? (
|
|
@@ -816,60 +818,6 @@ export function GitOpsTableView({
|
|
|
816
818
|
// GitOpsTableView's visual language and not generally useful elsewhere.
|
|
817
819
|
// =============================================================================
|
|
818
820
|
|
|
819
|
-
// Shape-stable loading stand-ins. Both mirror the loaded anatomy so data
|
|
820
|
-
// resolves in place: rows appear inside the table frame, facets inside the
|
|
821
|
-
// rail — no half-height rail dangling next to a centered spinner, no layout
|
|
822
|
-
// jump when the response lands.
|
|
823
|
-
function GitOpsTableSkeleton() {
|
|
824
|
-
return (
|
|
825
|
-
<div aria-live="polite" aria-label="Loading GitOps applications…" className="divide-y divide-theme-border-light">
|
|
826
|
-
{Array.from({ length: 10 }, (_, i) => (
|
|
827
|
-
<div key={i} className="flex items-center gap-6 px-4 py-3" style={{ opacity: 1 - i * 0.09 }}>
|
|
828
|
-
<div className="min-w-0 flex-[2] space-y-2">
|
|
829
|
-
<div className="h-4 w-2/3 animate-pulse rounded bg-theme-hover" />
|
|
830
|
-
<div className="h-3 w-1/2 animate-pulse rounded bg-theme-hover" />
|
|
831
|
-
</div>
|
|
832
|
-
<div className="hidden h-4 flex-1 animate-pulse rounded bg-theme-hover md:block" />
|
|
833
|
-
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
834
|
-
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
835
|
-
<div className="hidden h-4 flex-[1.5] animate-pulse rounded bg-theme-hover lg:block" />
|
|
836
|
-
</div>
|
|
837
|
-
))}
|
|
838
|
-
</div>
|
|
839
|
-
)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function GitOpsSidebarSkeleton() {
|
|
843
|
-
// Section stubs sized like the loaded rail (Sync ~4 rows, Health ~5,
|
|
844
|
-
// Automation ~3, Projects/Namespaces a few each) so the rail keeps its
|
|
845
|
-
// height while counts are unknown — showing real facet buttons with "0"
|
|
846
|
-
// counts would be false zeros.
|
|
847
|
-
const sections: Array<[string, number]> = [
|
|
848
|
-
['Sync', 4],
|
|
849
|
-
['Health', 5],
|
|
850
|
-
['Automation', 3],
|
|
851
|
-
['Projects', 4],
|
|
852
|
-
['Namespaces', 4],
|
|
853
|
-
]
|
|
854
|
-
return (
|
|
855
|
-
<div aria-hidden>
|
|
856
|
-
{sections.map(([title, rows]) => (
|
|
857
|
-
<div key={title} className="border-b border-theme-border-light px-3 py-3">
|
|
858
|
-
<div className="mb-2 h-3 w-24 animate-pulse rounded bg-theme-hover" />
|
|
859
|
-
<div className="space-y-1.5">
|
|
860
|
-
{Array.from({ length: rows }, (_, i) => (
|
|
861
|
-
<div key={i} className="flex items-center justify-between px-1.5 py-1">
|
|
862
|
-
<div className="h-3.5 animate-pulse rounded bg-theme-hover" style={{ width: `${52 - i * 6}%` }} />
|
|
863
|
-
<div className="h-3.5 w-6 animate-pulse rounded bg-theme-hover" />
|
|
864
|
-
</div>
|
|
865
|
-
))}
|
|
866
|
-
</div>
|
|
867
|
-
</div>
|
|
868
|
-
))}
|
|
869
|
-
</div>
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
821
|
function GitOpsFilterSidebar({
|
|
874
822
|
loading,
|
|
875
823
|
side,
|
|
@@ -936,7 +884,15 @@ function GitOpsFilterSidebar({
|
|
|
936
884
|
</div>
|
|
937
885
|
<div className="flex-1 overflow-y-auto">
|
|
938
886
|
{loading ? (
|
|
939
|
-
<
|
|
887
|
+
<BoardRailSkeleton
|
|
888
|
+
sections={[
|
|
889
|
+
['Sync', 4],
|
|
890
|
+
['Health', 5],
|
|
891
|
+
['Automation', 3],
|
|
892
|
+
['Projects', 4],
|
|
893
|
+
['Namespaces', 4],
|
|
894
|
+
]}
|
|
895
|
+
/>
|
|
940
896
|
) : (
|
|
941
897
|
<>
|
|
942
898
|
{AVAILABLE_MODES.length > 1 && (
|
|
@@ -1174,8 +1130,7 @@ function LabelsDropdown({
|
|
|
1174
1130
|
<div className="flex items-center gap-2">
|
|
1175
1131
|
<div className="relative flex-1">
|
|
1176
1132
|
<Search className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-theme-text-tertiary" />
|
|
1177
|
-
<
|
|
1178
|
-
type="text"
|
|
1133
|
+
<Input
|
|
1179
1134
|
value={search}
|
|
1180
1135
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
1181
1136
|
placeholder="Search labels..."
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Package, ExternalLink } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
|
|
5
5
|
import { groupManagedResourcesByKind } from '../../types/gitops'
|
|
6
6
|
import { pluralize } from '../../utils/pluralize'
|
|
7
|
+
import { Collapse, CollapseChevron } from '../ui/Collapse'
|
|
7
8
|
|
|
8
9
|
interface ManagedResourcesListProps {
|
|
9
10
|
resources: ManagedResource[]
|
|
@@ -45,18 +46,14 @@ export function ManagedResourcesList({
|
|
|
45
46
|
onClick={() => setExpanded(!expanded)}
|
|
46
47
|
className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
|
|
47
48
|
>
|
|
48
|
-
{expanded
|
|
49
|
-
<ChevronDown className="w-4 h-4 text-theme-text-tertiary" />
|
|
50
|
-
) : (
|
|
51
|
-
<ChevronRight className="w-4 h-4 text-theme-text-tertiary" />
|
|
52
|
-
)}
|
|
49
|
+
<CollapseChevron open={expanded} className="w-4 h-4" />
|
|
53
50
|
<Package className="w-4 h-4 text-theme-text-secondary" />
|
|
54
51
|
<span className="text-sm font-medium text-theme-text-secondary">
|
|
55
52
|
{title} ({resources.length})
|
|
56
53
|
</span>
|
|
57
54
|
</button>
|
|
58
55
|
|
|
59
|
-
{expanded
|
|
56
|
+
<Collapse open={expanded} mountLazily>
|
|
60
57
|
<div className="pl-6 space-y-3">
|
|
61
58
|
{sortedGroups.map(([kind, kindResources]) => (
|
|
62
59
|
<ResourceKindGroup
|
|
@@ -77,7 +74,7 @@ export function ManagedResourcesList({
|
|
|
77
74
|
</button>
|
|
78
75
|
)}
|
|
79
76
|
</div>
|
|
80
|
-
|
|
77
|
+
</Collapse>
|
|
81
78
|
</div>
|
|
82
79
|
)
|
|
83
80
|
}
|
|
@@ -98,12 +95,12 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
|
|
|
98
95
|
onClick={() => setExpanded(!expanded)}
|
|
99
96
|
className="flex items-center gap-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors mb-1"
|
|
100
97
|
>
|
|
101
|
-
|
|
98
|
+
<CollapseChevron open={expanded} className="w-3 h-3" />
|
|
102
99
|
<span className="font-medium">{kind}</span>
|
|
103
100
|
<span className="text-theme-text-tertiary">({resources.length})</span>
|
|
104
101
|
</button>
|
|
105
102
|
|
|
106
|
-
{expanded
|
|
103
|
+
<Collapse open={expanded} mountLazily>
|
|
107
104
|
<div className="ml-4 space-y-0.5">
|
|
108
105
|
{resources.map((resource, idx) => (
|
|
109
106
|
<ResourceItem
|
|
@@ -114,7 +111,7 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
|
|
|
114
111
|
/>
|
|
115
112
|
))}
|
|
116
113
|
</div>
|
|
117
|
-
|
|
114
|
+
</Collapse>
|
|
118
115
|
</div>
|
|
119
116
|
)
|
|
120
117
|
}
|
|
@@ -93,23 +93,32 @@ function parseInterval(interval: string): number | null {
|
|
|
93
93
|
if (!interval) return null
|
|
94
94
|
|
|
95
95
|
let totalSeconds = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
let index = 0
|
|
97
|
+
|
|
98
|
+
while (index < interval.length) {
|
|
99
|
+
const start = index
|
|
100
|
+
while (index < interval.length) {
|
|
101
|
+
const code = interval.charCodeAt(index)
|
|
102
|
+
if (code < 48 || code > 57) break
|
|
103
|
+
index += 1
|
|
104
|
+
}
|
|
105
|
+
if (start === index) {
|
|
106
|
+
index += 1
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const value = parseInt(interval.slice(start, index), 10)
|
|
110
|
+
const unit = interval[index]
|
|
111
|
+
if (unit === 'h') {
|
|
112
|
+
totalSeconds += value * 3600
|
|
113
|
+
index += 1
|
|
114
|
+
} else if (unit === 'm') {
|
|
115
|
+
totalSeconds += value * 60
|
|
116
|
+
index += 1
|
|
117
|
+
} else if (unit === 's') {
|
|
118
|
+
totalSeconds += value
|
|
119
|
+
index += 1
|
|
120
|
+
} else if (index < interval.length) {
|
|
121
|
+
index += 1
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|