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;