123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- import {useCallback, useMemo} from 'react';
- import partition from 'lodash/partition';
- import {
- type Column,
- explodeField,
- generateFieldAsString,
- isAggregateFieldOrEquation,
- type QueryFieldValue,
- type Sort,
- } from 'sentry/utils/discover/fields';
- import {
- decodeInteger,
- decodeList,
- decodeScalar,
- decodeSorts,
- } from 'sentry/utils/queryString';
- import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
- import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
- import {MAX_NUM_Y_AXES} from 'sentry/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector';
- import {useQueryParamState} from 'sentry/views/dashboards/widgetBuilder/hooks/useQueryParamState';
- import {DEFAULT_RESULTS_LIMIT} from 'sentry/views/dashboards/widgetBuilder/utils';
- export type WidgetBuilderStateQueryParams = {
- dataset?: WidgetType;
- description?: string;
- displayType?: DisplayType;
- field?: (string | undefined)[];
- legendAlias?: string[];
- limit?: number;
- query?: string[];
- sort?: string[];
- title?: string;
- yAxis?: string[];
- };
- export const BuilderStateAction = {
- SET_TITLE: 'SET_TITLE',
- SET_DESCRIPTION: 'SET_DESCRIPTION',
- SET_DISPLAY_TYPE: 'SET_DISPLAY_TYPE',
- SET_DATASET: 'SET_DATASET',
- SET_FIELDS: 'SET_FIELDS',
- SET_Y_AXIS: 'SET_Y_AXIS',
- SET_QUERY: 'SET_QUERY',
- SET_SORT: 'SET_SORT',
- SET_LIMIT: 'SET_LIMIT',
- SET_LEGEND_ALIAS: 'SET_LEGEND_ALIAS',
- } as const;
- type WidgetAction =
- | {payload: string; type: typeof BuilderStateAction.SET_TITLE}
- | {payload: string; type: typeof BuilderStateAction.SET_DESCRIPTION}
- | {payload: DisplayType; type: typeof BuilderStateAction.SET_DISPLAY_TYPE}
- | {payload: WidgetType; type: typeof BuilderStateAction.SET_DATASET}
- | {payload: Column[]; type: typeof BuilderStateAction.SET_FIELDS}
- | {payload: Column[]; type: typeof BuilderStateAction.SET_Y_AXIS}
- | {payload: string[]; type: typeof BuilderStateAction.SET_QUERY}
- | {payload: Sort[]; type: typeof BuilderStateAction.SET_SORT}
- | {payload: number; type: typeof BuilderStateAction.SET_LIMIT}
- | {payload: string[]; type: typeof BuilderStateAction.SET_LEGEND_ALIAS};
- export interface WidgetBuilderState {
- dataset?: WidgetType;
- description?: string;
- displayType?: DisplayType;
- fields?: Column[];
- legendAlias?: string[];
- limit?: number;
- query?: string[];
- sort?: Sort[];
- title?: string;
- yAxis?: Column[];
- }
- function useWidgetBuilderState(): {
- dispatch: (action: WidgetAction) => void;
- state: WidgetBuilderState;
- } {
- const [title, setTitle] = useQueryParamState<string>({fieldName: 'title'});
- const [description, setDescription] = useQueryParamState<string>({
- fieldName: 'description',
- });
- const [displayType, setDisplayType] = useQueryParamState<DisplayType>({
- fieldName: 'displayType',
- deserializer: deserializeDisplayType,
- });
- const [dataset, setDataset] = useQueryParamState<WidgetType>({
- fieldName: 'dataset',
- deserializer: deserializeDataset,
- });
- const [fields, setFields] = useQueryParamState<Column[]>({
- fieldName: 'field',
- decoder: decodeList,
- deserializer: deserializeFields,
- serializer: serializeFields,
- });
- const [yAxis, setYAxis] = useQueryParamState<Column[]>({
- fieldName: 'yAxis',
- decoder: decodeList,
- deserializer: deserializeFields,
- serializer: serializeFields,
- });
- const [query, setQuery] = useQueryParamState<string[]>({
- fieldName: 'query',
- decoder: decodeList,
- deserializer: deserializeQuery,
- });
- const [sort, setSort] = useQueryParamState<Sort[]>({
- fieldName: 'sort',
- decoder: decodeSorts,
- serializer: serializeSorts,
- });
- const [limit, setLimit] = useQueryParamState<number>({
- fieldName: 'limit',
- decoder: decodeScalar,
- deserializer: deserializeLimit,
- });
- const [legendAlias, setLegendAlias] = useQueryParamState<string[]>({
- fieldName: 'legendAlias',
- decoder: decodeList,
- });
- const state = useMemo(
- () => ({
- title,
- description,
- displayType,
- dataset,
- fields,
- yAxis,
- query,
- sort,
- limit,
- legendAlias,
- }),
- [
- title,
- description,
- displayType,
- dataset,
- fields,
- yAxis,
- query,
- sort,
- limit,
- legendAlias,
- ]
- );
- const dispatch = useCallback(
- (action: WidgetAction) => {
- switch (action.type) {
- case BuilderStateAction.SET_TITLE:
- setTitle(action.payload);
- break;
- case BuilderStateAction.SET_DESCRIPTION:
- setDescription(action.payload);
- break;
- case BuilderStateAction.SET_DISPLAY_TYPE:
- setDisplayType(action.payload);
- const [aggregates, columns] = partition(fields, field => {
- const fieldString = generateFieldAsString(field);
- return isAggregateFieldOrEquation(fieldString);
- });
- if (action.payload === DisplayType.TABLE) {
- setYAxis([]);
- setLegendAlias([]);
- const newFields = [...columns, ...aggregates, ...(yAxis ?? [])];
- setFields(newFields);
- // Keep the sort if it's already contained in the new fields
- // Otherwise, reset sorting to the first field
- if (
- newFields.length > 0 &&
- !newFields.find(field => generateFieldAsString(field) === sort?.[0]?.field)
- ) {
- setSort([
- {
- kind: 'desc',
- field: generateFieldAsString(newFields[0] as QueryFieldValue),
- },
- ]);
- }
- } else if (action.payload === DisplayType.BIG_NUMBER) {
- // TODO: Reset the selected aggregate here for widgets with equations
- setSort([]);
- setYAxis([]);
- setLegendAlias([]);
- // Columns are ignored for big number widgets because there is no grouping
- setFields([...aggregates, ...(yAxis ?? [])]);
- setQuery(query?.slice(0, 1));
- } else {
- setFields(columns);
- setYAxis([
- ...aggregates.slice(0, MAX_NUM_Y_AXES),
- ...(yAxis?.slice(0, MAX_NUM_Y_AXES) ?? []),
- ]);
- }
- break;
- case BuilderStateAction.SET_DATASET:
- setDataset(action.payload);
- let nextDisplayType = displayType;
- if (action.payload === WidgetType.ISSUE) {
- // Issues only support table display type
- setDisplayType(DisplayType.TABLE);
- nextDisplayType = DisplayType.TABLE;
- }
- const config = getDatasetConfig(action.payload);
- setFields(
- config.defaultWidgetQuery.fields?.map(field => explodeField({field}))
- );
- if (
- nextDisplayType === DisplayType.TABLE ||
- nextDisplayType === DisplayType.BIG_NUMBER
- ) {
- setYAxis([]);
- setFields(
- config.defaultWidgetQuery.fields?.map(field => explodeField({field}))
- );
- } else {
- setFields([]);
- setYAxis(
- config.defaultWidgetQuery.aggregates?.map(aggregate =>
- explodeField({field: aggregate})
- )
- );
- }
- setQuery([config.defaultWidgetQuery.conditions]);
- setSort(decodeSorts(config.defaultWidgetQuery.orderby));
- break;
- case BuilderStateAction.SET_FIELDS:
- setFields(action.payload);
- const isRemoved = action.payload.length < (fields?.length ?? 0);
- if (
- displayType === DisplayType.TABLE &&
- action.payload.length > 0 &&
- !action.payload.find(
- field => generateFieldAsString(field) === sort?.[0]?.field
- )
- ) {
- if (dataset === WidgetType.ISSUE) {
- // Issue widgets can sort their tables by limited fields that aren't
- // in the fields array.
- return;
- }
- if (isRemoved) {
- setSort([
- {
- kind: 'desc',
- field: generateFieldAsString(action.payload[0] as QueryFieldValue),
- },
- ]);
- } else {
- // Find the index of the first field that doesn't match the old fields.
- const changedFieldIndex = action.payload.findIndex(
- field =>
- !fields?.find(
- originalField =>
- generateFieldAsString(originalField) ===
- generateFieldAsString(field)
- )
- );
- if (changedFieldIndex !== -1) {
- // At this point, we can assume the fields are the same length so
- // using the changedFieldIndex in action.payload is safe.
- setSort([
- {
- kind: sort?.[0]?.kind ?? 'desc',
- field: generateFieldAsString(
- action.payload[changedFieldIndex] as QueryFieldValue
- ),
- },
- ]);
- }
- }
- }
- break;
- case BuilderStateAction.SET_Y_AXIS:
- setYAxis(action.payload);
- break;
- case BuilderStateAction.SET_QUERY:
- setQuery(action.payload);
- break;
- case BuilderStateAction.SET_SORT:
- setSort(action.payload);
- break;
- case BuilderStateAction.SET_LIMIT:
- setLimit(action.payload);
- break;
- case BuilderStateAction.SET_LEGEND_ALIAS:
- setLegendAlias(action.payload);
- break;
- default:
- break;
- }
- },
- [
- setTitle,
- setDescription,
- setDisplayType,
- setDataset,
- setFields,
- setYAxis,
- setQuery,
- setSort,
- setLimit,
- setLegendAlias,
- fields,
- yAxis,
- displayType,
- query,
- sort,
- dataset,
- ]
- );
- return {
- state,
- dispatch,
- };
- }
- /**
- * Decodes the display type from the query params
- * Returns the default display type if the value is not a valid display type
- */
- function deserializeDisplayType(value: string): DisplayType {
- if (Object.values(DisplayType).includes(value as DisplayType)) {
- return value as DisplayType;
- }
- return DisplayType.TABLE;
- }
- /**
- * Decodes the dataset from the query params
- * Returns the default dataset if the value is not a valid dataset
- */
- function deserializeDataset(value: string): WidgetType {
- if (Object.values(WidgetType).includes(value as WidgetType)) {
- return value as WidgetType;
- }
- return WidgetType.ERRORS;
- }
- /**
- * Takes fields from the query params in list form and converts
- * them into a list of fields and functions
- */
- function deserializeFields(fields: string[]): Column[] {
- return fields.map(stringifiedField => {
- try {
- const {field, alias} = JSON.parse(stringifiedField);
- return explodeField({field, alias});
- } catch (error) {
- return explodeField({field: stringifiedField, alias: undefined});
- }
- });
- }
- /**
- * Takes fields in the field and function format and coverts
- * them into a list of strings compatible with query params
- */
- export function serializeFields(fields: Column[]): string[] {
- return fields.map(field => {
- if (field.alias) {
- return JSON.stringify({
- field: generateFieldAsString(field),
- alias: field.alias,
- });
- }
- return generateFieldAsString(field);
- });
- }
- function serializeSorts(sorts: Sort[]): string[] {
- return sorts.map(sort => {
- const direction = sort.kind === 'desc' ? '-' : '';
- return `${direction}${sort.field}`;
- });
- }
- /**
- * Decodes the limit from the query params
- * Returns the default limit if the value is not a valid limit
- */
- function deserializeLimit(value: string): number {
- return decodeInteger(value, DEFAULT_RESULTS_LIMIT);
- }
- /**
- * Decodes the query from the query params
- * Returns an array with an empty string if the query is empty
- */
- function deserializeQuery(queries: string[]): string[] {
- if (queries.length === 0) {
- return [''];
- }
- return queries;
- }
- export default useWidgetBuilderState;
|