) {
const { datasource, dsSettings } = model.useState();
const { data, queries } = model.queryRunner.useState();
+ const { openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext();
if (!datasource || !dsSettings || !data) {
return null;
}
-
const showAddButton = !isSharedDashboardQuery(dsSettings.name);
+ // Make the final query library action button by injecting actual addQuery functionality into the button.
+ const addQueryActionButton = makeQueryActionButton((queries) => {
+ for (const query of queries) {
+ model.onQueriesChange(addQuery(model.getQueries(), query));
+ }
+ });
+
return (
{showAddButton && (
-
- Add query
-
+ <>
+
+ Add query
+
+ {config.featureToggles.queryLibrary && (
+ {
+ openQueryLibraryDrawer(getDatasourceNames(datasource, queries), addQueryActionButton);
+ }}
+ variant="secondary"
+ data-testid={selectors.components.QueryTab.addQuery}
+ >
+ Add query from library
+
+ )}
+ >
)}
{config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && (
void) {
+ return function AddQueryFromLibraryButton(props: QueryActionButtonProps) {
+ const label = t('dashboards.query-library.add-query-button', 'Add query');
+ return (
+ {
+ addQueries(props.queries);
+ props.onClick();
+ }}
+ >
+ {label}
+
+ );
+ };
+}
+
+function getDatasourceNames(datasource: DataSourceApi, queries: DataQuery[]): string[] {
+ if (datasource.uid === '-- Mixed --') {
+ // If datasource is mixed, the datasource UID is on the query. Here we map the UIDs to datasource names.
+ const dsSrv = getDataSourceSrv();
+ return queries.map((ds) => dsSrv.getInstanceSettings(ds.datasource)?.name).filter((name) => name !== undefined);
+ } else {
+ return [datasource.name];
+ }
+}
+
interface QueriesTabProps extends PanelDataTabHeaderProps {
model: PanelDataQueriesTab;
}
diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx
index 1764a81ed16..96b5d540d84 100644
--- a/public/app/features/explore/ExplorePage.tsx
+++ b/public/app/features/explore/ExplorePage.tsx
@@ -1,30 +1,22 @@
import { css, cx } from '@emotion/css';
-import { useEffect, useState } from 'react';
-import { useLocalStorage } from 'react-use';
+import { useEffect } from 'react';
-import { CoreApp, GrafanaTheme2 } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { DataQuery } from '@grafana/schema/dist/esm/index';
-import { Badge, ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui';
-import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
+import { ErrorBoundaryAlert, useStyles2, useTheme2 } from '@grafana/ui';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useNavModel } from 'app/core/hooks/useNavModel';
-import { Trans, t } from 'app/core/internationalization';
+import { Trans } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { useSelector } from 'app/types';
import { ExploreQueryParams } from 'app/types/explore';
-import { RowActionComponents } from '../query/components/QueryActionComponent';
-
import { CorrelationEditorModeBar } from './CorrelationEditorModeBar';
import { ExploreActions } from './ExploreActions';
import { ExploreDrawer } from './ExploreDrawer';
import { ExplorePaneContainer } from './ExplorePaneContainer';
import { useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext';
-import { QUERY_LIBRARY_LOCAL_STORAGE_KEYS } from './QueryLibrary/QueryLibrary';
-import { queryLibraryTrackAddFromQueryRow } from './QueryLibrary/QueryLibraryAnalyticsEvents';
-import { QueryTemplateForm } from './QueryLibrary/QueryTemplateForm';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
@@ -34,7 +26,6 @@ import { useTimeSrvFix } from './hooks/useTimeSrvFix';
import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors';
const MIN_PANE_WIDTH = 200;
-const QUERY_LIBRARY_ACTION_KEY = 'queryLibraryAction';
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
return ;
@@ -58,13 +49,8 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
const panes = useSelector(selectPanesEntries);
const hasSplit = useSelector(isSplit);
const correlationDetails = useSelector(selectCorrelationDetails);
- const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext();
+ const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext();
const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false);
- const [queryToAdd, setQueryToAdd] = useState();
- const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage(
- QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton,
- true
- );
useEffect(() => {
//This is needed for breadcrumbs and topnav.
@@ -74,38 +60,6 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
});
}, [chrome, navModel]);
- useEffect(() => {
- const hasQueryLibrary = config.featureToggles.queryLibrary || false;
- if (hasQueryLibrary) {
- RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, {
- scope: CoreApp.Explore,
- queryActionComponent: (props) =>
- showQueryLibraryBadgeButton ? (
- {
- setQueryToAdd(props.query);
- setShowQueryLibraryBadgeButton(false);
- }}
- style={{ cursor: 'pointer' }}
- />
- ) : (
- {
- setQueryToAdd(props.query);
- }}
- />
- ),
- });
- }
- }, [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton]);
-
useKeyboardShortcuts();
return (
@@ -139,7 +93,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
})}
{drawerOpened && (
-
+
{
setDrawerOpened(false);
@@ -147,24 +101,6 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
/>
)}
- setQueryToAdd(undefined)}
- >
- {
- setQueryToAdd(undefined);
- }}
- onSave={(isSuccess) => {
- if (isSuccess) {
- setQueryToAdd(undefined);
- queryLibraryTrackAddFromQueryRow(queryToAdd?.datasource?.type || '');
- }
- }}
- queryToAdd={queryToAdd!}
- />
-
);
}
diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx
index 7cb37a62a45..48c7831c9d2 100644
--- a/public/app/features/explore/ExploreRunQueryButton.tsx
+++ b/public/app/features/explore/ExploreRunQueryButton.tsx
@@ -3,7 +3,7 @@ import { ConnectedProps, connect } from 'react-redux';
import { config, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
-import { Button, ButtonVariant, Dropdown, Menu, ToolbarButton } from '@grafana/ui';
+import { Button, Dropdown, Menu, ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useSelector } from 'app/types';
@@ -22,7 +22,6 @@ interface ExploreRunQueryButtonProps {
queries: DataQuery[];
rootDatasourceUid?: string;
disabled?: boolean;
- variant?: ButtonVariant;
onClick?: () => void;
}
@@ -37,7 +36,6 @@ export function ExploreRunQueryButton({
rootDatasourceUid,
queries,
disabled = false,
- variant = 'secondary',
onClick,
changeDatasource,
setQueries,
@@ -84,7 +82,7 @@ export function ExploreRunQueryButton({
const buttonText = runQueryText(exploreId, rootDatasourceUid);
return (
{
runQuery(exploreId);
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx
index 1807bfd7383..7d2dff9097e 100644
--- a/public/app/features/explore/ExploreToolbar.tsx
+++ b/public/app/features/explore/ExploreToolbar.tsx
@@ -5,7 +5,7 @@ import { shallowEqual } from 'react-redux';
import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data';
import { Components } from '@grafana/e2e-selectors';
-import { reportInteraction } from '@grafana/runtime';
+import { config, reportInteraction } from '@grafana/runtime';
import {
defaultIntervals,
PageToolbar,
@@ -91,7 +91,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
const correlationDetails = useSelector(selectCorrelationDetails);
const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
- const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext();
+ const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext();
const shouldRotateSplitIcon = useMemo(
() => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane),
@@ -206,7 +206,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
const navBarActions = [ ];
- if (queryLibraryAvailable) {
+ if (config.featureToggles.queryLibrary) {
navBarActions.unshift( );
} else {
navBarActions.unshift(
diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx
index 7aedc47296f..abcbc468bd5 100644
--- a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx
+++ b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx
@@ -1,29 +1,25 @@
import { PropsWithChildren, useState, createContext, useContext, useEffect } from 'react';
-import { config } from '@grafana/runtime';
import { useSelector } from 'app/types';
import { selectRichHistorySettings } from '../state/selectors';
export enum Tabs {
- QueryLibrary = 'Query library',
RichHistory = 'Query history',
Starred = 'Starred',
Settings = 'Settings',
}
-type QueryLibraryContextType = {
- selectedTab?: Tabs;
+type RichHistoryContextType = {
+ selectedTab: Tabs;
setSelectedTab: (tab: Tabs) => void;
- queryLibraryAvailable: boolean;
drawerOpened: boolean;
setDrawerOpened: (value: boolean) => void;
};
-export const QueriesDrawerContext = createContext({
- selectedTab: undefined,
+export const QueriesDrawerContext = createContext({
+ selectedTab: Tabs.RichHistory,
setSelectedTab: () => {},
- queryLibraryAvailable: false,
drawerOpened: false,
setDrawerOpened: () => {},
});
@@ -33,24 +29,20 @@ export function useQueriesDrawerContext() {
}
export function QueriesDrawerContextProvider({ children }: PropsWithChildren) {
- const queryLibraryAvailable = config.featureToggles.queryLibrary === true;
- const [selectedTab, setSelectedTab] = useState(
- queryLibraryAvailable ? Tabs.QueryLibrary : undefined
- );
+ const [selectedTab, setSelectedTab] = useState(Tabs.RichHistory);
const [drawerOpened, setDrawerOpened] = useState(false);
const settings = useSelector(selectRichHistorySettings);
useEffect(() => {
- if (settings && !queryLibraryAvailable) {
+ if (settings) {
setSelectedTab(settings.starredTabAsFirstTab ? Tabs.Starred : Tabs.RichHistory);
}
- }, [settings, setSelectedTab, queryLibraryAvailable]);
+ }, [settings, setSelectedTab]);
return (
) {
+ return ;
+}
+
type Props = {
variant: 'compact' | 'full';
};
+/**
+ * Dropdown button that can either open a Query History drawer or a Query Library drawer.
+ * @param variant
+ * @constructor
+ */
export function QueriesDrawerDropdown({ variant }: Props) {
- const { selectedTab, setSelectedTab, queryLibraryAvailable, drawerOpened, setDrawerOpened } =
- useQueriesDrawerContext();
+ const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext();
+
+ const {
+ openDrawer: openQueryLibraryDrawer,
+ closeDrawer: closeQueryLibraryDrawer,
+ isDrawerOpen: isQueryLibraryDrawerOpen,
+ } = useQueryLibraryContext();
+
+ const [queryOption, setQueryOption] = useState<'library' | 'history'>('library');
+
+ const exploreActiveDS = useSelector(selectExploreDSMaps);
const styles = useStyles2(getStyles);
- if (!queryLibraryAvailable) {
+ // In case query library is not enabled we show only simple button for query history in the parent.
+ if (!config.featureToggles.queryLibrary) {
return undefined;
}
- function toggle(tab: Tabs) {
- tab === Tabs.QueryLibrary && queryLibraryTrackToggle(!drawerOpened);
+ function toggleRichHistory() {
+ setQueryOption('history');
+ setDrawerOpened(!drawerOpened);
+ }
- setSelectedTab(tab);
- setDrawerOpened(false);
- setDrawerOpened(true);
+ function toggleQueryLibrary() {
+ setQueryOption('library');
+ if (isQueryLibraryDrawerOpen) {
+ closeQueryLibraryDrawer();
+ } else {
+ // Prefill the query library filter with the dataSource.
+ // Get current dataSource that is open. As this is only used in Explore we get it from Explore state.
+ const listOfDatasources = createDatasourcesList();
+ const activeDatasources = exploreActiveDS.dsToExplore
+ .map((eDs) => {
+ return listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name;
+ })
+ .filter((name): name is string => !!name);
+
+ openQueryLibraryDrawer(activeDatasources, ExploreRunQueryButtonWrapper);
+ }
+ queryLibraryTrackToggle(!isQueryLibraryDrawerOpen);
}
const menu = (
- toggle(Tabs.QueryLibrary)} />
- toggle(Tabs.RichHistory)} />
+ toggleQueryLibrary()} />
+ toggleRichHistory()} />
);
+ const buttonLabel = queryOption === 'library' ? i18n.queryLibrary : i18n.queryHistory;
+ const toggle = queryOption === 'library' ? toggleQueryLibrary : toggleRichHistory;
+
return (
{
- setDrawerOpened(!drawerOpened);
- selectedTab === Tabs.QueryLibrary && queryLibraryTrackToggle(!drawerOpened);
- }}
- aria-label={selectedTab}
+ variant={drawerOpened || isQueryLibraryDrawerOpen ? 'active' : 'canvas'}
+ onClick={() => toggle()}
+ aria-label={buttonLabel}
>
- {variant === 'full' ? selectedTab : undefined}
+ {variant === 'full' ? buttonLabel : undefined}
- {drawerOpened ? (
- {
- setDrawerOpened(false);
- selectedTab === Tabs.QueryLibrary && queryLibraryTrackToggle(false);
- }}
- >
+
+ {/* Show either a drops down button so that user can select QL or QH, or show a close button if one of them is
+ already open.*/}
+ {drawerOpened || isQueryLibraryDrawerOpen ? (
+ toggle()}>
) : (
-
+
)}
diff --git a/public/app/features/explore/QueriesDrawer/mocks.tsx b/public/app/features/explore/QueriesDrawer/mocks.tsx
index 37bf17fada4..3aba930f566 100644
--- a/public/app/features/explore/QueriesDrawer/mocks.tsx
+++ b/public/app/features/explore/QueriesDrawer/mocks.tsx
@@ -8,13 +8,12 @@ type Props = {
} & PropsWithChildren;
export function QueriesDrawerContextProviderMock(props: Props) {
- const [selectedTab, setSelectedTab] = useState(Tabs.QueryLibrary);
+ const [selectedTab, setSelectedTab] = useState(Tabs.RichHistory);
const [drawerOpened, setDrawerOpened] = useState(false);
return (
void;
+ query?: DataQuery;
+};
+
+export function AddToQueryLibraryModal({ query, close, isOpen }: Props) {
+ return (
+ close()}
+ >
+ {
+ close();
+ }}
+ onSave={(isSuccess) => {
+ if (isSuccess) {
+ close();
+ queryLibraryTrackAddFromQueryRow(query?.datasource?.type || '');
+ }
+ }}
+ queryToAdd={query!}
+ />
+
+ );
+}
diff --git a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx
index fffc454ee3a..c9b81574b1f 100644
--- a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx
+++ b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx
@@ -2,11 +2,13 @@ import { useLocalStorage } from 'react-use';
import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo';
import { QueryTemplatesList } from './QueryTemplatesList';
+import { QueryActionButton } from './types';
export interface QueryLibraryProps {
// List of active datasources to filter the query library by
// E.g in Explore the active datasources are the datasources that are currently selected in the query editor
activeDatasources?: string[];
+ queryActionButton?: QueryActionButton;
}
export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = {
@@ -16,7 +18,7 @@ export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = {
},
};
-export function QueryLibrary({ activeDatasources }: QueryLibraryProps) {
+export function QueryLibrary({ activeDatasources, queryActionButton }: QueryLibraryProps) {
const [notifyUserAboutQueryLibrary, setNotifyUserAboutQueryLibrary] = useLocalStorage(
QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.notifyUserAboutQueryLibrary,
true
@@ -28,7 +30,7 @@ export function QueryLibrary({ activeDatasources }: QueryLibraryProps) {
isOpen={notifyUserAboutQueryLibrary || false}
onDismiss={() => setNotifyUserAboutQueryLibrary(false)}
/>
-
+
>
);
}
diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx
new file mode 100644
index 00000000000..c48495b9103
--- /dev/null
+++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx
@@ -0,0 +1,79 @@
+import { act, render, screen, waitFor } from '@testing-library/react';
+import { ComponentType } from 'react';
+
+import { PromQuery } from '@grafana/prometheus';
+
+import { useQueryLibraryContext, QueryLibraryContextProvider, QueryLibraryContextType } from './QueryLibraryContext';
+
+// Bit of mocking here mainly so we don't have to mock too much of the API calls here and keep this test focused on the
+// context state management and correct rendering.
+
+jest.mock('./AddToQueryLibraryModal', () => ({
+ __esModule: true,
+ AddToQueryLibraryModal: (props: { isOpen: boolean; query: unknown }) =>
+ props.isOpen && QUERY_MODAL {JSON.stringify(props.query)}
,
+}));
+
+jest.mock('./QueryLibraryDrawer', () => ({
+ __esModule: true,
+ QueryLibraryDrawer: (props: {
+ isOpen: boolean;
+ activeDatasources: string[] | undefined;
+ queryActionButton: ComponentType;
+ }) =>
+ props.isOpen && (
+
+ QUERY_DRAWER {JSON.stringify(props.activeDatasources)} {props.queryActionButton &&
}
+
+ ),
+}));
+
+function setup() {
+ let ctx: { current: QueryLibraryContextType | undefined } = { current: undefined };
+ function TestComp() {
+ ctx.current = useQueryLibraryContext();
+ return
;
+ }
+ // rendering instead of just using renderHook so we can check if the modal and drawer actually render.
+ const renderResult = render(
+
+
+
+ );
+
+ return { ctx, renderResult };
+}
+
+describe('QueryLibraryContext', () => {
+ it('should not render modal or drawer by default', () => {
+ setup();
+ // should catch both modal and drawer
+ expect(screen.queryByText(/QUERY_MODAL/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/QUERY_DRAWER/i)).not.toBeInTheDocument();
+ });
+
+ it('should be able to open modal', async () => {
+ const { ctx } = setup();
+ act(() => {
+ ctx.current!.openAddQueryModal({ refId: 'A', expr: 'http_requests_total{job="test"}' } as PromQuery);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText(/QUERY_MODAL/i)).toBeInTheDocument();
+ expect(screen.queryByText(/http_requests_total\{job=\\"test\\"}/i)).toBeInTheDocument();
+ });
+ });
+
+ it('should be able to open drawer', async () => {
+ const { ctx } = setup();
+ act(() => {
+ ctx.current!.openDrawer(['PROM_TEST_DS'], () => QUERY_ACTION_BUTTON
);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText(/QUERY_DRAWER/i)).toBeInTheDocument();
+ expect(screen.queryByText(/PROM_TEST_DS/i)).toBeInTheDocument();
+ expect(screen.queryByText(/QUERY_ACTION_BUTTON/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx
new file mode 100644
index 00000000000..65a815c7401
--- /dev/null
+++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx
@@ -0,0 +1,118 @@
+import { PropsWithChildren, useState, createContext, useContext, useCallback, useMemo } from 'react';
+
+import { DataQuery } from '@grafana/schema';
+
+import { AddToQueryLibraryModal } from './AddToQueryLibraryModal';
+import { QueryLibraryDrawer } from './QueryLibraryDrawer';
+import { QueryActionButton, QueryActionButtonProps } from './types';
+
+/**
+ * Context with state and action to interact with Query Library. The Query Library feature consists of a drawer
+ * that shows existing queries and allows users to use them and manage them and then an AddQueryModal which allows
+ * users to save a query into the library. Both of those are included in Grafana AppChrome component.
+ *
+ * Use this context to interact with those components, showing, hiding and setting initial state for them.
+ */
+export type QueryLibraryContextType = {
+ /**
+ * Opens a drawer with query library.
+ * @param datasourceFilters Data source names that will be used for initial filter in the library.
+ * @param queryActionButton Action button will be shown in the library next to the query and can implement context
+ * specific actions with the library, like running the query or updating some query in the current app.
+ */
+ openDrawer: (datasourceFilters: string[], queryActionButton: QueryActionButton) => void;
+ closeDrawer: () => void;
+ isDrawerOpen: boolean;
+
+ /**
+ * Opens a modal for adding a query to the library.
+ * @param query
+ */
+ openAddQueryModal: (query: DataQuery) => void;
+ closeAddQueryModal: () => void;
+};
+
+export const QueryLibraryContext = createContext({
+ openDrawer: () => {},
+ closeDrawer: () => {},
+ isDrawerOpen: false,
+
+ openAddQueryModal: () => {},
+ closeAddQueryModal: () => {},
+});
+
+export function useQueryLibraryContext() {
+ return useContext(QueryLibraryContext);
+}
+
+export function QueryLibraryContextProvider({ children }: PropsWithChildren) {
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+ const [activeDatasources, setActiveDatasources] = useState([]);
+ const [isAddQueryModalOpen, setIsAddQueryModalOpen] = useState(false);
+ const [activeQuery, setActiveQuery] = useState(undefined);
+ const [queryActionButton, setQueryActionButton] = useState(undefined);
+
+ const openDrawer = useCallback((datasourceFilters: string[], queryActionButton: QueryActionButton) => {
+ setActiveDatasources(datasourceFilters);
+ // Because the queryActionButton can be a function component it would be called as a callback if just passed in.
+ setQueryActionButton(() => queryActionButton);
+ setIsDrawerOpen(true);
+ }, []);
+
+ const closeDrawer = useCallback(() => {
+ setActiveDatasources([]);
+ setQueryActionButton(undefined);
+ setIsDrawerOpen(false);
+ }, []);
+
+ const openAddQueryModal = useCallback((query: DataQuery) => {
+ setActiveQuery(query);
+ setIsAddQueryModalOpen(true);
+ }, []);
+
+ const closeAddQueryModal = useCallback(() => {
+ setActiveQuery(undefined);
+ setIsAddQueryModalOpen(false);
+ }, []);
+
+ // We wrap the action button one time to add the closeDrawer behaviour. This way whoever injects the action button
+ // does not need to remember to do it nor the query table inside that renders it needs to know about the drawer.
+ const finalActionButton = useMemo(() => {
+ if (!queryActionButton) {
+ return queryActionButton;
+ }
+ return (props: QueryActionButtonProps) => {
+ const QButton = queryActionButton;
+ return (
+ {
+ props.onClick();
+ closeDrawer();
+ }}
+ />
+ );
+ };
+ }, [closeDrawer, queryActionButton]);
+
+ return (
+
+ {children}
+
+
+
+ );
+}
diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx
new file mode 100644
index 00000000000..18e2caead3d
--- /dev/null
+++ b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx
@@ -0,0 +1,53 @@
+import { skipToken } from '@reduxjs/toolkit/query/react';
+
+import { selectors } from '@grafana/e2e-selectors';
+import { TabbedContainer, TabConfig } from '@grafana/ui';
+
+import { t } from '../../../core/internationalization';
+import { useListQueryTemplateQuery } from '../../query-library';
+import { QUERY_LIBRARY_GET_LIMIT } from '../../query-library/api/factory';
+import { ExploreDrawer } from '../ExploreDrawer';
+
+import { QueryLibrary } from './QueryLibrary';
+import { QueryActionButton } from './types';
+
+type Props = {
+ isOpen: boolean;
+ // List of datasource names to filter query templates by
+ activeDatasources: string[] | undefined;
+ close: () => void;
+ queryActionButton?: QueryActionButton;
+};
+
+/**
+ * Drawer with query library feature. Handles its own state and should be included in some top level component.
+ */
+export function QueryLibraryDrawer({ isOpen, activeDatasources, close, queryActionButton }: Props) {
+ const { data } = useListQueryTemplateQuery(isOpen ? {} : skipToken);
+ const queryTemplatesCount = data?.items?.length ?? 0;
+
+ // TODO: the tabbed container is here mainly for close button and some margins maybe make sense to use something
+ // else as there is only one tab.
+ const tabs: TabConfig[] = [
+ {
+ label: `${t('explore.rich-history.query-library', 'Query library')} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`,
+ value: 'Query library',
+ content: ,
+ icon: 'book',
+ },
+ ];
+
+ return (
+ isOpen && (
+
+
+
+ )
+ );
+}
diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx
index 801b9899802..06dcf044093 100644
--- a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx
+++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx
@@ -28,11 +28,6 @@ export type QueryDetails = {
description: string;
};
-const VisibilityOptions = [
- { value: 'Public', label: t('explore.query-library.public', 'Public') },
- { value: 'Private', label: t('explore.query-library.private', 'Private') },
-];
-
const getInstuctions = (isAdd: boolean) => {
return isAdd
? t(
@@ -155,7 +150,14 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData }
-
+
{
+ const actual = jest.requireActual('app/features/query-library');
+ return {
+ ...actual,
+ useDeleteQueryTemplateMutation: () => [() => {}],
+ useListQueryTemplateQuery: () => {
+ return {
+ data: data,
+ isLoading: false,
+ error: null,
+ };
+ },
+ };
+});
+
+jest.mock('./utils/dataFetching', () => {
+ return {
+ __esModule: true,
+ useLoadQueryMetadata: () => {
+ return {
+ loading: false,
+ value: [
+ {
+ index: '0',
+ uid: '0',
+ datasourceName: 'prometheus',
+ datasourceRef: { type: 'prometheus', uid: 'Prometheus0' },
+ datasourceType: 'prometheus',
+ createdAtTimestamp: 0,
+ query: { refId: 'A' },
+ queryText: 'http_requests_total{job="test"}',
+ description: 'template0',
+ user: {
+ uid: 'viewer:JohnDoe',
+ displayName: 'John Doe',
+ avatarUrl: '',
+ },
+ error: undefined,
+ },
+ ],
+ };
+ },
+ useLoadUsers: () => {
+ return {
+ value: {
+ display: [
+ {
+ avatarUrl: '',
+ displayName: 'john doe',
+ identity: {
+ name: 'JohnDoe',
+ type: 'viewer',
+ },
+ },
+ ],
+ },
+ loading: false,
+ error: null,
+ };
+ },
+ };
+});
+
+describe('QueryTemplatesList', () => {
+ it('renders empty state', async () => {
+ data = {};
+ render( );
+ await waitFor(() => {
+ expect(screen.getByText(/You haven't saved any queries to your library yet/)).toBeInTheDocument();
+ });
+ });
+
+ it('renders query', async () => {
+ data.items = testItems;
+ render( );
+ await waitFor(() => {
+ // We don't really show query template title for some reason so creator name
+ expect(screen.getByText(/John Doe/)).toBeInTheDocument();
+ });
+ });
+
+ it('renders actionButton for query', async () => {
+ data.items = testItems;
+ let passedProps: QueryActionButtonProps;
+
+ const queryActionButton = (props: QueryActionButtonProps) => {
+ passedProps = props;
+ return TEST_ACTION_BUTTON ;
+ };
+
+ render( );
+ await waitFor(() => {
+ // We don't really show query template title for some reason so creator name
+ expect(screen.getByText(/John Doe/)).toBeInTheDocument();
+ expect(screen.getByText(/TEST_ACTION_BUTTON/)).toBeInTheDocument();
+ // We didn't put much else into the query object but should be enough to check the prop
+ expect(passedProps.queries).toMatchObject([{ refId: 'A' }]);
+ });
+ });
+});
+
+const testItems = [
+ {
+ metadata: {
+ name: 'TEST_QUERY',
+ creationTimestamp: '2025-01-01T11:11:11.00Z',
+ annotations: {
+ [CREATED_BY_KEY]: 'viewer:JohnDoe',
+ },
+ },
+ spec: {
+ title: 'Test Query title',
+ targets: [
+ {
+ variables: {},
+ properties: {
+ refId: 'A',
+ datasource: {
+ uid: 'Prometheus',
+ type: 'prometheus',
+ },
+ },
+ },
+ ],
+ },
+ },
+];
diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
index 7f238354704..54dc5951b88 100644
--- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
+++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
@@ -1,24 +1,22 @@
import { css } from '@emotion/css';
-import { compact, uniq, uniqBy } from 'lodash';
+import { uniqBy } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
-import { getAppEvents, getDataSourceSrv } from '@grafana/runtime';
+import { getAppEvents } from '@grafana/runtime';
import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack, Badge } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
-import { createQueryText } from 'app/core/utils/richHistory';
import { useListQueryTemplateQuery } from 'app/features/query-library';
-import { getUserInfo } from 'app/features/query-library/api/user';
import { QueryTemplate } from 'app/features/query-library/types';
-import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers';
+import { UserDataQueryResponse } from '../../query-library/api/types';
import { QueryLibraryProps } from './QueryLibrary';
import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents';
import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo';
import QueryTemplatesTable from './QueryTemplatesTable';
-import { QueryTemplateRow } from './QueryTemplatesTable/types';
+import { useLoadQueryMetadata, useLoadUsers } from './utils/dataFetching';
import { searchQueryLibrary } from './utils/search';
interface QueryTemplatesListProps extends QueryLibraryProps {}
@@ -31,115 +29,29 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
const [datasourceFilters, setDatasourceFilters] = useState>>(
props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || []
);
- const [userData, setUserData] = useState([]);
const [userFilters, setUserFilters] = useState>>([]);
-
- const [allQueryTemplateRows, setAllQueryTemplateRows] = useState([]);
- const [isRowsLoading, setIsRowsLoading] = useState(true);
const styles = useStyles2(getStyles);
- useEffect(() => {
- let shouldCancel = true;
+ const loadUsersResult = useLoadUsersWithError(data);
+ const userNames = loadUsersResult.value ? loadUsersResult.value.display.map((user) => user.displayName) : [];
- const fetchRows = async () => {
- if (!data) {
- setIsRowsLoading(false);
- return;
- }
+ const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.value);
- let userDataList;
- const userQtList = uniq(compact(data.map((qt) => qt.user?.uid)));
- const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&');
- try {
- userDataList = await getUserInfo(`?${usersParam}`);
- } catch (error) {
- getAppEvents().publish({
- type: AppEvents.alertError.name,
- payload: [
- t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', {
- error: JSON.stringify(error),
- }),
- ],
- });
- setIsRowsLoading(false);
- return;
- }
-
- setUserData(userDataList.display.map((user) => user.displayName));
-
- const rowsPromises = data.map(async (queryTemplate: QueryTemplate, index: number) => {
- try {
- const datasourceRef = queryTemplate.targets[0]?.datasource;
- const datasourceApi = await getDataSourceSrv().get(datasourceRef);
- const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || '';
- const query = queryTemplate.targets[0];
- const queryText = createQueryText(query, datasourceApi);
- const datasourceName = datasourceApi?.name || '';
- const extendedUserData = userDataList.display.find(
- (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid
- );
-
- return {
- index: index.toString(),
- uid: queryTemplate.uid,
- datasourceName,
- datasourceRef,
- datasourceType,
- createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0,
- query,
- queryText,
- description: queryTemplate.title,
- user: {
- uid: queryTemplate.user?.uid || '',
- displayName: extendedUserData?.displayName || '',
- avatarUrl: extendedUserData?.avatarUrl || '',
- },
- };
- } catch (error) {
- getAppEvents().publish({
- type: AppEvents.alertError.name,
- payload: [
- t(
- 'query-library.query-template-get-error',
- 'Error attempting to get query template from the library: {{error}}',
- { error: JSON.stringify(error) }
- ),
- ],
- });
- return { index: index.toString(), error };
- }
- });
-
- const results = await Promise.allSettled(rowsPromises);
- const rows = results.filter((result) => result.status === 'fulfilled').map((result) => result.value);
-
- if (shouldCancel) {
- setAllQueryTemplateRows(rows);
- setIsRowsLoading(false);
- }
- };
-
- fetchRows();
-
- return () => {
- shouldCancel = false;
- };
- }, [data]);
-
- const queryTemplateRows = useMemo(
+ // Filtering right now is done just on the frontend until there is better backend support for this.
+ const filteredRows = useMemo(
() =>
searchQueryLibrary(
- allQueryTemplateRows,
+ loadQueryMetadataResult.value || [],
searchQuery,
datasourceFilters.map((f) => f.value || ''),
userFilters.map((f) => f.value || '')
),
- [allQueryTemplateRows, searchQuery, datasourceFilters, userFilters]
+ [loadQueryMetadataResult.value, searchQuery, datasourceFilters, userFilters]
);
const datasourceNames = useMemo(() => {
- return uniqBy(allQueryTemplateRows, 'datasourceName').map((row) => row.datasourceName);
- }, [allQueryTemplateRows]);
+ return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName);
+ }, [loadQueryMetadataResult.value]);
if (error) {
return (
@@ -149,7 +61,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
);
}
- if (isLoading || isRowsLoading) {
+ if (isLoading || loadUsersResult.loading || loadQueryMetadataResult.loading) {
return ;
}
@@ -197,13 +109,14 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
User name(s):
{
setUserFilters(items);
actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource();
}}
value={userFilters}
- options={userData.map((r) => {
+ options={userNames.map((r) => {
return { value: r, label: r };
})}
placeholder={'Filter queries for user name(s)'}
@@ -219,11 +132,83 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
onClick={() => setIsModalOpen(true)}
/>
-
+
>
);
}
+/**
+ * Wrap useLoadUsers with error handling.
+ * @param data
+ */
+function useLoadUsersWithError(data: QueryTemplate[] | undefined) {
+ const userUIDs = useMemo(() => data?.map((qt) => qt.user?.uid).filter((uid) => uid !== undefined), [data]);
+ const loadUsersResult = useLoadUsers(userUIDs);
+ useEffect(() => {
+ if (loadUsersResult.error) {
+ getAppEvents().publish({
+ type: AppEvents.alertError.name,
+ payload: [
+ t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', {
+ error: JSON.stringify(loadUsersResult.error),
+ }),
+ ],
+ });
+ }
+ }, [loadUsersResult.error]);
+ return loadUsersResult;
+}
+
+/**
+ * Wrap useLoadQueryMetadata with error handling.
+ * @param queryTemplates
+ * @param userDataList
+ */
+function useLoadQueryMetadataWithError(
+ queryTemplates: QueryTemplate[] | undefined,
+ userDataList: UserDataQueryResponse | undefined
+) {
+ const result = useLoadQueryMetadata(queryTemplates, userDataList);
+
+ // useLoadQueryMetadata returns errors in the values so we filter and group them and later alert only one time for
+ // all the errors. This way we show data that is loaded even if some rows errored out.
+ // TODO: maybe we could show the rows with incomplete data to see exactly which ones errored out. I assume this
+ // can happen for example when data source for saved query was deleted. Would be nice if user would still be able
+ // to delete such row or decide what to do.
+ const [values, errors] = useMemo(() => {
+ let errors: Error[] = [];
+ let values = [];
+ if (!result.loading) {
+ for (const value of result.value!) {
+ if (value.error) {
+ errors.push(value.error);
+ } else {
+ values.push(value);
+ }
+ }
+ }
+ return [values, errors];
+ }, [result]);
+
+ useEffect(() => {
+ if (errors.length) {
+ getAppEvents().publish({
+ type: AppEvents.alertError.name,
+ payload: [
+ t('query-library.query-template-get-error', 'Error attempting to load query template metadata: {{error}}', {
+ error: JSON.stringify(errors),
+ }),
+ ],
+ });
+ }
+ }, [errors]);
+
+ return {
+ loading: result.loading,
+ value: values,
+ };
+}
+
const getStyles = (theme: GrafanaTheme2) => ({
searchInput: css({
maxWidth: theme.spacing(55),
diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx
index 88174485be1..575f10bd063 100644
--- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx
+++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx
@@ -9,14 +9,13 @@ import { useDeleteQueryTemplateMutation } from 'app/features/query-library';
import { dispatch } from 'app/store/store';
import { ShowConfirmModalEvent } from 'app/types/events';
-import ExploreRunQueryButton from '../../ExploreRunQueryButton';
-import { useQueriesDrawerContext } from '../../QueriesDrawer/QueriesDrawerContext';
import {
queryLibaryTrackDeleteQuery,
queryLibraryTrackAddOrEditDescription,
queryLibraryTrackRunQuery,
} from '../QueryLibraryAnalyticsEvents';
import { QueryTemplateForm } from '../QueryTemplateForm';
+import { QueryActionButton } from '../types';
import { useQueryLibraryListStyles } from './styles';
import { QueryTemplateRow } from './types';
@@ -25,12 +24,12 @@ interface ActionsCellProps {
queryUid?: string;
queryTemplate: QueryTemplateRow;
rootDatasourceUid?: string;
+ QueryActionButton?: QueryActionButton;
}
-function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCellProps) {
+function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid, QueryActionButton }: ActionsCellProps) {
const [deleteQueryTemplate] = useDeleteQueryTemplateMutation();
const [editFormOpen, setEditFormOpen] = useState(false);
- const { setDrawerOpened } = useQueriesDrawerContext();
const styles = useQueryLibraryListStyles();
const onDeleteQuery = (queryUid: string) => {
@@ -82,15 +81,15 @@ function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCell
queryLibraryTrackAddOrEditDescription();
}}
/>
- {
- setDrawerOpened(false);
- queryLibraryTrackRunQuery(queryTemplate.datasourceType || '');
- }}
- />
+ {QueryActionButton && (
+ {
+ queryLibraryTrackRunQuery(queryTemplate.datasourceType || '');
+ }}
+ />
+ )}
= (rowA, rowB, _, desc) => {
return desc ? timeA - timeB : timeB - timeA;
};
-const columns: Array> = [
- { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell },
- { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => },
- { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' },
- { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort },
- {
- id: 'actions',
- header: '',
- cell: ({ row: { original } }) => (
-
- ),
- },
-];
+function createColumns(queryActionButton?: QueryActionButton): Array> {
+ return [
+ { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell },
+ { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => },
+ { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' },
+ { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort },
+ {
+ id: 'actions',
+ header: '',
+ cell: ({ row: { original } }) => (
+
+ ),
+ },
+ ];
+}
type Props = {
queryTemplateRows: QueryTemplateRow[];
+ queryActionButton?: QueryActionButton;
};
-export default function QueryTemplatesTable({ queryTemplateRows }: Props) {
+export default function QueryTemplatesTable({ queryTemplateRows, queryActionButton }: Props) {
const styles = useStyles2(getStyles);
+ const columns = createColumns(queryActionButton);
+
return (
{
+ openAddQueryModal(query);
+ setShowQueryLibraryBadgeButton(false);
+ }}
+ style={{ cursor: 'pointer' }}
+ />
+ ) : (
+ {
+ openAddQueryModal(query);
+ }}
+ />
+ );
+}
diff --git a/public/app/features/explore/QueryLibrary/types.ts b/public/app/features/explore/QueryLibrary/types.ts
new file mode 100644
index 00000000000..b36c7280fe8
--- /dev/null
+++ b/public/app/features/explore/QueryLibrary/types.ts
@@ -0,0 +1,11 @@
+import { ComponentType } from 'react';
+
+import { DataQuery } from '@grafana/schema';
+
+export type QueryActionButtonProps = {
+ queries: DataQuery[];
+ datasourceUid?: string;
+ onClick: () => void;
+};
+
+export type QueryActionButton = ComponentType;
diff --git a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts
new file mode 100644
index 00000000000..67078936d93
--- /dev/null
+++ b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts
@@ -0,0 +1,106 @@
+import { compact, uniq } from 'lodash';
+import { useAsync } from 'react-use';
+import { AsyncState } from 'react-use/lib/useAsync';
+
+import { getDataSourceSrv } from '@grafana/runtime';
+import { DataQuery, DataSourceRef } from '@grafana/schema';
+
+import { createQueryText } from '../../../../core/utils/richHistory';
+import { getDatasourceSrv } from '../../../plugins/datasource_srv';
+import { UserDataQueryResponse } from '../../../query-library/api/types';
+import { getUserInfo } from '../../../query-library/api/user';
+import { QueryTemplate } from '../../../query-library/types';
+
+export function useLoadUsers(userUIDs: string[] | undefined) {
+ return useAsync(async () => {
+ if (!userUIDs) {
+ return undefined;
+ }
+ const userQtList = uniq(compact(userUIDs));
+ const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&');
+ return await getUserInfo(`?${usersParam}`);
+ }, [userUIDs]);
+}
+
+// Explicitly type the result so TS knows to discriminate between the error result and good result by the error prop
+// value.
+type MetadataValue =
+ | {
+ index: string;
+ uid: string;
+ datasourceName: string;
+ datasourceRef: DataSourceRef | undefined | null;
+ datasourceType: string;
+ createdAtTimestamp: number;
+ query: DataQuery;
+ queryText: string;
+ description: string;
+ user: {
+ uid: string;
+ displayName: string;
+ avatarUrl: string;
+ };
+ error: undefined;
+ }
+ | {
+ index: string;
+ error: Error;
+ };
+
+/**
+ * Map metadata to query templates we get from the DB.
+ * @param queryTemplates
+ * @param userDataList
+ */
+export function useLoadQueryMetadata(
+ queryTemplates: QueryTemplate[] | undefined,
+ userDataList: UserDataQueryResponse | undefined
+): AsyncState {
+ return useAsync(async () => {
+ if (!(queryTemplates && userDataList)) {
+ return [];
+ }
+
+ const rowsPromises = queryTemplates.map(
+ async (queryTemplate: QueryTemplate, index: number): Promise => {
+ try {
+ const datasourceRef = queryTemplate.targets[0]?.datasource;
+ const datasourceApi = await getDataSourceSrv().get(datasourceRef);
+ const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || '';
+ const query = queryTemplate.targets[0];
+ const queryText = createQueryText(query, datasourceApi);
+ const datasourceName = datasourceApi?.name || '';
+ const extendedUserData = userDataList.display.find(
+ (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid
+ );
+
+ return {
+ index: index.toString(),
+ uid: queryTemplate.uid,
+ datasourceName,
+ datasourceRef,
+ datasourceType,
+ createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0,
+ query,
+ queryText,
+ description: queryTemplate.title,
+ user: {
+ uid: queryTemplate.user?.uid || '',
+ displayName: extendedUserData?.displayName || '',
+ avatarUrl: extendedUserData?.avatarUrl || '',
+ },
+ error: undefined,
+ };
+ } catch (error) {
+ // Instead of throwing we collect the errors in the result so upstream code can decide what to do.
+ return {
+ index: index.toString(),
+ error: error instanceof Error ? error : new Error('unknown error ' + JSON.stringify(error)),
+ };
+ }
+ }
+ );
+
+ return Promise.all(rowsPromises);
+ }, [queryTemplates, userDataList]);
+}
diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx
index b614401cde6..60bce0a6ec3 100644
--- a/public/app/features/explore/RichHistory/RichHistory.tsx
+++ b/public/app/features/explore/RichHistory/RichHistory.tsx
@@ -11,15 +11,12 @@ import {
RichHistorySettings,
createDatasourcesList,
} from 'app/core/utils/richHistory';
-import { QUERY_LIBRARY_GET_LIMIT } from 'app/features/query-library/api/factory';
import { useSelector } from 'app/types';
import { RichHistoryQuery } from 'app/types/explore';
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
-import { useListQueryTemplateQuery } from '../../query-library';
-import { Tabs, useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext';
+import { Tabs } from '../QueriesDrawer/QueriesDrawerContext';
import { i18n } from '../QueriesDrawer/utils';
-import { QueryLibrary } from '../QueryLibrary/QueryLibrary';
import { selectExploreDSMaps } from '../state/selectors';
import { RichHistoryQueriesTab } from './RichHistoryQueriesTab';
@@ -55,8 +52,6 @@ export function RichHistory(props: RichHistoryProps) {
const [loading, setLoading] = useState(false);
- const { queryLibraryAvailable } = useQueriesDrawerContext();
-
const updateSettings = (settingsToUpdate: Partial) => {
props.updateHistorySettings({ ...props.richHistorySettings, ...settingsToUpdate });
};
@@ -98,16 +93,6 @@ export function RichHistory(props: RichHistoryProps) {
.map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name)
.filter((name): name is string => !!name);
- const { data } = useListQueryTemplateQuery({});
- const queryTemplatesCount = data?.items?.length ?? 0;
-
- const QueryLibraryTab: TabConfig = {
- label: `${i18n.queryLibrary} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`,
- value: Tabs.QueryLibrary,
- content: ,
- icon: 'book',
- };
-
const QueriesTab: TabConfig = {
label: i18n.queryHistory,
value: Tabs.RichHistory,
@@ -164,7 +149,7 @@ export function RichHistory(props: RichHistoryProps) {
icon: 'sliders-v-alt',
};
- let tabs = (queryLibraryAvailable ? [QueryLibraryTab] : []).concat([QueriesTab, StarredTab, SettingsTab]);
+ let tabs = [QueriesTab, StarredTab, SettingsTab];
return (
Loading...
diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts
index a74ffb783c1..41b3f41ae73 100644
--- a/public/app/features/explore/spec/helper/interactions.ts
+++ b/public/app/features/explore/spec/helper/interactions.ts
@@ -26,9 +26,17 @@ export const runQuery = async (exploreId = 'left') => {
};
export const openQueryHistory = async () => {
- const button = screen.getByRole('button', { name: 'Query history' });
- await userEvent.click(button);
- expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument();
+ let button = screen.queryByRole('button', { name: 'Query history' });
+ if (button) {
+ await userEvent.click(button);
+ expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument();
+ } else {
+ button = screen.getByRole('button', { name: 'Open query library or query history' });
+ await userEvent.click(button);
+ button = await screen.findByRole('menuitem', { name: 'Query history' });
+ await userEvent.click(button);
+ expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument();
+ }
};
export const openQueryLibrary = async () => {
@@ -41,13 +49,6 @@ export const openQueryLibrary = async () => {
});
};
-export const switchToQueryHistory = async () => {
- const tab = screen.getByRole('tab', {
- name: /query history/i,
- });
- await userEvent.click(tab);
-};
-
export const addQueryHistoryToQueryLibrary = async () => {
const button = withinQueryHistory().getByRole('button', { name: /add to library/i });
await userEvent.click(button);
diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx
index 2ec9733a4d4..c57cf52c435 100644
--- a/public/app/features/explore/spec/helper/setup.tsx
+++ b/public/app/features/explore/spec/helper/setup.tsx
@@ -47,6 +47,7 @@ import { ExploreQueryParams } from '../../../../types';
import { initialUserState } from '../../../profile/state/reducers';
import ExplorePage from '../../ExplorePage';
import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext';
+import { QueryLibraryContextProvider } from '../../QueryLibrary/QueryLibraryContext';
type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi };
@@ -182,25 +183,29 @@ export function setupExplore(options?: SetupOptions): {
-
- {options?.withAppChrome ? (
-
-
- }
- />
-
-
- ) : (
- }
- />
- )}
-
+
+
+ {options?.withAppChrome ? (
+
+
+ (
+
+ )}
+ />
+
+
+ ) : (
+ }
+ />
+ )}
+
+
diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx
index 90d889c4625..1e3d7b3a75d 100644
--- a/public/app/features/explore/spec/queryHistory.test.tsx
+++ b/public/app/features/explore/spec/queryHistory.test.tsx
@@ -7,6 +7,7 @@ import store from 'app/core/store';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import * as localStorage from '../../../core/history/RichHistoryLocalStorage';
+import { Tabs } from '../QueriesDrawer/QueriesDrawerContext';
import {
assertDataSourceFilterVisibility,
@@ -127,6 +128,7 @@ describe('Explore: Query History', () => {
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
queryHistoryEnabled: false,
+ selectedTab: Tabs.RichHistory,
});
});
diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx
index dc942fa21a8..a9cc5345b23 100644
--- a/public/app/features/explore/spec/queryLibrary.test.tsx
+++ b/public/app/features/explore/spec/queryLibrary.test.tsx
@@ -16,7 +16,6 @@ import {
openQueryHistory,
openQueryLibrary,
submitAddToQueryLibrary,
- switchToQueryHistory,
} from './helper/interactions';
import { setupExplore, waitForExplore } from './helper/setup';
@@ -115,8 +114,7 @@ describe('QueryLibrary', () => {
it('Shows add to query library button only when the toggle is enabled', async () => {
setupQueryLibrary();
await waitForExplore();
- await openQueryLibrary();
- await switchToQueryHistory();
+ await openQueryHistory();
await assertQueryHistory(['{"expr":"TEST"}']);
await assertAddToQueryLibraryButtonExists(true);
});
@@ -134,8 +132,7 @@ describe('QueryLibrary', () => {
it('Shows a notification when a template is added and hides the add button', async () => {
setupQueryLibrary();
await waitForExplore();
- await openQueryLibrary();
- await switchToQueryHistory();
+ await openQueryHistory();
await assertQueryHistory(['{"expr":"TEST"}']);
await addQueryHistoryToQueryLibrary();
await submitAddToQueryLibrary({ description: 'Test' });
diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx
index 7094901511f..a3dc05930d7 100644
--- a/public/app/features/query/components/QueryEditorRow.tsx
+++ b/public/app/features/query/components/QueryEditorRow.tsx
@@ -24,7 +24,7 @@ import {
toLegacyResponseData,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
-import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
+import { AngularComponent, config, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { Badge, ErrorBoundaryAlert } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import {
@@ -40,6 +40,8 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
+import { SaveQueryButton as SaveQueryToQueryLibraryButton } from '../../explore/QueryLibrary/SaveQueryButton';
+
import { QueryActionComponent, RowActionComponents } from './QueryActionComponent';
import { QueryEditorRowHeader } from './QueryEditorRowHeader';
import { QueryErrorAlert } from './QueryErrorAlert';
@@ -487,6 +489,7 @@ export class QueryEditorRow extends PureComponent
)}
{this.renderExtraActions()}
+ {config.featureToggles.queryLibrary && }
-
-
-
-
-
- {props.pageBanners.map((Banner, index) => (
-
+
+
+
+
+
+
+ {props.pageBanners.map((Banner, index) => (
+
+ ))}
+ {props.routes}
+
+ {props.bodyRenderHooks.map((Hook, index) => (
+
))}
- {props.routes}
-
- {props.bodyRenderHooks.map((Hook, index) => (
-
- ))}
-
-
-
+
+
+
+
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 7781727270f..3a270e122c3 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1077,6 +1077,12 @@
"angular-deprecation-description": "Angular panels options can only be edited using the JSON editor.",
"angular-deprecation-heading": "Panel options"
},
+ "panel-queries": {
+ "add-query-from-library": "Add query from library"
+ },
+ "query-library": {
+ "add-query-button": "Add query"
+ },
"settings": {
"variables": {
"dependencies": {
@@ -1188,6 +1194,7 @@
"close-tooltip": "Close query history",
"datasource-a-z": "Data source A-Z",
"datasource-z-a": "Data source Z-A",
+ "library-history-dropdown": "Open query library or query history",
"newest-first": "Newest first",
"oldest-first": "Oldest first",
"query-history": "Query history",
@@ -2796,7 +2803,7 @@
"query-library": {
"datasource-names": "Datasource name(s):",
"delete-query-button": "Delete query",
- "query-template-get-error": "Error attempting to get query template from the library: {{error}}",
+ "query-template-get-error": "Error attempting to load query template metadata: {{error}}",
"search": "Search by data source, query content or description",
"user-info-get-error": "Error attempting to get user info from the library: {{error}}",
"user-names": "User name(s):"
@@ -2811,6 +2818,7 @@
"hide-response": "Hide response",
"remove-query": "Remove query",
"save-to-query-library": "Save to query library",
+ "save-to-query-library-new": "New: Save to query library",
"show-response": "Show response",
"toggle-edit-mode": "Toggle text edit mode"
},
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 006e0f4a07f..1495a78caee 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1077,6 +1077,12 @@
"angular-deprecation-description": "Åʼnģūľäř päʼnęľş őpŧįőʼnş čäʼn őʼnľy þę ęđįŧęđ ūşįʼnģ ŧĥę ĴŜØŃ ęđįŧőř.",
"angular-deprecation-heading": "Päʼnęľ őpŧįőʼnş"
},
+ "panel-queries": {
+ "add-query-from-library": "Åđđ qūęřy ƒřőm ľįþřäřy"
+ },
+ "query-library": {
+ "add-query-button": "Åđđ qūęřy"
+ },
"settings": {
"variables": {
"dependencies": {
@@ -1188,6 +1194,7 @@
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
"datasource-a-z": "Đäŧä şőūřčę Å-Ż",
"datasource-z-a": "Đäŧä şőūřčę Ż-Å",
+ "library-history-dropdown": "Øpęʼn qūęřy ľįþřäřy őř qūęřy ĥįşŧőřy",
"newest-first": "Ńęŵęşŧ ƒįřşŧ",
"oldest-first": "Øľđęşŧ ƒįřşŧ",
"query-history": "Qūęřy ĥįşŧőřy",
@@ -2796,7 +2803,7 @@
"query-library": {
"datasource-names": "Đäŧäşőūřčę ʼnämę(ş):",
"delete-query-button": "Đęľęŧę qūęřy",
- "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ qūęřy ŧęmpľäŧę ƒřőm ŧĥę ľįþřäřy: {{error}}",
+ "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ľőäđ qūęřy ŧęmpľäŧę męŧäđäŧä: {{error}}",
"search": "Ŝęäřčĥ þy đäŧä şőūřčę, qūęřy čőʼnŧęʼnŧ őř đęşčřįpŧįőʼn",
"user-info-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ ūşęř įʼnƒő ƒřőm ŧĥę ľįþřäřy: {{error}}",
"user-names": "Ůşęř ʼnämę(ş):"
@@ -2811,6 +2818,7 @@
"hide-response": "Ħįđę řęşpőʼnşę",
"remove-query": "Ŗęmővę qūęřy",
"save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy",
+ "save-to-query-library-new": "Ńęŵ: Ŝävę ŧő qūęřy ľįþřäřy",
"show-response": "Ŝĥőŵ řęşpőʼnşę",
"toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę"
},