forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 0
/
richHistory.ts
390 lines (336 loc) · 12.6 KB
/
richHistory.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
// Libraries
import _ from 'lodash';
// Services & Utils
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
import appEvents from 'app/core/app_events';
import store from 'app/core/store';
import { getExploreDatasources } from '../../features/explore/state/selectors';
// Types
import { RichHistoryQuery } from 'app/types/explore';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
export const RICH_HISTORY_SETTING_KEYS = {
retentionPeriod: 'grafana.explore.richHistory.retentionPeriod',
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
};
export enum SortOrder {
Descending = 'Descending',
Ascending = 'Ascending',
DatasourceAZ = 'Datasource A-Z',
DatasourceZA = 'Datasource Z-A',
}
/*
* Add queries to rich history. Save only queries within the retention period, or that are starred.
* Side-effect: store history in local storage
*/
export function addToRichHistory(
richHistory: RichHistoryQuery[],
datasourceId: string,
datasourceName: string | null,
queries: DataQuery[],
starred: boolean,
comment: string | null,
sessionName: string
): any {
const ts = Date.now();
/* Save only queries, that are not falsy (e.g. empty object, null, ...) */
const newQueriesToSave: DataQuery[] = queries && queries.filter(query => notEmptyQuery(query));
const retentionPeriod: number = store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7);
const retentionPeriodLastTs = createRetentionPeriodBoundary(retentionPeriod, false);
/* Keep only queries, that are within the selected retention period or that are starred.
* If no queries, initialize with empty array
*/
const queriesToKeep = richHistory.filter(q => q.ts > retentionPeriodLastTs || q.starred === true) || [];
if (newQueriesToSave.length > 0) {
/* Compare queries of a new query and last saved queries. If they are the same, (except selected properties,
* which can be different) don't save it in rich history.
*/
const newQueriesToCompare = newQueriesToSave.map(q => _.omit(q, ['key', 'refId']));
const lastQueriesToCompare =
queriesToKeep.length > 0 &&
queriesToKeep[0].queries.map(q => {
return _.omit(q, ['key', 'refId']);
});
if (_.isEqual(newQueriesToCompare, lastQueriesToCompare)) {
return richHistory;
}
let updatedHistory = [
{ queries: newQueriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
...queriesToKeep,
];
try {
store.setObject(RICH_HISTORY_KEY, updatedHistory);
return updatedHistory;
} catch (error) {
appEvents.emit(AppEvents.alertError, [error]);
return richHistory;
}
}
return richHistory;
}
export function getRichHistory(): RichHistoryQuery[] {
const richHistory: RichHistoryQuery[] = store.getObject(RICH_HISTORY_KEY, []);
const transformedRichHistory = migrateRichHistory(richHistory);
return transformedRichHistory;
}
export function deleteAllFromRichHistory() {
return store.delete(RICH_HISTORY_KEY);
}
export function updateStarredInRichHistory(richHistory: RichHistoryQuery[], ts: number) {
const updatedHistory = richHistory.map(query => {
/* Timestamps are currently unique - we can use them to identify specific queries */
if (query.ts === ts) {
const isStarred = query.starred;
const updatedQuery = Object.assign({}, query, { starred: !isStarred });
return updatedQuery;
}
return query;
});
try {
store.setObject(RICH_HISTORY_KEY, updatedHistory);
return updatedHistory;
} catch (error) {
appEvents.emit(AppEvents.alertError, [error]);
return richHistory;
}
}
export function updateCommentInRichHistory(
richHistory: RichHistoryQuery[],
ts: number,
newComment: string | undefined
) {
const updatedHistory = richHistory.map(query => {
if (query.ts === ts) {
const updatedQuery = Object.assign({}, query, { comment: newComment });
return updatedQuery;
}
return query;
});
try {
store.setObject(RICH_HISTORY_KEY, updatedHistory);
return updatedHistory;
} catch (error) {
appEvents.emit(AppEvents.alertError, [error]);
return richHistory;
}
}
export function deleteQueryInRichHistory(richHistory: RichHistoryQuery[], ts: number) {
const updatedHistory = richHistory.filter(query => query.ts !== ts);
try {
store.setObject(RICH_HISTORY_KEY, updatedHistory);
return updatedHistory;
} catch (error) {
appEvents.emit(AppEvents.alertError, [error]);
return richHistory;
}
}
export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) => {
let sortFunc;
if (sortOrder === SortOrder.Ascending) {
sortFunc = (a: RichHistoryQuery, b: RichHistoryQuery) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0);
}
if (sortOrder === SortOrder.Descending) {
sortFunc = (a: RichHistoryQuery, b: RichHistoryQuery) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
}
if (sortOrder === SortOrder.DatasourceZA) {
sortFunc = (a: RichHistoryQuery, b: RichHistoryQuery) =>
a.datasourceName < b.datasourceName ? -1 : a.datasourceName > b.datasourceName ? 1 : 0;
}
if (sortOrder === SortOrder.DatasourceAZ) {
sortFunc = (a: RichHistoryQuery, b: RichHistoryQuery) =>
a.datasourceName < b.datasourceName ? 1 : a.datasourceName > b.datasourceName ? -1 : 0;
}
return array.sort(sortFunc);
};
export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */
range: { from: 'now-1h', to: 'now' },
datasource: query.datasourceName,
queries: query.queries,
context: 'explore',
};
const serializedState = serializeStateToUrlParam(exploreState, true);
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0];
const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState });
return url;
};
/* Needed for slider in Rich history to map numerical values to meaningful strings */
export const mapNumbertoTimeInSlider = (num: number) => {
let str;
switch (num) {
case 0:
str = 'today';
break;
case 1:
str = 'yesterday';
break;
case 7:
str = 'a week ago';
break;
case 14:
str = 'two weeks ago';
break;
default:
str = `${num} days ago`;
}
return str;
};
export const createRetentionPeriodBoundary = (days: number, isLastTs: boolean) => {
const today = new Date();
const date = new Date(today.setDate(today.getDate() - days));
/*
* As a retention period boundaries, we consider:
* - The last timestamp equals to the 24:00 of the last day of retention
* - The first timestamp that equals to the 00:00 of the first day of retention
*/
const boundary = isLastTs ? date.setHours(24, 0, 0, 0) : date.setHours(0, 0, 0, 0);
return boundary;
};
export function createDateStringFromTs(ts: number) {
return dateTimeFormat(ts, {
format: 'MMMM D',
});
}
export function getQueryDisplayText(query: DataQuery): string {
/* If datasource doesn't have getQueryDisplayText, create query display text by
* stringifying query that was stripped of key, refId and datasource for nicer
* formatting and improved readability
*/
const strippedQuery = _.omit(query, ['key', 'refId', 'datasource']);
return JSON.stringify(strippedQuery);
}
export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder) {
let heading = '';
if (sortOrder === SortOrder.DatasourceAZ || sortOrder === SortOrder.DatasourceZA) {
heading = query.datasourceName;
} else {
heading = createDateStringFromTs(query.ts);
}
return heading;
}
export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi | undefined) {
/* query DatasourceInstance is necessary because we use its getQueryDisplayText method
* to format query text
*/
if (queryDsInstance?.getQueryDisplayText) {
return queryDsInstance.getQueryDisplayText(query);
}
return getQueryDisplayText(query);
}
export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortOrder) {
let mappedQueriesToHeadings: any = {};
query.forEach(q => {
let heading = createQueryHeading(q, sortOrder);
if (!(heading in mappedQueriesToHeadings)) {
mappedQueriesToHeadings[heading] = [q];
} else {
mappedQueriesToHeadings[heading] = [...mappedQueriesToHeadings[heading], q];
}
});
return mappedQueriesToHeadings;
}
/* Create datasource list with images. If specific datasource retrieved from Rich history is not part of
* exploreDatasources add generic datasource image and add property isRemoved = true.
*/
export function createDatasourcesList(queriesDatasources: string[]) {
const exploreDatasources = getExploreDatasources();
const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = [];
queriesDatasources.forEach(queryDsName => {
const index = exploreDatasources.findIndex(exploreDs => exploreDs.name === queryDsName);
if (index !== -1) {
datasources.push({
label: queryDsName,
value: queryDsName,
imgUrl: exploreDatasources[index].meta.info.logos.small,
isRemoved: false,
});
} else {
datasources.push({
label: queryDsName,
value: queryDsName,
imgUrl: 'public/img/icn-datasource.svg',
isRemoved: true,
});
}
});
return datasources;
}
export function notEmptyQuery(query: DataQuery) {
/* Check if query has any other properties besides key, refId and datasource.
* If not, then we consider it empty query.
*/
const strippedQuery = _.omit(query, ['key', 'refId', 'datasource']);
const queryKeys = Object.keys(strippedQuery);
if (queryKeys.length > 0) {
return true;
}
return false;
}
export function filterQueriesBySearchFilter(queries: RichHistoryQuery[], searchFilter: string) {
return queries.filter(query => {
if (query.comment.includes(searchFilter)) {
return true;
}
const listOfMatchingQueries = query.queries.filter(query =>
// Remove fields in which we don't want to be searching
Object.values(_.omit(query, ['datasource', 'key', 'refId', 'hide', 'queryType'])).some((value: any) =>
value?.toString().includes(searchFilter)
)
);
return listOfMatchingQueries.length > 0;
});
}
export function filterQueriesByDataSource(queries: RichHistoryQuery[], listOfDatasourceFilters: string[] | null) {
return listOfDatasourceFilters && listOfDatasourceFilters.length > 0
? queries.filter(q => listOfDatasourceFilters.includes(q.datasourceName))
: queries;
}
export function filterQueriesByTime(queries: RichHistoryQuery[], timeFilter: [number, number]) {
return queries.filter(
q =>
q.ts < createRetentionPeriodBoundary(timeFilter[0], true) &&
q.ts > createRetentionPeriodBoundary(timeFilter[1], false)
);
}
export function filterAndSortQueries(
queries: RichHistoryQuery[],
sortOrder: SortOrder,
listOfDatasourceFilters: string[] | null,
searchFilter: string,
timeFilter?: [number, number]
) {
const filteredQueriesByDs = filterQueriesByDataSource(queries, listOfDatasourceFilters);
const filteredQueriesByDsAndSearchFilter = filterQueriesBySearchFilter(filteredQueriesByDs, searchFilter);
const filteredQueriesToBeSorted = timeFilter
? filterQueriesByTime(filteredQueriesByDsAndSearchFilter, timeFilter)
: filteredQueriesByDsAndSearchFilter;
return sortQueries(filteredQueriesToBeSorted, sortOrder);
}
/* These functions are created to migrate string queries (from 6.7 release) to DataQueries. They can be removed after 7.1 release. */
function migrateRichHistory(richHistory: RichHistoryQuery[]) {
const transformedRichHistory = richHistory.map(query => {
const transformedQueries: DataQuery[] = query.queries.map((q, index) => createDataQuery(query, q, index));
return { ...query, queries: transformedQueries };
});
return transformedRichHistory;
}
function createDataQuery(query: RichHistoryQuery, individualQuery: DataQuery | string, index: number) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVXYZ';
if (typeof individualQuery === 'object') {
return individualQuery;
} else if (isParsable(individualQuery)) {
return JSON.parse(individualQuery);
}
return { expr: individualQuery, refId: letters[index] };
}
function isParsable(string: string) {
try {
JSON.parse(string);
} catch (e) {
return false;
}
return true;
}