123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- import {Fragment, useCallback, useMemo, useState} from 'react';
- import {css} from '@emotion/react';
- import type {ModalRenderProps} from 'sentry/actionCreators/modal';
- import {Button, LinkButton} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {
- MetricWidgetTitle,
- type MetricWidgetTitleState,
- } from 'sentry/components/modals/metricWidgetViewerModal/header';
- import {Queries} from 'sentry/components/modals/metricWidgetViewerModal/queries';
- import {MetricVisualization} from 'sentry/components/modals/metricWidgetViewerModal/visualization';
- import type {WidgetViewerModalOptions} from 'sentry/components/modals/widgetViewerModal';
- import {t} from 'sentry/locale';
- import type {Organization} from 'sentry/types/organization';
- import {getMetricsUrl} from 'sentry/utils/metrics';
- import {toDisplayType} from 'sentry/utils/metrics/dashboard';
- import {hasCustomMetricsExtractionRules} from 'sentry/utils/metrics/features';
- import {parseMRI} from 'sentry/utils/metrics/mri';
- import {MetricExpressionType} from 'sentry/utils/metrics/types';
- import {
- useVirtualMetricsContext,
- VirtualMetricsContextProvider,
- } from 'sentry/utils/metrics/virtualMetricsContext';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import type {
- DashboardMetricsEquation,
- DashboardMetricsQuery,
- Order,
- } from 'sentry/views/dashboards/metrics/types';
- import {
- expressionsToWidget,
- getMetricEquations,
- getMetricQueries,
- getMetricWidgetTitle,
- useGenerateExpressionId,
- } from 'sentry/views/dashboards/metrics/utils';
- import {DisplayType} from 'sentry/views/dashboards/types';
- import {MetricDetails} from 'sentry/views/metrics/widgetDetails';
- import {OrganizationContext} from 'sentry/views/organizationContext';
- interface Props extends ModalRenderProps, WidgetViewerModalOptions {
- organization: Organization;
- }
- function MetricWidgetViewerModal({
- organization,
- widget,
- Footer,
- Body,
- Header,
- closeModal,
- CloseButton,
- onMetricWidgetEdit,
- dashboardFilters,
- }: Props) {
- const {selection} = usePageFilters();
- const {resolveVirtualMRI, getVirtualMRIQuery, isLoading} = useVirtualMetricsContext();
- const [userHasModified, setUserHasModified] = useState(false);
- const [displayType, setDisplayType] = useState(widget.displayType);
- const [metricQueries, setMetricQueries] = useState<DashboardMetricsQuery[]>(() =>
- getMetricQueries(widget, dashboardFilters, getVirtualMRIQuery)
- );
- const [metricEquations, setMetricEquations] = useState<DashboardMetricsEquation[]>(() =>
- getMetricEquations(widget)
- );
- const filteredEquations = useMemo(
- () => metricEquations.filter(equation => equation.formula !== ''),
- [metricEquations]
- );
- const expressions = useMemo(
- () => [...metricQueries, ...filteredEquations],
- [metricQueries, filteredEquations]
- );
- const generateQueryId = useGenerateExpressionId(metricQueries);
- const generateEquationId = useGenerateExpressionId(metricEquations);
- const widgetMQL = useMemo(() => getMetricWidgetTitle(expressions), [expressions]);
- const [title, setTitle] = useState<MetricWidgetTitleState>({
- stored: widget.title,
- edited: widget.title,
- isEditing: false,
- });
- const handleTitleChange = useCallback(
- (patch: Partial<MetricWidgetTitleState>) => {
- setTitle(curr => ({...curr, ...patch}));
- setUserHasModified(true);
- },
- [setTitle]
- );
- const handleQueryChange = useCallback(
- (data: Partial<DashboardMetricsQuery>, index: number) => {
- setMetricQueries(curr => {
- const updated = [...curr];
- updated[index] = {...updated[index], ...data} as DashboardMetricsQuery;
- return updated;
- });
- setUserHasModified(true);
- },
- [setMetricQueries]
- );
- const handleEquationChange = useCallback(
- (data: Partial<DashboardMetricsEquation>, index: number) => {
- setMetricEquations(curr => {
- const updated = [...curr];
- updated[index] = {...updated[index], ...data} as DashboardMetricsEquation;
- return updated;
- });
- setUserHasModified(true);
- },
- [setMetricEquations]
- );
- const handleOrderChange = useCallback(
- ({id, order}: {id: number; order: Order}) => {
- setUserHasModified(true);
- const queryIdx = metricQueries.findIndex(query => query.id === id);
- if (queryIdx > -1) {
- setMetricQueries(curr => {
- return curr.map((query, i) => {
- const orderBy = i === queryIdx ? order : undefined;
- return {...query, orderBy};
- });
- });
- return;
- }
- const equationIdx = filteredEquations.findIndex(equation => equation.id === id);
- if (equationIdx > -1) {
- setMetricEquations(curr => {
- return curr.map((equation, i) => {
- const orderBy = i === equationIdx ? order : undefined;
- return {...equation, orderBy};
- });
- });
- }
- },
- [filteredEquations, metricQueries]
- );
- const addQuery = useCallback(
- (queryIndex?: number) => {
- setMetricQueries(curr => {
- const query = metricQueries[queryIndex ?? metricQueries.length - 1];
- return [
- ...(displayType === DisplayType.BIG_NUMBER
- ? curr.map(q => ({...q, isHidden: true}))
- : curr),
- {
- ...query,
- id: generateQueryId(),
- },
- ];
- });
- setUserHasModified(true);
- },
- [displayType, generateQueryId, metricQueries]
- );
- const addEquation = useCallback(() => {
- setMetricEquations(curr => {
- return [
- ...curr,
- {
- formula: '',
- name: '',
- id: generateEquationId(),
- type: MetricExpressionType.EQUATION,
- isHidden: false,
- },
- ];
- });
- // Hide all queries when adding an equation to a big number widget
- if (displayType === DisplayType.BIG_NUMBER) {
- setMetricQueries(curr => curr.map(q => ({...q, isHidden: true})));
- }
- setUserHasModified(true);
- }, [displayType, generateEquationId]);
- const removeEquation = useCallback(
- (index: number) => {
- setMetricEquations(curr => {
- const updated = [...curr];
- updated.splice(index, 1);
- return updated;
- });
- // Show the last query when removing an equation from a big number widget
- if (displayType === DisplayType.BIG_NUMBER) {
- setMetricQueries(curr =>
- curr.map((q, idx) => (idx === curr.length - 1 ? {...q, isHidden: false} : q))
- );
- }
- setUserHasModified(true);
- },
- [displayType]
- );
- const removeQuery = useCallback(
- (index: number) => {
- setMetricQueries(curr => {
- const updated = [...curr];
- updated.splice(index, 1);
- // Make sure the last query is visible for big number widgets
- if (displayType === DisplayType.BIG_NUMBER && filteredEquations.length === 0) {
- updated[updated.length - 1].isHidden = false;
- }
- return updated;
- });
- setUserHasModified(true);
- },
- [displayType, filteredEquations.length]
- );
- const handleSubmit = useCallback(() => {
- const resolvedQueries = metricQueries.map(query => {
- const {type} = parseMRI(query.mri);
- if (type !== 'v' || !query.condition) {
- return query;
- }
- const {mri, aggregation} = resolveVirtualMRI(
- query.mri,
- query.condition,
- query.aggregation
- );
- return {
- ...query,
- mri,
- aggregation,
- };
- });
- const convertedWidget = expressionsToWidget(
- [...resolvedQueries, ...filteredEquations],
- title.edited,
- toDisplayType(displayType),
- widget.interval
- );
- onMetricWidgetEdit?.({...widget, ...convertedWidget});
- closeModal();
- }, [
- metricQueries,
- filteredEquations,
- title.edited,
- displayType,
- widget,
- onMetricWidgetEdit,
- closeModal,
- resolveVirtualMRI,
- ]);
- const handleDisplayTypeChange = useCallback((type: DisplayType) => {
- setDisplayType(type);
- setUserHasModified(true);
- }, []);
- const handleClose = useCallback(() => {
- if (
- userHasModified &&
- // eslint-disable-next-line no-alert
- !window.confirm(t('You have unsaved changes, are you sure you want to close?'))
- ) {
- return;
- }
- closeModal();
- }, [userHasModified, closeModal]);
- const {mri, aggregation, query, condition} = metricQueries[0];
- if (isLoading) {
- return <LoadingIndicator />;
- }
- return (
- <Fragment>
- <OrganizationContext.Provider value={organization}>
- <Header>
- <MetricWidgetTitle
- title={title}
- onTitleChange={handleTitleChange}
- placeholder={widgetMQL}
- description={widget.description}
- />
- <CloseButton onClick={handleClose} />
- </Header>
- <Body>
- <Queries
- displayType={displayType}
- metricQueries={metricQueries}
- metricEquations={metricEquations}
- onQueryChange={handleQueryChange}
- onEquationChange={handleEquationChange}
- addEquation={addEquation}
- addQuery={addQuery}
- removeEquation={removeEquation}
- removeQuery={removeQuery}
- />
- <MetricVisualization
- expressions={expressions}
- displayType={displayType}
- onDisplayTypeChange={handleDisplayTypeChange}
- onOrderChange={handleOrderChange}
- interval={widget.interval}
- />
- <MetricDetails
- mri={mri}
- aggregation={aggregation}
- condition={condition}
- query={query}
- />
- </Body>
- <Footer>
- <ButtonBar gap={1}>
- <LinkButton
- to={getMetricsUrl(organization.slug, {
- widgets: [...metricQueries, ...metricEquations],
- ...selection.datetime,
- project: selection.projects,
- environment: selection.environments,
- })}
- >
- {t('Open in Metrics')}
- </LinkButton>
- <Button priority="primary" onClick={handleSubmit}>
- {t('Save changes')}
- </Button>
- </ButtonBar>
- </Footer>
- </OrganizationContext.Provider>
- </Fragment>
- );
- }
- function WrappedMetricWidgetViewerModal(props: Props) {
- return hasCustomMetricsExtractionRules(props.organization) ? (
- <VirtualMetricsContextProvider>
- <MetricWidgetViewerModal {...props} />
- </VirtualMetricsContextProvider>
- ) : (
- <MetricWidgetViewerModal {...props} />
- );
- }
- export default WrappedMetricWidgetViewerModal;
- export const modalCss = css`
- width: 100%;
- max-width: 1200px;
- `;
|