visualization.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Alert from 'sentry/components/alert';
  4. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {MetricsQueryApiResponse} from 'sentry/types';
  11. import {DEFAULT_SORT_STATE} from 'sentry/utils/metrics/constants';
  12. import type {FocusedMetricsSeries, SortState} from 'sentry/utils/metrics/types';
  13. import {
  14. type MetricsQueryApiQueryParams,
  15. useMetricsQuery,
  16. } from 'sentry/utils/metrics/useMetricsQuery';
  17. import usePageFilters from 'sentry/utils/usePageFilters';
  18. import {DASHBOARD_CHART_GROUP} from 'sentry/views/dashboards/dashboard';
  19. import {BigNumber, getBigNumberData} from 'sentry/views/dashboards/metrics/bigNumber';
  20. import {getTableData, MetricTable} from 'sentry/views/dashboards/metrics/table';
  21. import type {Order} from 'sentry/views/dashboards/metrics/types';
  22. import {toMetricDisplayType} from 'sentry/views/dashboards/metrics/utils';
  23. import {DisplayType} from 'sentry/views/dashboards/types';
  24. import {displayTypes} from 'sentry/views/dashboards/widgetBuilder/utils';
  25. import {LoadingScreen} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
  26. import {getIngestionSeriesId, MetricChart} from 'sentry/views/ddm/chart/chart';
  27. import {SummaryTable} from 'sentry/views/ddm/summaryTable';
  28. import {useSeriesHover} from 'sentry/views/ddm/useSeriesHover';
  29. import {createChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
  30. import {getChartTimeseries, getWidgetTitle} from 'sentry/views/ddm/widget';
  31. function useFocusedSeries({
  32. timeseriesData,
  33. queries,
  34. onChange,
  35. }: {
  36. queries: MetricsQueryApiQueryParams[];
  37. timeseriesData: MetricsQueryApiResponse | null;
  38. onChange?: () => void;
  39. }) {
  40. const [focusedSeries, setFocusedSeries] = useState<FocusedMetricsSeries[]>([]);
  41. const chartSeries = useMemo(() => {
  42. return timeseriesData
  43. ? getChartTimeseries(timeseriesData, queries, {
  44. getChartPalette: createChartPalette,
  45. focusedSeries: focusedSeries && new Set(focusedSeries?.map(s => s.id)),
  46. })
  47. : [];
  48. }, [timeseriesData, focusedSeries, queries]);
  49. const toggleSeriesVisibility = useCallback(
  50. (series: FocusedMetricsSeries) => {
  51. onChange?.();
  52. // The focused series array is not populated yet, so we can add all series except the one that was de-selected
  53. if (!focusedSeries || focusedSeries.length === 0) {
  54. setFocusedSeries(
  55. chartSeries
  56. .filter(s => s.id !== series.id)
  57. .map(s => ({
  58. id: s.id,
  59. groupBy: s.groupBy,
  60. }))
  61. );
  62. return;
  63. }
  64. const filteredSeries = focusedSeries.filter(s => s.id !== series.id);
  65. if (filteredSeries.length === focusedSeries.length) {
  66. // The series was not focused before so we can add it
  67. filteredSeries.push(series);
  68. }
  69. setFocusedSeries(filteredSeries);
  70. },
  71. [chartSeries, focusedSeries, onChange]
  72. );
  73. const setSeriesVisibility = useCallback(
  74. (series: FocusedMetricsSeries) => {
  75. onChange?.();
  76. if (focusedSeries?.length === 1 && focusedSeries[0].id === series.id) {
  77. setFocusedSeries([]);
  78. return;
  79. }
  80. setFocusedSeries([series]);
  81. },
  82. [focusedSeries, onChange]
  83. );
  84. useEffect(() => {
  85. setFocusedSeries([]);
  86. }, [queries]);
  87. return {
  88. toggleSeriesVisibility,
  89. setSeriesVisibility,
  90. chartSeries,
  91. };
  92. }
  93. const supportedDisplayTypes = Object.keys(displayTypes).map(value => ({
  94. label: displayTypes[value],
  95. value,
  96. }));
  97. interface MetricVisualizationProps {
  98. displayType: DisplayType;
  99. onDisplayTypeChange: (displayType: DisplayType) => void;
  100. queries: MetricsQueryApiQueryParams[];
  101. onOrderChange?: ({id, order}: {id: number; order: Order}) => void;
  102. }
  103. export function MetricVisualization({
  104. queries,
  105. displayType,
  106. onDisplayTypeChange,
  107. onOrderChange,
  108. }: MetricVisualizationProps) {
  109. const {selection} = usePageFilters();
  110. const {
  111. data: timeseriesData,
  112. isLoading,
  113. isError,
  114. error,
  115. } = useMetricsQuery(queries, selection, {
  116. intervalLadder: displayType === DisplayType.BAR ? 'bar' : 'dashboard',
  117. });
  118. const widgetMQL = useMemo(() => getWidgetTitle(queries), [queries]);
  119. const visualizationComponent = useMemo(() => {
  120. if (!timeseriesData) {
  121. return null;
  122. }
  123. if (displayType === DisplayType.TABLE) {
  124. return (
  125. <MetricTableVisualization
  126. isLoading={isLoading}
  127. timeseriesData={timeseriesData}
  128. queries={queries}
  129. onOrderChange={onOrderChange}
  130. />
  131. );
  132. }
  133. if (displayType === DisplayType.BIG_NUMBER) {
  134. return (
  135. <MetricBigNumberVisualization
  136. timeseriesData={timeseriesData}
  137. isLoading={isLoading}
  138. queries={queries}
  139. />
  140. );
  141. }
  142. return (
  143. <MetricChartVisualization
  144. isLoading={isLoading}
  145. timeseriesData={timeseriesData}
  146. queries={queries}
  147. displayType={displayType}
  148. />
  149. );
  150. }, [timeseriesData, displayType, isLoading, queries, onOrderChange]);
  151. if (!timeseriesData || isError) {
  152. return (
  153. <StyledMetricChartContainer>
  154. {isLoading && <LoadingIndicator />}
  155. {isError && (
  156. <Alert type="error">
  157. {(error?.responseJSON?.detail as string) ||
  158. t('Error while fetching metrics data')}
  159. </Alert>
  160. )}
  161. </StyledMetricChartContainer>
  162. );
  163. }
  164. return (
  165. <StyledOuterContainer>
  166. <ViualizationHeader>
  167. <WidgetTitle>
  168. <StyledTooltip
  169. title={widgetMQL}
  170. showOnlyOnOverflow
  171. delay={500}
  172. overlayStyle={{maxWidth: '90vw'}}
  173. >
  174. {widgetMQL}
  175. </StyledTooltip>
  176. </WidgetTitle>
  177. <CompactSelect
  178. size="sm"
  179. triggerProps={{prefix: t('Visualization')}}
  180. value={displayType}
  181. options={supportedDisplayTypes}
  182. onChange={({value}) => onDisplayTypeChange(value as DisplayType)}
  183. />
  184. </ViualizationHeader>
  185. {visualizationComponent}
  186. </StyledOuterContainer>
  187. );
  188. }
  189. interface MetricTableVisualizationProps {
  190. isLoading: boolean;
  191. queries: MetricsQueryApiQueryParams[];
  192. timeseriesData: MetricsQueryApiResponse;
  193. onOrderChange?: ({id, order}: {id: number; order: Order}) => void;
  194. }
  195. function MetricTableVisualization({
  196. timeseriesData,
  197. queries,
  198. isLoading,
  199. onOrderChange,
  200. }: MetricTableVisualizationProps) {
  201. const tableData = useMemo(() => {
  202. return getTableData(timeseriesData, queries);
  203. }, [timeseriesData, queries]);
  204. const handleOrderChange = useCallback(
  205. (column: {id: number; order: Order}) => {
  206. onOrderChange?.(column);
  207. },
  208. [onOrderChange]
  209. );
  210. return (
  211. <Fragment>
  212. <TransparentLoadingMask visible={isLoading} />
  213. <MetricTable
  214. isLoading={isLoading}
  215. data={tableData}
  216. onOrderChange={handleOrderChange}
  217. />
  218. </Fragment>
  219. );
  220. }
  221. function MetricBigNumberVisualization({
  222. timeseriesData,
  223. queries,
  224. isLoading,
  225. }: MetricTableVisualizationProps) {
  226. const bigNumberData = useMemo(() => {
  227. return timeseriesData ? getBigNumberData(timeseriesData, queries) : undefined;
  228. }, [timeseriesData, queries]);
  229. if (!bigNumberData) {
  230. return null;
  231. }
  232. return (
  233. <Fragment>
  234. <LoadingScreen loading={isLoading} />
  235. <BigNumber>{bigNumberData}</BigNumber>
  236. </Fragment>
  237. );
  238. }
  239. interface MetricChartVisualizationProps extends MetricTableVisualizationProps {
  240. displayType: DisplayType;
  241. }
  242. function MetricChartVisualization({
  243. timeseriesData,
  244. queries,
  245. displayType,
  246. isLoading,
  247. }: MetricChartVisualizationProps) {
  248. const {chartRef, setHoveredSeries} = useSeriesHover();
  249. const handleHoverSeries = useCallback(
  250. (seriesId: string) => {
  251. setHoveredSeries([seriesId, getIngestionSeriesId(seriesId)]);
  252. },
  253. [setHoveredSeries]
  254. );
  255. const {chartSeries, toggleSeriesVisibility, setSeriesVisibility} = useFocusedSeries({
  256. timeseriesData,
  257. queries,
  258. onChange: () => handleHoverSeries(''),
  259. });
  260. const [tableSort, setTableSort] = useState<SortState>(DEFAULT_SORT_STATE);
  261. return (
  262. <Fragment>
  263. <TransparentLoadingMask visible={isLoading} />
  264. <MetricChart
  265. ref={chartRef}
  266. series={chartSeries}
  267. displayType={toMetricDisplayType(displayType)}
  268. group={DASHBOARD_CHART_GROUP}
  269. height={200}
  270. />
  271. <SummaryTable
  272. series={chartSeries}
  273. onSortChange={setTableSort}
  274. sort={tableSort}
  275. onRowClick={setSeriesVisibility}
  276. onColorDotClick={toggleSeriesVisibility}
  277. onRowHover={handleHoverSeries}
  278. />
  279. </Fragment>
  280. );
  281. }
  282. const StyledOuterContainer = styled('div')`
  283. display: flex;
  284. flex-direction: column;
  285. gap: ${space(3)};
  286. `;
  287. const StyledMetricChartContainer = styled('div')`
  288. gap: ${space(3)};
  289. display: flex;
  290. flex-direction: column;
  291. justify-content: center;
  292. height: 100%;
  293. `;
  294. const ViualizationHeader = styled('div')`
  295. display: flex;
  296. justify-content: space-between;
  297. align-items: center;
  298. gap: ${space(1)};
  299. `;
  300. const WidgetTitle = styled('div')`
  301. flex-grow: 1;
  302. font-size: ${p => p.theme.fontSizeMedium};
  303. display: inline-grid;
  304. grid-auto-flow: column;
  305. `;
  306. const StyledTooltip = styled(Tooltip)`
  307. ${p => p.theme.overflowEllipsis};
  308. `;