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;