|
1 | | -import { PanelRightClose, PanelRightOpen } from "lucide-react"; |
| 1 | +import { useDeferredValue, useEffect, useMemo, useState } from "react"; |
| 2 | +import { Check, Copy, PanelRightClose, PanelRightOpen, Search } from "lucide-react"; |
| 3 | +import { copyTextToClipboard } from "../lib/clipboard"; |
2 | 4 | import type { ResourceDetails, ResourceRow } from "../types/kube"; |
3 | 5 | import { StatusDot } from "./status"; |
4 | 6 |
|
| 7 | +type CopyStatus = "idle" | "copied" | "failed"; |
| 8 | + |
5 | 9 | export function Inspector({ |
6 | 10 | collapsed, |
7 | 11 | details, |
@@ -90,10 +94,88 @@ function LiveObject({ |
90 | 94 | detailsError: string; |
91 | 95 | detailsLoading: boolean; |
92 | 96 | }) { |
| 97 | + const [query, setQuery] = useState(""); |
| 98 | + const [copyStatus, setCopyStatus] = useState<CopyStatus>("idle"); |
| 99 | + const deferredQuery = useDeferredValue(query.trim().toLowerCase()); |
| 100 | + const yamlText = detailsLoading ? "Loading YAML..." : details.yaml || detailsError || "No YAML returned."; |
| 101 | + const yamlLines = useMemo(() => yamlText.split(/\r?\n/), [yamlText]); |
| 102 | + const queryTerms = useMemo(() => (deferredQuery ? deferredQuery.split(/\s+/) : []), [deferredQuery]); |
| 103 | + const visibleLines = useMemo(() => { |
| 104 | + if (!queryTerms.length) { |
| 105 | + return yamlLines.map((line, index) => ({ line, number: index + 1 })); |
| 106 | + } |
| 107 | + |
| 108 | + return yamlLines |
| 109 | + .map((line, index) => ({ line, number: index + 1 })) |
| 110 | + .filter(({ line }) => { |
| 111 | + const haystack = line.toLowerCase(); |
| 112 | + return queryTerms.every((term) => haystack.includes(term)); |
| 113 | + }); |
| 114 | + }, [queryTerms, yamlLines]); |
| 115 | + const CopyIcon = copyStatus === "copied" ? Check : Copy; |
| 116 | + const copyLabel = copyStatus === "copied" ? "Copied" : copyStatus === "failed" ? "Blocked" : "Copy"; |
| 117 | + |
| 118 | + async function copyYaml() { |
| 119 | + if (!details.yaml) { |
| 120 | + return; |
| 121 | + } |
| 122 | + |
| 123 | + try { |
| 124 | + await copyTextToClipboard(details.yaml); |
| 125 | + setCopyStatus("copied"); |
| 126 | + } catch { |
| 127 | + setCopyStatus("failed"); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + useEffect(() => { |
| 132 | + if (copyStatus === "idle") { |
| 133 | + return; |
| 134 | + } |
| 135 | + |
| 136 | + const timeout = window.setTimeout(() => setCopyStatus("idle"), 1_600); |
| 137 | + return () => window.clearTimeout(timeout); |
| 138 | + }, [copyStatus]); |
| 139 | + |
93 | 140 | return ( |
94 | 141 | <details className="inspector-yaml"> |
95 | | - <summary>Live object</summary> |
96 | | - <pre>{detailsLoading ? "Loading YAML..." : details.yaml || detailsError || "No YAML returned."}</pre> |
| 142 | + <summary> |
| 143 | + <span>Live object</span> |
| 144 | + <small>{queryTerms.length ? `${visibleLines.length}/${yamlLines.length} lines` : `${yamlLines.length} lines`}</small> |
| 145 | + </summary> |
| 146 | + <div className="inspector-yaml-toolbar"> |
| 147 | + <label> |
| 148 | + <Search size={13} /> |
| 149 | + <input |
| 150 | + aria-label="Find YAML" |
| 151 | + value={query} |
| 152 | + onChange={(event) => setQuery(event.target.value)} |
| 153 | + placeholder="Find YAML..." |
| 154 | + /> |
| 155 | + </label> |
| 156 | + <button |
| 157 | + className={copyStatus === "idle" ? "" : copyStatus} |
| 158 | + disabled={!details.yaml} |
| 159 | + title="Copy full YAML" |
| 160 | + type="button" |
| 161 | + onClick={copyYaml} |
| 162 | + > |
| 163 | + <CopyIcon size={13} /> |
| 164 | + <span>{copyLabel}</span> |
| 165 | + </button> |
| 166 | + </div> |
| 167 | + <div aria-label="Live object YAML" className="inspector-yaml-code"> |
| 168 | + {visibleLines.length ? ( |
| 169 | + visibleLines.map(({ line, number }) => ( |
| 170 | + <span className="yaml-line" key={number}> |
| 171 | + <span>{number}</span> |
| 172 | + <code>{line || " "}</code> |
| 173 | + </span> |
| 174 | + )) |
| 175 | + ) : ( |
| 176 | + <span className="yaml-empty">No matching YAML lines</span> |
| 177 | + )} |
| 178 | + </div> |
97 | 179 | </details> |
98 | 180 | ); |
99 | 181 | } |
|
0 commit comments