import {Fragment, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Location} from 'history'; import moment from 'moment'; import {getInterval} from 'sentry/components/charts/utils'; import {CompactSelect} from 'sentry/components/compactSelect'; import DatePageFilter from 'sentry/components/datePageFilter'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {ReactEchartsRef, Series} from 'sentry/types/echarts'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart'; import ChartPanel from 'sentry/views/starfish/components/chartPanel'; import {INTERNAL_API_REGEX} from 'sentry/views/starfish/modules/APIModule/constants'; import {HostDetails} from 'sentry/views/starfish/modules/APIModule/hostDetails'; import {queryToSeries} from 'sentry/views/starfish/modules/databaseModule/utils'; import {PERIOD_REGEX} from 'sentry/views/starfish/utils/dates'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries'; import {EndpointDataRow} from 'sentry/views/starfish/views/endpointDetails'; import EndpointTable from './endpointTable'; import HostTable from './hostTable'; import { getEndpointDomainsEventView, getEndpointDomainsQuery, getEndpointGraphEventView, getEndpointGraphQuery, useGetTransactionsForHosts, } from './queries'; const HTTP_ACTION_OPTIONS = [ {value: '', label: 'All'}, ...['GET', 'POST', 'PUT', 'DELETE'].map(action => ({ value: action, label: action, })), ]; type Props = { location: Location; onSelect: (row: EndpointDataRow) => void; }; export type DataRow = { count: number; description: string; domain: string; }; export default function APIModuleView({location, onSelect}: Props) { const themes = useTheme(); const pageFilter = usePageFilters(); const [state, setState] = useState<{ action: string; domain: string; transaction: string; }>({ action: '', domain: '', transaction: '', }); const endpointTableRef = useRef(null); const organization = useOrganization(); const endpointsDomainEventView = getEndpointDomainsEventView({ datetime: pageFilter.selection.datetime, }); const endpointsDomainQuery = getEndpointDomainsQuery({ datetime: pageFilter.selection.datetime, }); const {selection} = pageFilter; const {projects, environments, datetime} = selection; useApiQuery( [ `/organizations/${organization.slug}/events-starfish/`, { query: { ...{ environment: environments, project: projects.map(proj => String(proj)), }, ...normalizeDateTimeParams(datetime), }, }, ], { staleTime: 10, } ); const {isLoading: _isDomainsLoading, data: domains} = useSpansQuery({ eventView: endpointsDomainEventView, queryString: endpointsDomainQuery, initialData: [], }); const endpointsGraphEventView = getEndpointGraphEventView({ datetime: pageFilter.selection.datetime, }); const {isLoading: isGraphLoading, data: graphData} = useSpansQuery({ eventView: endpointsGraphEventView, queryString: getEndpointGraphQuery({ datetime: pageFilter.selection.datetime, }), initialData: [], }); const quantiles = [ 'p50(span.self_time)', 'p75(span.self_time)', 'p95(span.self_time)', 'p99(span.self_time)', ]; const seriesByQuantile: {[quantile: string]: Series} = {}; quantiles.forEach(quantile => { seriesByQuantile[quantile] = { seriesName: quantile, data: [], }; }); const countSeries: Series = { seriesName: 'count', data: [], }; const failureRateSeries: Series = { seriesName: 'failure_rate', data: [], }; graphData.forEach(datum => { quantiles.forEach(quantile => { seriesByQuantile[quantile].data.push({ value: datum[quantile], name: datum.interval, }); }); countSeries.data.push({ value: datum['count()'], name: datum.interval, }); failureRateSeries.data.push({ value: datum['failure_rate()'], name: datum.interval, }); }); const [_, num, unit] = pageFilter.selection.datetime.period?.match(PERIOD_REGEX) ?? []; const startTime = num && unit ? moment().subtract(num, unit as 'h' | 'd') : moment(pageFilter.selection.datetime.start); const endTime = moment(pageFilter.selection.datetime.end ?? undefined); const [zeroFilledQuantiles, zeroFilledCounts, zeroFilledFailureRate] = [ seriesByQuantile, [countSeries], [failureRateSeries], ].map(seriesGroup => Object.values(seriesGroup).map(series => zeroFillSeries(series, moment.duration(12, 'hours'), startTime, endTime) ) ); const setAction = (action: string) => { setState({ ...state, action, }); }; const setDomain = (domain: string) => { setState({ ...state, domain, }); }; const domainOptions = [ {value: '', label: 'All'}, ...domains .filter(({domain}) => domain !== '') .map(({domain}) => ({ value: domain, label: domain, })), ]; const interval = getInterval(pageFilter.selection.datetime, 'low'); const {isLoading: isTopTransactionDataLoading, data: topTransactionsData} = useGetTransactionsForHosts( domains .map(({domain}) => domain) .filter(domain => !domain.match(INTERNAL_API_REGEX)), interval ); const tpmTransactionSeries = queryToSeries(topTransactionsData, 'group', 'epm()'); const p75TransactionSeries = queryToSeries( topTransactionsData, 'group', 'p75(transaction.duration)' ); const loading = isGraphLoading || isTopTransactionDataLoading; useSynchronizeCharts([!loading]); return ( void 0} /> { setDomain(domain); // TODO: Cheap way to scroll to the endpoints table without waiting for async request setTimeout(() => { endpointTableRef.current?.scrollIntoView({ behavior: 'smooth', inline: 'start', }); }, 200); }} /> setAction(value)} /> setDomain(value)} />
{state.domain && }
); } function APIModuleChart({ data, loading, chartColors, forwardedRef, chartGroup, }: { data: Series[]; loading: boolean; chartColors?: string[]; chartGroup?: string; forwardedRef?: React.RefObject; }) { return ( ); } const ChartsContainer = styled('div')` display: flex; flex-direction: row; flex-wrap: wrap; gap: ${space(2)}; `; const ChartsContainerItem = styled('div')` flex: 1; `; const FilterOptionsContainer = styled('div')` display: flex; flex-direction: row; gap: ${space(1)}; margin-bottom: ${space(2)}; `;