grafana/public/app/core/utils/richHistory.ts
Kristina bbe9c8661a
Explore: Use rich history local storage for autocomplete (#81386)
* move autocomplete logic

* Tests

* Write to correct history

* Remove historyUpdatedAction and related code

* add helpful comments

* Add option to mute all errors/warnings for autocomplete

* Add back in legacy local storage query history for transition period

* Move params to an object for easier use and defaults

* Do not make time filter required

* fix tests

* change deprecation version and add issue number
2024-02-23 09:44:21 -06:00

288 lines
9.1 KiB
TypeScript

import { omit } from 'lodash';
import { DataQuery, DataSourceApi, dateTimeFormat, ExploreUrlState, urlUtil } from '@grafana/data';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { getDataSourceSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createWarningNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
import { dispatch } from 'app/store/store';
import { RichHistoryQuery } from 'app/types/explore';
import {
RichHistoryResults,
RichHistoryServiceError,
RichHistoryStorageWarning,
RichHistoryStorageWarningDetails,
} from '../history/RichHistoryStorage';
import { getLocalRichHistoryStorage, getRichHistoryStorage } from '../history/richHistoryStorageProvider';
import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from './richHistoryTypes';
export { RichHistorySearchFilters, RichHistorySettings, SortOrder };
/*
* Add queries to rich history. Save only queries within the retention period, or that are starred.
* Side-effect: store history in local storage
*/
type addToRichHistoryParams = {
localOverride: boolean;
datasource: { uid: string; name?: string };
queries: DataQuery[];
starred: boolean;
comment?: string;
showNotif?: {
quotaExceededError?: boolean;
limitExceededWarning?: boolean;
otherErrors?: boolean;
};
};
export async function addToRichHistory(
params: addToRichHistoryParams
): Promise<{ richHistoryStorageFull?: boolean; limitExceeded?: boolean }> {
const { queries, localOverride, datasource, starred, comment, showNotif } = params;
// default showing of errors to true
const showQuotaExceededError = showNotif?.quotaExceededError ?? true;
const showLimitExceededWarning = showNotif?.limitExceededWarning ?? true;
const showOtherErrors = showNotif?.otherErrors ?? true;
/* Save only queries, that are not falsy (e.g. empty object, null, ...) */
const newQueriesToSave: DataQuery[] = queries && queries.filter((query) => notEmptyQuery(query));
if (newQueriesToSave.length > 0) {
let richHistoryStorageFull = false;
let limitExceeded = false;
let warning: RichHistoryStorageWarningDetails | undefined;
try {
// for autocomplete we want to ensure writing to local storage
const storage = localOverride ? getLocalRichHistoryStorage() : getRichHistoryStorage();
const result = await storage.addToRichHistory({
datasourceUid: datasource.uid,
datasourceName: datasource.name ?? '',
queries: newQueriesToSave,
starred,
comment: comment ?? '',
});
warning = result.warning;
} catch (error) {
if (error instanceof Error) {
if (error.name === RichHistoryServiceError.StorageFull) {
richHistoryStorageFull = true;
showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message)));
} else if (showOtherErrors && error.name !== RichHistoryServiceError.DuplicatedEntry) {
dispatch(
notifyApp(
createErrorNotification(
t('explore.rich-history-utils-notification.update-failed', 'Rich History update failed'),
error.message
)
)
);
}
}
// Saving failed. Do not add new entry.
return { richHistoryStorageFull, limitExceeded };
}
// Limit exceeded but new entry was added. Notify that old entries have been removed.
if (warning && warning.type === RichHistoryStorageWarning.LimitExceeded) {
limitExceeded = true;
showLimitExceededWarning && dispatch(notifyApp(createWarningNotification(warning.message)));
}
return { richHistoryStorageFull, limitExceeded };
}
// Nothing to change
return {};
}
export async function getRichHistory(filters: RichHistorySearchFilters): Promise<RichHistoryResults> {
return await getRichHistoryStorage().getRichHistory(filters);
}
export async function updateRichHistorySettings(settings: RichHistorySettings): Promise<void> {
await getRichHistoryStorage().updateSettings(settings);
}
export async function getRichHistorySettings(): Promise<RichHistorySettings> {
return await getRichHistoryStorage().getSettings();
}
export async function deleteAllFromRichHistory(): Promise<void> {
return getRichHistoryStorage().deleteAll();
}
export async function updateStarredInRichHistory(id: string, starred: boolean) {
try {
return await getRichHistoryStorage().updateStarred(id, starred);
} catch (error) {
if (error instanceof Error) {
dispatch(
notifyApp(
createErrorNotification(
t('explore.rich-history-utils-notification.saving-failed', 'Saving rich history failed'),
error.message
)
)
);
}
return undefined;
}
}
export async function updateCommentInRichHistory(id: string, newComment: string | undefined) {
try {
return await getRichHistoryStorage().updateComment(id, newComment);
} catch (error) {
if (error instanceof Error) {
dispatch(
notifyApp(
createErrorNotification(
t('explore.rich-history-utils-notification.saving-failed', 'Saving rich history failed'),
error.message
)
)
);
}
return undefined;
}
}
export async function deleteQueryInRichHistory(id: string) {
try {
await getRichHistoryStorage().deleteRichHistory(id);
return id;
} catch (error) {
if (error instanceof Error) {
dispatch(
notifyApp(
createErrorNotification(
t('explore.rich-history-utils-notification.saving-failed', 'Saving rich history failed'),
error.message
)
)
);
}
return undefined;
}
}
export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */
range: {
from: t('explore.rich-history-utils.default-from', 'now-1h'),
to: t('explore.rich-history-utils.default-to', 'now'),
},
datasource: query.datasourceName,
queries: query.queries,
};
const serializedState = serializeStateToUrlParam(exploreState);
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 = t('explore.rich-history-utils.today', 'today');
break;
case 1:
str = t('explore.rich-history-utils.yesterday', 'yesterday');
break;
case 7:
str = t('explore.rich-history-utils.a-week-ago', 'a week ago');
break;
case 14:
str = t('explore.rich-history-utils.two-weeks-ago', 'two weeks ago');
break;
default:
str = t('explore.rich-history-utils.days-ago', '{{num}} days ago', { num: `${num}` });
}
return str;
};
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.createdAt);
}
return heading;
}
export function createQueryText(query: DataQuery, dsApi?: DataSourceApi) {
if (dsApi?.getQueryDisplayText) {
return dsApi.getQueryDisplayText(query);
}
return getQueryDisplayText(query);
}
export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortOrder) {
let mappedQueriesToHeadings: Record<string, RichHistoryQuery[]> = {};
query.forEach((q) => {
let heading = createQueryHeading(q, sortOrder);
if (!(heading in mappedQueriesToHeadings)) {
mappedQueriesToHeadings[heading] = [q];
} else {
mappedQueriesToHeadings[heading] = [...mappedQueriesToHeadings[heading], q];
}
});
return mappedQueriesToHeadings;
}
/*
* Create a list of all available data sources
*/
export function createDatasourcesList() {
return getDataSourceSrv()
.getList({ mixed: true })
.map((dsSettings) => {
return {
name: dsSettings.name,
uid: dsSettings.uid,
};
});
}
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;
}