123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import {Component, useContext} from 'react';
- import {useQuery} from '@tanstack/react-query';
- import {Location} from 'history';
- import {EventQuery} from 'sentry/actionCreators/events';
- import {Client, ResponseMeta} from 'sentry/api';
- import {t} from 'sentry/locale';
- import EventView, {
- ImmutableEventView,
- isAPIPayloadSimilar,
- LocationQuery,
- } from 'sentry/utils/discover/eventView';
- import {QueryBatching} from 'sentry/utils/performance/contexts/genericQueryBatcher';
- import {PerformanceEventViewContext} from 'sentry/utils/performance/contexts/performanceEventViewContext';
- import {OrganizationContext} from 'sentry/views/organizationContext';
- import useApi from '../useApi';
- export class QueryError {
- message: string;
- private originalError: any; // For debugging in case parseError picks a value that doesn't make sense.
- constructor(errorMessage: string, originalError?: any) {
- this.message = errorMessage;
- this.originalError = originalError;
- }
- getOriginalError() {
- return this.originalError;
- }
- }
- export type GenericChildrenProps<T> = {
- /**
- * Error, if not null.
- */
- error: null | QueryError;
- /**
- * Loading state of this query.
- */
- isLoading: boolean;
- /**
- * Pagelinks, if applicable. Can be provided to the Pagination component.
- */
- pageLinks: null | string;
- /**
- * Data / result.
- */
- tableData: T | null;
- };
- type OptionalContextProps = {
- eventView?: EventView | ImmutableEventView;
- orgSlug?: string;
- };
- type BaseDiscoverQueryProps = {
- /**
- * Used as the default source for cursor values.
- */
- location: Location;
- /**
- * Explicit cursor value if you aren't using `location.query.cursor` because there are
- * multiple paginated results on the page.
- */
- cursor?: string;
- /**
- * Appends a raw string to query to be able to sidestep the tokenizer.
- * @deprecated
- */
- forceAppendRawQueryString?: string;
- /**
- * Record limit to get.
- */
- limit?: number;
- /**
- * Include this whenever pagination won't be used. Limit can still be used when this is
- * passed, but cursor will be ignored.
- */
- noPagination?: boolean;
- /**
- * A container for query batching data and functions.
- */
- queryBatching?: QueryBatching;
- /**
- * Extra query parameters to be added.
- */
- queryExtras?: Record<string, string>;
- /**
- * Sets referrer parameter in the API Payload. Set of allowed referrers are defined
- * on the OrganizationEventsV2Endpoint view.
- */
- referrer?: string;
- /**
- * A callback to set an error so that the error can be rendered in parent components
- */
- setError?: (errObject: QueryError | undefined) => void;
- };
- export type DiscoverQueryPropsWithContext = BaseDiscoverQueryProps & OptionalContextProps;
- export type DiscoverQueryProps = BaseDiscoverQueryProps & {
- eventView: EventView | ImmutableEventView;
- orgSlug: string;
- };
- type InnerRequestProps<P> = DiscoverQueryProps & P;
- type OuterRequestProps<P> = DiscoverQueryPropsWithContext & P;
- export type ReactProps<T> = {
- children?: (props: GenericChildrenProps<T>) => React.ReactNode;
- };
- type ComponentProps<T, P> = {
- /**
- * Route to the endpoint
- */
- route: string;
- /**
- * A hook to modify data into the correct output after data has been received
- */
- afterFetch?: (data: any, props?: Props<T, P>) => T;
- /**
- * A hook before fetch that can be used to do things like clearing the api
- */
- beforeFetch?: (api: Client) => void;
- /**
- * A hook for parent orchestrators to pass down data based on query results, unlike afterFetch it is not meant for specializations as it will not modify data.
- */
- didFetch?: (data: T) => void;
- /**
- * Allows components to modify the payload before it is set.
- */
- getRequestPayload?: (props: Props<T, P>) => any;
- /**
- * An external hook to parse errors in case there are differences for a specific api.
- */
- parseError?: (error: any) => QueryError | null;
- /**
- * An external hook in addition to the event view check to check if data should be refetched
- */
- shouldRefetchData?: (prevProps: Props<T, P>, props: Props<T, P>) => boolean;
- };
- type Props<T, P> = InnerRequestProps<P> & ReactProps<T> & ComponentProps<T, P>;
- type OuterProps<T, P> = OuterRequestProps<P> & ReactProps<T> & ComponentProps<T, P>;
- type State<T> = {
- api: Client;
- tableFetchID: symbol | undefined;
- } & GenericChildrenProps<T>;
- /**
- * Generic component for discover queries
- */
- class _GenericDiscoverQuery<T, P> extends Component<Props<T, P>, State<T>> {
- state: State<T> = {
- isLoading: true,
- tableFetchID: undefined,
- error: null,
- tableData: null,
- pageLinks: null,
- api: new Client(),
- };
- componentDidMount() {
- this.fetchData();
- }
- componentDidUpdate(prevProps: Props<T, P>) {
- // Reload data if the payload changes
- const refetchCondition = this._shouldRefetchData(prevProps);
- // or if we've moved from an invalid view state to a valid one,
- const eventViewValidation =
- prevProps.eventView.isValid() === false && this.props.eventView.isValid();
- const shouldRefetchExternal = this.props.shouldRefetchData
- ? this.props.shouldRefetchData(prevProps, this.props)
- : false;
- if (refetchCondition || eventViewValidation || shouldRefetchExternal) {
- this.fetchData();
- }
- }
- _shouldRefetchData = (prevProps: Props<T, P>): boolean => {
- const thisAPIPayload = getPayload(this.props);
- const otherAPIPayload = getPayload(prevProps);
- return (
- !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload) ||
- prevProps.limit !== this.props.limit ||
- prevProps.route !== this.props.route ||
- prevProps.cursor !== this.props.cursor
- );
- };
- /**
- * The error type isn't consistent across APIs. We see detail as just string some times, other times as an object.
- */
- _parseError = (error: any): QueryError | null => {
- if (this.props.parseError) {
- return this.props.parseError(error);
- }
- if (!error) {
- return null;
- }
- const detail = error.responseJSON?.detail;
- if (typeof detail === 'string') {
- return new QueryError(detail, error);
- }
- const message = detail?.message;
- if (typeof message === 'string') {
- return new QueryError(message, error);
- }
- const unknownError = new QueryError(t('An unknown error occurred.'), error);
- return unknownError;
- };
- fetchData = async () => {
- const {
- queryBatching,
- beforeFetch,
- afterFetch,
- didFetch,
- eventView,
- orgSlug,
- route,
- setError,
- } = this.props;
- const {api} = this.state;
- if (!eventView.isValid()) {
- return;
- }
- const url = `/organizations/${orgSlug}/${route}/`;
- const tableFetchID = Symbol(`tableFetchID`);
- const apiPayload: Partial<EventQuery & LocationQuery> = getPayload(this.props);
- this.setState({isLoading: true, tableFetchID});
- setError?.(undefined);
- beforeFetch?.(api);
- // clear any inflight requests since they are now stale
- api.clear();
- try {
- const [data, , resp] = await doDiscoverQuery<T>(
- api,
- url,
- apiPayload,
- queryBatching
- );
- if (this.state.tableFetchID !== tableFetchID) {
- // invariant: a different request was initiated after this request
- return;
- }
- const tableData = afterFetch ? afterFetch(data, this.props) : data;
- didFetch?.(tableData);
- this.setState(prevState => ({
- isLoading: false,
- tableFetchID: undefined,
- error: null,
- pageLinks: resp?.getResponseHeader('Link') ?? prevState.pageLinks,
- tableData,
- }));
- } catch (err) {
- const error = this._parseError(err);
- this.setState({
- isLoading: false,
- tableFetchID: undefined,
- error,
- tableData: null,
- });
- if (setError) {
- setError(error ?? undefined);
- }
- }
- };
- render() {
- const {isLoading, error, tableData, pageLinks} = this.state;
- const childrenProps: GenericChildrenProps<T> = {
- isLoading,
- error,
- tableData,
- pageLinks,
- };
- const children: ReactProps<T>['children'] = this.props.children; // Explicitly setting type due to issues with generics and React's children
- return children?.(childrenProps);
- }
- }
- // Shim to allow us to use generic discover query or any specialization with or without passing org slug or eventview, which are now contexts.
- // This will help keep tests working and we can remove extra uses of context-provided props and update tests as we go.
- export function GenericDiscoverQuery<T, P>(props: OuterProps<T, P>) {
- const organizationSlug = useContext(OrganizationContext)?.slug;
- const performanceEventView = useContext(PerformanceEventViewContext)?.eventView;
- const orgSlug = props.orgSlug ?? organizationSlug;
- const eventView = props.eventView ?? performanceEventView;
- if (orgSlug === undefined || eventView === undefined) {
- throw new Error('GenericDiscoverQuery requires both an orgSlug and eventView');
- }
- const _props: Props<T, P> = {
- ...props,
- orgSlug,
- eventView,
- };
- return <_GenericDiscoverQuery<T, P> {..._props} />;
- }
- export type DiscoverQueryRequestParams = Partial<EventQuery & LocationQuery>;
- export function doDiscoverQuery<T>(
- api: Client,
- url: string,
- params: DiscoverQueryRequestParams,
- queryBatching?: QueryBatching
- ): Promise<[T, string | undefined, ResponseMeta<T> | undefined]> {
- if (queryBatching?.batchRequest) {
- return queryBatching.batchRequest(api, url, {
- query: params,
- includeAllArgs: true,
- });
- }
- return api.requestPromise(url, {
- method: 'GET',
- includeAllArgs: true,
- query: {
- // marking params as any so as to not cause typescript errors
- ...(params as any),
- },
- });
- }
- function getPayload<T, P>(props: Props<T, P>) {
- const {
- cursor,
- limit,
- noPagination,
- referrer,
- getRequestPayload,
- eventView,
- location,
- forceAppendRawQueryString,
- } = props;
- const payload = getRequestPayload
- ? getRequestPayload(props)
- : eventView.getEventsAPIPayload(location, forceAppendRawQueryString);
- if (cursor) {
- payload.cursor = cursor;
- }
- if (limit) {
- payload.per_page = limit;
- }
- if (noPagination) {
- payload.noPagination = noPagination;
- }
- if (referrer) {
- payload.referrer = referrer;
- }
- Object.assign(payload, props.queryExtras ?? {});
- return payload;
- }
- export function useGenericDiscoverQuery<T, P>(props: Props<T, P>) {
- const api = useApi();
- const {orgSlug, route} = props;
- const url = `/organizations/${orgSlug}/${route}/`;
- const apiPayload = getPayload<T, P>(props);
- return useQuery<T, QueryError>([route, apiPayload], async () => {
- const [resp] = await doDiscoverQuery<T>(api, url, apiPayload, props.queryBatching);
- return resp;
- });
- }
- export default GenericDiscoverQuery;
|