github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/check/CheckPanel/index.tsx (about) 1 import CheckSummaryChart from "../CheckSummaryChart"; 2 import ControlDimension from "../Benchmark/ControlDimension"; 3 import ControlEmptyResultNode from "../common/node/ControlEmptyResultNode"; 4 import ControlErrorNode from "../common/node/ControlErrorNode"; 5 import ControlResultNode from "../common/node/ControlResultNode"; 6 import sortBy from "lodash/sortBy"; 7 import { 8 AlarmIcon, 9 CollapseBenchmarkIcon, 10 EmptyIcon, 11 ErrorIcon, 12 ExpandCheckNodeIcon, 13 InfoIcon, 14 OKIcon, 15 SkipIcon, 16 UnknownIcon, 17 } from "../../../../constants/icons"; 18 import { 19 CheckGroupingActions, 20 useCheckGrouping, 21 } from "../../../../hooks/useCheckGrouping"; 22 import { 23 CheckNode, 24 CheckResult, 25 CheckResultStatus, 26 CheckSeveritySummary, 27 } from "../common"; 28 import { classNames } from "../../../../utils/styles"; 29 import { useMemo } from "react"; 30 31 type CheckChildrenProps = { 32 depth: number; 33 children: CheckNode[]; 34 }; 35 36 type CheckResultsProps = { 37 empties: ControlEmptyResultNode[]; 38 errors: ControlErrorNode[]; 39 results: ControlResultNode[]; 40 }; 41 42 type CheckPanelProps = { 43 depth: number; 44 node: CheckNode; 45 }; 46 47 type CheckPanelSeverityProps = { 48 severity_summary: CheckSeveritySummary; 49 }; 50 51 type CheckPanelSeverityBadgeProps = { 52 label: string; 53 count: number; 54 title: string; 55 }; 56 57 type CheckEmptyResultRowProps = { 58 node: ControlEmptyResultNode; 59 }; 60 61 type CheckResultRowProps = { 62 result: CheckResult; 63 }; 64 65 type CheckErrorRowProps = { 66 error: string; 67 }; 68 69 type CheckResultRowStatusIconProps = { 70 status: CheckResultStatus; 71 }; 72 73 const getMargin = (depth) => { 74 switch (depth) { 75 case 1: 76 return "ml-[6px] md:ml-[24px]"; 77 case 2: 78 return "ml-[12px] md:ml-[48px]"; 79 case 3: 80 return "ml-[18px] md:ml-[72px]"; 81 case 4: 82 return "ml-[24px] md:ml-[96px]"; 83 case 5: 84 return "ml-[30px] md:ml-[120px]"; 85 case 6: 86 return "ml-[36px] md:ml-[144px]"; 87 default: 88 return "ml-0"; 89 } 90 }; 91 92 const CheckChildren = ({ children, depth }: CheckChildrenProps) => { 93 if (!children) { 94 return null; 95 } 96 97 return ( 98 <> 99 {children.map((child) => ( 100 <CheckPanel key={child.name} depth={depth} node={child} /> 101 ))} 102 </> 103 ); 104 }; 105 106 const CheckResultRowStatusIcon = ({ 107 status, 108 }: CheckResultRowStatusIconProps) => { 109 switch (status) { 110 case "alarm": 111 return <AlarmIcon className="h-5 w-5 text-alert" />; 112 case "error": 113 return <ErrorIcon className="h-5 w-5 text-alert" />; 114 case "ok": 115 return <OKIcon className="h-5 w-5 text-ok" />; 116 case "info": 117 return <InfoIcon className="h-5 w-5 text-info" />; 118 case "skip": 119 return <SkipIcon className="h-5 w-5 text-skip" />; 120 case "empty": 121 return <EmptyIcon className="h-5 w-5 text-skip" />; 122 default: 123 return <UnknownIcon className="h-5 w-5 text-skip" />; 124 } 125 }; 126 127 const getCheckResultRowIconTitle = (status: CheckResultStatus) => { 128 switch (status) { 129 case "error": 130 return "Error"; 131 case "alarm": 132 return "Alarm"; 133 case "ok": 134 return "OK"; 135 case "info": 136 return "Info"; 137 case "skip": 138 return "Skipped"; 139 case "empty": 140 return "No results"; 141 } 142 }; 143 144 const CheckResultRow = ({ result }: CheckResultRowProps) => { 145 return ( 146 <div className="flex bg-dashboard-panel print:bg-white p-4 last:rounded-b-md space-x-4"> 147 <div 148 className="flex-shrink-0" 149 title={getCheckResultRowIconTitle(result.status)} 150 > 151 <CheckResultRowStatusIcon status={result.status} /> 152 </div> 153 <div className="flex flex-col md:flex-row flex-grow"> 154 <div className="md:flex-grow leading-4 mt-px">{result.reason}</div> 155 <div className="flex space-x-2 mt-2 md:mt-px md:text-right"> 156 {(result.dimensions || []).map((dimension) => ( 157 <ControlDimension 158 key={dimension.key} 159 dimensionKey={dimension.key} 160 dimensionValue={dimension.value} 161 /> 162 ))} 163 </div> 164 </div> 165 </div> 166 ); 167 }; 168 169 const CheckEmptyResultRow = ({ node }: CheckEmptyResultRowProps) => { 170 return ( 171 <div className="flex bg-dashboard-panel print:bg-white p-4 last:rounded-b-md space-x-4"> 172 <div 173 className="flex-shrink-0" 174 title={getCheckResultRowIconTitle("empty")} 175 > 176 <CheckResultRowStatusIcon status="empty" /> 177 </div> 178 <div className="leading-4 mt-px">{node.title}</div> 179 </div> 180 ); 181 }; 182 183 const CheckErrorRow = ({ error }: CheckErrorRowProps) => { 184 return ( 185 <div className="flex bg-dashboard-panel print:bg-white p-4 last:rounded-b-md space-x-4"> 186 <div 187 className="flex-shrink-0" 188 title={getCheckResultRowIconTitle("error")} 189 > 190 <CheckResultRowStatusIcon status="error" /> 191 </div> 192 <div className="leading-4 mt-px">{error}</div> 193 </div> 194 ); 195 }; 196 197 const CheckResults = ({ empties, errors, results }: CheckResultsProps) => { 198 if (empties.length === 0 && errors.length === 0 && results.length === 0) { 199 return null; 200 } 201 202 return ( 203 <div 204 className={classNames( 205 "border-t shadow-sm rounded-b-md divide-y divide-table-divide border-divide print:shadow-none print:border print:break-before-avoid-page print:break-after-avoid-page print:break-inside-auto" 206 )} 207 > 208 {empties.map((emptyNode) => ( 209 <CheckEmptyResultRow key={`${emptyNode.name}`} node={emptyNode} /> 210 ))} 211 {errors.map((errorNode) => ( 212 <CheckErrorRow key={`${errorNode.name}`} error={errorNode.error} /> 213 ))} 214 {results.map((resultNode) => ( 215 <CheckResultRow 216 key={`${resultNode.result.control.name}-${ 217 resultNode.result.resource 218 }${ 219 resultNode.result.dimensions 220 ? `-${resultNode.result.dimensions 221 .map((d) => `${d.key}=${d.value}`) 222 .join("-")}` 223 : "" 224 }`} 225 result={resultNode.result} 226 /> 227 ))} 228 </div> 229 ); 230 }; 231 232 const CheckPanelSeverityBadge = ({ 233 count, 234 label, 235 title, 236 }: CheckPanelSeverityBadgeProps) => { 237 return ( 238 <div 239 className={classNames( 240 "border rounded-md text-sm divide-x", 241 count > 0 ? "border-severity" : "border-skip", 242 count > 0 243 ? "bg-severity text-white divide-white" 244 : "text-skip divide-skip" 245 )} 246 title={title} 247 > 248 <span className={classNames("px-2 py-px")}>{label}</span> 249 {count > 0 && <span className={classNames("px-2 py-px")}>{count}</span>} 250 </div> 251 ); 252 }; 253 254 const CheckPanelSeverity = ({ severity_summary }: CheckPanelSeverityProps) => { 255 const critical = severity_summary["critical"]; 256 const high = severity_summary["high"]; 257 258 if (critical === undefined && high === undefined) { 259 return null; 260 } 261 262 return ( 263 <> 264 {critical !== undefined && ( 265 <CheckPanelSeverityBadge 266 label="Critical" 267 count={critical} 268 title={`${critical.toLocaleString()} critical severity ${ 269 critical === 1 ? "result" : "results" 270 }`} 271 /> 272 )} 273 {high !== undefined && ( 274 <CheckPanelSeverityBadge 275 label="High" 276 count={high} 277 title={`${high.toLocaleString()} high severity ${ 278 high === 1 ? "result" : "results" 279 }`} 280 /> 281 )} 282 </> 283 ); 284 }; 285 286 const CheckPanel = ({ depth, node }: CheckPanelProps) => { 287 const { firstChildSummaries, dispatch, groupingsConfig, nodeStates } = 288 useCheckGrouping(); 289 const expanded = nodeStates[node.name] 290 ? nodeStates[node.name].expanded 291 : false; 292 293 const [child_nodes, error_nodes, empty_nodes, result_nodes, can_be_expanded] = 294 useMemo(() => { 295 const children: CheckNode[] = []; 296 const errors: ControlErrorNode[] = []; 297 const empty: ControlEmptyResultNode[] = []; 298 const results: ControlResultNode[] = []; 299 for (const child of node.children || []) { 300 if (child.type === "error") { 301 errors.push(child as ControlErrorNode); 302 } else if (child.type === "result") { 303 results.push(child as ControlResultNode); 304 } else if (child.type === "empty_result") { 305 empty.push(child as ControlEmptyResultNode); 306 } else if (child.type !== "running") { 307 children.push(child); 308 } 309 } 310 return [ 311 sortBy(children, "sort"), 312 sortBy(errors, "sort"), 313 sortBy(empty, "sort"), 314 results, 315 children.length > 0 || 316 (groupingsConfig && 317 groupingsConfig.length > 0 && 318 groupingsConfig[groupingsConfig.length - 1].type === "result" && 319 (errors.length > 0 || empty.length > 0 || results.length > 0)), 320 ]; 321 }, [groupingsConfig, node]); 322 323 return ( 324 <> 325 <div 326 id={node.name} 327 className={classNames( 328 getMargin(depth - 1), 329 depth === 1 && node.type === "benchmark" 330 ? "print:break-before-page" 331 : null, 332 node.type === "benchmark" || node.type === "control" 333 ? "print:break-inside-avoid-page" 334 : null 335 )} 336 > 337 <section 338 className={classNames( 339 "bg-dashboard-panel shadow-sm rounded-md border-divide print:border print:bg-white print:shadow-none", 340 can_be_expanded ? "cursor-pointer" : null, 341 expanded && 342 (empty_nodes.length > 0 || 343 error_nodes.length > 0 || 344 result_nodes.length > 0) 345 ? "rounded-b-none border-b-0" 346 : null 347 )} 348 onClick={() => 349 can_be_expanded 350 ? dispatch({ 351 type: expanded 352 ? CheckGroupingActions.COLLAPSE_NODE 353 : CheckGroupingActions.EXPAND_NODE, 354 name: node.name, 355 }) 356 : null 357 } 358 > 359 <div className="p-4 flex items-center space-x-6"> 360 <div className="flex flex-grow justify-between items-center space-x-6"> 361 <div className="flex items-center space-x-4"> 362 <h3 363 id={`${node.name}-title`} 364 className="mt-0" 365 title={node.title} 366 > 367 {node.title} 368 </h3> 369 <CheckPanelSeverity severity_summary={node.severity_summary} /> 370 </div> 371 <div className="flex-shrink-0 w-40 md:w-72 lg:w-96"> 372 <CheckSummaryChart 373 status={node.status} 374 summary={node.summary} 375 firstChildSummaries={firstChildSummaries} 376 /> 377 </div> 378 </div> 379 {can_be_expanded && !expanded && ( 380 <ExpandCheckNodeIcon className="w-5 md:w-7 h-5 md:h-7 flex-shrink-0 text-foreground-lightest" /> 381 )} 382 {expanded && ( 383 <CollapseBenchmarkIcon className="w-5 md:w-7 h-5 md:h-7 flex-shrink-0 text-foreground-lightest" /> 384 )} 385 {!can_be_expanded && <div className="w-5 md:w-7 h-5 md:h-7" />} 386 </div> 387 </section> 388 {can_be_expanded && 389 expanded && 390 groupingsConfig && 391 groupingsConfig[groupingsConfig.length - 1].type === "result" && ( 392 <CheckResults 393 empties={empty_nodes} 394 errors={error_nodes} 395 results={result_nodes} 396 /> 397 )} 398 </div> 399 {can_be_expanded && expanded && ( 400 <CheckChildren children={child_nodes} depth={depth + 1} /> 401 )} 402 </> 403 ); 404 }; 405 406 export default CheckPanel;