Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
47 changes: 38 additions & 9 deletions static/app/components/modals/addDashboardWidgetModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import {browserHistory} from 'react-router';
import {components, OptionProps} from 'react-select';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import cloneDeep from 'lodash/cloneDeep';
Expand All @@ -15,7 +16,7 @@ import ButtonBar from 'app/components/buttonBar';
import WidgetQueriesForm from 'app/components/dashboards/widgetQueriesForm';
import SelectControl from 'app/components/forms/selectControl';
import {PanelAlert} from 'app/components/panels';
import {t} from 'app/locale';
import {t, tct} from 'app/locale';
import space from 'app/styles/space';
import {
DateString,
Expand All @@ -35,6 +36,7 @@ import {
DashboardDetails,
DashboardListItem,
DisplayType,
MAX_WIDGETS,
Widget,
WidgetQuery,
} from 'app/views/dashboardsV2/types';
Expand All @@ -47,6 +49,8 @@ import {generateFieldOptions} from 'app/views/eventsV2/utils';
import Input from 'app/views/settings/components/forms/controls/input';
import Field from 'app/views/settings/components/forms/field';

import Tooltip from '../tooltip';

export type DashboardWidgetModalOptions = {
organization: Organization;
dashboard?: DashboardDetails;
Expand Down Expand Up @@ -81,7 +85,7 @@ type State = {
queries: Widget['queries'];
loading: boolean;
errors?: Record<string, any>;
dashboards: SelectValue<string>[];
dashboards: DashboardListItem[];
selectedDashboard?: SelectValue<string>;
};

Expand Down Expand Up @@ -179,8 +183,8 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
if (
!selectedDashboard ||
!(
dashboards.find(({label, value}) => {
return label === selectedDashboard?.label && value === selectedDashboard?.value;
dashboards.find(({title, id}) => {
return title === selectedDashboard?.label && id === selectedDashboard?.value;
}) || selectedDashboard.value === 'new'
)
) {
Expand Down Expand Up @@ -288,10 +292,7 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
);

try {
const response = await promise;
const dashboards = response.map(({id, title}) => {
return {label: title, value: id};
});
const dashboards = await promise;
this.setState({
dashboards,
});
Expand All @@ -311,6 +312,13 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
}
renderDashboardSelector() {
const {errors, loading, dashboards} = this.state;
const dashboardOptions = dashboards.map(d => {
return {
label: d.title,
value: d.id,
isDisabled: d.widgetDisplay.length >= MAX_WIDGETS,
};
});
return (
<React.Fragment>
<p>
Expand All @@ -329,9 +337,30 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
>
<SelectControl
name="dashboard"
options={[{label: t('+ Create New Dashboard'), value: 'new'}, ...dashboards]}
options={[
{label: t('+ Create New Dashboard'), value: 'new'},
...dashboardOptions,
]}
onChange={(option: SelectValue<string>) => this.handleDashboardChange(option)}
disabled={loading}
components={{
Option: ({label, data, ...optionProps}: OptionProps<any>) => (
<Tooltip
disabled={!!!data.isDisabled}
title={tct('Max widgets ([maxWidgets]) per dashboard reached.', {
maxWidgets: MAX_WIDGETS,
})}
containerDisplayMode="block"
position="right"
>
<components.Option
label={label}
data={data}
{...(optionProps as any)}
/>
</Tooltip>
),
}}
/>
</Field>
</React.Fragment>
Expand Down
4 changes: 2 additions & 2 deletions static/app/views/dashboardsV2/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import withGlobalSelection from 'app/utils/withGlobalSelection';
import {DataSet} from './widget/utils';
import AddWidget, {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
import SortableWidget from './sortableWidget';
import {DashboardDetails, Widget} from './types';
import {DashboardDetails, MAX_WIDGETS, Widget} from './types';

type Props = {
api: Client;
Expand Down Expand Up @@ -225,7 +225,7 @@ class Dashboard extends Component<Props> {
<WidgetContainer>
<SortableContext items={items} strategy={rectSortingStrategy}>
{widgets.map((widget, index) => this.renderWidget(widget, index))}
{isEditing && (
{isEditing && widgets.length < MAX_WIDGETS && (
<AddWidget
orgFeatures={organization.features}
onAddWidget={this.handleStartAdd}
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/dashboardsV2/types.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {User} from 'app/types';

export const MAX_WIDGETS = 30;

export enum DisplayType {
AREA = 'area',
BAR = 'bar',
Expand Down
29 changes: 27 additions & 2 deletions tests/js/spec/components/modals/addDashboardWidgetModal.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {browserHistory} from 'react-router';

import {mountWithTheme} from 'sentry-test/enzyme';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {getOptionByLabel, selectByLabel} from 'sentry-test/select-new';
import {getOptionByLabel, openMenu, selectByLabel} from 'sentry-test/select-new';

import AddDashboardWidgetModal from 'app/components/modals/addDashboardWidgetModal';
import {t} from 'app/locale';
import TagStore from 'app/stores/tagStore';
import * as types from 'app/views/dashboardsV2/types';

const stubEl = props => <div>{props.children}</div>;

Expand Down Expand Up @@ -97,7 +98,13 @@ describe('Modals -> AddDashboardWidgetModal', function () {
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
body: [{id: '1', title: t('Test Dashboard')}],
body: [
TestStubs.Dashboard([], {
id: '1',
title: 'Test Dashboard',
widgetDisplay: ['area'],
}),
],
});
});

Expand All @@ -109,6 +116,7 @@ describe('Modals -> AddDashboardWidgetModal', function () {
const wrapper = mountModal({initialData, fromDiscover: true});
// @ts-expect-error
await tick();
await wrapper.update();
selectDashboard(wrapper, {label: t('+ Create New Dashboard'), value: 'new'});
await clickSubmit(wrapper);
expect(browserHistory.push).toHaveBeenCalledWith(
Expand All @@ -123,6 +131,7 @@ describe('Modals -> AddDashboardWidgetModal', function () {
const wrapper = mountModal({initialData, fromDiscover: true});
// @ts-expect-error
await tick();
await wrapper.update();
selectDashboard(wrapper, {label: t('Test Dashboard'), value: '1'});
await clickSubmit(wrapper);
expect(browserHistory.push).toHaveBeenCalledWith(
Expand All @@ -133,6 +142,22 @@ describe('Modals -> AddDashboardWidgetModal', function () {
wrapper.unmount();
});

it('disables dashboards with max widgets', async function () {
types.MAX_WIDGETS = 1;
const wrapper = mountModal({initialData, fromDiscover: true});
// @ts-expect-error
await tick();
await wrapper.update();
openMenu(wrapper, {name: 'dashboard', control: true});

const input = wrapper.find('SelectControl[name="dashboard"]');
expect(input.find('Option Option')).toHaveLength(2);
expect(input.find('Option Option').at(0).props().isDisabled).toBe(false);
expect(input.find('Option Option').at(1).props().isDisabled).toBe(true);

wrapper.unmount();
});

it('can update the title', async function () {
let widget = undefined;
const wrapper = mountModal({
Expand Down
57 changes: 55 additions & 2 deletions tests/js/spec/views/dashboardsV2/detail.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {mountGlobalModal} from 'sentry-test/modal';

import ProjectsStore from 'app/stores/projectsStore';
import {DashboardState} from 'app/views/dashboardsV2/types';
import * as types from 'app/views/dashboardsV2/types';
import ViewEditDashboard from 'app/views/dashboardsV2/view';

describe('Dashboards > Detail', function () {
Expand Down Expand Up @@ -225,8 +226,16 @@ describe('Dashboards > Detail', function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
body: [
TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
TestStubs.Dashboard([], {
id: 'default-overview',
title: 'Default',
widgetDisplay: ['area'],
}),
TestStubs.Dashboard([], {
id: '1',
title: 'Custom Errors',
widgetDisplay: ['area'],
}),
],
});
MockApiClient.addMockResponse({
Expand Down Expand Up @@ -337,6 +346,50 @@ describe('Dashboards > Detail', function () {
expect(modal.find('AddDashboardWidgetModal').props().widget).toEqual(widgets[0]);
});

it('shows add wiget option', async function () {
wrapper = mountWithTheme(
<ViewEditDashboard
organization={initialData.organization}
params={{orgId: 'org-slug', dashboardId: '1'}}
router={initialData.router}
location={initialData.router.location}
/>,
initialData.routerContext
);
await tick();
wrapper.update();

// Enter edit mode.
wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
wrapper.update();
expect(wrapper.find('AddWidget').exists()).toBe(true);

wrapper.unmount();
});

it('hides add widget option', async function () {
types.MAX_WIDGETS = 1;

wrapper = mountWithTheme(
<ViewEditDashboard
organization={initialData.organization}
params={{orgId: 'org-slug', dashboardId: '1'}}
router={initialData.router}
location={initialData.router.location}
/>,
initialData.routerContext
);
await tick();
wrapper.update();

// Enter edit mode.
wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
wrapper.update();
expect(wrapper.find('AddWidget').exists()).toBe(false);

wrapper.unmount();
});

it('hides and shows breadcrumbs based on feature', async function () {
const newOrg = initializeOrg({
organization: TestStubs.Organization({
Expand Down