mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
8d56f87473
commit
66e5a1c0b3
@ -5,32 +5,37 @@ import {
|
||||
mapNumbertoTimeInSlider,
|
||||
createDateStringFromTs,
|
||||
createQueryHeading,
|
||||
createDataQuery,
|
||||
deleteAllFromRichHistory,
|
||||
deleteQueryInRichHistory,
|
||||
} from './richHistory';
|
||||
import store from 'app/core/store';
|
||||
import { SortOrder } from './explore';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTime, DataQuery } from '@grafana/data';
|
||||
|
||||
const mock: any = {
|
||||
history: [
|
||||
storedHistory: [
|
||||
{
|
||||
comment: '',
|
||||
datasourceId: 'datasource historyId',
|
||||
datasourceName: 'datasource history name',
|
||||
queries: ['query1', 'query2'],
|
||||
queries: [
|
||||
{ expr: 'query1', refId: '1' },
|
||||
{ expr: 'query2', refId: '2' },
|
||||
],
|
||||
sessionName: '',
|
||||
starred: true,
|
||||
ts: 1,
|
||||
},
|
||||
],
|
||||
comment: '',
|
||||
datasourceId: 'datasourceId',
|
||||
datasourceName: 'datasourceName',
|
||||
queries: ['query3'],
|
||||
sessionName: '',
|
||||
starred: false,
|
||||
testComment: '',
|
||||
testDatasourceId: 'datasourceId',
|
||||
testDatasourceName: 'datasourceName',
|
||||
testQueries: [
|
||||
{ expr: 'query3', refId: 'B' },
|
||||
{ expr: 'query4', refId: 'C' },
|
||||
],
|
||||
testSessionName: '',
|
||||
testStarred: false,
|
||||
};
|
||||
|
||||
const key = 'grafana.explore.richHistory';
|
||||
@ -43,27 +48,27 @@ describe('addToRichHistory', () => {
|
||||
|
||||
const expectedResult = [
|
||||
{
|
||||
comment: mock.comment,
|
||||
datasourceId: mock.datasourceId,
|
||||
datasourceName: mock.datasourceName,
|
||||
queries: mock.queries,
|
||||
sessionName: mock.sessionName,
|
||||
starred: mock.starred,
|
||||
comment: mock.testComment,
|
||||
datasourceId: mock.testDatasourceId,
|
||||
datasourceName: mock.testDatasourceName,
|
||||
queries: mock.testQueries,
|
||||
sessionName: mock.testSessionName,
|
||||
starred: mock.testStarred,
|
||||
ts: 2,
|
||||
},
|
||||
mock.history[0],
|
||||
mock.storedHistory[0],
|
||||
];
|
||||
|
||||
it('should append query to query history', () => {
|
||||
Date.now = jest.fn(() => 2);
|
||||
const newHistory = addToRichHistory(
|
||||
mock.history,
|
||||
mock.datasourceId,
|
||||
mock.datasourceName,
|
||||
mock.queries,
|
||||
mock.starred,
|
||||
mock.comment,
|
||||
mock.sessionName
|
||||
mock.storedHistory,
|
||||
mock.testDatasourceId,
|
||||
mock.testDatasourceName,
|
||||
mock.testQueries,
|
||||
mock.testStarred,
|
||||
mock.testComment,
|
||||
mock.testSessionName
|
||||
);
|
||||
expect(newHistory).toEqual(expectedResult);
|
||||
});
|
||||
@ -72,13 +77,13 @@ describe('addToRichHistory', () => {
|
||||
Date.now = jest.fn(() => 2);
|
||||
|
||||
addToRichHistory(
|
||||
mock.history,
|
||||
mock.datasourceId,
|
||||
mock.datasourceName,
|
||||
mock.queries,
|
||||
mock.starred,
|
||||
mock.comment,
|
||||
mock.sessionName
|
||||
mock.storedHistory,
|
||||
mock.testDatasourceId,
|
||||
mock.testDatasourceName,
|
||||
mock.testQueries,
|
||||
mock.testStarred,
|
||||
mock.testComment,
|
||||
mock.testSessionName
|
||||
);
|
||||
expect(store.exists(key)).toBeTruthy();
|
||||
expect(store.getObject(key)).toMatchObject(expectedResult);
|
||||
@ -87,27 +92,27 @@ describe('addToRichHistory', () => {
|
||||
it('should not append duplicated query to query history', () => {
|
||||
Date.now = jest.fn(() => 2);
|
||||
const newHistory = addToRichHistory(
|
||||
mock.history,
|
||||
mock.history[0].datasourceId,
|
||||
mock.history[0].datasourceName,
|
||||
mock.history[0].queries,
|
||||
mock.starred,
|
||||
mock.comment,
|
||||
mock.sessionName
|
||||
mock.storedHistory,
|
||||
mock.storedHistory[0].datasourceId,
|
||||
mock.storedHistory[0].datasourceName,
|
||||
[{ expr: 'query1', refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
||||
mock.testStarred,
|
||||
mock.testComment,
|
||||
mock.testSessionName
|
||||
);
|
||||
expect(newHistory).toEqual([mock.history[0]]);
|
||||
expect(newHistory).toEqual([mock.storedHistory[0]]);
|
||||
});
|
||||
|
||||
it('should not save duplicated query to localStorage', () => {
|
||||
Date.now = jest.fn(() => 2);
|
||||
addToRichHistory(
|
||||
mock.history,
|
||||
mock.history[0].datasourceId,
|
||||
mock.history[0].datasourceName,
|
||||
mock.history[0].queries,
|
||||
mock.starred,
|
||||
mock.comment,
|
||||
mock.sessionName
|
||||
mock.storedHistory,
|
||||
mock.storedHistory[0].datasourceId,
|
||||
mock.storedHistory[0].datasourceName,
|
||||
[{ expr: 'query1', refId: 'A' } as DataQuery, { expr: 'query2', refId: 'B' } as DataQuery],
|
||||
mock.testStarred,
|
||||
mock.testComment,
|
||||
mock.testSessionName
|
||||
);
|
||||
expect(store.exists(key)).toBeFalsy();
|
||||
});
|
||||
@ -115,11 +120,11 @@ describe('addToRichHistory', () => {
|
||||
|
||||
describe('updateStarredInRichHistory', () => {
|
||||
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);
|
||||
});
|
||||
it('should update starred in localStorage', () => {
|
||||
updateStarredInRichHistory(mock.history, 1);
|
||||
updateStarredInRichHistory(mock.storedHistory, 1);
|
||||
expect(store.exists(key)).toBeTruthy();
|
||||
expect(store.getObject(key)[0].starred).toEqual(false);
|
||||
});
|
||||
@ -127,11 +132,11 @@ describe('updateStarredInRichHistory', () => {
|
||||
|
||||
describe('updateCommentInRichHistory', () => {
|
||||
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');
|
||||
});
|
||||
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.getObject(key)[0].comment).toEqual('new comment');
|
||||
});
|
||||
@ -139,11 +144,11 @@ describe('updateCommentInRichHistory', () => {
|
||||
|
||||
describe('deleteQueryInRichHistory', () => {
|
||||
it('should delete query in query in history', () => {
|
||||
const deletedHistory = deleteQueryInRichHistory(mock.history, 1);
|
||||
const deletedHistory = deleteQueryInRichHistory(mock.storedHistory, 1);
|
||||
expect(deletedHistory).toEqual([]);
|
||||
});
|
||||
it('should delete query in localStorage', () => {
|
||||
deleteQueryInRichHistory(mock.history, 1);
|
||||
deleteQueryInRichHistory(mock.storedHistory, 1);
|
||||
expect(store.exists(key)).toBeTruthy();
|
||||
expect(store.getObject(key)).toEqual([]);
|
||||
});
|
||||
@ -166,19 +171,12 @@ describe('createDateStringFromTs', () => {
|
||||
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.history[0].ts = 1 + -1 * dateTime().utcOffset() * 60 * 1000;
|
||||
const heading = createQueryHeading(mock.history[0], SortOrder.Ascending);
|
||||
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.history[0], SortOrder.DatasourceAZ);
|
||||
expect(heading).toEqual(mock.history[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' });
|
||||
const heading = createQueryHeading(mock.storedHistory[0], SortOrder.DatasourceAZ);
|
||||
expect(heading).toEqual(mock.storedHistory[0].datasourceName);
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
// 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 store from 'app/core/store';
|
||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
||||
@ -29,39 +29,42 @@ export function addToRichHistory(
|
||||
richHistory: RichHistoryQuery[],
|
||||
datasourceId: string,
|
||||
datasourceName: string | null,
|
||||
queries: string[],
|
||||
queries: DataQuery[],
|
||||
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);
|
||||
/* 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 exmpty array
|
||||
* If no queries, initialize with empty 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 */
|
||||
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 &&
|
||||
JSON.stringify(queriesToSave) === JSON.stringify(queriesToKeep[0].queries) &&
|
||||
JSON.stringify(datasourceName) === JSON.stringify(queriesToKeep[0].datasourceName)
|
||||
) {
|
||||
queriesToKeep[0].queries.map(q => {
|
||||
return _.omit(q, ['key', 'refId']);
|
||||
});
|
||||
|
||||
if (_.isEqual(newQueriesToCompare, lastQueriesToCompare)) {
|
||||
return richHistory;
|
||||
}
|
||||
|
||||
let updatedHistory = [
|
||||
{ queries: queriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
|
||||
{ queries: newQueriesToSave, ts, datasourceId, datasourceName, starred, comment, sessionName },
|
||||
...queriesToKeep,
|
||||
];
|
||||
|
||||
/* If updatedHistory is succesfully saved, return it. Otherwise return not updated richHistory. */
|
||||
try {
|
||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||
return updatedHistory;
|
||||
@ -74,8 +77,10 @@ export function addToRichHistory(
|
||||
return richHistory;
|
||||
}
|
||||
|
||||
export function getRichHistory() {
|
||||
return store.getObject(RICH_HISTORY_KEY, []);
|
||||
export function getRichHistory(): RichHistoryQuery[] {
|
||||
const richHistory: RichHistoryQuery[] = store.getObject(RICH_HISTORY_KEY, []);
|
||||
const transformedRichHistory = migrateRichHistory(richHistory);
|
||||
return transformedRichHistory;
|
||||
}
|
||||
|
||||
export function deleteAllFromRichHistory() {
|
||||
@ -168,14 +173,20 @@ export const copyStringToClipboard = (string: string) => {
|
||||
};
|
||||
|
||||
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,
|
||||
queries: query.queries,
|
||||
/* Default mode is metrics. Exceptions are Loki (logs) and Jaeger (tracing) data sources.
|
||||
* 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: {
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
@ -230,7 +241,12 @@ export function createDateStringFromTs(ts: number) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -243,23 +259,15 @@ export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder
|
||||
return heading;
|
||||
}
|
||||
|
||||
export function isParsable(string: string) {
|
||||
try {
|
||||
JSON.parse(string);
|
||||
} catch (e) {
|
||||
return false;
|
||||
export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi) {
|
||||
/* query DatasourceInstance is necessary because we use its getQueryDisplayText method
|
||||
* to format query text
|
||||
*/
|
||||
if (queryDsInstance?.getQueryDisplayText) {
|
||||
return queryDsInstance.getQueryDisplayText(query);
|
||||
}
|
||||
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;
|
||||
return getQueryDisplayText(query);
|
||||
}
|
||||
|
||||
export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortOrder) {
|
||||
@ -304,3 +312,46 @@ export function createDatasourcesList(queriesDatasources: string[]) {
|
||||
});
|
||||
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 {
|
||||
}
|
||||
|
||||
export const sortOrderOptions = [
|
||||
{ label: 'Time ascending', value: SortOrder.Ascending },
|
||||
{ label: 'Time descending', value: SortOrder.Descending },
|
||||
{ label: 'Newest first', value: SortOrder.Descending },
|
||||
{ label: 'Oldest first', value: SortOrder.Ascending },
|
||||
{ label: 'Data source A-Z', value: SortOrder.DatasourceAZ },
|
||||
{ label: 'Data source Z-A', value: SortOrder.DatasourceZA },
|
||||
];
|
||||
@ -50,15 +50,13 @@ interface RichHistoryState {
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = theme.isLight ? theme.palette.gray5 : theme.palette.dark6;
|
||||
const tabContentBg = theme.colors.bodyBg;
|
||||
return {
|
||||
container: css`
|
||||
height: 100%;
|
||||
`,
|
||||
tabContent: css`
|
||||
padding: ${theme.spacing.md};
|
||||
background-color: ${tabContentBg};
|
||||
background-color: ${theme.colors.bodyBg};
|
||||
`,
|
||||
close: css`
|
||||
position: absolute;
|
||||
@ -69,11 +67,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
`,
|
||||
tabs: css`
|
||||
padding-top: ${theme.spacing.sm};
|
||||
border-color: ${borderColor};
|
||||
border-color: ${theme.colors.formInputBorder};
|
||||
ul {
|
||||
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
|
||||
))}
|
||||
<IconButton className={styles.close} onClick={onClose} name="times" title="Close query history" />
|
||||
</TabsBar>
|
||||
<CustomScrollbar
|
||||
className={css`
|
||||
min-height: 100% !important;
|
||||
`}
|
||||
>
|
||||
<CustomScrollbar className={styles.scrollbar}>
|
||||
<TabContent className={styles.tabContent}>{tabs.find(t => t.value === activeTab)?.content}</TabContent>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
import { RichHistoryCard, Props } from './RichHistoryCard';
|
||||
import { ExploreId } from '../../../types/explore';
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { DataSourceApi, DataQuery } from '@grafana/data';
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
@ -12,7 +12,11 @@ const setup = (propOverrides?: Partial<Props>) => {
|
||||
datasourceId: 'datasource 1',
|
||||
starred: false,
|
||||
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: '',
|
||||
},
|
||||
dsImg: '/app/img',
|
||||
@ -26,7 +30,7 @@ const setup = (propOverrides?: Partial<Props>) => {
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = mount(<RichHistoryCard {...props} />);
|
||||
const wrapper = shallow(<RichHistoryCard {...props} />);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
@ -36,7 +40,11 @@ const starredQueryWithComment = {
|
||||
datasourceId: 'datasource 1',
|
||||
starred: true,
|
||||
comment: 'test comment',
|
||||
queries: ['query1', 'query2', 'query3'],
|
||||
queries: [
|
||||
{ query: 'query1', refId: 'A' },
|
||||
{ query: 'query2', refId: 'B' },
|
||||
{ query: 'query3', refId: 'C' },
|
||||
],
|
||||
sessionName: '',
|
||||
};
|
||||
|
||||
@ -49,19 +57,19 @@ describe('RichHistoryCard', () => {
|
||||
.find({ 'aria-label': 'Query text' })
|
||||
.at(0)
|
||||
.text()
|
||||
).toEqual('query1');
|
||||
).toEqual('{"expr":"query1"}');
|
||||
expect(
|
||||
wrapper
|
||||
.find({ 'aria-label': 'Query text' })
|
||||
.at(1)
|
||||
.text()
|
||||
).toEqual('query2');
|
||||
).toEqual('{"expr":"query2"}');
|
||||
expect(
|
||||
wrapper
|
||||
.find({ 'aria-label': 'Query text' })
|
||||
.at(2)
|
||||
.text()
|
||||
).toEqual('query3');
|
||||
).toEqual('{"expr":"query3"}');
|
||||
});
|
||||
it('should render data source icon', () => {
|
||||
const wrapper = setup();
|
||||
@ -79,29 +87,29 @@ describe('RichHistoryCard', () => {
|
||||
describe('commenting', () => {
|
||||
it('should render comment, if comment present', () => {
|
||||
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');
|
||||
});
|
||||
it('should have title "Edit comment" at comment icon, if comment present', () => {
|
||||
const wrapper = setup({ query: starredQueryWithComment });
|
||||
expect(wrapper.find({ title: 'Edit comment' }).hostNodes()).toHaveLength(1);
|
||||
expect(wrapper.find({ title: 'Add comment' }).hostNodes()).toHaveLength(0);
|
||||
expect(wrapper.find({ title: 'Edit comment' })).toHaveLength(1);
|
||||
expect(wrapper.find({ title: 'Add comment' })).toHaveLength(0);
|
||||
});
|
||||
it('should have title "Add comment" at comment icon, if no comment present', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find({ title: 'Add comment' }).hostNodes()).toHaveLength(1);
|
||||
expect(wrapper.find({ title: 'Edit comment' }).hostNodes()).toHaveLength(0);
|
||||
expect(wrapper.find({ title: 'Add comment' })).toHaveLength(1);
|
||||
expect(wrapper.find({ title: 'Edit comment' })).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('starring', () => {
|
||||
it('should have title "Star query", if not starred', () => {
|
||||
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', () => {
|
||||
const wrapper = setup({ query: starredQueryWithComment });
|
||||
expect(wrapper.find({ title: 'Unstar query' }).hostNodes()).toHaveLength(1);
|
||||
expect(wrapper.find({ title: 'Unstar query' })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { css, cx } from 'emotion';
|
||||
import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui';
|
||||
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
|
||||
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 { StoreState } from 'app/types';
|
||||
|
||||
@ -27,8 +27,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
const rigtColumnWidth = '240px';
|
||||
const rigtColumnContentWidth = '170px';
|
||||
|
||||
const borderColor = theme.isLight ? theme.palette.gray5 : theme.palette.gray25;
|
||||
|
||||
/* If datasource was removed, card will have inactive color */
|
||||
const cardColor = theme.isLight
|
||||
? isRemoved
|
||||
@ -42,7 +40,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
queryCard: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid ${borderColor};
|
||||
border: 1px solid ${theme.colors.formInputBorder};
|
||||
margin: ${theme.spacing.sm} 0;
|
||||
background-color: ${cardColor};
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
@ -57,7 +55,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
padding: ${theme.spacing.sm};
|
||||
border-bottom: none;
|
||||
:first-of-type {
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
border-bottom: 1px solid ${theme.colors.formInputBorder};
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||
}
|
||||
img {
|
||||
@ -86,7 +84,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
width: calc(100% - ${rigtColumnWidth});
|
||||
`,
|
||||
queryRow: css`
|
||||
border-top: 1px solid ${borderColor};
|
||||
border-top: 1px solid ${theme.colors.formInputBorder};
|
||||
word-break: break-all;
|
||||
padding: 4px 2px;
|
||||
:first-child {
|
||||
@ -110,7 +108,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
}
|
||||
`,
|
||||
textArea: css`
|
||||
border: 1px solid ${borderColor};
|
||||
border: 1px solid ${theme.colors.formInputBorder};
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
@ -125,7 +123,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
justify-content: flex-end;
|
||||
button {
|
||||
height: auto;
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.md};
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.md};
|
||||
line-height: 1.4;
|
||||
span {
|
||||
white-space: normal !important;
|
||||
}
|
||||
@ -147,24 +146,33 @@ export function RichHistoryCard(props: Props) {
|
||||
} = props;
|
||||
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
|
||||
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 styles = getStyles(theme, isRemoved);
|
||||
|
||||
const onRunQuery = async () => {
|
||||
const dataQueries = query.queries.map((q, i) => createDataQuery(query, q, i));
|
||||
const queriesToRun = query.queries;
|
||||
if (query.datasourceName !== datasourceInstance?.name) {
|
||||
await changeDatasource(exploreId, query.datasourceName);
|
||||
setQueries(exploreId, dataQueries);
|
||||
setQueries(exploreId, queriesToRun);
|
||||
} else {
|
||||
setQueries(exploreId, dataQueries);
|
||||
setQueries(exploreId, queriesToRun);
|
||||
}
|
||||
};
|
||||
|
||||
const onCopyQuery = () => {
|
||||
const queries = query.queries.join('\n\n');
|
||||
copyStringToClipboard(queries);
|
||||
const queriesToCopy = query.queries.map(q => createQueryText(q, queryDsInstance)).join('\n');
|
||||
copyStringToClipboard(queriesToCopy);
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
|
||||
};
|
||||
|
||||
@ -183,6 +191,8 @@ export function RichHistoryCard(props: Props) {
|
||||
updateRichHistory(query.ts, 'starred');
|
||||
};
|
||||
|
||||
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
|
||||
|
||||
const onUpdateComment = () => {
|
||||
updateRichHistory(query.ts, 'comment', comment);
|
||||
toggleActiveUpdateComment();
|
||||
@ -243,9 +253,10 @@ export function RichHistoryCard(props: Props) {
|
||||
<div className={cx(styles.cardRow)}>
|
||||
<div className={styles.queryContainer}>
|
||||
{query.queries.map((q, i) => {
|
||||
const queryText = createQueryText(q, queryDsInstance);
|
||||
return (
|
||||
<div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}>
|
||||
{q}
|
||||
{queryText}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -22,22 +22,19 @@ import { RichHistory, Tabs } from './RichHistory';
|
||||
import { deleteRichHistory } from '../state/actions';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const containerBackground = theme.isLight ? theme.palette.gray95 : theme.palette.gray15;
|
||||
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;
|
||||
const shadowColor = theme.isLight ? theme.palette.gray4 : theme.palette.black;
|
||||
|
||||
return {
|
||||
container: css`
|
||||
position: fixed !important;
|
||||
bottom: 0;
|
||||
background: ${containerBackground};
|
||||
border-top: 1px solid ${containerBorderColor};
|
||||
background: ${theme.colors.pageHeaderBg};
|
||||
border-top: 1px solid ${theme.colors.formInputBorder};
|
||||
margin: 0px;
|
||||
margin-right: -${theme.spacing.md};
|
||||
margin-left: -${theme.spacing.md};
|
||||
box-shadow: 0 0 4px ${shadowColor};
|
||||
z-index: ${theme.zIndex.sidemenu};
|
||||
`,
|
||||
drawerActive: css`
|
||||
opacity: 1;
|
||||
@ -48,30 +45,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
transform: translateY(400px);
|
||||
`,
|
||||
rzHandle: css`
|
||||
background: ${handleBackground};
|
||||
background: ${theme.colors.formInputBorder};
|
||||
transition: 0.3s background ease-in-out;
|
||||
position: relative;
|
||||
width: 200px !important;
|
||||
height: 7px !important;
|
||||
left: calc(50% - 100px) !important;
|
||||
top: -4px !important;
|
||||
cursor: grab;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${handleBackgroundHover};
|
||||
|
||||
&:after {
|
||||
border-color: ${handleDotsHover};
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
border-top: 4px dotted ${handleDots};
|
||||
margin: 0 4px;
|
||||
background: ${theme.colors.formInputBorderHover};
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
@ -21,8 +21,7 @@ import {
|
||||
// Components
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
import { sortOrderOptions } from './RichHistory';
|
||||
import { LegacyForms, Slider } from '@grafana/ui';
|
||||
const { Select } = LegacyForms;
|
||||
import { Slider, Select } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
queries: RichHistoryQuery[];
|
||||
@ -60,12 +59,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
|
||||
width: calc(${cardWidth});
|
||||
`,
|
||||
containerSlider: css`
|
||||
width: 127px;
|
||||
width: 129px;
|
||||
margin-right: ${theme.spacing.sm};
|
||||
.slider {
|
||||
bottom: 10px;
|
||||
height: ${sliderHeight};
|
||||
width: 127px;
|
||||
width: 129px;
|
||||
padding: ${theme.spacing.sm} 0;
|
||||
}
|
||||
`,
|
||||
@ -141,9 +140,10 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
|
||||
|
||||
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
|
||||
const filteredQueriesByDatasource = datasourceFilters
|
||||
? queries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
|
||||
: queries;
|
||||
const filteredQueriesByDatasource =
|
||||
listOfDatasourceFilters && listOfDatasourceFilters?.length > 0
|
||||
? queries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
|
||||
: queries;
|
||||
|
||||
const sortedQueries = sortQueries(filteredQueriesByDatasource, sortOrder);
|
||||
const queriesWithinSelectedTimeline = sortedQueries?.filter(
|
||||
|
@ -15,8 +15,7 @@ import { sortQueries, createDatasourcesList } from '../../../core/utils/richHist
|
||||
// Components
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
import { sortOrderOptions } from './RichHistory';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
const { Select } = LegacyForms;
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
queries: RichHistoryQuery[];
|
||||
@ -87,9 +86,10 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
|
||||
|
||||
const starredQueries = queries.filter(q => q.starred === true);
|
||||
const starredQueriesFilteredByDatasource = datasourceFilters
|
||||
? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
|
||||
: starredQueries;
|
||||
const starredQueriesFilteredByDatasource =
|
||||
listOfDatasourceFilters && listOfDatasourceFilters?.length > 0
|
||||
? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
|
||||
: starredQueries;
|
||||
|
||||
const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder);
|
||||
|
||||
|
@ -44,7 +44,6 @@ import {
|
||||
updateStarredInRichHistory,
|
||||
updateCommentInRichHistory,
|
||||
deleteQueryInRichHistory,
|
||||
getQueryDisplayText,
|
||||
getRichHistory,
|
||||
} from 'app/core/utils/richHistory';
|
||||
// Types
|
||||
@ -487,17 +486,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
if (!data.error && firstResponse) {
|
||||
// Side-effect: Saving history in localstorage
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
const arrayOfStringifiedQueries = queries.map(query =>
|
||||
datasourceInstance?.getQueryDisplayText
|
||||
? datasourceInstance.getQueryDisplayText(query)
|
||||
: getQueryDisplayText(query)
|
||||
);
|
||||
|
||||
const nextRichHistory = addToRichHistory(
|
||||
richHistory || [],
|
||||
datasourceId,
|
||||
datasourceName,
|
||||
arrayOfStringifiedQueries,
|
||||
queries,
|
||||
false,
|
||||
'',
|
||||
''
|
||||
|
@ -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>> {
|
||||
// Hack for proxying metadata requests
|
||||
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
|
||||
|
@ -241,7 +241,7 @@ export type RichHistoryQuery = {
|
||||
datasourceId: string;
|
||||
starred: boolean;
|
||||
comment: string;
|
||||
queries: string[];
|
||||
queries: DataQuery[];
|
||||
sessionName: string;
|
||||
timeRange?: string;
|
||||
};
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
.explore-active-button {
|
||||
box-shadow: $btn-active-box-shadow;
|
||||
border-color: $orange-dark;
|
||||
border: 1px solid $orange-dark;
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
color: $orange-dark !important;
|
||||
|
Loading…
Reference in New Issue
Block a user