123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935 |
- import {Component, Fragment} from 'react';
- import {browserHistory} from 'react-router';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import cloneDeep from 'lodash/cloneDeep';
- import pick from 'lodash/pick';
- import set from 'lodash/set';
- import {validateWidget} from 'sentry/actionCreators/dashboards';
- import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {ModalRenderProps} from 'sentry/actionCreators/modal';
- import {Client} from 'sentry/api';
- import Button from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import IssueWidgetQueriesForm from 'sentry/components/dashboards/issueWidgetQueriesForm';
- import WidgetQueriesForm from 'sentry/components/dashboards/widgetQueriesForm';
- import FeatureBadge from 'sentry/components/featureBadge';
- import Input from 'sentry/components/forms/controls/input';
- import RadioGroup from 'sentry/components/forms/controls/radioGroup';
- import Field from 'sentry/components/forms/field';
- import FieldLabel from 'sentry/components/forms/field/fieldLabel';
- import SelectControl from 'sentry/components/forms/selectControl';
- import {PanelAlert} from 'sentry/components/panels';
- import {t, tct} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {
- DateString,
- Organization,
- PageFilters,
- SelectValue,
- SessionField,
- TagCollection,
- } from 'sentry/types';
- import {defined} from 'sentry/utils';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import {getColumnsAndAggregates} from 'sentry/utils/discover/fields';
- import Measurements from 'sentry/utils/measurements/measurements';
- import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/performance/spanOperationBreakdowns/constants';
- import withApi from 'sentry/utils/withApi';
- import withPageFilters from 'sentry/utils/withPageFilters';
- import withTags from 'sentry/utils/withTags';
- import {DISPLAY_TYPE_CHOICES} from 'sentry/views/dashboardsV2/data';
- import {assignTempId} from 'sentry/views/dashboardsV2/layoutUtils';
- import {
- DashboardDetails,
- DashboardListItem,
- DashboardWidgetSource,
- DisplayType,
- MAX_WIDGETS,
- Widget,
- WidgetQuery,
- WidgetType,
- } from 'sentry/views/dashboardsV2/types';
- import {generateIssueWidgetFieldOptions} from 'sentry/views/dashboardsV2/widgetBuilder/issueWidget/utils';
- import {
- generateReleaseWidgetFieldOptions,
- SESSIONS_FIELDS,
- SESSIONS_TAGS,
- } from 'sentry/views/dashboardsV2/widgetBuilder/releaseWidget/fields';
- import {
- mapErrors,
- NEW_DASHBOARD_ID,
- normalizeQueries,
- } from 'sentry/views/dashboardsV2/widgetBuilder/utils';
- import WidgetCard from 'sentry/views/dashboardsV2/widgetCard';
- import {WidgetTemplate} from 'sentry/views/dashboardsV2/widgetLibrary/data';
- import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
- import {TAB, TabsButtonBar} from './dashboardWidgetLibraryModal/tabsButtonBar';
- export type DashboardWidgetModalOptions = {
- organization: Organization;
- source: DashboardWidgetSource;
- dashboard?: DashboardDetails;
- defaultTableColumns?: readonly string[];
- defaultTitle?: string;
- defaultWidgetQuery?: WidgetQuery;
- displayType?: DisplayType;
- end?: DateString;
- onAddLibraryWidget?: (widgets: Widget[]) => void;
- onAddWidget?: (data: Widget) => void;
- onUpdateWidget?: (nextWidget: Widget) => void;
- selectedWidgets?: WidgetTemplate[];
- selection?: PageFilters;
- start?: DateString;
- statsPeriod?: string | null;
- widget?: Widget;
- };
- type Props = ModalRenderProps &
- DashboardWidgetModalOptions & {
- api: Client;
- organization: Organization;
- selection: PageFilters;
- tags: TagCollection;
- };
- type FlatValidationError = {
- [key: string]: string | FlatValidationError[] | FlatValidationError;
- };
- type State = {
- dashboards: DashboardListItem[];
- displayType: Widget['displayType'];
- interval: Widget['interval'];
- loading: boolean;
- queries: Widget['queries'];
- title: string;
- userHasModified: boolean;
- widgetType: WidgetType;
- errors?: Record<string, any>;
- selectedDashboard?: SelectValue<string>;
- };
- const newDiscoverQuery: WidgetQuery = {
- name: '',
- fields: ['count()'],
- columns: [],
- aggregates: ['count()'],
- conditions: '',
- orderby: '',
- };
- const newIssueQuery: WidgetQuery = {
- name: '',
- fields: ['issue', 'assignee', 'title'] as string[],
- columns: ['issue', 'assignee', 'title'],
- aggregates: [],
- conditions: '',
- orderby: '',
- };
- const newMetricsQuery: WidgetQuery = {
- name: '',
- fields: [`crash_free_rate(${SessionField.SESSION})`],
- columns: [],
- aggregates: [`crash_free_rate(${SessionField.SESSION})`],
- conditions: '',
- orderby: '',
- };
- const DiscoverDataset: [WidgetType, string] = [
- WidgetType.DISCOVER,
- t('All Events (Errors and Transactions)'),
- ];
- const IssueDataset: [WidgetType, string] = [
- WidgetType.ISSUE,
- t('Issues (States, Assignment, Time, etc.)'),
- ];
- const MetricsDataset: [WidgetType, React.ReactElement] = [
- WidgetType.RELEASE,
- <Fragment key="metrics-dataset">
- {t('Health (Releases, sessions)')} <FeatureBadge type="alpha" />
- </Fragment>,
- ];
- class AddDashboardWidgetModal extends Component<Props, State> {
- constructor(props: Props) {
- super(props);
- const {widget, defaultTitle, displayType, defaultWidgetQuery} = props;
- if (!widget) {
- this.state = {
- title: defaultTitle ?? '',
- displayType: displayType ?? DisplayType.TABLE,
- interval: '5m',
- queries: [defaultWidgetQuery ? {...defaultWidgetQuery} : {...newDiscoverQuery}],
- errors: undefined,
- loading: !!this.omitDashboardProp,
- dashboards: [],
- userHasModified: false,
- widgetType: WidgetType.DISCOVER,
- };
- return;
- }
- this.state = {
- title: widget.title,
- displayType: widget.displayType,
- interval: widget.interval,
- queries: normalizeQueries({
- displayType: widget.displayType,
- queries: widget.queries,
- }),
- errors: undefined,
- loading: false,
- dashboards: [],
- userHasModified: false,
- widgetType: widget.widgetType ?? WidgetType.DISCOVER,
- };
- }
- componentDidMount() {
- if (this.omitDashboardProp) {
- this.fetchDashboards();
- }
- }
- get omitDashboardProp() {
- // when opening from discover or issues page, the user selects the dashboard in the widget UI
- return [
- DashboardWidgetSource.DISCOVERV2,
- DashboardWidgetSource.ISSUE_DETAILS,
- ].includes(this.props.source);
- }
- get fromLibrary() {
- return this.props.source === DashboardWidgetSource.LIBRARY;
- }
- handleSubmit = async (event: React.FormEvent) => {
- event.preventDefault();
- const {
- api,
- closeModal,
- organization,
- onAddWidget,
- onUpdateWidget,
- widget: previousWidget,
- source,
- } = this.props;
- this.setState({loading: true});
- let errors: FlatValidationError = {};
- const widgetData: Widget = assignTempId(
- pick(this.state, ['title', 'displayType', 'interval', 'queries', 'widgetType'])
- );
- if (previousWidget) {
- widgetData.layout = previousWidget?.layout;
- }
- // Only Table and Top N views need orderby
- if (![DisplayType.TABLE, DisplayType.TOP_N].includes(widgetData.displayType)) {
- widgetData.queries.forEach(query => {
- query.orderby = '';
- });
- }
- try {
- await validateWidget(api, organization.slug, widgetData);
- if (typeof onUpdateWidget === 'function' && !!previousWidget) {
- onUpdateWidget({
- id: previousWidget?.id,
- layout: previousWidget?.layout,
- ...widgetData,
- });
- addSuccessMessage(t('Updated widget.'));
- trackAdvancedAnalyticsEvent('dashboards_views.edit_widget_modal.confirm', {
- organization,
- });
- } else if (onAddWidget) {
- onAddWidget(widgetData);
- addSuccessMessage(t('Added widget.'));
- trackAdvancedAnalyticsEvent('dashboards_views.add_widget_modal.confirm', {
- organization,
- data_set: widgetData.widgetType ?? WidgetType.DISCOVER,
- });
- }
- if (source === DashboardWidgetSource.DASHBOARDS) {
- closeModal();
- }
- } catch (err) {
- errors = mapErrors(err?.responseJSON ?? {}, {});
- this.setState({errors});
- } finally {
- this.setState({loading: false});
- if (this.omitDashboardProp) {
- this.handleSubmitFromSelectedDashboard(errors, widgetData);
- }
- if (this.fromLibrary) {
- this.handleSubmitFromLibrary(errors, widgetData);
- }
- }
- };
- handleSubmitFromSelectedDashboard = (
- errors: FlatValidationError,
- widgetData: Widget
- ) => {
- const {closeModal, organization, selection} = this.props;
- const {selectedDashboard, dashboards} = this.state;
- // Validate that a dashboard was selected since api call to /dashboards/widgets/ does not check for dashboard
- if (
- !selectedDashboard ||
- !(
- dashboards.find(({title, id}) => {
- return title === selectedDashboard?.label && id === selectedDashboard?.value;
- }) || selectedDashboard.value === NEW_DASHBOARD_ID
- )
- ) {
- errors.dashboard = t('This field may not be blank');
- this.setState({errors});
- }
- if (!Object.keys(errors).length && selectedDashboard) {
- closeModal();
- const queryData: {
- queryConditions: string[];
- queryFields: string[];
- queryNames: string[];
- queryOrderby: string;
- } = {
- queryNames: [],
- queryConditions: [],
- queryFields: [
- ...widgetData.queries[0].columns,
- ...widgetData.queries[0].aggregates,
- ],
- queryOrderby: widgetData.queries[0].orderby,
- };
- widgetData.queries.forEach(query => {
- queryData.queryNames.push(query.name);
- queryData.queryConditions.push(query.conditions);
- });
- const pathQuery = {
- displayType: widgetData.displayType,
- interval: widgetData.interval,
- title: widgetData.title,
- ...queryData,
- // Propagate page filters
- ...selection.datetime,
- project: selection.projects,
- environment: selection.environments,
- };
- trackAdvancedAnalyticsEvent('discover_views.add_to_dashboard.confirm', {
- organization,
- });
- if (selectedDashboard.value === NEW_DASHBOARD_ID) {
- browserHistory.push({
- pathname: `/organizations/${organization.slug}/dashboards/new/`,
- query: pathQuery,
- });
- } else {
- browserHistory.push({
- pathname: `/organizations/${organization.slug}/dashboard/${selectedDashboard.value}/`,
- query: pathQuery,
- });
- }
- }
- };
- handleSubmitFromLibrary = (errors: FlatValidationError, widgetData: Widget) => {
- const {closeModal, dashboard, onAddLibraryWidget, organization} = this.props;
- if (!dashboard) {
- errors.dashboard = t('This field may not be blank');
- this.setState({errors});
- addErrorMessage(t('Widget may only be added to a Dashboard'));
- }
- if (!Object.keys(errors).length && dashboard && onAddLibraryWidget) {
- onAddLibraryWidget([...dashboard.widgets, widgetData]);
- closeModal();
- }
- trackAdvancedAnalyticsEvent('dashboards_views.add_widget_modal.save', {
- organization,
- data_set: widgetData.widgetType ?? WidgetType.DISCOVER,
- });
- };
- handleDefaultFields = (newDisplayType: DisplayType) => {
- const {displayType, defaultWidgetQuery, defaultTableColumns, widget} = this.props;
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- const normalized = normalizeQueries({
- displayType: newDisplayType,
- queries: prevState.queries,
- });
- if (newDisplayType === DisplayType.TOP_N) {
- // TOP N display should only allow a single query
- normalized.splice(1);
- }
- if (
- newDisplayType === DisplayType.WORLD_MAP &&
- prevState.widgetType === WidgetType.RELEASE
- ) {
- // World Map display type only supports Discover Dataset
- // so set state to default discover query.
- set(
- newState,
- 'queries',
- normalizeQueries({
- displayType: newDisplayType,
- queries: [newDiscoverQuery],
- })
- );
- set(newState, 'widgetType', WidgetType.DISCOVER);
- return {...newState, errors: undefined};
- }
- if (!prevState.userHasModified) {
- // If the Widget is an issue widget,
- if (
- newDisplayType === DisplayType.TABLE &&
- widget?.widgetType === WidgetType.ISSUE
- ) {
- set(newState, 'queries', widget.queries);
- set(newState, 'widgetType', WidgetType.ISSUE);
- return {...newState, errors: undefined};
- }
- // Default widget provided by Add to Dashboard from Discover
- if (defaultWidgetQuery && defaultTableColumns) {
- // If switching to Table visualization, use saved query fields for Y-Axis if user has not made query changes
- // This is so the widget can reflect the same columns as the table in Discover without requiring additional user input
- if (newDisplayType === DisplayType.TABLE) {
- normalized.forEach(query => {
- query.fields = [...defaultTableColumns];
- const {columns, aggregates} = getColumnsAndAggregates([
- ...defaultTableColumns,
- ]);
- query.aggregates = aggregates;
- query.columns = columns;
- });
- } else if (newDisplayType === displayType) {
- // When switching back to original display type, default fields back to the fields provided from the discover query
- normalized.forEach(query => {
- query.aggregates = [...defaultWidgetQuery.aggregates];
- query.columns = [...defaultWidgetQuery.columns];
- query.fields = defined(defaultWidgetQuery.fields)
- ? [...defaultWidgetQuery.fields]
- : [...defaultWidgetQuery.columns, ...defaultWidgetQuery.aggregates];
- query.orderby = defaultWidgetQuery.orderby;
- });
- }
- }
- }
- if (prevState.widgetType === WidgetType.ISSUE) {
- set(newState, 'widgetType', WidgetType.DISCOVER);
- }
- set(newState, 'queries', normalized);
- return {...newState, errors: undefined};
- });
- };
- handleFieldChange = (field: string) => (value: string) => {
- const {organization, source} = this.props;
- const {displayType} = this.state;
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- set(newState, field, value);
- trackAdvancedAnalyticsEvent('dashboards_views.add_widget_modal.change', {
- from: source,
- field,
- value,
- widget_type: prevState.widgetType,
- organization,
- });
- return {...newState, errors: undefined};
- });
- if (field === 'displayType' && value !== displayType) {
- this.handleDefaultFields(value as DisplayType);
- }
- };
- handleQueryChange = (widgetQuery: WidgetQuery, index: number) => {
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- set(newState, `queries.${index}`, widgetQuery);
- set(newState, 'userHasModified', true);
- return {...newState, errors: undefined};
- });
- };
- handleQueryRemove = (index: number) => {
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- newState.queries.splice(index, 1);
- return {...newState, errors: undefined};
- });
- };
- handleAddSearchConditions = () => {
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- const query = cloneDeep(newDiscoverQuery);
- query.fields = this.state.queries[0].fields;
- query.aggregates = this.state.queries[0].aggregates;
- query.columns = this.state.queries[0].columns;
- newState.queries.push(query);
- return newState;
- });
- };
- defaultQuery(widgetType: string): WidgetQuery {
- switch (widgetType) {
- case WidgetType.ISSUE:
- return newIssueQuery;
- case WidgetType.RELEASE:
- return newMetricsQuery;
- case WidgetType.DISCOVER:
- default:
- return newDiscoverQuery;
- }
- }
- handleDatasetChange = (widgetType: string) => {
- const {widget} = this.props;
- this.setState(prevState => {
- const newState = cloneDeep(prevState);
- newState.queries.splice(0, newState.queries.length);
- set(newState, 'widgetType', widgetType);
- newState.queries.push(
- ...(widget?.widgetType === widgetType
- ? widget.queries
- : [this.defaultQuery(widgetType)])
- );
- set(newState, 'userHasModified', true);
- return {...newState, errors: undefined};
- });
- };
- canAddSearchConditions() {
- const rightDisplayType = ['line', 'area', 'stacked_area', 'bar'].includes(
- this.state.displayType
- );
- const underQueryLimit = this.state.queries.length < 3;
- return rightDisplayType && underQueryLimit;
- }
- async fetchDashboards() {
- const {api, organization} = this.props;
- const promise: Promise<DashboardListItem[]> = api.requestPromise(
- `/organizations/${organization.slug}/dashboards/`,
- {
- method: 'GET',
- query: {sort: 'myDashboardsAndRecentlyViewed'},
- }
- );
- try {
- const dashboards = await promise;
- this.setState({
- dashboards,
- });
- } catch (error) {
- const errorResponse = error?.responseJSON ?? null;
- if (errorResponse) {
- addErrorMessage(errorResponse);
- } else {
- addErrorMessage(t('Unable to fetch dashboards'));
- }
- }
- this.setState({loading: false});
- }
- handleDashboardChange(option: SelectValue<string>) {
- this.setState({selectedDashboard: option});
- }
- 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,
- tooltip:
- d.widgetDisplay.length >= MAX_WIDGETS &&
- tct('Max widgets ([maxWidgets]) per dashboard reached.', {
- maxWidgets: MAX_WIDGETS,
- }),
- tooltipOptions: {position: 'right'},
- };
- });
- return (
- <Fragment>
- <p>
- {t(
- `Choose which dashboard you'd like to add this query to. It will appear as a widget.`
- )}
- </p>
- <Field
- label={t('Custom Dashboard')}
- inline={false}
- flexibleControlStateSize
- stacked
- error={errors?.dashboard}
- style={{marginBottom: space(1), position: 'relative'}}
- required
- >
- <SelectControl
- name="dashboard"
- options={[
- {label: t('+ Create New Dashboard'), value: 'new'},
- ...dashboardOptions,
- ]}
- onChange={(option: SelectValue<string>) => this.handleDashboardChange(option)}
- disabled={loading}
- />
- </Field>
- </Fragment>
- );
- }
- renderWidgetQueryForm(
- querySelection: PageFilters,
- releaseWidgetFieldOptions: ReturnType<typeof generateReleaseWidgetFieldOptions>
- ) {
- const {organization, tags} = this.props;
- const state = this.state;
- const errors = state.errors;
- const issueWidgetFieldOptions = generateIssueWidgetFieldOptions();
- const fieldOptions = (measurementKeys: string[]) =>
- generateFieldOptions({
- organization,
- tagKeys: Object.values(tags).map(({key}) => key),
- measurementKeys,
- spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
- });
- switch (state.widgetType) {
- case WidgetType.ISSUE:
- return (
- <Fragment>
- <IssueWidgetQueriesForm
- organization={organization}
- selection={querySelection}
- fieldOptions={issueWidgetFieldOptions}
- query={state.queries[0]}
- error={errors?.queries?.[0]}
- onChange={widgetQuery => this.handleQueryChange(widgetQuery, 0)}
- />
- <WidgetCard
- organization={organization}
- selection={querySelection}
- widget={{...this.state, displayType: DisplayType.TABLE}}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- widgetLimitReached={false}
- renderErrorMessage={errorMessage =>
- typeof errorMessage === 'string' && (
- <PanelAlert type="error">{errorMessage}</PanelAlert>
- )
- }
- isSorting={false}
- currentWidgetDragging={false}
- noLazyLoad
- />
- </Fragment>
- );
- case WidgetType.RELEASE:
- return (
- <Fragment>
- <WidgetQueriesForm
- organization={organization}
- selection={querySelection}
- displayType={state.displayType}
- widgetType={state.widgetType}
- queries={state.queries}
- errors={errors?.queries}
- fieldOptions={releaseWidgetFieldOptions}
- onChange={(queryIndex: number, widgetQuery: WidgetQuery) =>
- this.handleQueryChange(widgetQuery, queryIndex)
- }
- canAddSearchConditions={this.canAddSearchConditions()}
- handleAddSearchConditions={this.handleAddSearchConditions}
- handleDeleteQuery={this.handleQueryRemove}
- />
- <WidgetCard
- organization={organization}
- selection={querySelection}
- widget={this.state}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- widgetLimitReached={false}
- renderErrorMessage={errorMessage =>
- typeof errorMessage === 'string' && (
- <PanelAlert type="error">{errorMessage}</PanelAlert>
- )
- }
- isSorting={false}
- currentWidgetDragging={false}
- noLazyLoad
- />
- </Fragment>
- );
- case WidgetType.DISCOVER:
- default:
- return (
- <Fragment>
- <Measurements>
- {({measurements}) => {
- const measurementKeys = Object.values(measurements).map(({key}) => key);
- const amendedFieldOptions = fieldOptions(measurementKeys);
- return (
- <WidgetQueriesForm
- organization={organization}
- selection={querySelection}
- fieldOptions={amendedFieldOptions}
- displayType={state.displayType}
- widgetType={state.widgetType}
- queries={state.queries}
- errors={errors?.queries}
- onChange={(queryIndex: number, widgetQuery: WidgetQuery) =>
- this.handleQueryChange(widgetQuery, queryIndex)
- }
- canAddSearchConditions={this.canAddSearchConditions()}
- handleAddSearchConditions={this.handleAddSearchConditions}
- handleDeleteQuery={this.handleQueryRemove}
- />
- );
- }}
- </Measurements>
- <WidgetCard
- organization={organization}
- selection={querySelection}
- widget={this.state}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- widgetLimitReached={false}
- renderErrorMessage={errorMessage =>
- typeof errorMessage === 'string' && (
- <PanelAlert type="error">{errorMessage}</PanelAlert>
- )
- }
- isSorting={false}
- currentWidgetDragging={false}
- noLazyLoad
- showStoredAlert
- />
- </Fragment>
- );
- }
- }
- render() {
- const {
- Footer,
- Body,
- Header,
- organization,
- widget: previousWidget,
- dashboard,
- selectedWidgets,
- onUpdateWidget,
- onAddLibraryWidget,
- source,
- selection,
- start,
- end,
- statsPeriod,
- } = this.props;
- const state = this.state;
- const errors = state.errors;
- const isUpdatingWidget = typeof onUpdateWidget === 'function' && !!previousWidget;
- const showDatasetSelector =
- [DashboardWidgetSource.DASHBOARDS, DashboardWidgetSource.LIBRARY].includes(
- source
- ) && state.displayType !== DisplayType.WORLD_MAP;
- const showIssueDatasetSelector =
- showDatasetSelector && state.displayType === DisplayType.TABLE;
- const showMetricsDatasetSelector =
- showDatasetSelector && organization.features.includes('dashboards-releases');
- const datasetChoices: [WidgetType, React.ReactElement | string][] = [DiscoverDataset];
- if (showIssueDatasetSelector) {
- datasetChoices.push(IssueDataset);
- }
- if (showMetricsDatasetSelector) {
- datasetChoices.push(MetricsDataset);
- }
- // Construct PageFilters object using statsPeriod/start/end props so we can
- // render widget graph using saved timeframe from Saved/Prebuilt Query
- const querySelection: PageFilters = statsPeriod
- ? {...selection, datetime: {start: null, end: null, period: statsPeriod, utc: null}}
- : start && end
- ? {...selection, datetime: {start, end, period: null, utc: null}}
- : selection;
- const metricsWidgetFieldOptions = generateReleaseWidgetFieldOptions(
- Object.values(SESSIONS_FIELDS),
- SESSIONS_TAGS
- );
- return (
- <Fragment>
- <Header closeButton>
- <h4>
- {this.omitDashboardProp
- ? t('Add Widget to Dashboard')
- : this.fromLibrary
- ? t('Add Widget(s)')
- : isUpdatingWidget
- ? t('Edit Widget')
- : t('Add Widget')}
- </h4>
- </Header>
- <Body>
- {this.omitDashboardProp && this.renderDashboardSelector()}
- {this.fromLibrary && dashboard && onAddLibraryWidget ? (
- <TabsButtonBar
- activeTab={TAB.Custom}
- organization={organization}
- dashboard={dashboard}
- selectedWidgets={selectedWidgets}
- customWidget={this.state}
- onAddWidget={onAddLibraryWidget}
- />
- ) : null}
- <DoubleFieldWrapper>
- <StyledField
- data-test-id="widget-name"
- label={t('Widget Name')}
- inline={false}
- flexibleControlStateSize
- stacked
- error={errors?.title}
- required
- >
- <Input
- data-test-id="widget-title-input"
- type="text"
- name="title"
- maxLength={255}
- required
- value={state.title}
- onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
- this.handleFieldChange('title')(event.target.value);
- }}
- disabled={state.loading}
- />
- </StyledField>
- <StyledField
- data-test-id="chart-type"
- label={t('Visualization Display')}
- inline={false}
- flexibleControlStateSize
- stacked
- error={errors?.displayType}
- required
- >
- <SelectControl
- options={DISPLAY_TYPE_CHOICES.slice()}
- name="displayType"
- value={state.displayType}
- onChange={option => this.handleFieldChange('displayType')(option.value)}
- disabled={state.loading}
- />
- </StyledField>
- </DoubleFieldWrapper>
- {(showIssueDatasetSelector || showMetricsDatasetSelector) && (
- <Fragment>
- <StyledFieldLabel>{t('Dataset')}</StyledFieldLabel>
- <StyledRadioGroup
- style={{flex: 1}}
- choices={datasetChoices}
- value={state.widgetType}
- label={t('Dataset')}
- onChange={this.handleDatasetChange}
- />
- </Fragment>
- )}
- {this.renderWidgetQueryForm(querySelection, metricsWidgetFieldOptions)}
- </Body>
- <Footer>
- <ButtonBar gap={1}>
- <Button
- external
- href="https://docs.sentry.io/product/dashboards/custom-dashboards/#widget-builder"
- >
- {t('Read the docs')}
- </Button>
- <Button
- data-test-id="add-widget"
- priority="primary"
- type="button"
- onClick={this.handleSubmit}
- disabled={state.loading}
- busy={state.loading}
- >
- {this.fromLibrary
- ? t('Save')
- : isUpdatingWidget
- ? t('Update Widget')
- : t('Add Widget')}
- </Button>
- </ButtonBar>
- </Footer>
- </Fragment>
- );
- }
- }
- const DoubleFieldWrapper = styled('div')`
- display: inline-grid;
- grid-template-columns: repeat(2, 1fr);
- grid-column-gap: ${space(1)};
- width: 100%;
- `;
- export const modalCss = css`
- width: 100%;
- max-width: 700px;
- margin: 70px auto;
- `;
- const StyledField = styled(Field)`
- position: relative;
- `;
- const StyledRadioGroup = styled(RadioGroup)`
- padding-bottom: ${space(2)};
- `;
- const StyledFieldLabel = styled(FieldLabel)`
- padding-bottom: ${space(1)};
- display: inline-flex;
- `;
- export default withApi(withPageFilters(withTags(AddDashboardWidgetModal)));
|