grafana/public/app/core/utils/richHistory.ts
Ivana Huckova 62c824e3a4
Explore: Rich History (#22570)
* Explore: Refactor active buttons css

* Explore: Add query history button

* WIP: Creating drawer

* WIP: Create custom drawer (for now)

* Revert changes to Drawer.tsx

* WIP: Layout finished

* Rich History: Set up boilerplate for Settings

* WIP: Query history cards

* Refactor, split components

* Add resizability, interactivity

* Save history to local storage

* Visualise queries from queryhistory local storage

* Set up query history settings

* Refactor

* Create link, un-refactored verison

* Copyable url

* WIP: Add slider

* Commenting feature

* Slider filtration

* Add headings

* Hide Rich history behind feature toggle

* Cleaning up, refactors

* Update tests

* Implement getQueryDisplayText

* Update lockfile for new dependencies

* Update heading based on sorting

* Fix typescript strinctNullCheck errors

* Fix Forms, new forms

* Fixes based on provided feedback

* Fixes, splitting component into two

* Add tooltips, add delete history button

* Clicking on card adds queries to query rows

* Delete history, width of drawers

* UI/UX changes, updates

* Add number of queries to headings, box shadows

* Fix slider, remove feature toggle

* Fix typo in the beta announcement

* Fix how the rich history state is rendered when initialization

* Move updateFilters when activeDatasourceFilter onlyto RichHistory, remove duplicated code

* Fix typescript strictnull errors, not used variables errors
2020-03-10 15:08:15 +01:00

254 lines
7.7 KiB
TypeScript

// Libraries
import _ from 'lodash';
// Services & Utils
import { DataQuery, ExploreMode } from '@grafana/data';
import { renderUrl } from 'app/core/utils/url';
import store from 'app/core/store';
import { serializeStateToUrlParam, SortOrder } from './explore';
// Types
import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore';
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
export const RICH_HISTORY_SETTING_KEYS = {
retentionPeriod: `${RICH_HISTORY_KEY}.retentionPeriod`,
starredTabAsFirstTab: `${RICH_HISTORY_KEY}.starredTabAsFirstTab`,
activeDatasourceOnly: `${RICH_HISTORY_KEY}.activeDatasourceOnly`,
};
/*
* 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: string[],
starred: boolean,
comment: string | null,
sessionName: string
): any {
const ts = Date.now();
/* Save only queries, that are not falsy (e.g. empty strings, null) */
const queriesToSave = queries.filter(expr => Boolean(expr));
const retentionPeriod = 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 exmpty array
*/
const queriesToKeep = richHistory.filter(q => q.ts > retentionPeriodLastTs || q.starred === true) || [];
if (queriesToSave.length > 0) {
if (
/* Don't save duplicated queries for the same datasource */
queriesToKeep.length > 0 &&
JSON.stringify(queriesToSave) === JSON.stringify(queriesToKeep[0].queries) &&
JSON.stringify(datasourceName) === JSON.stringify(queriesToKeep[0].datasourceName)
) {
return richHistory;
}
let newHistory = [
{ queries: queriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
...queriesToKeep,
];
/* Combine all queries of a datasource type into one rich history */
store.setObject(RICH_HISTORY_KEY, newHistory);
return newHistory;
}
return richHistory;
}
export function getRichHistory() {
return store.getObject(RICH_HISTORY_KEY, []);
}
export function deleteAllFromRichHistory() {
return store.delete(RICH_HISTORY_KEY);
}
export function updateStarredInRichHistory(richHistory: RichHistoryQuery[], ts: number) {
const updatedQueries = 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;
});
store.setObject(RICH_HISTORY_KEY, updatedQueries);
return updatedQueries;
}
export function updateCommentInRichHistory(
richHistory: RichHistoryQuery[],
ts: number,
newComment: string | undefined
) {
const updatedQueries = richHistory.map(query => {
if (query.ts === ts) {
const updatedQuery = Object.assign({}, query, { comment: newComment });
return updatedQuery;
}
return query;
});
store.setObject(RICH_HISTORY_KEY, updatedQueries);
return updatedQueries;
}
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 copyStringToClipboard = (string: string) => {
const el = document.createElement('textarea');
el.value = string;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const queries = query.queries.map(query => ({ expr: query }));
const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */
range: { from: 'now-1h', to: 'now' },
datasource: query.datasourceName,
queries,
/* Default mode. In the future, we can also save the query mode */
mode: query.datasourceId === 'loki' ? ExploreMode.Logs : ExploreMode.Metrics,
ui: {
showingGraph: true,
showingLogs: true,
showingTable: true,
},
context: 'explore',
};
const serializedState = serializeStateToUrlParam(exploreState, true);
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)[0];
const url = 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) {
const date = new Date(ts);
const month = date.toLocaleString('default', { month: 'long' });
const day = date.getDate();
return `${month} ${day}`;
}
export function getQueryDisplayText(query: DataQuery): string {
return JSON.stringify(query);
}
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 isParsable(string: string) {
try {
JSON.parse(string);
} catch (e) {
return false;
}
return true;
}
export function createDataQuery(query: RichHistoryQuery, queryString: string, index: number) {
let dataQuery;
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
isParsable(queryString)
? (dataQuery = JSON.parse(queryString))
: (dataQuery = { expr: queryString, refId: letters[index], datasource: query.datasourceName });
return dataQuery;
}
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;
}