mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Improve local storage error handling when rich history is added (#39943)
* Rich History: improve local storage error handling * Reduce number of max items and update docs * Rotate not-starred items and add tests * Add missing property to initial state * Unify date in richHistory tests * Show a warning message that rich history limit has been reached * Add missing param
This commit is contained in:
parent
08a20e2247
commit
ad757b48e7
@ -41,7 +41,9 @@ export class Store {
|
|||||||
this.set(key, json);
|
this.set(key, json);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Likely hitting storage quota
|
// Likely hitting storage quota
|
||||||
throw new Error(`Could not save item in localStorage: ${key}. [${error}]`);
|
const errorToThrow = new Error(`Could not save item in localStorage: ${key}. [${error}]`);
|
||||||
|
errorToThrow.name = error.name;
|
||||||
|
throw errorToThrow;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,11 @@ import {
|
|||||||
deleteQueryInRichHistory,
|
deleteQueryInRichHistory,
|
||||||
filterAndSortQueries,
|
filterAndSortQueries,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
|
MAX_HISTORY_ITEMS,
|
||||||
} from './richHistory';
|
} from './richHistory';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { dateTime, DataQuery } from '@grafana/data';
|
import { dateTime, DataQuery } from '@grafana/data';
|
||||||
|
import { RichHistoryQuery } from '../../types';
|
||||||
|
|
||||||
const mock: any = {
|
const mock: any = {
|
||||||
storedHistory: [
|
storedHistory: [
|
||||||
@ -41,177 +43,235 @@ const mock: any = {
|
|||||||
|
|
||||||
const key = 'grafana.explore.richHistory';
|
const key = 'grafana.explore.richHistory';
|
||||||
|
|
||||||
describe('addToRichHistory', () => {
|
describe('richHistory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
deleteAllFromRichHistory();
|
jest.useFakeTimers('modern');
|
||||||
expect(store.exists(key)).toBeFalsy();
|
jest.setSystemTime(new Date(1970, 0, 1));
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedResult = [
|
afterEach(() => {
|
||||||
{
|
jest.useRealTimers();
|
||||||
comment: mock.testComment,
|
|
||||||
datasourceId: mock.testDatasourceId,
|
|
||||||
datasourceName: mock.testDatasourceName,
|
|
||||||
queries: mock.testQueries,
|
|
||||||
sessionName: mock.testSessionName,
|
|
||||||
starred: mock.testStarred,
|
|
||||||
ts: 2,
|
|
||||||
},
|
|
||||||
mock.storedHistory[0],
|
|
||||||
];
|
|
||||||
|
|
||||||
it('should append query to query history', () => {
|
|
||||||
Date.now = jest.fn(() => 2);
|
|
||||||
const newHistory = addToRichHistory(
|
|
||||||
mock.storedHistory,
|
|
||||||
mock.testDatasourceId,
|
|
||||||
mock.testDatasourceName,
|
|
||||||
mock.testQueries,
|
|
||||||
mock.testStarred,
|
|
||||||
mock.testComment,
|
|
||||||
mock.testSessionName
|
|
||||||
);
|
|
||||||
expect(newHistory).toEqual(expectedResult);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save query history to localStorage', () => {
|
describe('addToRichHistory', () => {
|
||||||
Date.now = jest.fn(() => 2);
|
beforeEach(() => {
|
||||||
|
deleteAllFromRichHistory();
|
||||||
|
expect(store.exists(key)).toBeFalsy();
|
||||||
|
});
|
||||||
|
const expectedResult = [
|
||||||
|
{
|
||||||
|
comment: mock.testComment,
|
||||||
|
datasourceId: mock.testDatasourceId,
|
||||||
|
datasourceName: mock.testDatasourceName,
|
||||||
|
queries: mock.testQueries,
|
||||||
|
sessionName: mock.testSessionName,
|
||||||
|
starred: mock.testStarred,
|
||||||
|
ts: 2,
|
||||||
|
},
|
||||||
|
mock.storedHistory[0],
|
||||||
|
];
|
||||||
|
|
||||||
addToRichHistory(
|
it('should append query to query history', () => {
|
||||||
mock.storedHistory,
|
Date.now = jest.fn(() => 2);
|
||||||
mock.testDatasourceId,
|
const { richHistory: newHistory } = addToRichHistory(
|
||||||
mock.testDatasourceName,
|
mock.storedHistory,
|
||||||
mock.testQueries,
|
mock.testDatasourceId,
|
||||||
mock.testStarred,
|
mock.testDatasourceName,
|
||||||
mock.testComment,
|
mock.testQueries,
|
||||||
mock.testSessionName
|
mock.testStarred,
|
||||||
);
|
mock.testComment,
|
||||||
expect(store.exists(key)).toBeTruthy();
|
mock.testSessionName,
|
||||||
expect(store.getObject(key)).toMatchObject(expectedResult);
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(newHistory).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save query history to localStorage', () => {
|
||||||
|
Date.now = jest.fn(() => 2);
|
||||||
|
|
||||||
|
addToRichHistory(
|
||||||
|
mock.storedHistory,
|
||||||
|
mock.testDatasourceId,
|
||||||
|
mock.testDatasourceName,
|
||||||
|
mock.testQueries,
|
||||||
|
mock.testStarred,
|
||||||
|
mock.testComment,
|
||||||
|
mock.testSessionName,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(store.exists(key)).toBeTruthy();
|
||||||
|
expect(store.getObject(key)).toMatchObject(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not append duplicated query to query history', () => {
|
||||||
|
Date.now = jest.fn(() => 2);
|
||||||
|
const { richHistory: newHistory } = addToRichHistory(
|
||||||
|
mock.storedHistory,
|
||||||
|
mock.storedHistory[0].datasourceId,
|
||||||
|
mock.storedHistory[0].datasourceName,
|
||||||
|
[{ expr: 'query1', maxLines: null, refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
||||||
|
mock.testStarred,
|
||||||
|
mock.testComment,
|
||||||
|
mock.testSessionName,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(newHistory).toEqual([mock.storedHistory[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not save duplicated query to localStorage', () => {
|
||||||
|
Date.now = jest.fn(() => 2);
|
||||||
|
addToRichHistory(
|
||||||
|
mock.storedHistory,
|
||||||
|
mock.storedHistory[0].datasourceId,
|
||||||
|
mock.storedHistory[0].datasourceName,
|
||||||
|
[{ expr: 'query1', maxLines: null, refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
||||||
|
mock.testStarred,
|
||||||
|
mock.testComment,
|
||||||
|
mock.testSessionName,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(store.exists(key)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not save more than MAX_HISTORY_ITEMS', () => {
|
||||||
|
Date.now = jest.fn(() => 2);
|
||||||
|
const extraItems = 100;
|
||||||
|
|
||||||
|
// the history has more than MAX
|
||||||
|
let history = [];
|
||||||
|
// history = [ { starred: true, comment: "0" }, { starred: false, comment: "1" }, ... ]
|
||||||
|
for (let i = 0; i < MAX_HISTORY_ITEMS + extraItems; i++) {
|
||||||
|
history.push({
|
||||||
|
starred: i % 2 === 0,
|
||||||
|
comment: i.toString(),
|
||||||
|
queries: [],
|
||||||
|
ts: new Date(2019, 11, 31).getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const starredItemsInHistory = (MAX_HISTORY_ITEMS + extraItems) / 2;
|
||||||
|
const notStarredItemsInHistory = (MAX_HISTORY_ITEMS + extraItems) / 2;
|
||||||
|
|
||||||
|
expect(history.filter((h) => h.starred)).toHaveLength(starredItemsInHistory);
|
||||||
|
expect(history.filter((h) => !h.starred)).toHaveLength(notStarredItemsInHistory);
|
||||||
|
|
||||||
|
const { richHistory: newHistory } = addToRichHistory(
|
||||||
|
(history as any) as RichHistoryQuery[],
|
||||||
|
mock.storedHistory[0].datasourceId,
|
||||||
|
mock.storedHistory[0].datasourceName,
|
||||||
|
[{ expr: 'query1', maxLines: null, refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
||||||
|
true,
|
||||||
|
mock.testComment,
|
||||||
|
mock.testSessionName,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// one not starred replaced with a newly added starred item
|
||||||
|
const removedNotStarredItems = extraItems + 1; // + 1 to make space for the new item
|
||||||
|
expect(newHistory.filter((h) => h.starred)).toHaveLength(starredItemsInHistory + 1); // starred item added
|
||||||
|
expect(newHistory.filter((h) => !h.starred)).toHaveLength(starredItemsInHistory - removedNotStarredItems);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not append duplicated query to query history', () => {
|
describe('updateStarredInRichHistory', () => {
|
||||||
Date.now = jest.fn(() => 2);
|
it('should update starred in query in history', () => {
|
||||||
const newHistory = addToRichHistory(
|
const updatedStarred = updateStarredInRichHistory(mock.storedHistory, 1);
|
||||||
mock.storedHistory,
|
expect(updatedStarred[0].starred).toEqual(false);
|
||||||
mock.storedHistory[0].datasourceId,
|
});
|
||||||
mock.storedHistory[0].datasourceName,
|
it('should update starred in localStorage', () => {
|
||||||
[{ expr: 'query1', maxLines: null, refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
updateStarredInRichHistory(mock.storedHistory, 1);
|
||||||
mock.testStarred,
|
expect(store.exists(key)).toBeTruthy();
|
||||||
mock.testComment,
|
expect(store.getObject(key)[0].starred).toEqual(false);
|
||||||
mock.testSessionName
|
});
|
||||||
);
|
|
||||||
expect(newHistory).toEqual([mock.storedHistory[0]]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save duplicated query to localStorage', () => {
|
describe('updateCommentInRichHistory', () => {
|
||||||
Date.now = jest.fn(() => 2);
|
it('should update comment in query in history', () => {
|
||||||
addToRichHistory(
|
const updatedComment = updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
|
||||||
mock.storedHistory,
|
expect(updatedComment[0].comment).toEqual('new comment');
|
||||||
mock.storedHistory[0].datasourceId,
|
});
|
||||||
mock.storedHistory[0].datasourceName,
|
it('should update comment in localStorage', () => {
|
||||||
[{ expr: 'query1', maxLines: null, refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
|
||||||
mock.testStarred,
|
expect(store.exists(key)).toBeTruthy();
|
||||||
mock.testComment,
|
expect(store.getObject(key)[0].comment).toEqual('new comment');
|
||||||
mock.testSessionName
|
});
|
||||||
);
|
});
|
||||||
expect(store.exists(key)).toBeFalsy();
|
|
||||||
});
|
describe('deleteQueryInRichHistory', () => {
|
||||||
});
|
it('should delete query in query in history', () => {
|
||||||
|
const deletedHistory = deleteQueryInRichHistory(mock.storedHistory, 1);
|
||||||
describe('updateStarredInRichHistory', () => {
|
expect(deletedHistory).toEqual([]);
|
||||||
it('should update starred in query in history', () => {
|
});
|
||||||
const updatedStarred = updateStarredInRichHistory(mock.storedHistory, 1);
|
it('should delete query in localStorage', () => {
|
||||||
expect(updatedStarred[0].starred).toEqual(false);
|
deleteQueryInRichHistory(mock.storedHistory, 1);
|
||||||
});
|
expect(store.exists(key)).toBeTruthy();
|
||||||
it('should update starred in localStorage', () => {
|
expect(store.getObject(key)).toEqual([]);
|
||||||
updateStarredInRichHistory(mock.storedHistory, 1);
|
});
|
||||||
expect(store.exists(key)).toBeTruthy();
|
});
|
||||||
expect(store.getObject(key)[0].starred).toEqual(false);
|
|
||||||
});
|
describe('mapNumbertoTimeInSlider', () => {
|
||||||
});
|
it('should correctly map number to value', () => {
|
||||||
|
const value = mapNumbertoTimeInSlider(25);
|
||||||
describe('updateCommentInRichHistory', () => {
|
expect(value).toEqual('25 days ago');
|
||||||
it('should update comment in query in history', () => {
|
});
|
||||||
const updatedComment = updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
|
});
|
||||||
expect(updatedComment[0].comment).toEqual('new comment');
|
|
||||||
});
|
describe('createDateStringFromTs', () => {
|
||||||
it('should update comment in localStorage', () => {
|
it('should correctly create string value from timestamp', () => {
|
||||||
updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
|
const value = createDateStringFromTs(1583932327000);
|
||||||
expect(store.exists(key)).toBeTruthy();
|
expect(value).toEqual('March 11');
|
||||||
expect(store.getObject(key)[0].comment).toEqual('new comment');
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe('filterQueries', () => {
|
||||||
describe('deleteQueryInRichHistory', () => {
|
it('should filter out queries based on data source filter', () => {
|
||||||
it('should delete query in query in history', () => {
|
const filteredQueries = filterAndSortQueries(
|
||||||
const deletedHistory = deleteQueryInRichHistory(mock.storedHistory, 1);
|
mock.storedHistory,
|
||||||
expect(deletedHistory).toEqual([]);
|
SortOrder.Ascending,
|
||||||
});
|
['not provided data source'],
|
||||||
it('should delete query in localStorage', () => {
|
''
|
||||||
deleteQueryInRichHistory(mock.storedHistory, 1);
|
);
|
||||||
expect(store.exists(key)).toBeTruthy();
|
expect(filteredQueries).toHaveLength(0);
|
||||||
expect(store.getObject(key)).toEqual([]);
|
});
|
||||||
});
|
it('should keep queries based on data source filter', () => {
|
||||||
});
|
const filteredQueries = filterAndSortQueries(
|
||||||
|
mock.storedHistory,
|
||||||
describe('mapNumbertoTimeInSlider', () => {
|
SortOrder.Ascending,
|
||||||
it('should correctly map number to value', () => {
|
['datasource history name'],
|
||||||
const value = mapNumbertoTimeInSlider(25);
|
''
|
||||||
expect(value).toEqual('25 days ago');
|
);
|
||||||
});
|
expect(filteredQueries).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
it('should filter out all queries based on search filter', () => {
|
||||||
describe('createDateStringFromTs', () => {
|
const filteredQueries = filterAndSortQueries(
|
||||||
it('should correctly create string value from timestamp', () => {
|
mock.storedHistory,
|
||||||
const value = createDateStringFromTs(1583932327000);
|
SortOrder.Ascending,
|
||||||
expect(value).toEqual('March 11');
|
[],
|
||||||
});
|
'i do not exist in query'
|
||||||
});
|
);
|
||||||
|
expect(filteredQueries).toHaveLength(0);
|
||||||
describe('filterQueries', () => {
|
});
|
||||||
it('should filter out queries based on data source filter', () => {
|
it('should include queries based on search filter', () => {
|
||||||
const filteredQueries = filterAndSortQueries(
|
const filteredQueries = filterAndSortQueries(mock.storedHistory, SortOrder.Ascending, [], 'query1');
|
||||||
mock.storedHistory,
|
expect(filteredQueries).toHaveLength(1);
|
||||||
SortOrder.Ascending,
|
});
|
||||||
['not provided data source'],
|
});
|
||||||
''
|
|
||||||
);
|
describe('createQueryHeading', () => {
|
||||||
expect(filteredQueries).toHaveLength(0);
|
it('should correctly create heading for queries when sort order is ascending ', () => {
|
||||||
});
|
// Have to offset the timezone of a 1 microsecond epoch, and then reverse the changes
|
||||||
it('should keep queries based on data source filter', () => {
|
mock.storedHistory[0].ts = 1 + -1 * dateTime().utcOffset() * 60 * 1000;
|
||||||
const filteredQueries = filterAndSortQueries(
|
const heading = createQueryHeading(mock.storedHistory[0], SortOrder.Ascending);
|
||||||
mock.storedHistory,
|
expect(heading).toEqual('January 1');
|
||||||
SortOrder.Ascending,
|
});
|
||||||
['datasource history name'],
|
it('should correctly create heading for queries when sort order is datasourceAZ ', () => {
|
||||||
''
|
const heading = createQueryHeading(mock.storedHistory[0], SortOrder.DatasourceAZ);
|
||||||
);
|
expect(heading).toEqual(mock.storedHistory[0].datasourceName);
|
||||||
expect(filteredQueries).toHaveLength(1);
|
});
|
||||||
});
|
|
||||||
it('should filter out all queries based on search filter', () => {
|
|
||||||
const filteredQueries = filterAndSortQueries(
|
|
||||||
mock.storedHistory,
|
|
||||||
SortOrder.Ascending,
|
|
||||||
[],
|
|
||||||
'i do not exist in query'
|
|
||||||
);
|
|
||||||
expect(filteredQueries).toHaveLength(0);
|
|
||||||
});
|
|
||||||
it('should include queries based on search filter', () => {
|
|
||||||
const filteredQueries = filterAndSortQueries(mock.storedHistory, SortOrder.Ascending, [], 'query1');
|
|
||||||
expect(filteredQueries).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createQueryHeading', () => {
|
|
||||||
it('should correctly create heading for queries when sort order is ascending ', () => {
|
|
||||||
// Have to offset the timezone of a 1 microsecond epoch, and then reverse the changes
|
|
||||||
mock.storedHistory[0].ts = 1 + -1 * dateTime().utcOffset() * 60 * 1000;
|
|
||||||
const heading = createQueryHeading(mock.storedHistory[0], SortOrder.Ascending);
|
|
||||||
expect(heading).toEqual('January 1');
|
|
||||||
});
|
|
||||||
it('should correctly create heading for queries when sort order is datasourceAZ ', () => {
|
|
||||||
const heading = createQueryHeading(mock.storedHistory[0], SortOrder.DatasourceAZ);
|
|
||||||
expect(heading).toEqual(mock.storedHistory[0].datasourceName);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,7 @@ import { DataQuery, DataSourceApi, dateTimeFormat, urlUtil, ExploreUrlState } fr
|
|||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
import { createErrorNotification, createWarningNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { RichHistoryQuery } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
@ -34,6 +34,8 @@ export enum SortOrder {
|
|||||||
* Side-effect: store history in local storage
|
* Side-effect: store history in local storage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const MAX_HISTORY_ITEMS = 10000;
|
||||||
|
|
||||||
export function addToRichHistory(
|
export function addToRichHistory(
|
||||||
richHistory: RichHistoryQuery[],
|
richHistory: RichHistoryQuery[],
|
||||||
datasourceId: string,
|
datasourceId: string,
|
||||||
@ -41,8 +43,10 @@ export function addToRichHistory(
|
|||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
starred: boolean,
|
starred: boolean,
|
||||||
comment: string | null,
|
comment: string | null,
|
||||||
sessionName: string
|
sessionName: string,
|
||||||
): any {
|
showQuotaExceededError: boolean,
|
||||||
|
showLimitExceededWarning: boolean
|
||||||
|
): { richHistory: RichHistoryQuery[]; localStorageFull?: boolean; limitExceeded?: boolean } {
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
/* Save only queries, that are not falsy (e.g. empty object, null, ...) */
|
/* Save only queries, that are not falsy (e.g. empty object, null, ...) */
|
||||||
const newQueriesToSave: DataQuery[] = queries && queries.filter((query) => notEmptyQuery(query));
|
const newQueriesToSave: DataQuery[] = queries && queries.filter((query) => notEmptyQuery(query));
|
||||||
@ -66,24 +70,53 @@ export function addToRichHistory(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isEqual(newQueriesToCompare, lastQueriesToCompare)) {
|
if (isEqual(newQueriesToCompare, lastQueriesToCompare)) {
|
||||||
return richHistory;
|
return { richHistory };
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedHistory = [
|
// remove oldest non-starred items to give space for the recent query
|
||||||
{ queries: newQueriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
|
let limitExceeded = false;
|
||||||
|
let current = queriesToKeep.length - 1;
|
||||||
|
while (current >= 0 && queriesToKeep.length >= MAX_HISTORY_ITEMS) {
|
||||||
|
if (!queriesToKeep[current].starred) {
|
||||||
|
queriesToKeep.splice(current, 1);
|
||||||
|
limitExceeded = true;
|
||||||
|
}
|
||||||
|
current--;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedHistory: RichHistoryQuery[] = [
|
||||||
|
{
|
||||||
|
queries: newQueriesToSave,
|
||||||
|
ts,
|
||||||
|
datasourceId,
|
||||||
|
datasourceName: datasourceName ?? '',
|
||||||
|
starred,
|
||||||
|
comment: comment ?? '',
|
||||||
|
sessionName,
|
||||||
|
},
|
||||||
...queriesToKeep,
|
...queriesToKeep,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
showLimitExceededWarning &&
|
||||||
|
limitExceeded &&
|
||||||
|
dispatch(
|
||||||
|
notifyApp(
|
||||||
|
createWarningNotification(
|
||||||
|
`Query history reached the limit of ${MAX_HISTORY_ITEMS}. Old, not-starred items will be removed.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||||
return updatedHistory;
|
return { richHistory: updatedHistory, limitExceeded, localStorageFull: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message)));
|
showQuotaExceededError &&
|
||||||
return richHistory;
|
dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message)));
|
||||||
|
return { richHistory: updatedHistory, limitExceeded, localStorageFull: error.name === 'QuotaExceededError' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return richHistory;
|
return { richHistory };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRichHistory(): RichHistoryQuery[] {
|
export function getRichHistory(): RichHistoryQuery[] {
|
||||||
|
@ -49,6 +49,8 @@ function setup(queries: DataQuery[]) {
|
|||||||
right: undefined,
|
right: undefined,
|
||||||
richHistory: [],
|
richHistory: [],
|
||||||
autoLoadLogsVolume: false,
|
autoLoadLogsVolume: false,
|
||||||
|
localStorageFull: false,
|
||||||
|
richHistoryLimitExceededWarningShown: false,
|
||||||
};
|
};
|
||||||
const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState });
|
const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState });
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { ShowConfirmModalEvent } from '../../../types/events';
|
|||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { MAX_HISTORY_ITEMS } from '../../../core/utils/richHistory';
|
||||||
|
|
||||||
export interface RichHistorySettingsProps {
|
export interface RichHistorySettingsProps {
|
||||||
retentionPeriod: number;
|
retentionPeriod: number;
|
||||||
@ -80,7 +81,7 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Field
|
<Field
|
||||||
label="History time span"
|
label="History time span"
|
||||||
description="Select the period of time for which Grafana will save your query history"
|
description={`Select the period of time for which Grafana will save your query history. Up to ${MAX_HISTORY_ITEMS} entries will be stored.`}
|
||||||
className="space-between"
|
className="space-between"
|
||||||
>
|
>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
|
@ -21,6 +21,8 @@ export interface SyncTimesPayload {
|
|||||||
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
|
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
|
||||||
|
|
||||||
export const richHistoryUpdatedAction = createAction<any>('explore/richHistoryUpdated');
|
export const richHistoryUpdatedAction = createAction<any>('explore/richHistoryUpdated');
|
||||||
|
export const localStorageFullAction = createAction('explore/localStorageFullAction');
|
||||||
|
export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores new value of auto-load logs volume switch. Used only internally. changeAutoLogsVolume() is used to
|
* Stores new value of auto-load logs volume switch. Used only internally. changeAutoLogsVolume() is used to
|
||||||
@ -172,6 +174,8 @@ export const initialExploreState: ExploreState = {
|
|||||||
left: initialExploreItemState,
|
left: initialExploreItemState,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
richHistory: [],
|
richHistory: [],
|
||||||
|
localStorageFull: false,
|
||||||
|
richHistoryLimitExceededWarningShown: false,
|
||||||
autoLoadLogsVolume: store.getBool(AUTO_LOAD_LOGS_VOLUME_SETTING_KEY, false),
|
autoLoadLogsVolume: store.getBool(AUTO_LOAD_LOGS_VOLUME_SETTING_KEY, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -227,6 +231,20 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localStorageFullAction.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
localStorageFull: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (richHistoryLimitExceededAction.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
richHistoryLimitExceededWarningShown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (storeAutoLoadLogsVolumeAction.match(action)) {
|
if (storeAutoLoadLogsVolumeAction.match(action)) {
|
||||||
const autoLoadLogsVolume = action.payload;
|
const autoLoadLogsVolume = action.payload;
|
||||||
return {
|
return {
|
||||||
|
@ -32,7 +32,13 @@ import { notifyApp } from '../../../core/actions';
|
|||||||
import { runRequest } from '../../query/state/runRequest';
|
import { runRequest } from '../../query/state/runRequest';
|
||||||
import { decorateData } from '../utils/decorators';
|
import { decorateData } from '../utils/decorators';
|
||||||
import { createErrorNotification } from '../../../core/copy/appNotification';
|
import { createErrorNotification } from '../../../core/copy/appNotification';
|
||||||
import { richHistoryUpdatedAction, stateSave, storeAutoLoadLogsVolumeAction } from './main';
|
import {
|
||||||
|
localStorageFullAction,
|
||||||
|
richHistoryLimitExceededAction,
|
||||||
|
richHistoryUpdatedAction,
|
||||||
|
stateSave,
|
||||||
|
storeAutoLoadLogsVolumeAction,
|
||||||
|
} from './main';
|
||||||
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
|
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { updateTime } from './time';
|
import { updateTime } from './time';
|
||||||
import { historyUpdatedAction } from './history';
|
import { historyUpdatedAction } from './history';
|
||||||
@ -415,17 +421,25 @@ export const runQueries = (
|
|||||||
if (!data.error && firstResponse) {
|
if (!data.error && firstResponse) {
|
||||||
// Side-effect: Saving history in localstorage
|
// Side-effect: Saving history in localstorage
|
||||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
const nextRichHistory = addToRichHistory(
|
const { richHistory: nextRichHistory, localStorageFull, limitExceeded } = addToRichHistory(
|
||||||
richHistory || [],
|
richHistory || [],
|
||||||
datasourceId,
|
datasourceId,
|
||||||
datasourceName,
|
datasourceName,
|
||||||
queries,
|
queries,
|
||||||
false,
|
false,
|
||||||
'',
|
'',
|
||||||
''
|
'',
|
||||||
|
!getState().explore.localStorageFull,
|
||||||
|
!getState().explore.richHistoryLimitExceededWarningShown
|
||||||
);
|
);
|
||||||
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
||||||
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
||||||
|
if (localStorageFull) {
|
||||||
|
dispatch(localStorageFullAction());
|
||||||
|
}
|
||||||
|
if (limitExceeded) {
|
||||||
|
dispatch(richHistoryLimitExceededAction());
|
||||||
|
}
|
||||||
|
|
||||||
// We save queries to the URL here so that only successfully run queries change the URL.
|
// We save queries to the URL here so that only successfully run queries change the URL.
|
||||||
dispatch(stateSave({ replace: options?.replaceUrl }));
|
dispatch(stateSave({ replace: options?.replaceUrl }));
|
||||||
|
@ -46,6 +46,17 @@ export interface ExploreState {
|
|||||||
*/
|
*/
|
||||||
richHistory: RichHistoryQuery[];
|
richHistory: RichHistoryQuery[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if local storage quota was exceeded when a new item was added. This is to prevent showing
|
||||||
|
* multiple errors when local storage is full.
|
||||||
|
*/
|
||||||
|
localStorageFull: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if a warning message of hitting the exceeded number of items has been shown already.
|
||||||
|
*/
|
||||||
|
richHistoryLimitExceededWarningShown: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-loading logs volume after running the query
|
* Auto-loading logs volume after running the query
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user