import {Fragment, useCallback, useLayoutEffect, useMemo} from 'react'; import styled from '@emotion/styled'; import * as echarts from 'echarts/core'; import {Button} from 'sentry/components/button'; 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 { type MetricFormulaWidgetParams, MetricQueryType, type MetricQueryWidgetParams, type MetricsQuery, type MetricWidgetQueryParams, } from 'sentry/utils/metrics/types'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {DDM_CHART_GROUP} from 'sentry/views/ddm/constants'; import {useDDMContext} from 'sentry/views/ddm/context'; import {EquationSymbol} from 'sentry/views/ddm/equationSymbol copy'; import {FormulaInput} from 'sentry/views/ddm/formulaInput'; import {MetricFormulaContextMenu} from 'sentry/views/ddm/metricFormulaContextMenu'; import {MetricQueryContextMenu} from 'sentry/views/ddm/metricQueryContextMenu'; import {QueryBuilder} from 'sentry/views/ddm/queryBuilder'; import {getQuerySymbol, QuerySymbol} from 'sentry/views/ddm/querySymbol'; import {useFormulaDependencies} from 'sentry/views/ddm/utils/useFormulaDependencies'; export function Queries() { const { widgets, updateWidget, setSelectedWidgetIndex, showQuerySymbols, selectedWidgetIndex, isMultiChartMode, setIsMultiChartMode, addWidget, toggleWidgetVisibility, } = useDDMContext(); const organization = useOrganization(); const {selection} = usePageFilters(); const formulaDependencies = useFormulaDependencies(); // Make sure all charts are connected to the same group whenever the widgets definition changes useLayoutEffect(() => { echarts.connect(DDM_CHART_GROUP); }, [widgets]); const handleChange = useCallback( (index: number, widget: Partial) => { updateWidget(index, widget); }, [updateWidget] ); const handleAddWidget = useCallback( (type: MetricQueryType) => { trackAnalytics('ddm.widget.add', { organization, type: type === MetricQueryType.QUERY ? 'query' : 'equation', }); addWidget(type); }, [addWidget, organization] ); const querySymbols = useMemo(() => { const querySymbolSet = new Set(); for (const widget of widgets) { const symbol = getQuerySymbol(widget.id); if (widget.type === MetricQueryType.QUERY) { querySymbolSet.add(symbol); } } return querySymbolSet; }, [widgets]); const visibleWidgets = widgets.filter(widget => !widget.isHidden); return ( {widgets.map((widget, index) => ( setSelectedWidgetIndex(index)} > {widget.type === MetricQueryType.QUERY ? ( 1} /> ) : ( 1} formulaDependencies={formulaDependencies} /> )} ))} {t('One chart per query')} 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: MetricQueryWidgetParams; } function Query({ widget, projects, onChange, onToggleVisibility, index, isSelected, showQuerySymbols, canBeHidden, }: QueryProps) { const metricsQuery = useMemo( () => ({ mri: widget.mri, op: widget.op, groupBy: widget.groupBy, query: widget.query, }), [widget.groupBy, widget.mri, widget.op, widget.query] ); const handleToggle = useCallback(() => { onToggleVisibility(index); }, [index, onToggleVisibility]); const handleChange = useCallback( (data: Partial) => { const changes: Partial = {...data}; if (changes.mri || changes.groupBy) { changes.focusedSeries = undefined; } onChange(index, changes); }, [index, onChange] ); const isToggleDisabled = !canBeHidden && !widget.isHidden; return ( {showQuerySymbols && ( )} ); } interface FormulaProps { availableVariables: Set; canBeHidden: boolean; formulaDependencies: ReturnType; index: number; isSelected: boolean; onChange: (index: number, data: Partial) => void; onToggleVisibility: (index: number) => void; showQuerySymbols: boolean; widget: MetricFormulaWidgetParams; } function Formula({ availableVariables, index, widget, onChange, onToggleVisibility, canBeHidden, isSelected, showQuerySymbols, formulaDependencies, }: FormulaProps) { 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})} /> ); } interface QueryToggleProps { disabled: boolean; isHidden: boolean; isSelected: boolean; onChange: (isHidden: boolean) => void; queryId: number; type: MetricQueryType; } function QueryToggle({ isHidden, queryId, disabled, onChange, isSelected, type, }: QueryToggleProps) { let tooltipTitle = isHidden ? t('Show query') : t('Hide query'); if (disabled) { tooltipTitle = t('At least one query must be visible'); } return ( {type === MetricQueryType.QUERY ? ( onChange(!isHidden)} role="button" aria-label={isHidden ? t('Show query') : t('Hide query')} /> ) : ( onChange(!isHidden)} role="button" aria-label={isHidden ? t('Show query') : t('Hide query')} /> )} ); } const QueryWrapper = styled('div')<{hasSymbol: boolean}>` display: grid; gap: ${space(1)}; padding-bottom: ${space(1)}; grid-template-columns: 1fr max-content; ${p => p.hasSymbol && `grid-template-columns: min-content 1fr max-content;`} `; const StyledQuerySymbol = styled(QuerySymbol)<{isClickable: boolean}>` margin-top: 10px; cursor: not-allowed; ${p => p.isClickable && `cursor: pointer;`} `; const StyledEquationSymbol = styled(EquationSymbol)<{isClickable: boolean}>` margin-top: 10px; cursor: not-allowed; ${p => p.isClickable && `cursor: pointer;`} `; const Wrapper = styled('div')<{showQuerySymbols: boolean}>``; const Row = styled('div')` display: contents; `; const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>` align-items: center; display: flex; padding-bottom: ${space(2)}; padding-top: ${space(1)}; gap: ${space(2)}; ${p => p.addQuerySymbolSpacing && ` padding-left: ${space(1)}; margin-left: ${space(2)}; `} `; const SwitchWrapper = styled('label')` display: flex; margin: 0; align-items: center; gap: ${space(1)}; `;