|
@@ -1,4 +1,4 @@
|
|
|
-import {useState} from 'react';
|
|
|
+import {useEffect, useState} from 'react';
|
|
|
import {RouteComponentProps} from 'react-router';
|
|
|
import styled from '@emotion/styled';
|
|
|
import cloneDeep from 'lodash/cloneDeep';
|
|
@@ -117,6 +117,7 @@ type Props = RouteComponentProps<RouteParams, {}> & {
|
|
|
organization: Organization;
|
|
|
selection: PageFilters;
|
|
|
tags: TagCollection;
|
|
|
+ defaultTableColumns?: readonly string[];
|
|
|
defaultTitle?: string;
|
|
|
defaultWidgetQuery?: WidgetQuery;
|
|
|
displayType?: DisplayType;
|
|
@@ -152,6 +153,7 @@ function WidgetBuilder({
|
|
|
defaultWidgetQuery,
|
|
|
displayType,
|
|
|
defaultTitle,
|
|
|
+ defaultTableColumns,
|
|
|
tags,
|
|
|
}: Props) {
|
|
|
const {widgetId, orgId, dashboardId} = params;
|
|
@@ -209,8 +211,63 @@ function WidgetBuilder({
|
|
|
: DataSet.EVENTS,
|
|
|
};
|
|
|
});
|
|
|
+
|
|
|
const [blurTimeout, setBlurTimeout] = useState<null | number>(null);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ defaultFields();
|
|
|
+ }, [state.displayType]);
|
|
|
+
|
|
|
+ function defaultFields() {
|
|
|
+ setState(prevState => {
|
|
|
+ const newState = cloneDeep(prevState);
|
|
|
+ const normalized = normalizeQueries(prevState.displayType, prevState.queries);
|
|
|
+
|
|
|
+ if (prevState.displayType === DisplayType.TOP_N) {
|
|
|
+ // TOP N display should only allow a single query
|
|
|
+ normalized.splice(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!prevState.userHasModified) {
|
|
|
+ // If the Widget is an issue widget,
|
|
|
+ if (
|
|
|
+ prevState.displayType === DisplayType.TABLE &&
|
|
|
+ widget?.widgetType &&
|
|
|
+ WIDGET_TYPE_TO_DATA_SET[widget.widgetType] === DataSet.ISSUES
|
|
|
+ ) {
|
|
|
+ set(newState, 'queries', widget.queries);
|
|
|
+ set(newState, 'dataSet', DataSet.ISSUES);
|
|
|
+ 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 (prevState.displayType === DisplayType.TABLE) {
|
|
|
+ normalized.forEach(query => {
|
|
|
+ query.fields = [...defaultTableColumns];
|
|
|
+ });
|
|
|
+ } else if (prevState.displayType === displayType) {
|
|
|
+ // When switching back to original display type, default fields back to the fields provided from the discover query
|
|
|
+ normalized.forEach(query => {
|
|
|
+ query.fields = [...defaultWidgetQuery.fields];
|
|
|
+ query.orderby = defaultWidgetQuery.orderby;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (prevState.dataSet === DataSet.ISSUES) {
|
|
|
+ set(newState, 'dataSet', DataSet.EVENTS);
|
|
|
+ }
|
|
|
+
|
|
|
+ set(newState, 'queries', normalized);
|
|
|
+
|
|
|
+ return {...newState, errors: undefined};
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
function handleDataSetChange(newDataSet: string) {
|
|
|
setState(prevState => {
|
|
|
const newState = cloneDeep(prevState);
|
|
@@ -260,37 +317,25 @@ function WidgetBuilder({
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- if (
|
|
|
- isEditing &&
|
|
|
- (!defined(widgetId) ||
|
|
|
- !dashboard.widgets.find(dashboardWidget => dashboardWidget.id === String(widgetId)))
|
|
|
- ) {
|
|
|
- return (
|
|
|
- <SentryDocumentTitle title={dashboard.title} orgSlug={orgSlug}>
|
|
|
- <PageContent>
|
|
|
- <LoadingError message={t('Widget not found.')} />
|
|
|
- </PageContent>
|
|
|
- </SentryDocumentTitle>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- function handleChangeField(newFields: QueryFieldValue[]) {
|
|
|
+ function handleChangeYAxisOrColumnField(newFields: QueryFieldValue[]) {
|
|
|
const fieldStrings = newFields.map(generateFieldAsString);
|
|
|
const aggregateAliasFieldStrings = fieldStrings.map(getAggregateAlias);
|
|
|
|
|
|
for (const index in state.queries) {
|
|
|
const queryIndex = Number(index);
|
|
|
const query = state.queries[queryIndex];
|
|
|
+
|
|
|
const descending = query.orderby.startsWith('-');
|
|
|
const orderbyAggregateAliasField = query.orderby.replace('-', '');
|
|
|
- const prevAggregateAliasFieldStrings = query.fields.map(getAggregateAlias);
|
|
|
+ const prevAggregateAliasFieldStrings = query.fields.map(field =>
|
|
|
+ getAggregateAlias(field)
|
|
|
+ );
|
|
|
const newQuery = cloneDeep(query);
|
|
|
-
|
|
|
newQuery.fields = fieldStrings;
|
|
|
-
|
|
|
- if (!aggregateAliasFieldStrings.includes(orderbyAggregateAliasField)) {
|
|
|
- newQuery.orderby = '';
|
|
|
-
|
|
|
+ if (
|
|
|
+ !aggregateAliasFieldStrings.includes(orderbyAggregateAliasField) &&
|
|
|
+ query.orderby !== ''
|
|
|
+ ) {
|
|
|
if (prevAggregateAliasFieldStrings.length === newFields.length) {
|
|
|
// The Field that was used in orderby has changed. Get the new field.
|
|
|
newQuery.orderby = `${descending && '-'}${
|
|
@@ -298,6 +343,8 @@ function WidgetBuilder({
|
|
|
prevAggregateAliasFieldStrings.indexOf(orderbyAggregateAliasField)
|
|
|
]
|
|
|
}`;
|
|
|
+ } else {
|
|
|
+ newQuery.orderby = '';
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -305,6 +352,36 @@ function WidgetBuilder({
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function getAmendedFieldOptions(measurements: MeasurementCollection) {
|
|
|
+ return generateFieldOptions({
|
|
|
+ organization,
|
|
|
+ tagKeys: Object.values(tags).map(({key}) => key),
|
|
|
+ measurementKeys: Object.values(measurements).map(({key}) => key),
|
|
|
+ spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ isEditing &&
|
|
|
+ (!defined(widgetId) ||
|
|
|
+ !dashboard.widgets.find(dashboardWidget => dashboardWidget.id === String(widgetId)))
|
|
|
+ ) {
|
|
|
+ return (
|
|
|
+ <SentryDocumentTitle title={dashboard.title} orgSlug={orgSlug}>
|
|
|
+ <PageContent>
|
|
|
+ <LoadingError message={t('Widget not found.')} />
|
|
|
+ </PageContent>
|
|
|
+ </SentryDocumentTitle>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const widgetType =
|
|
|
+ state.dataSet === DataSet.EVENTS
|
|
|
+ ? WidgetType.DISCOVER
|
|
|
+ : state.dataSet === DataSet.ISSUES
|
|
|
+ ? WidgetType.ISSUE
|
|
|
+ : WidgetType.METRICS;
|
|
|
+
|
|
|
const canAddSearchConditions =
|
|
|
[
|
|
|
DisplayType.LINE,
|
|
@@ -319,22 +396,7 @@ function WidgetBuilder({
|
|
|
DisplayType.BIG_NUMBER,
|
|
|
].includes(state.displayType);
|
|
|
|
|
|
- const widgetType =
|
|
|
- state.dataSet === DataSet.EVENTS
|
|
|
- ? WidgetType.DISCOVER
|
|
|
- : state.dataSet === DataSet.ISSUES
|
|
|
- ? WidgetType.ISSUE
|
|
|
- : WidgetType.METRICS;
|
|
|
-
|
|
|
const explodedFields = state.queries[0].fields.map(field => explodeField({field}));
|
|
|
- function getAmendedFieldOptions(measurements: MeasurementCollection) {
|
|
|
- return generateFieldOptions({
|
|
|
- organization,
|
|
|
- tagKeys: Object.values(tags).map(({key}) => key),
|
|
|
- measurementKeys: Object.values(measurements).map(({key}) => key),
|
|
|
- spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
|
|
|
- });
|
|
|
- }
|
|
|
|
|
|
return (
|
|
|
<SentryDocumentTitle title={dashboard.title} orgSlug={orgSlug}>
|
|
@@ -360,35 +422,37 @@ function WidgetBuilder({
|
|
|
'This is a preview of how your widget will appear in the dashboard.'
|
|
|
)}
|
|
|
>
|
|
|
- <DisplayTypeOptions
|
|
|
- name="displayType"
|
|
|
- options={DISPLAY_TYPES_OPTIONS}
|
|
|
- value={state.displayType}
|
|
|
- onChange={(option: {label: string; value: DisplayType}) => {
|
|
|
- setState({...state, displayType: option.value});
|
|
|
- }}
|
|
|
- />
|
|
|
- <WidgetCard
|
|
|
- organization={organization}
|
|
|
- selection={pageFilters}
|
|
|
- widget={{
|
|
|
- title: state.title,
|
|
|
- displayType: state.displayType,
|
|
|
- interval: state.interval,
|
|
|
- queries: state.queries,
|
|
|
- widgetType,
|
|
|
- }}
|
|
|
- isEditing={false}
|
|
|
- widgetLimitReached={false}
|
|
|
- renderErrorMessage={errorMessage =>
|
|
|
- typeof errorMessage === 'string' && (
|
|
|
- <PanelAlert type="error">{errorMessage}</PanelAlert>
|
|
|
- )
|
|
|
- }
|
|
|
- isSorting={false}
|
|
|
- currentWidgetDragging={false}
|
|
|
- noLazyLoad
|
|
|
- />
|
|
|
+ <VisualizationWrapper>
|
|
|
+ <DisplayTypeOptions
|
|
|
+ name="displayType"
|
|
|
+ options={DISPLAY_TYPES_OPTIONS}
|
|
|
+ value={state.displayType}
|
|
|
+ onChange={(option: {label: string; value: DisplayType}) => {
|
|
|
+ setState({...state, displayType: option.value});
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <WidgetCard
|
|
|
+ organization={organization}
|
|
|
+ selection={pageFilters}
|
|
|
+ widget={{
|
|
|
+ title: state.title,
|
|
|
+ displayType: state.displayType,
|
|
|
+ interval: state.interval,
|
|
|
+ queries: state.queries,
|
|
|
+ widgetType,
|
|
|
+ }}
|
|
|
+ isEditing={false}
|
|
|
+ widgetLimitReached={false}
|
|
|
+ renderErrorMessage={errorMessage =>
|
|
|
+ typeof errorMessage === 'string' && (
|
|
|
+ <PanelAlert type="error">{errorMessage}</PanelAlert>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ isSorting={false}
|
|
|
+ currentWidgetDragging={false}
|
|
|
+ noLazyLoad
|
|
|
+ />
|
|
|
+ </VisualizationWrapper>
|
|
|
</BuildStep>
|
|
|
<BuildStep
|
|
|
title={t('Choose your data set')}
|
|
@@ -414,20 +478,17 @@ function WidgetBuilder({
|
|
|
>
|
|
|
{state.dataSet === DataSet.EVENTS ? (
|
|
|
<Measurements>
|
|
|
- {({measurements}) => {
|
|
|
- const amendedFieldOptions = getAmendedFieldOptions(measurements);
|
|
|
- return (
|
|
|
- <ColumnFields
|
|
|
- displayType={state.displayType}
|
|
|
- organization={organization}
|
|
|
- widgetType={widgetType}
|
|
|
- columns={explodedFields}
|
|
|
- errors={state.errors?.queries}
|
|
|
- fieldOptions={amendedFieldOptions}
|
|
|
- onChange={handleChangeField}
|
|
|
- />
|
|
|
- );
|
|
|
- }}
|
|
|
+ {({measurements}) => (
|
|
|
+ <ColumnFields
|
|
|
+ displayType={state.displayType}
|
|
|
+ organization={organization}
|
|
|
+ widgetType={widgetType}
|
|
|
+ columns={explodedFields}
|
|
|
+ errors={state.errors?.queries}
|
|
|
+ fieldOptions={getAmendedFieldOptions(measurements)}
|
|
|
+ onChange={handleChangeYAxisOrColumnField}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</Measurements>
|
|
|
) : (
|
|
|
<ColumnFields
|
|
@@ -459,19 +520,16 @@ function WidgetBuilder({
|
|
|
description="Description of what this means"
|
|
|
>
|
|
|
<Measurements>
|
|
|
- {({measurements}) => {
|
|
|
- const amendedFieldOptions = getAmendedFieldOptions(measurements);
|
|
|
- return (
|
|
|
- <YAxisSelector
|
|
|
- widgetType={widgetType}
|
|
|
- displayType={state.displayType}
|
|
|
- fields={explodedFields}
|
|
|
- fieldOptions={amendedFieldOptions}
|
|
|
- onChange={handleChangeField}
|
|
|
- // TODO: errors={getFirstQueryError('fields')}
|
|
|
- />
|
|
|
- );
|
|
|
- }}
|
|
|
+ {({measurements}) => (
|
|
|
+ <YAxisSelector
|
|
|
+ widgetType={widgetType}
|
|
|
+ displayType={state.displayType}
|
|
|
+ fields={explodedFields}
|
|
|
+ fieldOptions={getAmendedFieldOptions(measurements)}
|
|
|
+ onChange={handleChangeYAxisOrColumnField}
|
|
|
+ errors={state.errors?.queries}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</Measurements>
|
|
|
</BuildStep>
|
|
|
)}
|
|
@@ -624,6 +682,12 @@ const PageContentWithoutPadding = styled(PageContent)`
|
|
|
padding: 0;
|
|
|
`;
|
|
|
|
|
|
+const VisualizationWrapper = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ margin-right: ${space(2)};
|
|
|
+`;
|
|
|
+
|
|
|
const DataSetChoices = styled(RadioGroup)`
|
|
|
@media (min-width: ${p => p.theme.breakpoints[2]}) {
|
|
|
grid-auto-flow: column;
|