|
@@ -1,5 +1,5 @@
|
|
|
import {Location, Query} from 'history';
|
|
|
-import {isString, cloneDeep, pick} from 'lodash';
|
|
|
+import {isString, cloneDeep, pick, isEqual} from 'lodash';
|
|
|
|
|
|
import {DEFAULT_PER_PAGE} from 'app/constants';
|
|
|
import {EventViewv1} from 'app/types';
|
|
@@ -7,13 +7,41 @@ import {SavedQuery as LegacySavedQuery} from 'app/views/discover/types';
|
|
|
import {SavedQuery, NewQuery} from 'app/stores/discoverSavedQueriesStore';
|
|
|
|
|
|
import {AUTOLINK_FIELDS, SPECIAL_FIELDS, FIELD_FORMATTERS} from './data';
|
|
|
-import {MetaType, EventQuery, getAggregateAlias} from './utils';
|
|
|
+import {MetaType, EventQuery, getAggregateAlias, decodeColumnOrder} from './utils';
|
|
|
+import {TableColumn, TableColumnSort} from './table/types';
|
|
|
+
|
|
|
+type LocationQuery = {
|
|
|
+ project?: string | string[];
|
|
|
+ environment?: string | string[];
|
|
|
+ start?: string | string[];
|
|
|
+ end?: string | string[];
|
|
|
+ utc?: string | string[];
|
|
|
+ statsPeriod?: string | string[];
|
|
|
+ cursor?: string | string[];
|
|
|
+};
|
|
|
+
|
|
|
+const EXTERNAL_QUERY_STRING_KEYS: Readonly<Array<keyof LocationQuery>> = [
|
|
|
+ 'project',
|
|
|
+ 'environment',
|
|
|
+ 'start',
|
|
|
+ 'end',
|
|
|
+ 'utc',
|
|
|
+ 'statsPeriod',
|
|
|
+ 'cursor',
|
|
|
+];
|
|
|
|
|
|
export type Sort = {
|
|
|
kind: 'asc' | 'desc';
|
|
|
field: string;
|
|
|
};
|
|
|
|
|
|
+const reverseSort = (sort: Sort): Sort => {
|
|
|
+ return {
|
|
|
+ kind: sort.kind === 'desc' ? 'asc' : 'desc',
|
|
|
+ field: sort.field,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
export type Field = {
|
|
|
field: string;
|
|
|
title: string;
|
|
@@ -21,6 +49,67 @@ export type Field = {
|
|
|
// width: number;
|
|
|
};
|
|
|
|
|
|
+const isSortEqualToField = (
|
|
|
+ sort: Sort,
|
|
|
+ field: Field,
|
|
|
+ tableDataMeta: MetaType
|
|
|
+): boolean => {
|
|
|
+ const sortKey = getSortKeyFromField(field, tableDataMeta);
|
|
|
+ return sort.field === sortKey;
|
|
|
+};
|
|
|
+
|
|
|
+const fieldToSort = (field: Field, tableDataMeta: MetaType): Sort | undefined => {
|
|
|
+ const sortKey = getSortKeyFromField(field, tableDataMeta);
|
|
|
+
|
|
|
+ if (!sortKey) {
|
|
|
+ return void 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ kind: 'desc',
|
|
|
+ field: sortKey,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+function getSortKeyFromFieldWithoutMeta(field: Field): string | null {
|
|
|
+ const column = getAggregateAlias(field.field);
|
|
|
+ if (SPECIAL_FIELDS.hasOwnProperty(column)) {
|
|
|
+ return SPECIAL_FIELDS[column as keyof typeof SPECIAL_FIELDS].sortField;
|
|
|
+ }
|
|
|
+
|
|
|
+ return column;
|
|
|
+}
|
|
|
+
|
|
|
+function getSortKeyFromField(field: Field, tableDataMeta: MetaType): string | null {
|
|
|
+ const column = getAggregateAlias(field.field);
|
|
|
+ if (SPECIAL_FIELDS.hasOwnProperty(column)) {
|
|
|
+ return SPECIAL_FIELDS[column as keyof typeof SPECIAL_FIELDS].sortField;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FIELD_FORMATTERS.hasOwnProperty(tableDataMeta[column])) {
|
|
|
+ return FIELD_FORMATTERS[tableDataMeta[column] as keyof typeof FIELD_FORMATTERS]
|
|
|
+ .sortField
|
|
|
+ ? column
|
|
|
+ : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+export function isFieldSortable(field: Field, tableDataMeta: MetaType): boolean {
|
|
|
+ return !!getSortKeyFromField(field, tableDataMeta);
|
|
|
+}
|
|
|
+
|
|
|
+const generateFieldAsString = (props: {aggregation: string; field: string}): string => {
|
|
|
+ const {aggregation, field} = props;
|
|
|
+
|
|
|
+ const hasAggregation = aggregation.length > 0;
|
|
|
+
|
|
|
+ const fieldAsString = hasAggregation ? `${aggregation}(${field})` : field;
|
|
|
+
|
|
|
+ return fieldAsString;
|
|
|
+};
|
|
|
+
|
|
|
const decodeFields = (location: Location): Array<Field> => {
|
|
|
const {query} = location;
|
|
|
|
|
@@ -70,7 +159,10 @@ const fromSorts = (sorts: string | string[] | undefined): Array<Sort> => {
|
|
|
|
|
|
sorts = isString(sorts) ? [sorts] : sorts;
|
|
|
|
|
|
- return sorts.reduce((acc: Array<Sort>, sort: string) => {
|
|
|
+ // NOTE: sets are iterated in insertion order
|
|
|
+ const uniqueSorts = [...new Set(sorts)];
|
|
|
+
|
|
|
+ return uniqueSorts.reduce((acc: Array<Sort>, sort: string) => {
|
|
|
acc.push(parseSort(sort));
|
|
|
return acc;
|
|
|
}, []);
|
|
@@ -102,24 +194,23 @@ const encodeSort = (sort: Sort): string => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const encodeSorts = (sorts: Array<Sort>): Array<string> => {
|
|
|
+const encodeSorts = (sorts: Readonly<Array<Sort>>): Array<string> => {
|
|
|
return sorts.map(encodeSort);
|
|
|
};
|
|
|
|
|
|
-const decodeTags = (location: Location): Array<string> => {
|
|
|
- const {query} = location;
|
|
|
-
|
|
|
- if (!query || !query.tag) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- const tags: Array<string> = isString(query.tag) ? [query.tag] : query.tag;
|
|
|
+const collectQueryStringByKey = (query: Query, key: string): Array<string> => {
|
|
|
+ const needle = query[key];
|
|
|
+ const collection: Array<string> = Array.isArray(needle)
|
|
|
+ ? needle
|
|
|
+ : typeof needle === 'string'
|
|
|
+ ? [needle]
|
|
|
+ : [];
|
|
|
|
|
|
- return tags.reduce((acc: Array<string>, tag: string) => {
|
|
|
- tag = tag.trim();
|
|
|
+ return collection.reduce((acc: Array<string>, item: string) => {
|
|
|
+ item = item.trim();
|
|
|
|
|
|
- if (tag.length > 0) {
|
|
|
- acc.push(tag);
|
|
|
+ if (item.length > 0) {
|
|
|
+ acc.push(item);
|
|
|
}
|
|
|
|
|
|
return acc;
|
|
@@ -195,37 +286,60 @@ const queryStringFromSavedQuery = (saved: LegacySavedQuery | SavedQuery): string
|
|
|
class EventView {
|
|
|
id: string | undefined;
|
|
|
name: string | undefined;
|
|
|
- fields: Field[];
|
|
|
- sorts: Sort[];
|
|
|
- tags: string[];
|
|
|
+ fields: Readonly<Field[]>;
|
|
|
+ sorts: Readonly<Sort[]>;
|
|
|
+ tags: Readonly<string[]>;
|
|
|
query: string | undefined;
|
|
|
- project: number[];
|
|
|
- range: string | undefined;
|
|
|
+ project: Readonly<number[]>;
|
|
|
start: string | undefined;
|
|
|
end: string | undefined;
|
|
|
+ statsPeriod: string | undefined;
|
|
|
+ environment: Readonly<string[]>;
|
|
|
|
|
|
constructor(props: {
|
|
|
id: string | undefined;
|
|
|
name: string | undefined;
|
|
|
- fields: Field[];
|
|
|
- sorts: Sort[];
|
|
|
- tags: string[];
|
|
|
+ fields: Readonly<Field[]>;
|
|
|
+ sorts: Readonly<Sort[]>;
|
|
|
+ tags: Readonly<string[]>;
|
|
|
query?: string | undefined;
|
|
|
- project: number[];
|
|
|
- range: string | undefined;
|
|
|
+ project: Readonly<number[]>;
|
|
|
start: string | undefined;
|
|
|
end: string | undefined;
|
|
|
+ statsPeriod: string | undefined;
|
|
|
+ environment: Readonly<string[]>;
|
|
|
}) {
|
|
|
- this.id = props.id;
|
|
|
+ // only include sort keys that are included in the fields
|
|
|
+
|
|
|
+ const sortKeys = props.fields
|
|
|
+ .map(field => {
|
|
|
+ return getSortKeyFromFieldWithoutMeta(field);
|
|
|
+ })
|
|
|
+ .filter(
|
|
|
+ (sortKey): sortKey is string => {
|
|
|
+ return !!sortKey;
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const sort = props.sorts.find(currentSort => {
|
|
|
+ return sortKeys.includes(currentSort.field);
|
|
|
+ });
|
|
|
+
|
|
|
+ const sorts = sort ? [sort] : [];
|
|
|
+
|
|
|
+ const id = props.id !== null && props.id !== void 0 ? String(props.id) : void 0;
|
|
|
+
|
|
|
+ this.id = id;
|
|
|
this.name = props.name;
|
|
|
this.fields = props.fields;
|
|
|
- this.sorts = props.sorts;
|
|
|
+ this.sorts = sorts;
|
|
|
this.tags = props.tags;
|
|
|
this.query = props.query;
|
|
|
this.project = props.project;
|
|
|
- this.range = props.range;
|
|
|
this.start = props.start;
|
|
|
this.end = props.end;
|
|
|
+ this.statsPeriod = props.statsPeriod;
|
|
|
+ this.environment = props.environment;
|
|
|
}
|
|
|
|
|
|
static fromLocation(location: Location): EventView {
|
|
@@ -234,12 +348,13 @@ class EventView {
|
|
|
name: decodeScalar(location.query.name),
|
|
|
fields: decodeFields(location),
|
|
|
sorts: decodeSorts(location),
|
|
|
- tags: decodeTags(location),
|
|
|
+ tags: collectQueryStringByKey(location.query, 'tag'),
|
|
|
query: decodeQuery(location),
|
|
|
project: decodeProjects(location),
|
|
|
start: decodeScalar(location.query.start),
|
|
|
end: decodeScalar(location.query.end),
|
|
|
- range: decodeScalar(location.query.range),
|
|
|
+ statsPeriod: decodeScalar(location.query.statsPeriod),
|
|
|
+ environment: collectQueryStringByKey(location.query, 'environment'),
|
|
|
});
|
|
|
}
|
|
|
|
|
@@ -259,9 +374,10 @@ class EventView {
|
|
|
query: eventViewV1.data.query,
|
|
|
project: [],
|
|
|
id: undefined,
|
|
|
- range: undefined,
|
|
|
start: undefined,
|
|
|
end: undefined,
|
|
|
+ statsPeriod: undefined,
|
|
|
+ environment: [],
|
|
|
});
|
|
|
}
|
|
|
|
|
@@ -287,59 +403,77 @@ class EventView {
|
|
|
project: saved.projects,
|
|
|
start: saved.start,
|
|
|
end: saved.end,
|
|
|
- range: saved.range,
|
|
|
sorts: fromSorts(saved.orderby),
|
|
|
tags: [],
|
|
|
+ statsPeriod: saved.range,
|
|
|
+ environment: collectQueryStringByKey(
|
|
|
+ {
|
|
|
+ environment: (saved as SavedQuery).environment as string[],
|
|
|
+ },
|
|
|
+ 'environment'
|
|
|
+ ),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
toNewQuery(): NewQuery {
|
|
|
- const orderby = this.sorts ? encodeSorts(this.sorts)[0] : undefined;
|
|
|
- return {
|
|
|
- id: this.id,
|
|
|
+ const orderby = this.sorts.length > 0 ? encodeSorts(this.sorts)[0] : undefined;
|
|
|
+
|
|
|
+ const newQuery: NewQuery = {
|
|
|
version: 2,
|
|
|
+ id: this.id,
|
|
|
name: this.name || '',
|
|
|
+ fields: this.getFields(),
|
|
|
+ fieldnames: this.getFieldNames(),
|
|
|
+ orderby,
|
|
|
+ // TODO: tags?
|
|
|
query: this.query || '',
|
|
|
projects: this.project,
|
|
|
start: this.start,
|
|
|
end: this.end,
|
|
|
- range: this.range,
|
|
|
- fields: this.fields.map(item => item.field),
|
|
|
- fieldnames: this.fields.map(item => item.title),
|
|
|
- orderby,
|
|
|
+ range: this.statsPeriod,
|
|
|
+ environment: this.environment,
|
|
|
};
|
|
|
+
|
|
|
+ if (!newQuery.query) {
|
|
|
+ // if query is an empty string, then it cannot be saved, so we omit it
|
|
|
+ // from the payload
|
|
|
+ delete newQuery.query;
|
|
|
+ }
|
|
|
+
|
|
|
+ return newQuery;
|
|
|
}
|
|
|
|
|
|
generateQueryStringObject(): Query {
|
|
|
const output = {
|
|
|
id: this.id,
|
|
|
- field: this.fields.map(item => item.field),
|
|
|
- fieldnames: this.fields.map(item => item.title),
|
|
|
+ name: this.name,
|
|
|
+ field: this.getFields(),
|
|
|
+ fieldnames: this.getFieldNames(),
|
|
|
sort: encodeSorts(this.sorts),
|
|
|
tag: this.tags,
|
|
|
query: this.query,
|
|
|
};
|
|
|
- const conditionalFields = ['name', 'project', 'start', 'end', 'range'];
|
|
|
- for (const field of conditionalFields) {
|
|
|
+
|
|
|
+ for (const field of EXTERNAL_QUERY_STRING_KEYS) {
|
|
|
if (this[field] && this[field].length) {
|
|
|
output[field] = this[field];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return cloneDeep(output);
|
|
|
+ return cloneDeep(output as any);
|
|
|
}
|
|
|
|
|
|
isValid(): boolean {
|
|
|
return this.fields.length > 0;
|
|
|
}
|
|
|
|
|
|
- getFieldTitles(): string[] {
|
|
|
+ getFieldNames(): string[] {
|
|
|
return this.fields.map(field => {
|
|
|
return field.title;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- getFieldNames(): string[] {
|
|
|
+ getFields(): string[] {
|
|
|
return this.fields.map(field => {
|
|
|
return field.field;
|
|
|
});
|
|
@@ -358,6 +492,234 @@ class EventView {
|
|
|
return this.fields.length;
|
|
|
}
|
|
|
|
|
|
+ getColumns(): TableColumn<React.ReactText>[] {
|
|
|
+ return decodeColumnOrder({
|
|
|
+ field: this.getFields(),
|
|
|
+ fieldnames: this.getFieldNames(),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ clone(): EventView {
|
|
|
+ // NOTE: We rely on usage of Readonly from TypeScript to ensure we do not mutate
|
|
|
+ // the attributes of EventView directly. This enables us to quickly
|
|
|
+ // clone new instances of EventView.
|
|
|
+
|
|
|
+ return new EventView({
|
|
|
+ id: this.id,
|
|
|
+ name: this.name,
|
|
|
+ fields: this.fields,
|
|
|
+ sorts: this.sorts,
|
|
|
+ tags: this.tags,
|
|
|
+ query: this.query,
|
|
|
+ project: this.project,
|
|
|
+ start: this.start,
|
|
|
+ end: this.end,
|
|
|
+ statsPeriod: this.statsPeriod,
|
|
|
+ environment: this.environment,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ withNewColumn(newColumn: {
|
|
|
+ aggregation: string;
|
|
|
+ field: string;
|
|
|
+ fieldname: string;
|
|
|
+ }): EventView {
|
|
|
+ const field = newColumn.field.trim();
|
|
|
+
|
|
|
+ const aggregation = newColumn.aggregation.trim();
|
|
|
+
|
|
|
+ const fieldAsString = generateFieldAsString({field, aggregation});
|
|
|
+
|
|
|
+ const name = newColumn.fieldname.trim();
|
|
|
+ const hasName = name.length > 0;
|
|
|
+
|
|
|
+ const newField: Field = {
|
|
|
+ field: fieldAsString,
|
|
|
+ title: hasName ? name : fieldAsString,
|
|
|
+ };
|
|
|
+
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ newEventView.fields = [...newEventView.fields, newField];
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
+ }
|
|
|
+
|
|
|
+ withUpdatedColumn(
|
|
|
+ columnIndex: number,
|
|
|
+ updatedColumn: {
|
|
|
+ aggregation: string;
|
|
|
+ field: string;
|
|
|
+ fieldname: string;
|
|
|
+ },
|
|
|
+ tableDataMeta: MetaType
|
|
|
+ ): EventView {
|
|
|
+ const {field, aggregation, fieldname} = updatedColumn;
|
|
|
+
|
|
|
+ const columnToBeUpdated = this.fields[columnIndex];
|
|
|
+
|
|
|
+ const fieldAsString = generateFieldAsString({field, aggregation});
|
|
|
+
|
|
|
+ const updateField = columnToBeUpdated.field !== fieldAsString;
|
|
|
+ const updateFieldName = columnToBeUpdated.title !== fieldname;
|
|
|
+
|
|
|
+ if (!updateField && !updateFieldName) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ const updatedField: Field = {
|
|
|
+ field: fieldAsString,
|
|
|
+ title: fieldname,
|
|
|
+ };
|
|
|
+
|
|
|
+ const fields = [...newEventView.fields];
|
|
|
+ fields[columnIndex] = updatedField;
|
|
|
+
|
|
|
+ newEventView.fields = fields;
|
|
|
+
|
|
|
+ // if the updated column is one of the sorted columns, we may need to remove
|
|
|
+ // it from the list of sorts
|
|
|
+
|
|
|
+ const needleSortIndex = this.sorts.findIndex(sort => {
|
|
|
+ return isSortEqualToField(sort, columnToBeUpdated, tableDataMeta);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (needleSortIndex >= 0) {
|
|
|
+ const needleSort = this.sorts[needleSortIndex];
|
|
|
+
|
|
|
+ const numOfColumns = this.fields.reduce((sum, currentField) => {
|
|
|
+ if (isSortEqualToField(needleSort, currentField, tableDataMeta)) {
|
|
|
+ return sum + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return sum;
|
|
|
+ }, 0);
|
|
|
+
|
|
|
+ // do not bother deleting the sort key if there are more than one columns
|
|
|
+ // of it in the table.
|
|
|
+
|
|
|
+ if (numOfColumns <= 1) {
|
|
|
+ const sorts = [...newEventView.sorts];
|
|
|
+ sorts.splice(needleSortIndex, 1);
|
|
|
+ newEventView.sorts = [...new Set(sorts)];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (newEventView.sorts.length <= 0 && newEventView.fields.length > 0) {
|
|
|
+ // establish a default sort by finding the first sortable field
|
|
|
+
|
|
|
+ if (isFieldSortable(updatedField, tableDataMeta)) {
|
|
|
+ // use the current updated field as the sort key
|
|
|
+ const sort = fieldToSort(updatedField, tableDataMeta)!;
|
|
|
+
|
|
|
+ // preserve the sort kind
|
|
|
+ sort.kind = needleSort.kind;
|
|
|
+
|
|
|
+ newEventView.sorts = [sort];
|
|
|
+ } else {
|
|
|
+ const sortableFieldIndex = newEventView.fields.findIndex(currentField => {
|
|
|
+ return isFieldSortable(currentField, tableDataMeta);
|
|
|
+ });
|
|
|
+ if (sortableFieldIndex >= 0) {
|
|
|
+ const fieldToBeSorted = newEventView.fields[sortableFieldIndex];
|
|
|
+ const sort = fieldToSort(fieldToBeSorted, tableDataMeta)!;
|
|
|
+ newEventView.sorts = [sort];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
+ }
|
|
|
+
|
|
|
+ withDeletedColumn(columnIndex: number, tableDataMeta: MetaType): EventView {
|
|
|
+ // Disallow removal of the orphan column, and check for out-of-bounds
|
|
|
+ if (this.fields.length <= 1 || this.fields.length <= columnIndex || columnIndex < 0) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // delete the column
|
|
|
+
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ const fields = [...newEventView.fields];
|
|
|
+ fields.splice(columnIndex, 1);
|
|
|
+ newEventView.fields = fields;
|
|
|
+
|
|
|
+ // if the deleted column is one of the sorted columns, we need to remove
|
|
|
+ // it from the list of sorts
|
|
|
+
|
|
|
+ const columnToBeDeleted = this.fields[columnIndex];
|
|
|
+
|
|
|
+ const needleSortIndex = this.sorts.findIndex(sort => {
|
|
|
+ return isSortEqualToField(sort, columnToBeDeleted, tableDataMeta);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (needleSortIndex >= 0) {
|
|
|
+ const needleSort = this.sorts[needleSortIndex];
|
|
|
+
|
|
|
+ const numOfColumns = this.fields.reduce((sum, field) => {
|
|
|
+ if (isSortEqualToField(needleSort, field, tableDataMeta)) {
|
|
|
+ return sum + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return sum;
|
|
|
+ }, 0);
|
|
|
+
|
|
|
+ // do not bother deleting the sort key if there are more than one columns
|
|
|
+ // of it in the table.
|
|
|
+
|
|
|
+ if (numOfColumns <= 1) {
|
|
|
+ const sorts = [...newEventView.sorts];
|
|
|
+ sorts.splice(needleSortIndex, 1);
|
|
|
+ newEventView.sorts = [...new Set(sorts)];
|
|
|
+
|
|
|
+ if (newEventView.sorts.length <= 0 && newEventView.fields.length > 0) {
|
|
|
+ // establish a default sort by finding the first sortable field
|
|
|
+
|
|
|
+ const sortableFieldIndex = newEventView.fields.findIndex(field => {
|
|
|
+ return isFieldSortable(field, tableDataMeta);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (sortableFieldIndex >= 0) {
|
|
|
+ const fieldToBeSorted = newEventView.fields[sortableFieldIndex];
|
|
|
+ const sort = fieldToSort(fieldToBeSorted, tableDataMeta)!;
|
|
|
+ newEventView.sorts = [sort];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
+ }
|
|
|
+
|
|
|
+ withMovedColumn({fromIndex, toIndex}: {fromIndex: number; toIndex: number}): EventView {
|
|
|
+ if (fromIndex === toIndex) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ const fields = [...newEventView.fields];
|
|
|
+
|
|
|
+ fields.splice(toIndex, 0, fields.splice(fromIndex, 1)[0]);
|
|
|
+
|
|
|
+ newEventView.fields = fields;
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
+ }
|
|
|
+
|
|
|
+ getSorts(): TableColumnSort<React.ReactText>[] {
|
|
|
+ return this.sorts.map(sort => {
|
|
|
+ return {
|
|
|
+ key: sort.field,
|
|
|
+ order: sort.kind,
|
|
|
+ } as TableColumnSort<string>;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // returns query input for the search
|
|
|
getQuery(inputQuery: string | string[] | null | undefined): string {
|
|
|
const queryParts: string[] = [];
|
|
|
|
|
@@ -384,39 +746,38 @@ class EventView {
|
|
|
return queryParts.join(' ');
|
|
|
}
|
|
|
|
|
|
+ getTagsAPIPayload(
|
|
|
+ location: Location
|
|
|
+ ): Exclude<EventQuery & LocationQuery, 'sort' | 'cursor'> {
|
|
|
+ const payload = this.getEventsAPIPayload(location);
|
|
|
+
|
|
|
+ if (payload.sort) {
|
|
|
+ delete payload.sort;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (payload.cursor) {
|
|
|
+ delete payload.cursor;
|
|
|
+ }
|
|
|
+
|
|
|
+ return payload;
|
|
|
+ }
|
|
|
+
|
|
|
// Takes an EventView instance and converts it into the format required for the events API
|
|
|
- getEventsAPIPayload(location: Location): EventQuery {
|
|
|
- const query = location.query || {};
|
|
|
-
|
|
|
- type LocationQuery = {
|
|
|
- project?: string;
|
|
|
- environment?: string;
|
|
|
- start?: string;
|
|
|
- end?: string;
|
|
|
- utc?: string;
|
|
|
- statsPeriod?: string;
|
|
|
- cursor?: string;
|
|
|
- sort?: string;
|
|
|
- };
|
|
|
+ getEventsAPIPayload(location: Location): EventQuery & LocationQuery {
|
|
|
+ const query = (location && location.query) || {};
|
|
|
+
|
|
|
+ // pick only the query strings that we care about
|
|
|
|
|
|
- const picked = pick<LocationQuery>(query || {}, [
|
|
|
- 'project',
|
|
|
- 'environment',
|
|
|
- 'start',
|
|
|
- 'end',
|
|
|
- 'utc',
|
|
|
- 'statsPeriod',
|
|
|
- 'cursor',
|
|
|
- 'sort',
|
|
|
- ]);
|
|
|
+ const picked = pickRelevantLocationQueryStrings(location);
|
|
|
|
|
|
- const fieldNames = this.getFieldNames();
|
|
|
+ const sort = this.sorts.length > 0 ? encodeSort(this.sorts[0]) : undefined;
|
|
|
+ const fields = this.getFields();
|
|
|
|
|
|
- const defaultSort = fieldNames.length > 0 ? [fieldNames[0]] : undefined;
|
|
|
+ // generate event query
|
|
|
|
|
|
- const eventQuery: EventQuery = Object.assign(picked, {
|
|
|
- field: [...new Set(fieldNames)],
|
|
|
- sort: picked.sort ? picked.sort : defaultSort,
|
|
|
+ const eventQuery: EventQuery & LocationQuery = Object.assign(picked, {
|
|
|
+ field: [...new Set(fields)],
|
|
|
+ sort,
|
|
|
per_page: DEFAULT_PER_PAGE,
|
|
|
query: this.getQuery(query.query),
|
|
|
});
|
|
@@ -428,28 +789,84 @@ class EventView {
|
|
|
return eventQuery;
|
|
|
}
|
|
|
|
|
|
- getDefaultSort(): string | undefined {
|
|
|
- if (this.sorts.length <= 0) {
|
|
|
- return undefined;
|
|
|
- }
|
|
|
+ isFieldSorted(field: Field, tableDataMeta: MetaType): Sort | undefined {
|
|
|
+ const needle = this.sorts.find(sort => {
|
|
|
+ return isSortEqualToField(sort, field, tableDataMeta);
|
|
|
+ });
|
|
|
|
|
|
- return encodeSort(this.sorts[0]);
|
|
|
+ return needle;
|
|
|
}
|
|
|
|
|
|
- getSortKey(fieldname: string, meta: MetaType): string | null {
|
|
|
- const column = getAggregateAlias(fieldname);
|
|
|
- if (SPECIAL_FIELDS.hasOwnProperty(column)) {
|
|
|
- return SPECIAL_FIELDS[column as keyof typeof SPECIAL_FIELDS].sortField;
|
|
|
+ sortOnField(field: Field, tableDataMeta: MetaType): EventView {
|
|
|
+ // check if field can be sorted
|
|
|
+ if (!isFieldSortable(field, tableDataMeta)) {
|
|
|
+ return this;
|
|
|
}
|
|
|
|
|
|
- if (FIELD_FORMATTERS.hasOwnProperty(meta[column])) {
|
|
|
- return FIELD_FORMATTERS[meta[column] as keyof typeof FIELD_FORMATTERS].sortField
|
|
|
- ? column
|
|
|
- : null;
|
|
|
+ const needleIndex = this.sorts.findIndex(sort => {
|
|
|
+ return isSortEqualToField(sort, field, tableDataMeta);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (needleIndex >= 0) {
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ const currentSort = this.sorts[needleIndex];
|
|
|
+
|
|
|
+ const sorts = [...newEventView.sorts];
|
|
|
+ sorts[needleIndex] = reverseSort(currentSort);
|
|
|
+
|
|
|
+ newEventView.sorts = sorts;
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
}
|
|
|
|
|
|
- return null;
|
|
|
+ // field is currently not sorted; so, we sort on it
|
|
|
+
|
|
|
+ const newEventView = this.clone();
|
|
|
+
|
|
|
+ // invariant: this is not falsey, since sortKey exists
|
|
|
+ const sort = fieldToSort(field, tableDataMeta)!;
|
|
|
+
|
|
|
+ newEventView.sorts = [sort];
|
|
|
+
|
|
|
+ return newEventView;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const isAPIPayloadSimilar = (
|
|
|
+ current: EventQuery & LocationQuery,
|
|
|
+ other: EventQuery & LocationQuery
|
|
|
+): boolean => {
|
|
|
+ const currentKeys = new Set(Object.keys(current));
|
|
|
+ const otherKeys = new Set(Object.keys(other));
|
|
|
+
|
|
|
+ if (!isEqual(currentKeys, otherKeys)) {
|
|
|
+ return false;
|
|
|
}
|
|
|
+
|
|
|
+ for (const key of currentKeys) {
|
|
|
+ const currentValue = current[key];
|
|
|
+ const currentTarget = Array.isArray(currentValue)
|
|
|
+ ? new Set(currentValue)
|
|
|
+ : currentValue;
|
|
|
+
|
|
|
+ const otherValue = other[key];
|
|
|
+ const otherTarget = Array.isArray(otherValue) ? new Set(otherValue) : otherValue;
|
|
|
+
|
|
|
+ if (!isEqual(currentTarget, otherTarget)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+export function pickRelevantLocationQueryStrings(location: Location): LocationQuery {
|
|
|
+ const query = location.query || {};
|
|
|
+
|
|
|
+ const picked = pick<LocationQuery>(query || {}, EXTERNAL_QUERY_STRING_KEYS);
|
|
|
+
|
|
|
+ return picked;
|
|
|
}
|
|
|
|
|
|
export default EventView;
|