mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
Query Library: Notifications and query counter (#94444)
* Notifications about the feature * i18n * Fix test
This commit is contained in:
parent
5f61266931
commit
5f26fd87c7
@ -1,10 +1,11 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema/dist/esm/index';
|
||||
import { ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { Badge, ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
@ -21,6 +22,7 @@ import { ExploreActions } from './ExploreActions';
|
||||
import { ExploreDrawer } from './ExploreDrawer';
|
||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||
import { QueriesDrawerContextProvider, 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';
|
||||
@ -63,6 +65,10 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext();
|
||||
const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false);
|
||||
const [queryToAdd, setQueryToAdd] = useState<DataQuery | undefined>();
|
||||
const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage(
|
||||
QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton,
|
||||
true
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//This is needed for breadcrumbs and topnav.
|
||||
@ -77,19 +83,32 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
if (hasQueryLibrary) {
|
||||
RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, {
|
||||
scope: CoreApp.Explore,
|
||||
queryActionComponent: (props) => (
|
||||
<QueryOperationAction
|
||||
key={props.key}
|
||||
title={t('query-operation.header.save-to-query-library', 'Save to query library')}
|
||||
icon="save"
|
||||
onClick={() => {
|
||||
setQueryToAdd(props.query);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
queryActionComponent: (props) =>
|
||||
showQueryLibraryBadgeButton ? (
|
||||
<Badge
|
||||
key={props.key}
|
||||
text={`New: ${t('query-operation.header.save-to-query-library', 'Save to query library')}`}
|
||||
icon="save"
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
setQueryToAdd(props.query);
|
||||
setShowQueryLibraryBadgeButton(false);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
) : (
|
||||
<QueryOperationAction
|
||||
key={props.key}
|
||||
title={t('query-operation.header.save-to-query-library', 'Save to query library')}
|
||||
icon="save"
|
||||
onClick={() => {
|
||||
setQueryToAdd(props.query);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton]);
|
||||
|
||||
useKeyboardShortcuts();
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo';
|
||||
import { QueryTemplatesList } from './QueryTemplatesList';
|
||||
|
||||
export interface QueryLibraryProps {
|
||||
@ -6,6 +9,26 @@ export interface QueryLibraryProps {
|
||||
activeDatasources?: string[];
|
||||
}
|
||||
|
||||
export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = {
|
||||
explore: {
|
||||
notifyUserAboutQueryLibrary: 'grafana.explore.query-library.notifyUserAboutQueryLibrary',
|
||||
newButton: 'grafana.explore.query-library.newButton',
|
||||
},
|
||||
};
|
||||
|
||||
export function QueryLibrary({ activeDatasources }: QueryLibraryProps) {
|
||||
return <QueryTemplatesList activeDatasources={activeDatasources} />;
|
||||
const [notifyUserAboutQueryLibrary, setNotifyUserAboutQueryLibrary] = useLocalStorage(
|
||||
QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.notifyUserAboutQueryLibrary,
|
||||
true
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryLibraryExpmInfo
|
||||
isOpen={notifyUserAboutQueryLibrary || false}
|
||||
onDismiss={() => setNotifyUserAboutQueryLibrary(false)}
|
||||
/>
|
||||
<QueryTemplatesList activeDatasources={activeDatasources} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Alert, Modal } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export function QueryLibraryExpmInfo({ isOpen, onDismiss }: Props) {
|
||||
return (
|
||||
<Modal title="Query Library" isOpen={isOpen} onDismiss={onDismiss}>
|
||||
<Alert
|
||||
severity="info"
|
||||
title="Query library is in the experimental mode. It is a place where you can save your queries and share them with
|
||||
your team. Once you save a query, it will be available for the whole organization to use."
|
||||
/>
|
||||
<Alert severity="info" title=" Currently we are limiting the number of saved queries per organization to 1000." />
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="Although it's unlikely, some data loss may occur during the experimental phase."
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -66,9 +66,7 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData }
|
||||
.then(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-added', 'Query template successfully added to the library'),
|
||||
],
|
||||
payload: [t('explore.query-library.query-template-added', 'Query successfully saved to the library')],
|
||||
});
|
||||
return true;
|
||||
})
|
||||
@ -76,7 +74,7 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData }
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-add-error', 'Error attempting to add this query to the library'),
|
||||
t('explore.query-library.query-template-add-error', 'Error attempting to save this query to the library'),
|
||||
],
|
||||
});
|
||||
return false;
|
||||
|
@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { getAppEvents, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack } from '@grafana/ui';
|
||||
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 { useAllQueryTemplatesQuery } from 'app/features/query-library';
|
||||
@ -15,6 +15,7 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
|
||||
import { QueryLibraryProps } from './QueryLibrary';
|
||||
import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents';
|
||||
import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo';
|
||||
import QueryTemplatesTable from './QueryTemplatesTable';
|
||||
import { QueryTemplateRow } from './QueryTemplatesTable/types';
|
||||
import { searchQueryLibrary } from './utils/search';
|
||||
@ -23,6 +24,7 @@ interface QueryTemplatesListProps extends QueryLibraryProps {}
|
||||
|
||||
export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
const { data, isLoading, error } = useAllQueryTemplatesQuery();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [datasourceFilters, setDatasourceFilters] = useState<Array<SelectableValue<string>>>(
|
||||
props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || []
|
||||
@ -163,6 +165,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryLibraryExpmInfo isOpen={isModalOpen} onDismiss={() => setIsModalOpen(false)} />
|
||||
<Stack gap={0.5}>
|
||||
<FilterInput
|
||||
className={styles.searchInput}
|
||||
@ -204,6 +207,15 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
placeholder={'Filter queries for user name(s)'}
|
||||
aria-label={'Filter queries for user name(s)'}
|
||||
/>
|
||||
<Badge
|
||||
text=""
|
||||
icon="info"
|
||||
aria-label="info"
|
||||
tooltip={'Click here for more informationn about Query library'}
|
||||
color="blue"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
</Stack>
|
||||
<QueryTemplatesTable queryTemplateRows={queryTemplateRows} />
|
||||
</>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
RichHistorySettings,
|
||||
createDatasourcesList,
|
||||
} from 'app/core/utils/richHistory';
|
||||
import { QUERY_LIBRARY_GET_LIMIT, queryLibraryApi } from 'app/features/query-library/api/factory';
|
||||
import { useSelector } from 'app/types';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
@ -96,8 +97,10 @@ export function RichHistory(props: RichHistoryProps) {
|
||||
.map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name)
|
||||
.filter((name): name is string => !!name);
|
||||
|
||||
const queryTemplatesCount = useSelector(queryLibraryApi.endpoints.allQueryTemplates.select()).data?.length || 0;
|
||||
|
||||
const QueryLibraryTab: TabConfig = {
|
||||
label: i18n.queryLibrary,
|
||||
label: `${i18n.queryLibrary} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`,
|
||||
value: Tabs.QueryLibrary,
|
||||
content: <QueryLibrary activeDatasources={activeDatasources} />,
|
||||
icon: 'book',
|
||||
|
@ -3,7 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Button, Modal } from '@grafana/ui';
|
||||
import { isQueryLibraryEnabled } from 'app/features/query-library';
|
||||
import { isQueryLibraryEnabled, useAllQueryTemplatesQuery } from 'app/features/query-library';
|
||||
|
||||
import {
|
||||
queryLibraryTrackAddFromQueryHistory,
|
||||
@ -16,6 +16,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RichHistoryAddToLibrary = ({ query }: Props) => {
|
||||
const { refetch } = useAllQueryTemplatesQuery();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasBeenSaved, setHasBeenSaved] = useState(false);
|
||||
|
||||
@ -45,6 +46,7 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => {
|
||||
if (isSuccess) {
|
||||
setIsOpen(false);
|
||||
setHasBeenSaved(true);
|
||||
refetch();
|
||||
queryLibraryTrackAddFromQueryHistory(query.datasource?.type || '');
|
||||
}
|
||||
}}
|
||||
|
@ -137,7 +137,7 @@ describe('QueryLibrary', () => {
|
||||
expect(testEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'alert-success',
|
||||
payload: ['Query template successfully added to the library'],
|
||||
payload: ['Query successfully saved to the library'],
|
||||
})
|
||||
);
|
||||
await assertAddToQueryLibraryButtonExists(false);
|
||||
|
@ -7,7 +7,7 @@ import { baseQuery } from './query';
|
||||
|
||||
// Currently, we are loading all query templates
|
||||
// Organizations can have maximum of 1000 query templates
|
||||
const GET_LIMIT = 1000;
|
||||
export const QUERY_LIBRARY_GET_LIMIT = 1000;
|
||||
|
||||
export const queryLibraryApi = createApi({
|
||||
baseQuery,
|
||||
@ -15,7 +15,7 @@ export const queryLibraryApi = createApi({
|
||||
endpoints: (builder) => ({
|
||||
allQueryTemplates: builder.query<QueryTemplate[], void>({
|
||||
query: () => ({
|
||||
url: `?limit=${GET_LIMIT}`,
|
||||
url: `?limit=${QUERY_LIBRARY_GET_LIMIT}`,
|
||||
}),
|
||||
transformResponse: convertDataQueryResponseToQueryTemplates,
|
||||
providesTags: ['QueryTemplatesList'],
|
||||
|
@ -831,8 +831,8 @@
|
||||
"private": "Private",
|
||||
"public": "Public",
|
||||
"query-deleted": "Query deleted",
|
||||
"query-template-add-error": "Error attempting to add this query to the library",
|
||||
"query-template-added": "Query template successfully added to the library",
|
||||
"query-template-add-error": "Error attempting to save this query to the library",
|
||||
"query-template-added": "Query successfully saved to the library",
|
||||
"query-template-edit-error": "Error attempting to edit this query",
|
||||
"query-template-edited": "Query template successfully edited",
|
||||
"save": "Save"
|
||||
|
@ -831,8 +831,8 @@
|
||||
"private": "Přįväŧę",
|
||||
"public": "Pūþľįč",
|
||||
"query-deleted": "Qūęřy đęľęŧęđ",
|
||||
"query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő şävę ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-added": "Qūęřy şūččęşşƒūľľy şävęđ ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-edit-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ęđįŧ ŧĥįş qūęřy",
|
||||
"query-template-edited": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy ęđįŧęđ",
|
||||
"save": "Ŝävę"
|
||||
|
Loading…
Reference in New Issue
Block a user