QueryHistory: Improve handling of mixed datasource entries (#62214)

* QueryHistory: Improve handling of mixed datasource entries

* remove todo

* remove todo

* fix comment submit test

* disable running queries if at least one doesn't have a datasource

* remove unnecessary code

* add tests for diabled buttons state
This commit is contained in:
Giordano Ricci 2023-02-01 10:11:17 +00:00 committed by GitHub
parent f985f02584
commit c66ab3a9e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 440 additions and 117 deletions

View File

@ -234,12 +234,9 @@ export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder
return heading;
}
export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi | undefined) {
/* query DatasourceInstance is necessary because we use its getQueryDisplayText method
* to format query text
*/
if (queryDsInstance?.getQueryDisplayText) {
return queryDsInstance.getQueryDisplayText(query);
export function createQueryText(query: DataQuery, dsApi?: DataSourceApi) {
if (dsApi?.getQueryDisplayText) {
return dsApi.getQueryDisplayText(query);
}
return getQueryDisplayText(query);
@ -270,7 +267,6 @@ export function createDatasourcesList() {
return {
name: dsSettings.name,
uid: dsSettings.uid,
imgUrl: dsSettings.meta.info.logos.small,
};
});
}

View File

@ -1,11 +1,10 @@
import { render, screen, fireEvent, getByText } from '@testing-library/react';
import { render, screen, fireEvent, getByText, waitFor } from '@testing-library/react';
import React from 'react';
import { DataSourceApi } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
import { DataQuery, DataSourceRef } from '@grafana/schema';
import appEvents from 'app/core/app_events';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ExploreId, RichHistoryQuery } from 'app/types/explore';
@ -14,10 +13,54 @@ import { RichHistoryCard, Props } from './RichHistoryCard';
const starRichHistoryMock = jest.fn();
const deleteRichHistoryMock = jest.fn();
const mockDS = mockDataSource({
name: 'CloudManager',
type: DataSourceType.Alertmanager,
});
class MockDatasourceApi<T extends DataQuery> implements DataSourceApi<T> {
name: string;
id: number;
type: string;
uid: string;
meta: DataSourcePluginMeta<{}>;
constructor(name: string, id: number, type: string, uid: string, others?: Partial<DataSourceApi>) {
this.name = name;
this.id = id;
this.type = type;
this.uid = uid;
this.meta = {
info: {
logos: {
small: `${type}.png`,
},
},
} as DataSourcePluginMeta;
Object.assign(this, others);
}
query(): ReturnType<DataSourceApi['query']> {
throw new Error('Method not implemented.');
}
testDatasource(): ReturnType<DataSourceApi['testDatasource']> {
throw new Error('Method not implemented.');
}
getRef(): DataSourceRef {
throw new Error('Method not implemented.');
}
}
const dsStore: Record<string, DataSourceApi> = {
alertmanager: new MockDatasourceApi('Alertmanager', 3, 'alertmanager', 'alertmanager'),
loki: new MockDatasourceApi('Loki', 2, 'loki', 'loki'),
prometheus: new MockDatasourceApi<MockQuery>('Prometheus', 1, 'prometheus', 'prometheus', {
getQueryDisplayText: (query: MockQuery) => query.queryText || 'Unknwon query',
}),
mixed: new MixedDatasource({
id: 4,
name: 'Mixed',
type: 'mixed',
uid: 'mixed',
meta: { info: { logos: { small: 'mixed.png' } }, mixed: true },
} as DataSourceInstanceSettings),
};
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@ -27,19 +70,33 @@ jest.mock('@grafana/runtime', () => ({
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
get: () => Promise.resolve(mockDS),
getList: () => [mockDS],
getInstanceSettings: () => mockDS,
get: (ref: DataSourceRef | string) => {
const uid = typeof ref === 'string' ? ref : ref.uid;
if (!uid) {
return Promise.reject();
}
if (dsStore[uid]) {
return Promise.resolve(dsStore[uid]);
}
return Promise.reject();
},
}),
};
});
const copyStringToClipboard = jest.fn();
jest.mock('app/core/utils/explore', () => ({
...jest.requireActual('app/core/utils/explore'),
copyStringToClipboard: (str: string) => copyStringToClipboard(str),
}));
jest.mock('app/core/app_events', () => ({
publish: jest.fn(),
}));
interface MockQuery extends DataQuery {
query: string;
queryText?: string;
}
const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
@ -47,8 +104,8 @@ const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
query: {
id: '1',
createdAt: 1,
datasourceUid: 'Test datasource uid',
datasourceName: 'Test datasource',
datasourceUid: 'loki',
datasourceName: 'Loki',
starred: false,
comment: '',
queries: [
@ -57,15 +114,13 @@ const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
{ query: 'query3', refId: 'C' },
],
},
dsImg: '/app/img',
isRemoved: false,
changeDatasource: jest.fn(),
starHistoryItem: starRichHistoryMock,
deleteHistoryItem: deleteRichHistoryMock,
commentHistoryItem: jest.fn(),
setQueries: jest.fn(),
exploreId: ExploreId.left,
datasourceInstance: { name: 'Datasource' } as DataSourceApi,
datasourceInstance: dsStore.loki,
};
Object.assign(props, propOverrides);
@ -107,12 +162,226 @@ describe('RichHistoryCard', () => {
expect(datasourceIcon).toBeInTheDocument();
expect(datasourceName).toBeInTheDocument();
});
it('should render "Data source does not exist anymore" if removed data source', async () => {
setup({ isRemoved: true });
setup({
query: {
id: '2',
createdAt: 1,
datasourceUid: 'non-existent DS',
datasourceName: 'Test datasource',
starred: false,
comment: '',
queries: [
{ query: 'query1', refId: 'A' },
{ query: 'query2', refId: 'B' },
{ query: 'query3', refId: 'C' },
],
},
});
const datasourceName = await screen.findByLabelText('Data source name');
expect(datasourceName).toHaveTextContent('Data source does not exist anymore');
});
describe('copy queries to clipboard', () => {
it('should copy query model to clipboard when copying a query from a non existent datasource', async () => {
setup({
query: {
id: '2',
createdAt: 1,
datasourceUid: 'non-existent DS',
datasourceName: 'Test datasource',
starred: false,
comment: '',
queries: [{ query: 'query1', refId: 'A' }],
},
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
fireEvent.click(copyQueriesButton);
await waitFor(() => {
expect(copyStringToClipboard).toHaveBeenCalledTimes(1);
});
expect(copyStringToClipboard).toHaveBeenCalledWith(JSON.stringify({ query: 'query1' }));
});
it('should copy query model to clipboard when copying a query from a datasource that does not implement getQueryDisplayText', async () => {
setup({
query: {
id: '2',
createdAt: 1,
datasourceUid: 'loki',
datasourceName: 'Test datasource',
starred: false,
comment: '',
queries: [{ query: 'query1', refId: 'A' }],
},
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
fireEvent.click(copyQueriesButton);
await waitFor(() => {
expect(copyStringToClipboard).toHaveBeenCalledTimes(1);
});
expect(copyStringToClipboard).toHaveBeenCalledWith(JSON.stringify({ query: 'query1' }));
});
it('should copy query text to clipboard when copying a query from a datasource that implements getQueryDisplayText', async () => {
setup({
query: {
id: '2',
createdAt: 1,
datasourceUid: 'prometheus',
datasourceName: 'Test datasource',
starred: false,
comment: '',
queries: [{ query: 'query1', refId: 'A', queryText: 'query1' }],
},
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
fireEvent.click(copyQueriesButton);
await waitFor(() => {
expect(copyStringToClipboard).toHaveBeenCalledTimes(1);
});
expect(copyStringToClipboard).toHaveBeenCalledWith('query1');
});
it('should use each datasource getQueryDisplayText when copying queries', async () => {
setup({
query: {
id: '2',
createdAt: 1,
datasourceUid: 'mixed',
datasourceName: 'Mixed',
starred: false,
comment: '',
queries: [
{ query: 'query1', refId: 'A', queryText: 'query1', datasource: { uid: 'prometheus' } },
{ query: 'query2', refId: 'B', datasource: { uid: 'loki' } },
],
},
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
fireEvent.click(copyQueriesButton);
await waitFor(() => {
expect(copyStringToClipboard).toHaveBeenCalledTimes(1);
});
expect(copyStringToClipboard).toHaveBeenCalledWith(`query1\n${JSON.stringify({ query: 'query2' })}`);
});
});
describe('run queries', () => {
it('should be disabled if at least one query datasource is missing when using mixed', async () => {
const setQueries = jest.fn();
const changeDatasource = jest.fn();
const queries: MockQuery[] = [
{ query: 'query1', refId: 'A', datasource: { uid: 'nonexistent-ds' } },
{ query: 'query2', refId: 'B', datasource: { uid: 'loki' } },
];
setup({
setQueries,
changeDatasource,
query: {
id: '2',
createdAt: 1,
datasourceUid: 'mixed',
datasourceName: 'Mixed',
starred: false,
comment: '',
queries,
},
});
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
expect(runQueryButton).toBeDisabled();
});
it('should be disabled if at datasource is missing', async () => {
const setQueries = jest.fn();
const changeDatasource = jest.fn();
const queries: MockQuery[] = [
{ query: 'query1', refId: 'A' },
{ query: 'query2', refId: 'B' },
];
setup({
setQueries,
changeDatasource,
query: {
id: '2',
createdAt: 1,
datasourceUid: 'nonexistent-ds',
datasourceName: 'nonexistent-ds',
starred: false,
comment: '',
queries,
},
});
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
expect(runQueryButton).toBeDisabled();
});
it('should only set new queries when running queries from the same datasource', async () => {
const setQueries = jest.fn();
const changeDatasource = jest.fn();
const queries: MockQuery[] = [
{ query: 'query1', refId: 'A' },
{ query: 'query2', refId: 'B' },
];
setup({
setQueries,
changeDatasource,
query: {
id: '2',
createdAt: 1,
datasourceUid: 'loki',
datasourceName: 'Loki',
starred: false,
comment: '',
queries,
},
});
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
fireEvent.click(runQueryButton);
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
expect(changeDatasource).not.toHaveBeenCalled();
});
it('should change datasource to mixed and set new queries when running queries from mixed datasource', async () => {
const setQueries = jest.fn();
const changeDatasource = jest.fn();
const queries: MockQuery[] = [
{ query: 'query1', refId: 'A', datasource: { type: 'loki', uid: 'loki' } },
{ query: 'query2', refId: 'B', datasource: { type: 'prometheus', uid: 'prometheus' } },
];
setup({
setQueries,
changeDatasource,
query: {
id: '2',
createdAt: 1,
datasourceUid: 'mixed',
datasourceName: 'Mixed',
starred: false,
comment: '',
queries,
},
});
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
fireEvent.click(runQueryButton);
await waitFor(() => {
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
expect(changeDatasource).toHaveBeenCalledWith(expect.any(String), 'mixed');
});
});
});
describe('commenting', () => {
it('should render comment, if comment present', async () => {
setup({ query: starredQueryWithComment });

View File

@ -1,26 +1,26 @@
import { css, cx } from '@emotion/css';
import React, { useState, useEffect } from 'react';
import React, { useCallback, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { useAsync } from 'react-use';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, DataSourceApi } from '@grafana/data';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { TextArea, Button, IconButton, useStyles2 } from '@grafana/ui';
import { TextArea, Button, IconButton, useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import appEvents from 'app/core/app_events';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { copyStringToClipboard } from 'app/core/utils/explore';
import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { changeDatasource } from 'app/features/explore/state/datasource';
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from 'app/features/explore/state/history';
import { setQueries } from 'app/features/explore/state/query';
import { dispatch } from 'app/store/store';
import { StoreState } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { ShowConfirmModalEvent } from '../../../types/events';
import { changeDatasource } from '../state/datasource';
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from '../state/history';
import { setQueries } from '../state/query';
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const { datasourceInstance } = explore[exploreId]!;
@ -42,8 +42,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
interface OwnProps<T extends DataQuery = DataQuery> {
query: RichHistoryQuery<T>;
dsImg: string;
isRemoved: boolean;
}
export type Props<T extends DataQuery = DataQuery> = ConnectedProps<typeof connector> & OwnProps<T>;
@ -58,6 +56,7 @@ const getStyles = (theme: GrafanaTheme2) => {
return {
queryCard: css`
position: relative;
display: flex;
flex-direction: column;
border: 1px solid ${theme.colors.border.weak};
@ -84,12 +83,6 @@ const getStyles = (theme: GrafanaTheme2) => {
margin-right: ${theme.spacing(1)};
}
`,
datasourceContainer: css`
display: flex;
align-items: center;
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
`,
queryActionButtons: css`
max-width: ${rightColumnContentWidth};
display: flex;
@ -103,15 +96,6 @@ const getStyles = (theme: GrafanaTheme2) => {
font-weight: ${theme.typography.fontWeightMedium};
width: calc(100% - ${rightColumnWidth});
`,
queryRow: css`
border-top: 1px solid ${theme.colors.border.weak};
word-break: break-all;
padding: 4px 2px;
:first-child {
border-top: none;
padding: 0 0 4px 0;
}
`,
updateCommentContainer: css`
width: calc(100% + ${rightColumnWidth});
margin-top: ${theme.spacing(1)};
@ -143,14 +127,21 @@ const getStyles = (theme: GrafanaTheme2) => {
}
}
`,
loader: css`
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: ${theme.colors.background.secondary};
`,
};
};
export function RichHistoryCard(props: Props) {
const {
query,
dsImg,
isRemoved,
commentHistoryItem,
starHistoryItem,
deleteHistoryItem,
@ -161,16 +152,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);
const { value, loading } = useAsync(async () => {
let dsInstance: DataSourceApi | undefined;
try {
dsInstance = await getDataSourceSrv().get(query.datasourceUid);
} catch (e) {}
useEffect(() => {
const getQueryDsInstance = async () => {
const ds = await getDataSourceSrv().get(query.datasourceUid);
setQueryDsInstance(ds);
return {
dsInstance,
queries: await Promise.all(
query.queries.map(async (query) => {
let datasource;
if (dsInstance?.meta.mixed) {
try {
datasource = await getDataSourceSrv().get(query.datasource);
} catch (e) {}
} else {
datasource = dsInstance;
}
return {
query,
datasource,
};
})
),
};
getQueryDsInstance();
}, [query.datasourceUid]);
}, [query.datasourceUid, query.queries]);
const styles = useStyles2(getStyles);
@ -178,25 +186,34 @@ export function RichHistoryCard(props: Props) {
const queriesToRun = query.queries;
const differentDataSource = query.datasourceUid !== datasourceInstance?.uid;
if (differentDataSource) {
await changeDatasource(exploreId, query.datasourceUid, { importQueries: true });
setQueries(exploreId, queriesToRun);
} else {
setQueries(exploreId, queriesToRun);
await changeDatasource(exploreId, query.datasourceUid);
}
setQueries(exploreId, queriesToRun);
reportInteraction('grafana_explore_query_history_run', {
queryHistoryEnabled: config.queryHistoryEnabled,
differentDataSource,
});
};
const onCopyQuery = () => {
const onCopyQuery = async () => {
const datasources = [...query.queries.map((q) => q.datasource?.type || 'unknown')];
reportInteraction('grafana_explore_query_history_copy_query', {
datasources,
mixed: Boolean(queryDsInstance?.meta.mixed),
mixed: Boolean(value?.dsInstance?.meta.mixed),
});
const queriesToCopy = query.queries.map((q) => createQueryText(q, queryDsInstance)).join('\n');
copyStringToClipboard(queriesToCopy);
if (loading || !value) {
return;
}
const queriesText = value.queries
.map((q) => {
return createQueryText(q.query, q.datasource);
})
.join('\n');
copyStringToClipboard(queriesText);
dispatch(notifyApp(createSuccessNotification('Query copied to clipboard')));
};
@ -273,9 +290,7 @@ export function RichHistoryCard(props: Props) {
className={styles.textArea}
/>
<div className={styles.commentButtonRow}>
<Button onClick={onUpdateComment} aria-label="Submit button">
Save comment
</Button>
<Button onClick={onUpdateComment}>Save comment</Button>
<Button variant="secondary" onClick={onCancelUpdateComment}>
Cancel
</Button>
@ -291,7 +306,7 @@ export function RichHistoryCard(props: Props) {
title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
/>
<IconButton name="copy" onClick={onCopyQuery} title="Copy query to clipboard" />
{!isRemoved && (
{value?.dsInstance && (
<IconButton name="share-alt" onClick={onCreateShortLink} title="Copy shortened link to clipboard" />
)}
<IconButton name="trash-alt" title={'Delete query'} onClick={onDeleteQuery} />
@ -307,23 +322,14 @@ export function RichHistoryCard(props: Props) {
return (
<div className={styles.queryCard}>
<div className={styles.cardRow}>
<div className={styles.datasourceContainer}>
<img src={dsImg} aria-label="Data source icon" />
<div aria-label="Data source name">
{isRemoved ? 'Data source does not exist anymore' : query.datasourceName}
</div>
</div>
<DatasourceInfo dsApi={value?.dsInstance} size="sm" />
{queryActionButtons}
</div>
<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}>
{queryText}
</div>
);
{value?.queries.map((q, i) => {
return <Query query={q} key={`${q}-${i}`} showDsInfo={value?.dsInstance?.meta.mixed} />;
})}
{!activeUpdateComment && query.comment && (
<div aria-label="Query comment" className={styles.comment}>
@ -334,12 +340,89 @@ export function RichHistoryCard(props: Props) {
</div>
{!activeUpdateComment && (
<div className={styles.runButton}>
<Button variant="secondary" onClick={onRunQuery} disabled={isRemoved}>
<Button
variant="secondary"
onClick={onRunQuery}
disabled={!value?.dsInstance || value.queries.some((query) => !query.datasource)}
>
{datasourceInstance?.uid === query.datasourceUid ? 'Run query' : 'Switch data source and run query'}
</Button>
</div>
)}
</div>
{loading && <LoadingPlaceholder text="loading..." className={styles.loader} />}
</div>
);
}
const getQueryStyles = (theme: GrafanaTheme2) => ({
queryRow: css`
border-top: 1px solid ${theme.colors.border.weak};
display: flex;
flex-direction: row;
padding: 4px 0px;
gap: 4px;
:first-child {
border-top: none;
}
`,
dsInfoContainer: css`
display: flex;
align-items: center;
`,
queryText: css`
word-break: break-all;
`,
});
interface QueryProps {
query: {
query: DataQuery;
datasource?: DataSourceApi;
};
/** Show datasource info (icon+name) alongside the query text */
showDsInfo?: boolean;
}
const Query = ({ query, showDsInfo = false }: QueryProps) => {
const styles = useStyles2(getQueryStyles);
return (
<div className={styles.queryRow}>
{showDsInfo && (
<div className={styles.dsInfoContainer}>
<DatasourceInfo dsApi={query.datasource} size="md" />
{': '}
</div>
)}
<span aria-label="Query text" className={styles.queryText}>
{createQueryText(query.query, query.datasource)}
</span>
</div>
);
};
const getDsInfoStyles = (size: 'sm' | 'md') => (theme: GrafanaTheme2) =>
css`
display: flex;
align-items: center;
font-size: ${theme.typography[size === 'sm' ? 'bodySmall' : 'body'].fontSize};
font-weight: ${theme.typography.fontWeightMedium};
white-space: nowrap;
`;
function DatasourceInfo({ dsApi, size }: { dsApi?: DataSourceApi; size: 'sm' | 'md' }) {
const getStyles = useCallback((theme: GrafanaTheme2) => getDsInfoStyles(size)(theme), [size]);
const styles = useStyles2(getStyles);
return (
<div className={styles}>
<img
src={dsApi?.meta.info.logos.small || 'public/img/icn-datasource.svg'}
alt={dsApi?.type || 'Data source does not exist anymore'}
aria-label="Data source icon"
/>
<div aria-label="Data source name">{dsApi?.name || 'Data source does not exist anymore'}</div>
</div>
);
}

View File

@ -240,16 +240,7 @@ export function RichHistoryQueriesTab(props: Props) {
</span>
</div>
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => {
const idx = listOfDatasources.findIndex((d) => d.uid === q.datasourceUid);
return (
<RichHistoryCard
query={q}
key={q.id}
exploreId={exploreId}
dsImg={idx === -1 ? 'public/img/icn-datasource.svg' : listOfDatasources[idx].imgUrl}
isRemoved={idx === -1}
/>
);
return <RichHistoryCard query={q} key={q.id} exploreId={exploreId} />;
})}
</div>
);

View File

@ -153,16 +153,7 @@ export function RichHistoryStarredTab(props: Props) {
{loading && <span>Loading results...</span>}
{!loading &&
queries.map((q) => {
const idx = listOfDatasources.findIndex((d) => d.uid === q.datasourceUid);
return (
<RichHistoryCard
query={q}
key={q.id}
exploreId={exploreId}
dsImg={idx === -1 ? 'public/img/icn-datasource.svg' : listOfDatasources[idx].imgUrl}
isRemoved={idx === -1}
/>
);
return <RichHistoryCard query={q} key={q.id} exploreId={exploreId} />;
})}
{queries.length && queries.length !== totalQueries ? (
<div>

View File

@ -71,7 +71,7 @@ export const commentQueryHistory = async (
const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
await userEvent.clear(input);
await userEvent.type(input, comment);
await invokeAction(queryIndex, 'Submit button', exploreId);
await invokeAction(queryIndex, 'Save comment', exploreId);
};
export const deleteQueryHistory = async (queryIndex: number, exploreId: ExploreId = ExploreId.left) => {

View File

@ -41,7 +41,7 @@ export function changeDatasource(
exploreId: ExploreId,
datasourceUid: string,
options?: { importQueries: boolean }
): ThunkResult<void> {
): ThunkResult<Promise<void>> {
return async (dispatch, getState) => {
const orgId = getState().user.orgId;
const { history, instance } = await loadAndInitDatasource(orgId, { uid: datasourceUid });

View File

@ -305,7 +305,7 @@ export const importQueries = (
sourceDataSource: DataSourceApi | undefined | null,
targetDataSource: DataSourceApi,
singleQueryChangeRef?: string // when changing one query DS to another in a mixed environment, we do not want to change all queries, just the one being changed
): ThunkResult<void> => {
): ThunkResult<Promise<void>> => {
return async (dispatch) => {
if (!sourceDataSource) {
// explore not initialized

View File

@ -1,4 +1,4 @@
import { cloneDeep, groupBy, omit } from 'lodash';
import { cloneDeep, groupBy } from 'lodash';
import { forkJoin, from, Observable, of, OperatorFunction } from 'rxjs';
import { catchError, map, mergeAll, mergeMap, reduce, toArray } from 'rxjs/operators';
@ -98,13 +98,6 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
return Promise.resolve({});
}
getQueryDisplayText(query: DataQuery) {
const strippedQuery = omit(query, ['key', 'refId', 'datasource']);
const strippedQueryJSON = JSON.stringify(strippedQuery);
const prefix = query.datasource?.type ? `${query.datasource?.type}: ` : '';
return `${prefix}${strippedQueryJSON}`;
}
private isQueryable(query: BatchedQueries): boolean {
return query && Array.isArray(query.targets) && query.targets.length > 0;
}