import {Fragment, useCallback, useLayoutEffect, useMemo} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import * as echarts from 'echarts/core'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import {Button} from 'sentry/components/button'; import {EquationInput} from 'sentry/components/metrics/equationInput'; import {EquationSymbol} from 'sentry/components/metrics/equationSymbol'; import {QueryBuilder} from 'sentry/components/metrics/queryBuilder'; import {getQuerySymbol, QuerySymbol} from 'sentry/components/metrics/querySymbol'; import SwitchButton from 'sentry/components/switchButton'; import {Tooltip} from 'sentry/components/tooltip'; import {IconAdd} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {hasMetricsNewInputs} from 'sentry/utils/metrics/features'; import { isMetricsQueryWidget, MetricExpressionType, type MetricsEquationWidget, type MetricsQuery, type MetricsQueryWidget, type MetricsWidget, } from 'sentry/utils/metrics/types'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {METRIC_CHART_GROUP} from 'sentry/views/metrics/constants'; import {useMetricsContext} from 'sentry/views/metrics/context'; import {MetricFormulaContextMenu} from 'sentry/views/metrics/metricFormulaContextMenu'; import {MetricQueryContextMenu} from 'sentry/views/metrics/metricQueryContextMenu'; import {useFormulaDependencies} from 'sentry/views/metrics/utils/useFormulaDependencies'; export function Queries() { const { widgets, updateWidget, setSelectedWidgetIndex, showQuerySymbols, selectedWidgetIndex, isMultiChartMode, setIsMultiChartMode, addWidget, toggleWidgetVisibility, } = useMetricsContext(); const organization = useOrganization(); const {selection} = usePageFilters(); const formulaDependencies = useFormulaDependencies(); const metricsNewInputs = hasMetricsNewInputs(organization); // Make sure all charts are connected to the same group whenever the widgets definition changes useLayoutEffect(() => { echarts.connect(METRIC_CHART_GROUP); }, [widgets]); const handleChange = useCallback( (index: number, widget: Partial) => { updateWidget(index, widget); }, [updateWidget] ); const handleAddWidget = useCallback( (type: MetricExpressionType) => { trackAnalytics('ddm.widget.add', { organization, type: type === MetricExpressionType.QUERY ? 'query' : 'equation', }); addWidget(type); }, [addWidget, organization] ); const querySymbols = useMemo(() => { const querySymbolSet = new Set(); for (const widget of widgets) { const symbol = getQuerySymbol(widget.id, metricsNewInputs); if (isMetricsQueryWidget(widget)) { querySymbolSet.add(symbol); } } return querySymbolSet; }, [widgets, metricsNewInputs]); const visibleWidgets = widgets.filter(widget => !widget.isHidden); return ( {widgets.map((widget, index) => ( setSelectedWidgetIndex(index)} > {isMetricsQueryWidget(widget) ? ( 1} /> ) : ( 1} formulaDependencies={formulaDependencies} metricsNewInputs={metricsNewInputs} /> )} ))} {widgets.length > 1 && ( {t('One chart per metric')} setIsMultiChartMode(!isMultiChartMode)} /> )} ); } interface QueryProps { canBeHidden: boolean; index: number; isSelected: boolean; onChange: (index: number, data: Partial) => void; onToggleVisibility: (index: number) => void; projects: number[]; showQuerySymbols: boolean; widget: MetricsQueryWidget; } function Query({ widget, projects, onChange, onToggleVisibility, index, isSelected, showQuerySymbols, canBeHidden, }: QueryProps) { const organizations = useOrganization(); const hasMetricsNewInput = hasMetricsNewInputs(organizations); const metricsQuery = useMemo( () => ({ mri: widget.mri, aggregation: widget.aggregation, condition: widget.condition, groupBy: widget.groupBy, query: widget.query, }), [widget.mri, widget.aggregation, widget.condition, widget.groupBy, widget.query] ); const handleToggle = useCallback(() => { onToggleVisibility(index); }, [index, onToggleVisibility]); const handleChange = useCallback( (data: Partial) => { onChange(index, data); }, [index, onChange] ); const isToggleDisabled = !canBeHidden && !widget.isHidden; return ( {showQuerySymbols && ( )} ); } interface FormulaProps { availableVariables: Set; canBeHidden: boolean; formulaDependencies: ReturnType; index: number; isSelected: boolean; metricsNewInputs: boolean; onChange: (index: number, data: Partial) => void; onToggleVisibility: (index: number) => void; showQuerySymbols: boolean; widget: MetricsEquationWidget; } function Formula({ availableVariables, index, widget, onChange, onToggleVisibility, canBeHidden, isSelected, showQuerySymbols, formulaDependencies, metricsNewInputs, }: FormulaProps) { const organizations = useOrganization(); const hasMetricsNewInput = hasMetricsNewInputs(organizations); const handleToggle = useCallback(() => { onToggleVisibility(index); }, [index, onToggleVisibility]); const handleChange = useCallback( (data: Partial) => { onChange(index, data); }, [index, onChange] ); const isToggleDisabled = !canBeHidden && !widget.isHidden; return ( {showQuerySymbols && ( )} handleChange({formula})} metricsNewInputs={metricsNewInputs} /> ); } interface QueryToggleProps { disabled: boolean; isHidden: boolean; isSelected: boolean; onChange: (isHidden: boolean) => void; queryId: number; type: MetricExpressionType; } function QueryToggle({isHidden, queryId, disabled, onChange, type}: QueryToggleProps) { const tooltipTitle = type === MetricExpressionType.QUERY ? isHidden ? t('Show metric') : t('Hide metric') : isHidden ? t('Show equation') : t('Hide equation'); return ( {type === MetricExpressionType.QUERY ? ( onChange(!isHidden)} role="button" aria-label={tooltipTitle} /> ) : ( onChange(!isHidden)} role="button" aria-label={tooltipTitle} /> )} ); } const QueryWrapper = styled('div')<{hasMetricsNewInput: boolean; hasSymbol: boolean}>` display: contents; ${p => !p.hasMetricsNewInput && css` display: grid; gap: ${space(1)}; padding-bottom: ${space(1)}; grid-template-columns: 1fr max-content; ${p.hasSymbol && `grid-template-columns: min-content 1fr max-content;`} `} `; const StyledQuerySymbol = styled(QuerySymbol)<{isClickable: boolean}>` cursor: not-allowed; ${p => p.isClickable && `cursor: pointer;`} `; const StyledEquationSymbol = styled(EquationSymbol)<{isClickable: boolean}>` cursor: not-allowed; ${p => p.isClickable && `cursor: pointer;`} `; const Wrapper = styled('div')<{hasMetricsNewInput: boolean; showQuerySymbols: boolean}>` ${p => p.hasMetricsNewInput && css` display: grid; gap: ${space(1)}; grid-template-columns: ${p.showQuerySymbols ? 'min-content 1fr max-content' : '1fr max-content'}; @media (min-width: ${p.theme.breakpoints.small}) { grid-template-columns: ${p.showQuerySymbols ? 'min-content 1fr 1fr max-content' : '1fr 1fr max-content'}; } @media (min-width: ${p.theme.breakpoints.large}) { grid-template-columns: ${p.showQuerySymbols ? 'min-content 1fr max-content max-content max-content' : '1fr max-content max-content max-content'}; } @media (min-width: ${p.theme.breakpoints.xxlarge}) { grid-template-columns: ${p.showQuerySymbols ? 'min-content max-content max-content max-content 1fr max-content' : 'max-content max-content max-content 1fr max-content'}; } `} `; const EquationInputWrapper = styled('div')<{hasMetricsNewInput: boolean}>` ${p => p.hasMetricsNewInput && css` grid-template-columns: subgrid; grid-column-start: 2; @media (min-width: ${p.theme.breakpoints.small}) { grid-column-end: 4; } @media (min-width: ${p.theme.breakpoints.large}) { grid-column-end: 5; } @media (min-width: ${p.theme.breakpoints.xxlarge}) { grid-column-end: 6; } `} `; const Row = styled('div')` display: contents; `; const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>` align-items: center; display: flex; flex-wrap: wrap; padding: ${space(2)} 0; gap: ${space(2)}; ${p => p.addQuerySymbolSpacing && ` padding-left: ${space(1)}; margin-left: 38px; `} `; const SwitchWrapper = styled('label')` display: flex; margin: 0; align-items: center; gap: ${space(1)}; white-space: nowrap; `;