Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions static/app/actionCreators/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {PrivateGamingSdkAccessModalProps} from 'sentry/components/modals/pr
import type {ReprocessEventModalOptions} from 'sentry/components/modals/reprocessEventModal';
import type {TokenRegenerationConfirmationModalProps} from 'sentry/components/modals/tokenRegenerationConfirmationModal';
import type {AddToDashboardModalProps} from 'sentry/components/modals/widgetBuilder/addToDashboardModal';
import type {LinkToDashboardModalProps} from 'sentry/components/modals/widgetBuilder/linkToDashboardModal';
import type {OverwriteWidgetModalProps} from 'sentry/components/modals/widgetBuilder/overwriteWidgetModal';
import type {WidgetViewerModalOptions} from 'sentry/components/modals/widgetViewerModal';
import type {ConsoleModalProps} from 'sentry/components/onboarding/consoleModal';
Expand Down Expand Up @@ -278,6 +279,17 @@ export async function openAddToDashboardModal(options: AddToDashboardModalProps)
});
}

export async function openLinkToDashboardModal(options: LinkToDashboardModalProps) {
const {LinkToDashboardModal, modalCss} = await import(
'sentry/components/modals/widgetBuilder/linkToDashboardModal'
);

openModal(deps => <LinkToDashboardModal {...deps} {...options} />, {
closeEvents: 'escape-key',
modalCss,
});
}

export async function openImportDashboardFromFileModal(
options: ImportDashboardFromFileModalProps
) {
Expand Down
203 changes: 203 additions & 0 deletions static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import {Fragment, useCallback, useEffect, useState, type ReactNode} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {fetchDashboard, fetchDashboards} from 'sentry/actionCreators/dashboards';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
import {Button} from 'sentry/components/core/button';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {Select} from 'sentry/components/core/select';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {SelectValue} from 'sentry/types/core';
import useApi from 'sentry/utils/useApi';
import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {
DashboardCreateLimitWrapper,
type DashboardCreateLimitWrapperResult,

Check failure on line 19 in static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx

View workflow job for this annotation

GitHub Actions / typescript

'DashboardCreateLimitWrapperResult' is declared but its value is never read.
} from 'sentry/views/dashboards/createLimitWrapper';
import type {DashboardDetails, DashboardListItem} from 'sentry/views/dashboards/types';
import {MAX_WIDGETS} from 'sentry/views/dashboards/types';
import {getSavedPageFilters} from 'sentry/views/dashboards/utils';
import {NEW_DASHBOARD_ID} from 'sentry/views/dashboards/widgetBuilder/utils';

export type LinkToDashboardModalProps = {
source?: string; // TODO: perhpas make this an enum
};

type Props = ModalRenderProps & LinkToDashboardModalProps;

const SELECT_DASHBOARD_MESSAGE = t('Select a dashboard');

export function LinkToDashboardModal({Header, Body, Footer, closeModal}: Props) {
const api = useApi();
const navigate = useNavigate();
const organization = useOrganization();
const [dashboards, setDashboards] = useState<DashboardListItem[] | null>(null);
const [selectedDashboard, setSelectedDashboard] = useState<DashboardDetails | null>(
null
);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(null);

const {dashboardId: currentDashboardId} = useParams<{dashboardId: string}>();

useEffect(() => {
// Track mounted state so we dont call setState on unmounted components
let unmounted = false;

fetchDashboards(api, organization.slug).then(response => {
// If component has unmounted, dont set state
if (unmounted) {
return;
}

setDashboards(response);
});

return () => {
unmounted = true;
};
}, [api, organization.slug]);

useEffect(() => {
// Track mounted state so we dont call setState on unmounted components
let unmounted = false;

if (selectedDashboardId === NEW_DASHBOARD_ID || selectedDashboardId === null) {
setSelectedDashboard(null);
} else {
fetchDashboard(api, organization.slug, selectedDashboardId).then(response => {
// If component has unmounted, dont set state
if (unmounted) {
return;
}

setSelectedDashboard(response);
});
}

return () => {
unmounted = true;
};
}, [api, organization.slug, selectedDashboardId]);

const canSubmit = selectedDashboardId !== null;

const getOptions = useCallback(
(
hasReachedDashboardLimit: boolean,
isLoading: boolean,
limitMessage: ReactNode | null
) => {
if (dashboards === null) {
return null;
}

return [
{
label: t('+ Create New Dashboard'),
value: 'new',
disabled: hasReachedDashboardLimit || isLoading,
tooltip: hasReachedDashboardLimit ? limitMessage : undefined,
tooltipOptions: {position: 'right', isHoverable: true},
},
Comment on lines +91 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't want the user to be able to link to a creation flow, do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to do that tbh, depends on what we decide UX wise, but I could definitely see a scenario where you create a summary dashboard off a widget.

...dashboards
.filter(dashboard =>
// if adding from a dashboard, currentDashboardId will be set and we'll remove it from the list of options
currentDashboardId ? dashboard.id !== currentDashboardId : true
)
.map(({title, id, widgetDisplay}) => ({
label: title,
value: id,
disabled: widgetDisplay.length >= MAX_WIDGETS,
tooltip:
widgetDisplay.length >= MAX_WIDGETS &&
tct('Max widgets ([maxWidgets]) per dashboard reached.', {
maxWidgets: MAX_WIDGETS,
}),
tooltipOptions: {position: 'right'},
})),
].filter(Boolean) as Array<SelectValue<string>>;
},
[currentDashboardId, dashboards]
);

function goToDashboard() {
const dashboardsPath =
selectedDashboardId === NEW_DASHBOARD_ID
? `/organizations/${organization.slug}/dashboards/new/`
: `/organizations/${organization.slug}/dashboard/${selectedDashboardId}/`;

const pathname = dashboardsPath;

navigate({
pathname,
query: {
...(selectedDashboard ? getSavedPageFilters(selectedDashboard) : {}),
},
});
closeModal();
}

return (
<Fragment>
<Header closeButton>{t('Link to Dashboard')}</Header>
<Body>
<Wrapper>
<DashboardCreateLimitWrapper>
{({hasReachedDashboardLimit, isLoading, limitMessage}) => (
<Select
disabled={dashboards === null}
name="dashboard"
placeholder={t('Select Dashboard')}
value={selectedDashboardId}
options={getOptions(hasReachedDashboardLimit, isLoading, limitMessage)}
onChange={(option: SelectValue<string>) => {
if (option.disabled) {
return;
}
setSelectedDashboardId(option.value);
}}
/>
)}
</DashboardCreateLimitWrapper>
</Wrapper>
</Body>
<Footer>
<StyledButtonBar gap="lg">
<Button
disabled={!canSubmit}
title={canSubmit ? undefined : SELECT_DASHBOARD_MESSAGE}
onClick={() => goToDashboard()}
aria-label={t('Link to dashboard')}
>
{t('Link to dashboard')}
</Button>
</StyledButtonBar>
</Footer>
</Fragment>
);
}

const Wrapper = styled('div')`
margin-bottom: ${space(2)};
`;

const StyledButtonBar = styled(ButtonBar)`
@media (max-width: ${props => props.theme.breakpoints.sm}) {
grid-template-rows: repeat(2, 1fr);
gap: ${space(1.5)};
width: 100%;

> button {
width: 100%;
}
}
`;

export const modalCss = css`
max-width: 700px;
margin: 70px auto;
`;
11 changes: 10 additions & 1 deletion static/app/views/dashboards/createLimitWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type {ReactNode} from 'react';

import HookOrDefault from 'sentry/components/hookOrDefault';

export type DashboardCreateLimitWrapperResult = {
dashboardsLimit: number;
hasReachedDashboardLimit: boolean;
isLoading: boolean;
limitMessage: ReactNode | null;
};

export const DashboardCreateLimitWrapper = HookOrDefault({
hookName: 'component:dashboards-limit-provider',
defaultComponent: ({children}) =>
Expand All @@ -9,6 +18,6 @@ export const DashboardCreateLimitWrapper = HookOrDefault({
dashboardsLimit: 0,
isLoading: false,
limitMessage: null,
})
} satisfies DashboardCreateLimitWrapperResult)
: children,
});
6 changes: 6 additions & 0 deletions static/app/views/dashboards/hooks/useHasDrillDownFlows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import useOrganization from 'sentry/utils/useOrganization';

export function useHasDrillDownFlows() {
const organization = useOrganization();
return organization.features.includes('dashboards-drilldown-flow');
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {css} from '@emotion/react';
import styled from '@emotion/styled';
import cloneDeep from 'lodash/cloneDeep';

import {openLinkToDashboardModal} from 'sentry/actionCreators/modal';
import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import {Button} from 'sentry/components/core/button';
import {CompactSelect} from 'sentry/components/core/compactSelect';
Expand All @@ -13,7 +14,7 @@ import {Input} from 'sentry/components/core/input';
import {Radio} from 'sentry/components/core/radio';
import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
import FieldGroup from 'sentry/components/forms/fieldGroup';
import {IconDelete} from 'sentry/icons';
import {IconDelete, IconLink} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {SelectValue} from 'sentry/types/core';
Expand All @@ -34,6 +35,7 @@ import useCustomMeasurements from 'sentry/utils/useCustomMeasurements';
import useOrganization from 'sentry/utils/useOrganization';
import useTags from 'sentry/utils/useTags';
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
import {useHasDrillDownFlows} from 'sentry/views/dashboards/hooks/useHasDrillDownFlows';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import SortableVisualizeFieldWrapper from 'sentry/views/dashboards/widgetBuilder/components/common/sortableFieldWrapper';
Expand Down Expand Up @@ -272,6 +274,7 @@ function Visualize({error, setError}: VisualizeProps) {
state.displayType !== DisplayType.TABLE &&
state.displayType !== DisplayType.BIG_NUMBER;
const isBigNumberWidget = state.displayType === DisplayType.BIG_NUMBER;
const isTableWidget = state.displayType === DisplayType.TABLE;
const {tags: numericSpanTags} = useTraceItemTags('number');
const {tags: stringSpanTags} = useTraceItemTags('string');

Expand Down Expand Up @@ -372,6 +375,7 @@ function Visualize({error, setError}: VisualizeProps) {
const hasExploreEquations = organization.features.includes(
'visibility-explore-equations'
);
const hasDrillDownFlows = useHasDrillDownFlows();

return (
<Fragment>
Expand Down Expand Up @@ -776,7 +780,20 @@ function Visualize({error, setError}: VisualizeProps) {
}}
/>
)}
<StyledDeleteButton
{hasDrillDownFlows && isTableWidget && (
<Button
borderless
icon={<IconLink />}
aria-label={t('Link field')}
size="zero"
onClick={() => {
openLinkToDashboardModal({
source,
});
}}
/>
)}
<Button
borderless
icon={<IconDelete />}
size="zero"
Expand Down Expand Up @@ -1034,8 +1051,6 @@ export const FieldRow = styled('div')`
min-width: 0;
`;

export const StyledDeleteButton = styled(Button)``;

export const FieldExtras = styled('div')<{isChartWidget: boolean}>`
display: flex;
flex-direction: row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
ParameterRefinements,
PrimarySelectRow,
StyledArithmeticInput,
StyledDeleteButton,
} from 'sentry/views/dashboards/widgetBuilder/components/visualize/index';
import {ColumnCompactSelect} from 'sentry/views/dashboards/widgetBuilder/components/visualize/selectRow';
import {FieldValueKind, type FieldValue} from 'sentry/views/discover/table/types';
Expand Down Expand Up @@ -204,7 +203,7 @@ function VisualizeGhostField({
onChange={() => {}}
/>
)}
<StyledDeleteButton
<Button
borderless
icon={<IconDelete />}
size="zero"
Expand Down
Loading