github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/Card/index.tsx (about)

     1  import DashboardIcon from "../common/DashboardIcon";
     2  import get from "lodash/get";
     3  import has from "lodash/has";
     4  import IntegerDisplay from "../../IntegerDisplay";
     5  import isNumber from "lodash/isNumber";
     6  import isObject from "lodash/isObject";
     7  import LoadingIndicator from "../LoadingIndicator";
     8  import useDeepCompareEffect from "use-deep-compare-effect";
     9  import useTemplateRender from "../../../hooks/useTemplateRender";
    10  import {
    11    BasePrimitiveProps,
    12    ExecutablePrimitiveProps,
    13    isNumericCol,
    14    LeafNodeData,
    15  } from "../common";
    16  import { classNames } from "../../../utils/styles";
    17  import { DashboardRunState, PanelProperties } from "../../../types";
    18  import { getColumn } from "../../../utils/data";
    19  import { getComponent, registerComponent } from "../index";
    20  import {
    21    getIconClasses,
    22    getIconForType,
    23    getTextClasses,
    24    getWrapperClasses,
    25  } from "../../../utils/card";
    26  import { ThemeNames } from "../../../hooks/useTheme";
    27  import { useDashboard } from "../../../hooks/useDashboard";
    28  import { useEffect, useState } from "react";
    29  
    30  const Table = getComponent("table");
    31  
    32  export type CardType = "alert" | "info" | "ok" | "table" | null;
    33  
    34  export type CardProperties = {
    35    label?: string;
    36    value?: any;
    37    icon?: string;
    38    href?: string;
    39  };
    40  
    41  export type CardProps = PanelProperties &
    42    Omit<BasePrimitiveProps, "display_type"> &
    43    ExecutablePrimitiveProps & {
    44      display_type?: CardType;
    45      properties: CardProperties;
    46    };
    47  
    48  type CardDataFormat = "simple" | "formal";
    49  
    50  type CardState = {
    51    loading: boolean;
    52    label: string | null;
    53    value: any | null;
    54    type: CardType;
    55    icon: string | null;
    56    href: string | null;
    57  };
    58  
    59  const getDataFormat = (data: LeafNodeData): CardDataFormat => {
    60    if (data.columns.length > 1) {
    61      return "formal";
    62    }
    63    return "simple";
    64  };
    65  
    66  const getDefaultState = (
    67    status: DashboardRunState,
    68    properties: CardProperties,
    69    display_type: CardType | undefined
    70  ) => {
    71    return {
    72      loading: status === "running",
    73      label: properties.label || null,
    74      value: isNumber(properties.value)
    75        ? properties.value
    76        : properties.value || null,
    77      type: display_type || null,
    78      icon: getIconForType(display_type, properties.icon),
    79      href: properties.href || null,
    80    };
    81  };
    82  
    83  const useCardState = ({
    84    data,
    85    display_type,
    86    properties,
    87    status,
    88  }: CardProps) => {
    89    const [calculatedProperties, setCalculatedProperties] = useState<CardState>(
    90      getDefaultState(status, properties, display_type)
    91    );
    92  
    93    useEffect(() => {
    94      if (
    95        !data ||
    96        !data.columns ||
    97        !data.rows ||
    98        data.columns.length === 0 ||
    99        data.rows.length === 0
   100      ) {
   101        setCalculatedProperties(
   102          getDefaultState(status, properties, display_type)
   103        );
   104        return;
   105      }
   106  
   107      const dataFormat = getDataFormat(data);
   108  
   109      if (dataFormat === "simple") {
   110        const firstCol = data.columns[0];
   111        const isNumericValue = isNumericCol(firstCol.data_type);
   112        const row = data.rows[0];
   113        const value = row[firstCol.name];
   114        setCalculatedProperties({
   115          loading: false,
   116          label: firstCol.name,
   117          value:
   118            value !== null && value !== undefined && isNumericValue
   119              ? value.toLocaleString()
   120              : value,
   121          type: display_type || null,
   122          icon: getIconForType(display_type, properties.icon),
   123          href: properties.href || null,
   124        });
   125      } else {
   126        const formalLabel = get(data, "rows[0].label", null);
   127        const formalValue = get(data, `rows[0].value`, null);
   128        const formalType = get(data, `rows[0].type`, null);
   129        const formalIcon = get(data, `rows[0].icon`, null);
   130        const formalHref = get(data, `rows[0].href`, null);
   131        const valueCol = getColumn(data.columns, "value");
   132        const isNumericValue = !!valueCol && isNumericCol(valueCol.data_type);
   133        setCalculatedProperties({
   134          loading: false,
   135          label: formalLabel,
   136          value:
   137            formalValue !== null && formalValue !== undefined && isNumericValue
   138              ? formalValue.toLocaleString()
   139              : formalValue,
   140          type: formalType || display_type || null,
   141          icon: getIconForType(
   142            formalType || display_type,
   143            formalIcon || properties.icon
   144          ),
   145          href: formalHref || properties.href || null,
   146        });
   147      }
   148    }, [data, display_type, properties, status]);
   149  
   150    return calculatedProperties;
   151  };
   152  
   153  const Label = ({ value }) => {
   154    if (!value) {
   155      return null;
   156    }
   157  
   158    if (isObject(value)) {
   159      return JSON.stringify(value);
   160    }
   161  
   162    return value;
   163  };
   164  
   165  const Card = (props: CardProps) => {
   166    const ExternalLink = getComponent("external_link");
   167    const state = useCardState(props);
   168    const [renderError, setRenderError] = useState<string | null>(null);
   169    const [renderedHref, setRenderedHref] = useState<string | null>(
   170      state.href || null
   171    );
   172    const textClasses = getTextClasses(state.type);
   173    const {
   174      themeContext: { theme },
   175    } = useDashboard();
   176    const { ready: templateRenderReady, renderTemplates } = useTemplateRender();
   177  
   178    useEffect(() => {
   179      if ((state.loading || !state.href) && (renderError || renderedHref)) {
   180        setRenderError(null);
   181        setRenderedHref(null);
   182      }
   183    }, [state.loading, state.href, renderError, renderedHref]);
   184  
   185    useDeepCompareEffect(() => {
   186      if (!templateRenderReady || state.loading || !state.href) {
   187        return;
   188      }
   189  
   190      const renderData = { ...state };
   191      if (props.data && props.data.columns && props.data.rows) {
   192        const row = props.data.rows[0];
   193        props.data.columns.forEach((col) => {
   194          if (!has(renderData, col.name)) {
   195            renderData[col.name] = row[col.name];
   196          }
   197        });
   198      }
   199  
   200      const doRender = async () => {
   201        const renderedResults = await renderTemplates(
   202          { card: state.href as string },
   203          [renderData]
   204        );
   205        if (
   206          !renderedResults ||
   207          renderedResults.length === 0 ||
   208          !renderedResults[0].card
   209        ) {
   210          setRenderedHref(null);
   211          setRenderError(null);
   212        } else if (renderedResults[0].card.result) {
   213          setRenderedHref(renderedResults[0].card.result as string);
   214          setRenderError(null);
   215        } else if (renderedResults[0].card.error) {
   216          setRenderError(renderedResults[0].card.error as string);
   217          setRenderedHref(null);
   218        }
   219      };
   220      doRender();
   221    }, [renderTemplates, templateRenderReady, state, props.data]);
   222  
   223    const card = (
   224      <div
   225        className={classNames(
   226          "relative pt-4 px-3 pb-4 sm:px-4 rounded-md overflow-hidden",
   227          getWrapperClasses(state.type)
   228        )}
   229      >
   230        <dt>
   231          <div className="absolute">
   232            <DashboardIcon
   233              className={classNames(getIconClasses(state.type), "h-8 w-8")}
   234              icon={state.icon}
   235            />
   236          </div>
   237          <p
   238            className={classNames(
   239              "text-sm font-medium truncate",
   240              state.icon ? "ml-11" : "ml-2",
   241              textClasses
   242            )}
   243            title={state.label || undefined}
   244          >
   245            {state.loading && "Loading..."}
   246            {!state.loading && !state.label && (
   247              <DashboardIcon
   248                className="h-5 w-5"
   249                icon="materialsymbols-outline:remove"
   250              />
   251            )}
   252            {!state.loading && state.label}
   253          </p>
   254        </dt>
   255        <dd
   256          className={classNames(
   257            "flex items-baseline",
   258            state.icon ? "ml-11" : "ml-2"
   259          )}
   260          title={state.value || undefined}
   261        >
   262          <p
   263            className={classNames(
   264              "text-4xl mt-1 font-semibold text-left truncate",
   265              textClasses
   266            )}
   267          >
   268            {state.loading && (
   269              <LoadingIndicator
   270                className={classNames(
   271                  "h-9 w-9 mt-1",
   272                  theme.name === ThemeNames.STEAMPIPE_DEFAULT
   273                    ? "text-black-scale-4"
   274                    : null
   275                )}
   276              />
   277            )}
   278            {!state.loading &&
   279              (state.value === null || state.value === undefined) && (
   280                <DashboardIcon
   281                  className="h-10 w-10"
   282                  icon="materialsymbols-outline:remove"
   283                />
   284              )}
   285            {state.value !== null &&
   286              state.value !== undefined &&
   287              !isNumber(state.value) && <Label value={state.value} />}
   288            {isNumber(state.value) && (
   289              <>
   290                <IntegerDisplay num={state.value} startAt="100k" />
   291              </>
   292            )}
   293          </p>
   294        </dd>
   295      </div>
   296    );
   297  
   298    if (renderedHref) {
   299      return <ExternalLink to={renderedHref}>{card}</ExternalLink>;
   300    }
   301  
   302    return card;
   303  };
   304  
   305  const CardWrapper = (props: CardProps) => {
   306    if (props.display_type === "table") {
   307      // @ts-ignore
   308      return <Table {...props} />;
   309    }
   310  
   311    return <Card {...props} />;
   312  };
   313  
   314  registerComponent("card", CardWrapper);
   315  
   316  export default CardWrapper;