import {Component} from 'react'; import styled from '@emotion/styled'; import cloneDeep from 'lodash/cloneDeep'; import Button from 'sentry/components/button'; import SearchBar from 'sentry/components/events/searchBar'; import Field from 'sentry/components/forms/field'; import SelectControl from 'sentry/components/forms/selectControl'; import Input from 'sentry/components/input'; import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {IconAdd, IconDelete} from 'sentry/icons'; import {t} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Organization, PageFilters, SelectValue} from 'sentry/types'; import {defined} from 'sentry/utils'; import { explodeField, generateFieldAsString, getColumnsAndAggregatesAsStrings, isEquation, stripDerivedMetricsPrefix, stripEquationPrefix, } from 'sentry/utils/discover/fields'; import {Widget, WidgetQuery, WidgetType} from 'sentry/views/dashboardsV2/types'; import {ReleaseSearchBar} from 'sentry/views/dashboardsV2/widgetBuilder/buildSteps/filterResultsStep/releaseSearchBar'; import { DISABLED_SORT, TAG_SORT_DENY_LIST, } from 'sentry/views/dashboardsV2/widgetBuilder/releaseWidget/fields'; import {generateFieldOptions} from 'sentry/views/eventsV2/utils'; import WidgetQueryFields from './widgetQueryFields'; export const generateOrderOptions = ({ aggregates, columns, widgetType, widgetBuilderNewDesign = false, }: { aggregates: string[]; columns: string[]; widgetType: WidgetType; widgetBuilderNewDesign?: boolean; }): SelectValue[] => { const isRelease = widgetType === WidgetType.RELEASE; const options: SelectValue[] = []; let equations = 0; (isRelease ? [...aggregates.map(stripDerivedMetricsPrefix), ...columns] : [...aggregates, ...columns] ) .filter(field => !!field) .filter(field => !DISABLED_SORT.includes(field)) .filter(field => (isRelease ? !TAG_SORT_DENY_LIST.includes(field) : true)) .forEach(field => { let alias; const label = stripEquationPrefix(field); // Equations are referenced via a standard alias following this pattern if (isEquation(field)) { alias = `equation[${equations}]`; equations += 1; } if (widgetBuilderNewDesign) { options.push({label, value: alias ?? field}); return; } options.push({ label: t('%s asc', label), value: alias ?? field, }); options.push({ label: t('%s desc', label), value: `-${alias ?? field}`, }); }); return options; }; type Props = { canAddSearchConditions: boolean; displayType: Widget['displayType']; fieldOptions: ReturnType; handleAddSearchConditions: () => void; handleDeleteQuery: (queryIndex: number) => void; onChange: (queryIndex: number, widgetQuery: WidgetQuery) => void; organization: Organization; queries: WidgetQuery[]; selection: PageFilters; errors?: Array>; widgetType?: Widget['widgetType']; }; /** * Contain widget queries interactions and signal changes via the onChange * callback. This component's state should live in the parent. */ class WidgetQueriesForm extends Component { componentWillUnmount() { window.clearTimeout(this.blurTimeout); } blurTimeout: number | undefined = undefined; // Handle scalar field values changing. handleFieldChange = (queryIndex: number, field: string) => { const {queries, onChange} = this.props; const widgetQuery = queries[queryIndex]; return function handleChange(value: string) { const newQuery = {...widgetQuery, [field]: value}; onChange(queryIndex, newQuery); }; }; getFirstQueryError(key: string) { const {errors} = this.props; if (!errors) { return undefined; } return errors.find(queryError => queryError && queryError[key]); } renderSearchBar(widgetQuery: WidgetQuery, queryIndex: number) { const {organization, selection, widgetType} = this.props; return widgetType === WidgetType.RELEASE ? ( { this.handleFieldChange(queryIndex, 'conditions')(field); }} pageFilters={selection} /> ) : ( { this.handleFieldChange(queryIndex, 'conditions')(field); }} useFormWrapper={false} maxQueryLength={MAX_QUERY_LENGTH} /> ); } render() { const { organization, errors, queries, canAddSearchConditions, handleAddSearchConditions, handleDeleteQuery, displayType, fieldOptions, onChange, widgetType = WidgetType.DISCOVER, } = this.props; const hideLegendAlias = ['table', 'world_map', 'big_number'].includes(displayType); const query = queries[0]; const explodedFields = defined(query.fields) ? query.fields.map(field => explodeField({field})) : [...query.columns, ...query.aggregates].map(field => explodeField({field})); return ( {queries.map((widgetQuery, queryIndex) => { return ( {this.renderSearchBar(widgetQuery, queryIndex)} {!hideLegendAlias && ( this.handleFieldChange(queryIndex, 'name')(event.target.value) } /> )} {queries.length > 1 && ( )} { const {aggregates, columns} = getColumnsAndAggregatesAsStrings(fields); const fieldStrings = fields.map(field => generateFieldAsString(field)); queries.forEach((widgetQuery, queryIndex) => { const newQuery = cloneDeep(widgetQuery); newQuery.fields = fieldStrings; newQuery.aggregates = aggregates; newQuery.columns = columns; if (defined(widgetQuery.orderby)) { const descending = widgetQuery.orderby.startsWith('-'); const orderby = widgetQuery.orderby.replace('-', ''); const prevFieldStrings = defined(widgetQuery.fields) ? widgetQuery.fields : [...widgetQuery.columns, ...widgetQuery.aggregates]; if (!aggregates.includes(orderby) && widgetQuery.orderby !== '') { if (prevFieldStrings.length === fields.length) { // The Field that was used in orderby has changed. Get the new field. newQuery.orderby = `${descending ? '-' : ''}${ aggregates[prevFieldStrings.indexOf(orderby)] }`; } else { newQuery.orderby = ''; } } } onChange(queryIndex, newQuery); }); }} /> {['table', 'top_n'].includes(displayType) && ( ) => this.handleFieldChange(0, 'orderby')(option.value) } /> )} ); } } const QueryWrapper = styled('div')` position: relative; `; export const SearchConditionsWrapper = styled('div')` display: flex; align-items: center; > * + * { margin-left: ${space(1)}; } `; const StyledSearchBar = styled(SearchBar)` flex-grow: 1; `; const LegendAliasInput = styled(Input)` width: 33%; `; export default WidgetQueriesForm;