123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- import {Component} from 'react';
- import isEqual from 'lodash/isEqual';
- import {Client, ResponseMeta} from 'sentry/api';
- import {isSelectionEqual} from 'sentry/components/organizations/pageFilters/utils';
- import {t} from 'sentry/locale';
- import {Organization, PageFilters} from 'sentry/types';
- import {Series} from 'sentry/types/echarts';
- import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
- import {DatasetConfig} from '../datasetConfig/base';
- import {DEFAULT_TABLE_LIMIT, DisplayType, Widget, WidgetQuery} from '../types';
- function getReferrer(displayType: DisplayType) {
- let referrer: string = '';
- if (displayType === DisplayType.TABLE) {
- referrer = 'api.dashboards.tablewidget';
- } else if (displayType === DisplayType.BIG_NUMBER) {
- referrer = 'api.dashboards.bignumberwidget';
- } else if (displayType === DisplayType.WORLD_MAP) {
- referrer = 'api.dashboards.worldmapwidget';
- } else {
- referrer = `api.dashboards.widget.${displayType}-chart`;
- }
- return referrer;
- }
- export type OnDataFetchedProps = {
- pageLinks?: string;
- tableResults?: TableDataWithTitle[];
- timeseriesResults?: Series[];
- totalIssuesCount?: string;
- };
- export type GenericWidgetQueriesChildrenProps = {
- loading: boolean;
- errorMessage?: string;
- pageLinks?: string;
- tableResults?: TableDataWithTitle[];
- timeseriesResults?: Series[];
- totalCount?: string;
- };
- export type GenericWidgetQueriesProps<SeriesResponse, TableResponse> = {
- api: Client;
- children: (props: GenericWidgetQueriesChildrenProps) => JSX.Element;
- config: DatasetConfig<SeriesResponse, TableResponse>;
- organization: Organization;
- selection: PageFilters;
- widget: Widget;
- afterFetchSeriesData?: (result: SeriesResponse) => void;
- afterFetchTableData?: (
- result: TableResponse,
- response?: ResponseMeta
- ) => void | {totalIssuesCount?: string};
- cursor?: string;
- customDidUpdateComparator?: (
- prevProps: GenericWidgetQueriesProps<SeriesResponse, TableResponse>,
- nextProps: GenericWidgetQueriesProps<SeriesResponse, TableResponse>
- ) => boolean;
- limit?: number;
- loading?: boolean;
- onDataFetched?: ({
- tableResults,
- timeseriesResults,
- totalIssuesCount,
- pageLinks,
- }: OnDataFetchedProps) => void;
- };
- type State<SeriesResponse> = {
- loading: boolean;
- errorMessage?: GenericWidgetQueriesChildrenProps['errorMessage'];
- pageLinks?: GenericWidgetQueriesChildrenProps['pageLinks'];
- queryFetchID?: symbol;
- rawResults?: SeriesResponse[];
- tableResults?: GenericWidgetQueriesChildrenProps['tableResults'];
- timeseriesResults?: GenericWidgetQueriesChildrenProps['timeseriesResults'];
- };
- class GenericWidgetQueries<SeriesResponse, TableResponse> extends Component<
- GenericWidgetQueriesProps<SeriesResponse, TableResponse>,
- State<SeriesResponse>
- > {
- state: State<SeriesResponse> = {
- loading: true,
- queryFetchID: undefined,
- errorMessage: undefined,
- timeseriesResults: undefined,
- rawResults: undefined,
- tableResults: undefined,
- pageLinks: undefined,
- };
- componentDidMount() {
- this._isMounted = true;
- if (!this.props.loading) {
- this.fetchData();
- }
- }
- componentDidUpdate(
- prevProps: GenericWidgetQueriesProps<SeriesResponse, TableResponse>
- ) {
- const {selection, widget, cursor, organization, config, customDidUpdateComparator} =
- this.props;
- // We do not fetch data whenever the query name changes.
- // Also don't count empty fields when checking for field changes
- const [prevWidgetQueryNames, prevWidgetQueries] = prevProps.widget.queries
- .map((query: WidgetQuery) => {
- query.aggregates = query.aggregates.filter(field => !!field);
- query.columns = query.columns.filter(field => !!field);
- return query;
- })
- .reduce(
- ([names, queries]: [string[], Omit<WidgetQuery, 'name'>[]], {name, ...rest}) => {
- names.push(name);
- queries.push(rest);
- return [names, queries];
- },
- [[], []]
- );
- const [widgetQueryNames, widgetQueries] = widget.queries
- .map((query: WidgetQuery) => {
- query.aggregates = query.aggregates.filter(
- field => !!field && field !== 'equation|'
- );
- query.columns = query.columns.filter(field => !!field && field !== 'equation|');
- return query;
- })
- .reduce(
- ([names, queries]: [string[], Omit<WidgetQuery, 'name'>[]], {name, ...rest}) => {
- names.push(name);
- queries.push(rest);
- return [names, queries];
- },
- [[], []]
- );
- if (
- customDidUpdateComparator
- ? customDidUpdateComparator(prevProps, this.props)
- : widget.limit !== prevProps.widget.limit ||
- !isEqual(widget.displayType, prevProps.widget.displayType) ||
- !isEqual(widget.interval, prevProps.widget.interval) ||
- !isEqual(widgetQueries, prevWidgetQueries) ||
- !isSelectionEqual(selection, prevProps.selection) ||
- cursor !== prevProps.cursor
- ) {
- this.fetchData();
- return;
- }
- if (
- !this.state.loading &&
- !isEqual(prevWidgetQueryNames, widgetQueryNames) &&
- this.state.rawResults?.length === widget.queries.length
- ) {
- // If the query names has changed, then update timeseries labels
- // eslint-disable-next-line react/no-did-update-set-state
- this.setState(prevState => {
- const timeseriesResults = widget.queries.reduce((acc: Series[], query, index) => {
- return acc.concat(
- config.transformSeries!(prevState.rawResults![index], query, organization)
- );
- }, []);
- return {...prevState, timeseriesResults};
- });
- }
- }
- componentWillUnmount() {
- this._isMounted = false;
- }
- private _isMounted: boolean = false;
- async fetchTableData(queryFetchID: symbol) {
- const {
- widget,
- limit,
- config,
- api,
- organization,
- selection,
- cursor,
- afterFetchTableData,
- onDataFetched,
- } = this.props;
- const responses = await Promise.all(
- widget.queries.map(query => {
- let requestLimit: number | undefined = limit ?? DEFAULT_TABLE_LIMIT;
- let requestCreator = config.getTableRequest;
- if (widget.displayType === DisplayType.WORLD_MAP) {
- requestLimit = undefined;
- requestCreator = config.getWorldMapRequest;
- }
- if (!requestCreator) {
- throw new Error(
- t('This display type is not supported by the selected dataset.')
- );
- }
- return requestCreator(
- api,
- query,
- organization,
- selection,
- requestLimit,
- cursor,
- getReferrer(widget.displayType)
- );
- })
- );
- let transformedTableResults: TableDataWithTitle[] = [];
- let responsePageLinks: string | undefined;
- let afterTableFetchData: OnDataFetchedProps | undefined;
- responses.forEach(([data, _textstatus, resp], i) => {
- afterTableFetchData = afterFetchTableData?.(data, resp) ?? {};
- // Cast so we can add the title.
- const transformedData = config.transformTable(
- data,
- widget.queries[0],
- organization,
- selection
- ) as TableDataWithTitle;
- transformedData.title = widget.queries[i]?.name ?? '';
- // Overwrite the local var to work around state being stale in tests.
- transformedTableResults = [...transformedTableResults, transformedData];
- // There is some inconsistency with the capitalization of "link" in response headers
- responsePageLinks =
- (resp?.getResponseHeader('Link') || resp?.getResponseHeader('link')) ?? undefined;
- });
- if (this._isMounted && this.state.queryFetchID === queryFetchID) {
- onDataFetched?.({
- tableResults: transformedTableResults,
- pageLinks: responsePageLinks,
- ...afterTableFetchData,
- });
- this.setState({
- tableResults: transformedTableResults,
- pageLinks: responsePageLinks,
- });
- }
- }
- async fetchSeriesData(queryFetchID: symbol) {
- const {
- widget,
- config,
- api,
- organization,
- selection,
- afterFetchSeriesData,
- onDataFetched,
- } = this.props;
- const responses = await Promise.all(
- widget.queries.map((_query, index) => {
- return config.getSeriesRequest!(
- api,
- widget,
- index,
- organization,
- selection,
- getReferrer(widget.displayType)
- );
- })
- );
- const transformedTimeseriesResults: Series[] = [];
- responses.forEach(([data], requestIndex) => {
- afterFetchSeriesData?.(data);
- const transformedResult = config.transformSeries!(
- data,
- widget.queries[requestIndex],
- organization
- );
- // 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) => {
- transformedTimeseriesResults[
- requestIndex * transformedResult.length + resultIndex
- ] = result;
- });
- });
- if (this._isMounted && this.state.queryFetchID === queryFetchID) {
- onDataFetched?.({timeseriesResults: transformedTimeseriesResults});
- this.setState({timeseriesResults: transformedTimeseriesResults});
- }
- }
- async fetchData() {
- const {widget} = this.props;
- const queryFetchID = Symbol('queryFetchID');
- this.setState({
- loading: true,
- tableResults: undefined,
- timeseriesResults: undefined,
- errorMessage: undefined,
- queryFetchID,
- });
- try {
- if (
- [DisplayType.TABLE, DisplayType.BIG_NUMBER, DisplayType.WORLD_MAP].includes(
- widget.displayType
- )
- ) {
- await this.fetchTableData(queryFetchID);
- } else {
- await this.fetchSeriesData(queryFetchID);
- }
- } catch (err) {
- if (this._isMounted) {
- this.setState({
- errorMessage:
- err?.responseJSON?.detail || err?.message || t('An unknown error occurred.'),
- });
- }
- } finally {
- if (this._isMounted) {
- this.setState({loading: false});
- }
- }
- }
- render() {
- const {children} = this.props;
- const {loading, tableResults, timeseriesResults, errorMessage, pageLinks} =
- this.state;
- return children({loading, tableResults, timeseriesResults, errorMessage, pageLinks});
- }
- }
- export default GenericWidgetQueries;
|