import {Fragment, useCallback, useEffect, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; import FeatureBadge from 'sentry/components/featureBadge'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {RESOURCE_THROUGHPUT_UNIT} from 'sentry/views/performance/browser/resources'; import ResourceTable from 'sentry/views/performance/browser/resources/resourceView/resourceTable'; import { FONT_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS, } from 'sentry/views/performance/browser/resources/shared/constants'; import RenderBlockingSelector from 'sentry/views/performance/browser/resources/shared/renderBlockingSelector'; import SelectControlWithProps from 'sentry/views/performance/browser/resources/shared/selectControlWithProps'; import {ResourceSpanOps} from 'sentry/views/performance/browser/resources/shared/types'; import { BrowserStarfishFields, useResourceModuleFilters, } from 'sentry/views/performance/browser/resources/utils/useResourceFilters'; import {useResourcePagesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcePagesQuery'; import {useResourceSort} from 'sentry/views/performance/browser/resources/utils/useResourceSort'; import {getResourceTypeFilter} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery'; import {ModuleName} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; import {SpanTimeCharts} from 'sentry/views/starfish/views/spans/spanTimeCharts'; import type {ModuleFilters} from 'sentry/views/starfish/views/spans/useModuleFilters'; const { SPAN_OP: RESOURCE_TYPE, SPAN_DOMAIN, TRANSACTION, RESOURCE_RENDER_BLOCKING_STATUS, } = BrowserStarfishFields; export const DEFAULT_RESOURCE_TYPES = [ ResourceSpanOps.SCRIPT, ResourceSpanOps.CSS, ResourceSpanOps.FONT, ResourceSpanOps.IMAGE, ]; type Option = { label: string | React.ReactElement; value: string; }; function ResourceView() { const filters = useResourceModuleFilters(); const sort = useResourceSort(); const spanTimeChartsFilters: ModuleFilters = { 'span.op': `[${DEFAULT_RESOURCE_TYPES.join(',')}]`, ...(filters[SPAN_DOMAIN] ? {[SPAN_DOMAIN]: filters[SPAN_DOMAIN]} : {}), }; const extraQuery = getResourceTypeFilter(undefined, DEFAULT_RESOURCE_TYPES); return ( ); } function ResourceTypeSelector({value}: {value?: string}) { const location = useLocation(); const {features} = useOrganization(); const hasImageView = features.includes('starfish-browser-resource-module-image-view'); const options: Option[] = [ {value: '', label: 'All'}, {value: 'resource.script', label: `${t('JavaScript')} (.js)`}, {value: 'resource.css', label: `${t('Stylesheet')} (.css)`}, { value: 'resource.font', label: `${t('Font')} (${FONT_FILE_EXTENSIONS.map(e => `.${e}`).join(', ')})`, }, ...(hasImageView ? [ { value: ResourceSpanOps.IMAGE, label: ( {`${t('Image')} (${IMAGE_FILE_EXTENSIONS.map(e => `.${e}`).join(', ')})`} ), }, ] : []), ]; return ( { browserHistory.push({ ...location, query: { ...location.query, [RESOURCE_TYPE]: newValue?.value, [QueryParameterNames.SPANS_CURSOR]: undefined, }, }); }} /> ); } export function TransactionSelector({ value, defaultResourceTypes, }: { defaultResourceTypes?: string[]; value?: string; }) { const [state, setState] = useState({ search: '', inputChanged: false, shouldRequeryOnInputChange: false, }); const location = useLocation(); const {data: pages, isLoading} = useResourcePagesQuery( defaultResourceTypes, state.search ); // If the maximum number of pages is returned, we need to requery on input change to get full results if (!state.shouldRequeryOnInputChange && pages && pages.length >= 100) { setState({...state, shouldRequeryOnInputChange: true}); } // Everytime loading is complete, reset the inputChanged state useEffect(() => { if (!isLoading && state.inputChanged) { setState({...state, inputChanged: false}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoading]); const optionsReady = !isLoading && !state.inputChanged; const options: Option[] = optionsReady ? [{value: '', label: 'All'}, ...pages.map(page => ({value: page, label: page}))] : []; // eslint-disable-next-line react-hooks/exhaustive-deps const debounceUpdateSearch = useCallback( debounce((search, currentState) => { setState({...currentState, search}); }, 500), [] ); return ( { if (state.shouldRequeryOnInputChange) { setState({...state, inputChanged: true}); debounceUpdateSearch(input, state); } }} noOptionsMessage={() => (optionsReady ? undefined : t('Loading...'))} onChange={newValue => { browserHistory.push({ ...location, query: { ...location.query, [TRANSACTION]: newValue?.value, [QueryParameterNames.SPANS_CURSOR]: undefined, }, }); }} /> ); } export const SpanTimeChartsContainer = styled('div')` margin-bottom: ${space(2)}; `; export const FilterOptionsContainer = styled('div')<{columnCount: number}>` display: grid; grid-template-columns: repeat(${props => props.columnCount}, 1fr); gap: ${space(2)}; margin-bottom: ${space(2)}; max-width: 800px; `; export default ResourceView;