Commit 66e5a1c0 by Ivana Huckova Committed by GitHub

Rich history: Fix create url and run query & style updates (#23627)

* Styling updates

* Create getQueryFromDisplayText method for Jaeger, Loki, Prometheus

* Fix createLink and runQuery methods for all datasources

* Update test

* Update Select from Legacy to current

* Update filtering

* Update public/app/core/utils/richHistory.test.ts

* Fix strictnullcheck errors

* Remove getQueryFromDisplayText method, as not needed

* Update saving of full query and use displayText for formatting

* Update tests

* Refactor create data query

* Remove parsing, store object instead

* Fix formatting error

* Remove object checking, transform everything to DataQuery

* Remove console.log

* Rename migrate function, add datasourceName as a useEffect dependency

* Fix z-index, move query
parent 8d56f874
...@@ -5,32 +5,37 @@ import { ...@@ -5,32 +5,37 @@ import {
mapNumbertoTimeInSlider, mapNumbertoTimeInSlider,
createDateStringFromTs, createDateStringFromTs,
createQueryHeading, createQueryHeading,
createDataQuery,
deleteAllFromRichHistory, deleteAllFromRichHistory,
deleteQueryInRichHistory, deleteQueryInRichHistory,
} from './richHistory'; } from './richHistory';
import store from 'app/core/store'; import store from 'app/core/store';
import { SortOrder } from './explore'; import { SortOrder } from './explore';
import { dateTime } from '@grafana/data'; import { dateTime, DataQuery } from '@grafana/data';
const mock: any = { const mock: any = {
history: [ storedHistory: [
{ {
comment: '', comment: '',
datasourceId: 'datasource historyId', datasourceId: 'datasource historyId',
datasourceName: 'datasource history name', datasourceName: 'datasource history name',
queries: ['query1', 'query2'], queries: [
{ expr: 'query1', refId: '1' },
{ expr: 'query2', refId: '2' },
],
sessionName: '', sessionName: '',
starred: true, starred: true,
ts: 1, ts: 1,
}, },
], ],
comment: '', testComment: '',
datasourceId: 'datasourceId', testDatasourceId: 'datasourceId',
datasourceName: 'datasourceName', testDatasourceName: 'datasourceName',
queries: ['query3'], testQueries: [
sessionName: '', { expr: 'query3', refId: 'B' },
starred: false, { expr: 'query4', refId: 'C' },
],
testSessionName: '',
testStarred: false,
}; };
const key = 'grafana.explore.richHistory'; const key = 'grafana.explore.richHistory';
...@@ -43,27 +48,27 @@ describe('addToRichHistory', () => { ...@@ -43,27 +48,27 @@ describe('addToRichHistory', () => {
const expectedResult = [ const expectedResult = [
{ {
comment: mock.comment, comment: mock.testComment,
datasourceId: mock.datasourceId, datasourceId: mock.testDatasourceId,
datasourceName: mock.datasourceName, datasourceName: mock.testDatasourceName,
queries: mock.queries, queries: mock.testQueries,
sessionName: mock.sessionName, sessionName: mock.testSessionName,
starred: mock.starred, starred: mock.testStarred,
ts: 2, ts: 2,
}, },
mock.history[0], mock.storedHistory[0],
]; ];
it('should append query to query history', () => { it('should append query to query history', () => {
Date.now = jest.fn(() => 2); Date.now = jest.fn(() => 2);
const newHistory = addToRichHistory( const newHistory = addToRichHistory(
mock.history, mock.storedHistory,
mock.datasourceId, mock.testDatasourceId,
mock.datasourceName, mock.testDatasourceName,
mock.queries, mock.testQueries,
mock.starred, mock.testStarred,
mock.comment, mock.testComment,
mock.sessionName mock.testSessionName
); );
expect(newHistory).toEqual(expectedResult); expect(newHistory).toEqual(expectedResult);
}); });
...@@ -72,13 +77,13 @@ describe('addToRichHistory', () => { ...@@ -72,13 +77,13 @@ describe('addToRichHistory', () => {
Date.now = jest.fn(() => 2); Date.now = jest.fn(() => 2);
addToRichHistory( addToRichHistory(
mock.history, mock.storedHistory,
mock.datasourceId, mock.testDatasourceId,
mock.datasourceName, mock.testDatasourceName,
mock.queries, mock.testQueries,
mock.starred, mock.testStarred,
mock.comment, mock.testComment,
mock.sessionName mock.testSessionName
); );
expect(store.exists(key)).toBeTruthy(); expect(store.exists(key)).toBeTruthy();
expect(store.getObject(key)).toMatchObject(expectedResult); expect(store.getObject(key)).toMatchObject(expectedResult);
...@@ -87,27 +92,27 @@ describe('addToRichHistory', () => { ...@@ -87,27 +92,27 @@ describe('addToRichHistory', () => {
it('should not append duplicated query to query history', () => { it('should not append duplicated query to query history', () => {
Date.now = jest.fn(() => 2); Date.now = jest.fn(() => 2);
const newHistory = addToRichHistory( const newHistory = addToRichHistory(
mock.history, mock.storedHistory,
mock.history[0].datasourceId, mock.storedHistory[0].datasourceId,
mock.history[0].datasourceName, mock.storedHistory[0].datasourceName,
mock.history[0].queries, [{ expr: 'query1', refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
mock.starred, mock.testStarred,
mock.comment, mock.testComment,
mock.sessionName mock.testSessionName
); );
expect(newHistory).toEqual([mock.history[0]]); expect(newHistory).toEqual([mock.storedHistory[0]]);
}); });
it('should not save duplicated query to localStorage', () => { it('should not save duplicated query to localStorage', () => {
Date.now = jest.fn(() => 2); Date.now = jest.fn(() => 2);
addToRichHistory( addToRichHistory(
mock.history, mock.storedHistory,
mock.history[0].datasourceId, mock.storedHistory[0].datasourceId,
mock.history[0].datasourceName, mock.storedHistory[0].datasourceName,
mock.history[0].queries, [{ expr: 'query1', refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
mock.starred, mock.testStarred,
mock.comment, mock.testComment,
mock.sessionName mock.testSessionName
); );
expect(store.exists(key)).toBeFalsy(); expect(store.exists(key)).toBeFalsy();
}); });
...@@ -115,11 +120,11 @@ describe('addToRichHistory', () => { ...@@ -115,11 +120,11 @@ describe('addToRichHistory', () => {
describe('updateStarredInRichHistory', () => { describe('updateStarredInRichHistory', () => {
it('should update starred in query in history', () => { it('should update starred in query in history', () => {
const updatedStarred = updateStarredInRichHistory(mock.history, 1); const updatedStarred = updateStarredInRichHistory(mock.storedHistory, 1);
expect(updatedStarred[0].starred).toEqual(false); expect(updatedStarred[0].starred).toEqual(false);
}); });
it('should update starred in localStorage', () => { it('should update starred in localStorage', () => {
updateStarredInRichHistory(mock.history, 1); updateStarredInRichHistory(mock.storedHistory, 1);
expect(store.exists(key)).toBeTruthy(); expect(store.exists(key)).toBeTruthy();
expect(store.getObject(key)[0].starred).toEqual(false); expect(store.getObject(key)[0].starred).toEqual(false);
}); });
...@@ -127,11 +132,11 @@ describe('updateStarredInRichHistory', () => { ...@@ -127,11 +132,11 @@ describe('updateStarredInRichHistory', () => {
describe('updateCommentInRichHistory', () => { describe('updateCommentInRichHistory', () => {
it('should update comment in query in history', () => { it('should update comment in query in history', () => {
const updatedComment = updateCommentInRichHistory(mock.history, 1, 'new comment'); const updatedComment = updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
expect(updatedComment[0].comment).toEqual('new comment'); expect(updatedComment[0].comment).toEqual('new comment');
}); });
it('should update comment in localStorage', () => { it('should update comment in localStorage', () => {
updateCommentInRichHistory(mock.history, 1, 'new comment'); updateCommentInRichHistory(mock.storedHistory, 1, 'new comment');
expect(store.exists(key)).toBeTruthy(); expect(store.exists(key)).toBeTruthy();
expect(store.getObject(key)[0].comment).toEqual('new comment'); expect(store.getObject(key)[0].comment).toEqual('new comment');
}); });
...@@ -139,11 +144,11 @@ describe('updateCommentInRichHistory', () => { ...@@ -139,11 +144,11 @@ describe('updateCommentInRichHistory', () => {
describe('deleteQueryInRichHistory', () => { describe('deleteQueryInRichHistory', () => {
it('should delete query in query in history', () => { it('should delete query in query in history', () => {
const deletedHistory = deleteQueryInRichHistory(mock.history, 1); const deletedHistory = deleteQueryInRichHistory(mock.storedHistory, 1);
expect(deletedHistory).toEqual([]); expect(deletedHistory).toEqual([]);
}); });
it('should delete query in localStorage', () => { it('should delete query in localStorage', () => {
deleteQueryInRichHistory(mock.history, 1); deleteQueryInRichHistory(mock.storedHistory, 1);
expect(store.exists(key)).toBeTruthy(); expect(store.exists(key)).toBeTruthy();
expect(store.getObject(key)).toEqual([]); expect(store.getObject(key)).toEqual([]);
}); });
...@@ -166,19 +171,12 @@ describe('createDateStringFromTs', () => { ...@@ -166,19 +171,12 @@ describe('createDateStringFromTs', () => {
describe('createQueryHeading', () => { describe('createQueryHeading', () => {
it('should correctly create heading for queries when sort order is ascending ', () => { 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 // Have to offset the timezone of a 1 microsecond epoch, and then reverse the changes
mock.history[0].ts = 1 + -1 * dateTime().utcOffset() * 60 * 1000; mock.storedHistory[0].ts = 1 + -1 * dateTime().utcOffset() * 60 * 1000;
const heading = createQueryHeading(mock.history[0], SortOrder.Ascending); const heading = createQueryHeading(mock.storedHistory[0], SortOrder.Ascending);
expect(heading).toEqual('January 1'); expect(heading).toEqual('January 1');
}); });
it('should correctly create heading for queries when sort order is datasourceAZ ', () => { it('should correctly create heading for queries when sort order is datasourceAZ ', () => {
const heading = createQueryHeading(mock.history[0], SortOrder.DatasourceAZ); const heading = createQueryHeading(mock.storedHistory[0], SortOrder.DatasourceAZ);
expect(heading).toEqual(mock.history[0].datasourceName); expect(heading).toEqual(mock.storedHistory[0].datasourceName);
});
});
describe('createDataQuery', () => {
it('should correctly create data query from rich history query', () => {
const dataQuery = createDataQuery(mock.history[0], mock.queries[0], 0);
expect(dataQuery).toEqual({ datasource: 'datasource history name', expr: 'query3', refId: 'A' });
}); });
}); });
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import _ from 'lodash'; import _ from 'lodash';
// Services & Utils // Services & Utils
import { DataQuery, ExploreMode, dateTime, AppEvents, urlUtil } from '@grafana/data'; import { DataQuery, DataSourceApi, ExploreMode, dateTime, AppEvents, urlUtil } from '@grafana/data';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import store from 'app/core/store'; import store from 'app/core/store';
import { serializeStateToUrlParam, SortOrder } from './explore'; import { serializeStateToUrlParam, SortOrder } from './explore';
...@@ -29,39 +29,42 @@ export function addToRichHistory( ...@@ -29,39 +29,42 @@ export function addToRichHistory(
richHistory: RichHistoryQuery[], richHistory: RichHistoryQuery[],
datasourceId: string, datasourceId: string,
datasourceName: string | null, datasourceName: string | null,
queries: string[], queries: DataQuery[],
starred: boolean, starred: boolean,
comment: string | null, comment: string | null,
sessionName: string sessionName: string
): any { ): any {
const ts = Date.now(); const ts = Date.now();
/* Save only queries, that are not falsy (e.g. empty strings, null) */ /* Save only queries, that are not falsy (e.g. empty object, null, ...) */
const queriesToSave = queries.filter(expr => Boolean(expr)); const newQueriesToSave: DataQuery[] = queries && queries.filter(query => notEmptyQuery(query));
const retentionPeriod: number = store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7);
const retentionPeriod = store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7);
const retentionPeriodLastTs = createRetentionPeriodBoundary(retentionPeriod, false); const retentionPeriodLastTs = createRetentionPeriodBoundary(retentionPeriod, false);
/* Keep only queries, that are within the selected retention period or that are starred. /* Keep only queries, that are within the selected retention period or that are starred.
* If no queries, initialize with exmpty array * If no queries, initialize with empty array
*/ */
const queriesToKeep = richHistory.filter(q => q.ts > retentionPeriodLastTs || q.starred === true) || []; const queriesToKeep = richHistory.filter(q => q.ts > retentionPeriodLastTs || q.starred === true) || [];
if (queriesToSave.length > 0) { if (newQueriesToSave.length > 0) {
if ( /* Compare queries of a new query and last saved queries. If they are the same, (except selected properties,
/* Don't save duplicated queries for the same datasource */ * 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.length > 0 &&
JSON.stringify(queriesToSave) === JSON.stringify(queriesToKeep[0].queries) && queriesToKeep[0].queries.map(q => {
JSON.stringify(datasourceName) === JSON.stringify(queriesToKeep[0].datasourceName) return _.omit(q, ['key', 'refId']);
) { });
if (_.isEqual(newQueriesToCompare, lastQueriesToCompare)) {
return richHistory; return richHistory;
} }
let updatedHistory = [ let updatedHistory = [
{ queries: queriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName }, { queries: newQueriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
...queriesToKeep, ...queriesToKeep,
]; ];
/* If updatedHistory is succesfully saved, return it. Otherwise return not updated richHistory. */
try { try {
store.setObject(RICH_HISTORY_KEY, updatedHistory); store.setObject(RICH_HISTORY_KEY, updatedHistory);
return updatedHistory; return updatedHistory;
...@@ -74,8 +77,10 @@ export function addToRichHistory( ...@@ -74,8 +77,10 @@ export function addToRichHistory(
return richHistory; return richHistory;
} }
export function getRichHistory() { export function getRichHistory(): RichHistoryQuery[] {
return store.getObject(RICH_HISTORY_KEY, []); const richHistory: RichHistoryQuery[] = store.getObject(RICH_HISTORY_KEY, []);
const transformedRichHistory = migrateRichHistory(richHistory);
return transformedRichHistory;
} }
export function deleteAllFromRichHistory() { export function deleteAllFromRichHistory() {
...@@ -168,14 +173,20 @@ export const copyStringToClipboard = (string: string) => { ...@@ -168,14 +173,20 @@ export const copyStringToClipboard = (string: string) => {
}; };
export const createUrlFromRichHistory = (query: RichHistoryQuery) => { export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const queries = query.queries.map(query => ({ expr: query }));
const exploreState: ExploreUrlState = { const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */ /* Default range, as we are not saving timerange in rich history */
range: { from: 'now-1h', to: 'now' }, range: { from: 'now-1h', to: 'now' },
datasource: query.datasourceName, datasource: query.datasourceName,
queries, queries: query.queries,
/* Default mode. In the future, we can also save the query mode */ /* Default mode is metrics. Exceptions are Loki (logs) and Jaeger (tracing) data sources.
mode: query.datasourceId === 'loki' ? ExploreMode.Logs : ExploreMode.Metrics, * In the future, we can remove this as we are working on metrics & logs logic.
**/
mode:
query.datasourceId === 'loki'
? ExploreMode.Logs
: query.datasourceId === 'jaeger'
? ExploreMode.Tracing
: ExploreMode.Metrics,
ui: { ui: {
showingGraph: true, showingGraph: true,
showingLogs: true, showingLogs: true,
...@@ -230,7 +241,12 @@ export function createDateStringFromTs(ts: number) { ...@@ -230,7 +241,12 @@ export function createDateStringFromTs(ts: number) {
} }
export function getQueryDisplayText(query: DataQuery): string { export function getQueryDisplayText(query: DataQuery): string {
return JSON.stringify(query); /* 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) { export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder) {
...@@ -243,23 +259,15 @@ export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder ...@@ -243,23 +259,15 @@ export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder
return heading; return heading;
} }
export function isParsable(string: string) { export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi) {
try { /* query DatasourceInstance is necessary because we use its getQueryDisplayText method
JSON.parse(string); * to format query text
} catch (e) { */
return false; if (queryDsInstance?.getQueryDisplayText) {
return queryDsInstance.getQueryDisplayText(query);
} }
return true;
}
export function createDataQuery(query: RichHistoryQuery, queryString: string, index: number) { return getQueryDisplayText(query);
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) { export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortOrder) {
...@@ -304,3 +312,46 @@ export function createDatasourcesList(queriesDatasources: string[]) { ...@@ -304,3 +312,46 @@ export function createDatasourcesList(queriesDatasources: string[]) {
}); });
return datasources; 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;
}
/* 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;
}
...@@ -24,8 +24,8 @@ export enum Tabs { ...@@ -24,8 +24,8 @@ export enum Tabs {
} }
export const sortOrderOptions = [ export const sortOrderOptions = [
{ label: 'Time ascending', value: SortOrder.Ascending }, { label: 'Newest first', value: SortOrder.Descending },
{ label: 'Time descending', value: SortOrder.Descending }, { label: 'Oldest first', value: SortOrder.Ascending },
{ label: 'Data source A-Z', value: SortOrder.DatasourceAZ }, { label: 'Data source A-Z', value: SortOrder.DatasourceAZ },
{ label: 'Data source Z-A', value: SortOrder.DatasourceZA }, { label: 'Data source Z-A', value: SortOrder.DatasourceZA },
]; ];
...@@ -50,15 +50,13 @@ interface RichHistoryState { ...@@ -50,15 +50,13 @@ interface RichHistoryState {
} }
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
const borderColor = theme.isLight ? theme.palette.gray5 : theme.palette.dark6;
const tabContentBg = theme.colors.bodyBg;
return { return {
container: css` container: css`
height: 100%; height: 100%;
`, `,
tabContent: css` tabContent: css`
padding: ${theme.spacing.md}; padding: ${theme.spacing.md};
background-color: ${tabContentBg}; background-color: ${theme.colors.bodyBg};
`, `,
close: css` close: css`
position: absolute; position: absolute;
...@@ -69,11 +67,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -69,11 +67,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
tabs: css` tabs: css`
padding-top: ${theme.spacing.sm}; padding-top: ${theme.spacing.sm};
border-color: ${borderColor}; border-color: ${theme.colors.formInputBorder};
ul { ul {
margin-left: ${theme.spacing.md}; margin-left: ${theme.spacing.md};
} }
`, `,
scrollbar: css`
min-height: 100% !important;
background-color: ${theme.colors.panelBg};
`,
}; };
}); });
...@@ -228,11 +230,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta ...@@ -228,11 +230,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
))} ))}
<IconButton className={styles.close} onClick={onClose} name="times" title="Close query history" /> <IconButton className={styles.close} onClick={onClose} name="times" title="Close query history" />
</TabsBar> </TabsBar>
<CustomScrollbar <CustomScrollbar className={styles.scrollbar}>
className={css`
min-height: 100% !important;
`}
>
<TabContent className={styles.tabContent}>{tabs.find(t => t.value === activeTab)?.content}</TabContent> <TabContent className={styles.tabContent}>{tabs.find(t => t.value === activeTab)?.content}</TabContent>
</CustomScrollbar> </CustomScrollbar>
</div> </div>
......
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { shallow } from 'enzyme';
import { RichHistoryCard, Props } from './RichHistoryCard'; import { RichHistoryCard, Props } from './RichHistoryCard';
import { ExploreId } from '../../../types/explore'; import { ExploreId } from '../../../types/explore';
import { DataSourceApi } from '@grafana/data'; import { DataSourceApi, DataQuery } from '@grafana/data';
const setup = (propOverrides?: Partial<Props>) => { const setup = (propOverrides?: Partial<Props>) => {
const props: Props = { const props: Props = {
...@@ -12,7 +12,11 @@ const setup = (propOverrides?: Partial<Props>) => { ...@@ -12,7 +12,11 @@ const setup = (propOverrides?: Partial<Props>) => {
datasourceId: 'datasource 1', datasourceId: 'datasource 1',
starred: false, starred: false,
comment: '', comment: '',
queries: ['query1', 'query2', 'query3'], queries: [
{ expr: 'query1', refId: 'A' } as DataQuery,
{ expr: 'query2', refId: 'B' } as DataQuery,
{ expr: 'query3', refId: 'C' } as DataQuery,
],
sessionName: '', sessionName: '',
}, },
dsImg: '/app/img', dsImg: '/app/img',
...@@ -26,7 +30,7 @@ const setup = (propOverrides?: Partial<Props>) => { ...@@ -26,7 +30,7 @@ const setup = (propOverrides?: Partial<Props>) => {
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
const wrapper = mount(<RichHistoryCard {...props} />); const wrapper = shallow(<RichHistoryCard {...props} />);
return wrapper; return wrapper;
}; };
...@@ -36,7 +40,11 @@ const starredQueryWithComment = { ...@@ -36,7 +40,11 @@ const starredQueryWithComment = {
datasourceId: 'datasource 1', datasourceId: 'datasource 1',
starred: true, starred: true,
comment: 'test comment', comment: 'test comment',
queries: ['query1', 'query2', 'query3'], queries: [
{ query: 'query1', refId: 'A' },
{ query: 'query2', refId: 'B' },
{ query: 'query3', refId: 'C' },
],
sessionName: '', sessionName: '',
}; };
...@@ -49,19 +57,19 @@ describe('RichHistoryCard', () => { ...@@ -49,19 +57,19 @@ describe('RichHistoryCard', () => {
.find({ 'aria-label': 'Query text' }) .find({ 'aria-label': 'Query text' })
.at(0) .at(0)
.text() .text()
).toEqual('query1'); ).toEqual('{"expr":"query1"}');
expect( expect(
wrapper wrapper
.find({ 'aria-label': 'Query text' }) .find({ 'aria-label': 'Query text' })
.at(1) .at(1)
.text() .text()
).toEqual('query2'); ).toEqual('{"expr":"query2"}');
expect( expect(
wrapper wrapper
.find({ 'aria-label': 'Query text' }) .find({ 'aria-label': 'Query text' })
.at(2) .at(2)
.text() .text()
).toEqual('query3'); ).toEqual('{"expr":"query3"}');
}); });
it('should render data source icon', () => { it('should render data source icon', () => {
const wrapper = setup(); const wrapper = setup();
...@@ -79,29 +87,29 @@ describe('RichHistoryCard', () => { ...@@ -79,29 +87,29 @@ describe('RichHistoryCard', () => {
describe('commenting', () => { describe('commenting', () => {
it('should render comment, if comment present', () => { it('should render comment, if comment present', () => {
const wrapper = setup({ query: starredQueryWithComment }); const wrapper = setup({ query: starredQueryWithComment });
expect(wrapper.find({ 'aria-label': 'Query comment' }).hostNodes()).toHaveLength(1); expect(wrapper.find({ 'aria-label': 'Query comment' })).toHaveLength(1);
expect(wrapper.find({ 'aria-label': 'Query comment' }).text()).toEqual('test comment'); expect(wrapper.find({ 'aria-label': 'Query comment' }).text()).toEqual('test comment');
}); });
it('should have title "Edit comment" at comment icon, if comment present', () => { it('should have title "Edit comment" at comment icon, if comment present', () => {
const wrapper = setup({ query: starredQueryWithComment }); const wrapper = setup({ query: starredQueryWithComment });
expect(wrapper.find({ title: 'Edit comment' }).hostNodes()).toHaveLength(1); expect(wrapper.find({ title: 'Edit comment' })).toHaveLength(1);
expect(wrapper.find({ title: 'Add comment' }).hostNodes()).toHaveLength(0); expect(wrapper.find({ title: 'Add comment' })).toHaveLength(0);
}); });
it('should have title "Add comment" at comment icon, if no comment present', () => { it('should have title "Add comment" at comment icon, if no comment present', () => {
const wrapper = setup(); const wrapper = setup();
expect(wrapper.find({ title: 'Add comment' }).hostNodes()).toHaveLength(1); expect(wrapper.find({ title: 'Add comment' })).toHaveLength(1);
expect(wrapper.find({ title: 'Edit comment' }).hostNodes()).toHaveLength(0); expect(wrapper.find({ title: 'Edit comment' })).toHaveLength(0);
}); });
}); });
describe('starring', () => { describe('starring', () => {
it('should have title "Star query", if not starred', () => { it('should have title "Star query", if not starred', () => {
const wrapper = setup(); const wrapper = setup();
expect(wrapper.find({ title: 'Star query' }).hostNodes()).toHaveLength(1); expect(wrapper.find({ title: 'Star query' })).toHaveLength(1);
}); });
it('should have title "Unstar query", if not starred', () => { it('should have title "Unstar query", if not starred', () => {
const wrapper = setup({ query: starredQueryWithComment }); const wrapper = setup({ query: starredQueryWithComment });
expect(wrapper.find({ title: 'Unstar query' }).hostNodes()).toHaveLength(1); expect(wrapper.find({ title: 'Unstar query' })).toHaveLength(1);
}); });
}); });
}); });
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui'; import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data'; import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
import { RichHistoryQuery, ExploreId } from 'app/types/explore'; import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { copyStringToClipboard, createUrlFromRichHistory, createDataQuery } from 'app/core/utils/richHistory'; import { copyStringToClipboard, createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
...@@ -27,8 +27,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -27,8 +27,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
const rigtColumnWidth = '240px'; const rigtColumnWidth = '240px';
const rigtColumnContentWidth = '170px'; const rigtColumnContentWidth = '170px';
const borderColor = theme.isLight ? theme.palette.gray5 : theme.palette.gray25;
/* If datasource was removed, card will have inactive color */ /* If datasource was removed, card will have inactive color */
const cardColor = theme.isLight const cardColor = theme.isLight
? isRemoved ? isRemoved
...@@ -42,7 +40,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -42,7 +40,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
queryCard: css` queryCard: css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid ${borderColor}; border: 1px solid ${theme.colors.formInputBorder};
margin: ${theme.spacing.sm} 0; margin: ${theme.spacing.sm} 0;
background-color: ${cardColor}; background-color: ${cardColor};
border-radius: ${theme.border.radius.sm}; border-radius: ${theme.border.radius.sm};
...@@ -57,7 +55,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -57,7 +55,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
padding: ${theme.spacing.sm}; padding: ${theme.spacing.sm};
border-bottom: none; border-bottom: none;
:first-of-type { :first-of-type {
border-bottom: 1px solid ${borderColor}; border-bottom: 1px solid ${theme.colors.formInputBorder};
padding: ${theme.spacing.xs} ${theme.spacing.sm}; padding: ${theme.spacing.xs} ${theme.spacing.sm};
} }
img { img {
...@@ -86,7 +84,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -86,7 +84,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
width: calc(100% - ${rigtColumnWidth}); width: calc(100% - ${rigtColumnWidth});
`, `,
queryRow: css` queryRow: css`
border-top: 1px solid ${borderColor}; border-top: 1px solid ${theme.colors.formInputBorder};
word-break: break-all; word-break: break-all;
padding: 4px 2px; padding: 4px 2px;
:first-child { :first-child {
...@@ -110,7 +108,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -110,7 +108,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
} }
`, `,
textArea: css` textArea: css`
border: 1px solid ${borderColor}; border: 1px solid ${theme.colors.formInputBorder};
background: inherit; background: inherit;
color: inherit; color: inherit;
width: 100%; width: 100%;
...@@ -125,7 +123,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => { ...@@ -125,7 +123,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
justify-content: flex-end; justify-content: flex-end;
button { button {
height: auto; height: auto;
padding: ${theme.spacing.sm} ${theme.spacing.md}; padding: ${theme.spacing.xs} ${theme.spacing.md};
line-height: 1.4;
span { span {
white-space: normal !important; white-space: normal !important;
} }
...@@ -147,24 +146,33 @@ export function RichHistoryCard(props: Props) { ...@@ -147,24 +146,33 @@ export function RichHistoryCard(props: Props) {
} = props; } = props;
const [activeUpdateComment, setActiveUpdateComment] = useState(false); const [activeUpdateComment, setActiveUpdateComment] = useState(false);
const [comment, setComment] = useState<string | undefined>(query.comment); const [comment, setComment] = useState<string | undefined>(query.comment);
const [queryDsInstance, setQueryDsInstance] = useState<DataSourceApi | undefined>(undefined);
useEffect(() => {
const getQueryDsInstance = async () => {
const ds = await getDataSourceSrv().get(query.datasourceName);
setQueryDsInstance(ds);
};
getQueryDsInstance();
}, [query.datasourceName]);
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme, isRemoved); const styles = getStyles(theme, isRemoved);
const onRunQuery = async () => { const onRunQuery = async () => {
const dataQueries = query.queries.map((q, i) => createDataQuery(query, q, i)); const queriesToRun = query.queries;
if (query.datasourceName !== datasourceInstance?.name) { if (query.datasourceName !== datasourceInstance?.name) {
await changeDatasource(exploreId, query.datasourceName); await changeDatasource(exploreId, query.datasourceName);
setQueries(exploreId, dataQueries); setQueries(exploreId, queriesToRun);
} else { } else {
setQueries(exploreId, dataQueries); setQueries(exploreId, queriesToRun);
} }
}; };
const onCopyQuery = () => { const onCopyQuery = () => {
const queries = query.queries.join('\n\n'); const queriesToCopy = query.queries.map(q => createQueryText(q, queryDsInstance)).join('\n');
copyStringToClipboard(queries); copyStringToClipboard(queriesToCopy);
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']); appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
}; };
...@@ -183,6 +191,8 @@ export function RichHistoryCard(props: Props) { ...@@ -183,6 +191,8 @@ export function RichHistoryCard(props: Props) {
updateRichHistory(query.ts, 'starred'); updateRichHistory(query.ts, 'starred');
}; };
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
const onUpdateComment = () => { const onUpdateComment = () => {
updateRichHistory(query.ts, 'comment', comment); updateRichHistory(query.ts, 'comment', comment);
toggleActiveUpdateComment(); toggleActiveUpdateComment();
...@@ -243,9 +253,10 @@ export function RichHistoryCard(props: Props) { ...@@ -243,9 +253,10 @@ export function RichHistoryCard(props: Props) {
<div className={cx(styles.cardRow)}> <div className={cx(styles.cardRow)}>
<div className={styles.queryContainer}> <div className={styles.queryContainer}>
{query.queries.map((q, i) => { {query.queries.map((q, i) => {
const queryText = createQueryText(q, queryDsInstance);
return ( return (
<div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}> <div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}>
{q} {queryText}
</div> </div>
); );
})} })}
......
...@@ -22,22 +22,19 @@ import { RichHistory, Tabs } from './RichHistory'; ...@@ -22,22 +22,19 @@ import { RichHistory, Tabs } from './RichHistory';
import { deleteRichHistory } from '../state/actions'; import { deleteRichHistory } from '../state/actions';
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
const containerBackground = theme.isLight ? theme.palette.gray95 : theme.palette.gray15; const shadowColor = theme.isLight ? theme.palette.gray4 : theme.palette.black;
const containerBorderColor = theme.isLight ? theme.palette.gray5 : theme.palette.dark6;
const handleBackground = theme.isLight ? theme.palette.white : theme.palette.gray15;
const handleDots = theme.isLight ? theme.palette.gray85 : theme.palette.gray33;
const handleBackgroundHover = theme.isLight ? theme.palette.gray85 : theme.palette.gray33;
const handleDotsHover = theme.isLight ? theme.palette.gray70 : theme.palette.dark7;
return { return {
container: css` container: css`
position: fixed !important; position: fixed !important;
bottom: 0; bottom: 0;
background: ${containerBackground}; background: ${theme.colors.pageHeaderBg};
border-top: 1px solid ${containerBorderColor}; border-top: 1px solid ${theme.colors.formInputBorder};
margin: 0px; margin: 0px;
margin-right: -${theme.spacing.md}; margin-right: -${theme.spacing.md};
margin-left: -${theme.spacing.md}; margin-left: -${theme.spacing.md};
box-shadow: 0 0 4px ${shadowColor};
z-index: ${theme.zIndex.sidemenu};
`, `,
drawerActive: css` drawerActive: css`
opacity: 1; opacity: 1;
...@@ -48,30 +45,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -48,30 +45,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
transform: translateY(400px); transform: translateY(400px);
`, `,
rzHandle: css` rzHandle: css`
background: ${handleBackground}; background: ${theme.colors.formInputBorder};
transition: 0.3s background ease-in-out; transition: 0.3s background ease-in-out;
position: relative; position: relative;
width: 200px !important; width: 200px !important;
height: 7px !important;
left: calc(50% - 100px) !important; left: calc(50% - 100px) !important;
top: -4px !important;
cursor: grab; cursor: grab;
border-radius: 4px; border-radius: 4px;
&:hover { &:hover {
background-color: ${handleBackgroundHover}; background: ${theme.colors.formInputBorderHover};
&:after {
border-color: ${handleDotsHover};
}
}
&:after {
content: '';
display: block;
height: 2px;
position: relative;
top: 4px;
border-top: 4px dotted ${handleDots};
margin: 0 4px;
} }
`, `,
}; };
......
...@@ -21,8 +21,7 @@ import { ...@@ -21,8 +21,7 @@ import {
// Components // Components
import RichHistoryCard from './RichHistoryCard'; import RichHistoryCard from './RichHistoryCard';
import { sortOrderOptions } from './RichHistory'; import { sortOrderOptions } from './RichHistory';
import { LegacyForms, Slider } from '@grafana/ui'; import { Slider, Select } from '@grafana/ui';
const { Select } = LegacyForms;
export interface Props { export interface Props {
queries: RichHistoryQuery[]; queries: RichHistoryQuery[];
...@@ -60,12 +59,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { ...@@ -60,12 +59,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
width: calc(${cardWidth}); width: calc(${cardWidth});
`, `,
containerSlider: css` containerSlider: css`
width: 127px; width: 129px;
margin-right: ${theme.spacing.sm}; margin-right: ${theme.spacing.sm};
.slider { .slider {
bottom: 10px; bottom: 10px;
height: ${sliderHeight}; height: ${sliderHeight};
width: 127px; width: 129px;
padding: ${theme.spacing.sm} 0; padding: ${theme.spacing.sm} 0;
} }
`, `,
...@@ -141,9 +140,10 @@ export function RichHistoryQueriesTab(props: Props) { ...@@ -141,9 +140,10 @@ export function RichHistoryQueriesTab(props: Props) {
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory); const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value); const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
const filteredQueriesByDatasource = datasourceFilters const filteredQueriesByDatasource =
? queries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName)) listOfDatasourceFilters && listOfDatasourceFilters?.length > 0
: queries; ? queries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
: queries;
const sortedQueries = sortQueries(filteredQueriesByDatasource, sortOrder); const sortedQueries = sortQueries(filteredQueriesByDatasource, sortOrder);
const queriesWithinSelectedTimeline = sortedQueries?.filter( const queriesWithinSelectedTimeline = sortedQueries?.filter(
......
...@@ -15,8 +15,7 @@ import { sortQueries, createDatasourcesList } from '../../../core/utils/richHist ...@@ -15,8 +15,7 @@ import { sortQueries, createDatasourcesList } from '../../../core/utils/richHist
// Components // Components
import RichHistoryCard from './RichHistoryCard'; import RichHistoryCard from './RichHistoryCard';
import { sortOrderOptions } from './RichHistory'; import { sortOrderOptions } from './RichHistory';
import { LegacyForms } from '@grafana/ui'; import { Select } from '@grafana/ui';
const { Select } = LegacyForms;
export interface Props { export interface Props {
queries: RichHistoryQuery[]; queries: RichHistoryQuery[];
...@@ -87,9 +86,10 @@ export function RichHistoryStarredTab(props: Props) { ...@@ -87,9 +86,10 @@ export function RichHistoryStarredTab(props: Props) {
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value); const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
const starredQueries = queries.filter(q => q.starred === true); const starredQueries = queries.filter(q => q.starred === true);
const starredQueriesFilteredByDatasource = datasourceFilters const starredQueriesFilteredByDatasource =
? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName)) listOfDatasourceFilters && listOfDatasourceFilters?.length > 0
: starredQueries; ? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
: starredQueries;
const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder); const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder);
......
...@@ -44,7 +44,6 @@ import { ...@@ -44,7 +44,6 @@ import {
updateStarredInRichHistory, updateStarredInRichHistory,
updateCommentInRichHistory, updateCommentInRichHistory,
deleteQueryInRichHistory, deleteQueryInRichHistory,
getQueryDisplayText,
getRichHistory, getRichHistory,
} from 'app/core/utils/richHistory'; } from 'app/core/utils/richHistory';
// Types // Types
...@@ -487,17 +486,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -487,17 +486,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
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 arrayOfStringifiedQueries = queries.map(query =>
datasourceInstance?.getQueryDisplayText
? datasourceInstance.getQueryDisplayText(query)
: getQueryDisplayText(query)
);
const nextRichHistory = addToRichHistory( const nextRichHistory = addToRichHistory(
richHistory || [], richHistory || [],
datasourceId, datasourceId,
datasourceName, datasourceName,
arrayOfStringifiedQueries, queries,
false, false,
'', '',
'' ''
......
...@@ -83,6 +83,10 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> { ...@@ -83,6 +83,10 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
}; };
} }
getQueryDisplayText(query: JaegerQuery) {
return query.query;
}
private _request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> { private _request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> {
// Hack for proxying metadata requests // Hack for proxying metadata requests
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`; const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
......
...@@ -241,7 +241,7 @@ export type RichHistoryQuery = { ...@@ -241,7 +241,7 @@ export type RichHistoryQuery = {
datasourceId: string; datasourceId: string;
starred: boolean; starred: boolean;
comment: string; comment: string;
queries: string[]; queries: DataQuery[];
sessionName: string; sessionName: string;
timeRange?: string; timeRange?: string;
}; };
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.explore-active-button { .explore-active-button {
box-shadow: $btn-active-box-shadow; box-shadow: $btn-active-box-shadow;
border-color: $orange-dark; border: 1px solid $orange-dark;
background-image: none; background-image: none;
background-color: transparent; background-color: transparent;
color: $orange-dark !important; color: $orange-dark !important;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment