123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- import * as React from 'react';
- import cloneDeep from 'lodash/cloneDeep';
- import isEqual from 'lodash/isEqual';
- import omit from 'lodash/omit';
- import {doMetricsRequest} from 'sentry/actionCreators/metrics';
- import {Client} from 'sentry/api';
- import {isSelectionEqual} from 'sentry/components/organizations/pageFilters/utils';
- import {t} from 'sentry/locale';
- import {MetricsApiResponse, OrganizationSummary, PageFilters} from 'sentry/types';
- import {Series} from 'sentry/types/echarts';
- import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
- import {TOP_N} from 'sentry/utils/discover/types';
- import {transformMetricsResponseToSeries} from 'sentry/utils/metrics/transformMetricsResponseToSeries';
- import {DisplayType, Widget} from '../types';
- import {getWidgetInterval} from '../utils';
- type Props = {
- api: Client;
- children: (
- props: Pick<State, 'loading' | 'timeseriesResults' | 'tableResults' | 'errorMessage'>
- ) => React.ReactNode;
- organization: OrganizationSummary;
- selection: PageFilters;
- widget: Widget;
- limit?: number;
- };
- type State = {
- loading: boolean;
- errorMessage?: string;
- queryFetchID?: symbol;
- rawResults?: MetricsApiResponse[];
- tableResults?: TableDataWithTitle[];
- timeseriesResults?: Series[];
- };
- class MetricsWidgetQueries extends React.Component<Props, State> {
- state: State = {
- loading: true,
- queryFetchID: undefined,
- errorMessage: undefined,
- timeseriesResults: undefined,
- rawResults: undefined,
- tableResults: undefined,
- };
- componentDidMount() {
- this._isMounted = true;
- this.fetchData();
- }
- componentDidUpdate(prevProps: Props) {
- const {loading, rawResults} = this.state;
- const {selection, widget, organization, limit} = this.props;
- const ignroredWidgetProps = [
- 'queries',
- 'title',
- 'id',
- 'layout',
- 'tempId',
- 'widgetType',
- ];
- const ignoredQueryProps = ['name', 'fields'];
- const widgetQueryNames = widget.queries.map(q => q.name);
- const prevWidgetQueryNames = prevProps.widget.queries.map(q => q.name);
- if (
- limit !== prevProps.limit ||
- organization.slug !== prevProps.organization.slug ||
- !isSelectionEqual(selection, prevProps.selection) ||
- // If the widget changed (ignore unimportant fields, + queries as they are handled lower)
- !isEqual(
- omit(widget, ignroredWidgetProps),
- omit(prevProps.widget, ignroredWidgetProps)
- ) ||
- // If the queries changed (ignore unimportant name, + fields as they are handled lower)
- !isEqual(
- widget.queries.map(q => omit(q, ignoredQueryProps)),
- prevProps.widget.queries.map(q => omit(q, ignoredQueryProps))
- ) ||
- // If the fields changed (ignore falsy/empty fields -> they can happen after clicking on Add Overlay)
- !isEqual(
- widget.queries.flatMap(q => q.fields.filter(field => !!field)),
- prevProps.widget.queries.flatMap(q => q.fields.filter(field => !!field))
- )
- ) {
- this.fetchData();
- return;
- }
- // If the query names have changed, then update timeseries labels
- if (
- !loading &&
- !isEqual(widgetQueryNames, prevWidgetQueryNames) &&
- rawResults?.length === widget.queries.length
- ) {
- // eslint-disable-next-line react/no-did-update-set-state
- this.setState(prevState => {
- return {
- ...prevState,
- timeseriesResults: prevState.rawResults?.flatMap((rawResult, index) =>
- transformMetricsResponseToSeries(rawResult, widget.queries[index].name)
- ),
- };
- });
- }
- }
- componentWillUnmount() {
- this._isMounted = false;
- }
- private _isMounted: boolean = false;
- fetchTabularData(_queryFetchID: symbol) {
- this.setState({loading: false, tableResults: []});
- // TODO(dam): implement the rest
- }
- fetchTimeseriesData(queryFetchID: symbol) {
- const {selection, api, organization, widget} = this.props;
- this.setState({loading: false, timeseriesResults: [], rawResults: []});
- const {environments, projects, datetime} = selection;
- const {start, end, period} = datetime;
- const interval = getWidgetInterval(widget, {start, end, period});
- const promises = widget.queries.map(query => {
- const requestData = {
- field: query.fields,
- orgSlug: organization.slug,
- end,
- environment: environments,
- // groupBy: query.groupBy // TODO(dam): add backend groupBy support
- interval,
- limit: widget.displayType === DisplayType.TOP_N ? TOP_N : undefined,
- orderBy: query.orderby,
- project: projects,
- query: query.conditions,
- start,
- statsPeriod: period,
- };
- return doMetricsRequest(api, requestData);
- });
- let completed = 0;
- promises.forEach(async (promise, requestIndex) => {
- try {
- const rawResults = await promise;
- if (!this._isMounted) {
- return;
- }
- this.setState(prevState => {
- if (prevState.queryFetchID !== queryFetchID) {
- // invariant: a different request was initiated after this request
- return prevState;
- }
- const timeseriesResults = [...(prevState.timeseriesResults ?? [])];
- const transformedResult = transformMetricsResponseToSeries(
- rawResults,
- widget.queries[requestIndex].name
- );
- // When charting timeseriesData on echarts, color association to a timeseries result
- // is order sensitive, ie series at index i on the timeseries array will use color at
- // index i on the color array. This means that on multi series results, we need to make
- // sure that the order of series in our results do not change between fetches to avoid
- // coloring inconsistencies between renders.
- transformedResult.forEach((result, resultIndex) => {
- timeseriesResults[requestIndex * transformedResult.length + resultIndex] =
- result;
- });
- const rawResultsClone = cloneDeep(prevState.rawResults ?? []);
- rawResultsClone[requestIndex] = rawResults;
- return {
- ...prevState,
- timeseriesResults,
- rawResults: rawResultsClone,
- };
- });
- } catch (err) {
- const errorMessage = err?.responseJSON?.detail || t('An unknown error occurred.');
- this.setState({errorMessage});
- } finally {
- completed++;
- if (!this._isMounted) {
- return;
- }
- this.setState(prevState => {
- if (prevState.queryFetchID !== queryFetchID) {
- // invariant: a different request was initiated after this request
- return prevState;
- }
- return {
- ...prevState,
- loading: completed === promises.length ? false : true,
- };
- });
- }
- });
- }
- fetchData() {
- const {widget} = this.props;
- if (widget.displayType === DisplayType.WORLD_MAP) {
- this.setState({errorMessage: t('World Map is not supported by metrics.')});
- return;
- }
- const queryFetchID = Symbol('queryFetchID');
- this.setState({loading: true, errorMessage: undefined, queryFetchID});
- if (['table', 'big_number'].includes(widget.displayType)) {
- this.fetchTabularData(queryFetchID);
- } else {
- this.fetchTimeseriesData(queryFetchID);
- }
- }
- render() {
- const {children} = this.props;
- const {loading, timeseriesResults, tableResults, errorMessage} = this.state;
- return children({
- loading,
- timeseriesResults,
- tableResults,
- errorMessage,
- });
- }
- }
- export default MetricsWidgetQueries;
|