Query Library: Notifications and query counter (#94444)

* Notifications about the feature

* i18n

* Fix test
This commit is contained in:
Haris Rozajac 2024-10-09 06:54:11 -06:00 committed by GitHub
parent 5f61266931
commit 5f26fd87c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 107 additions and 27 deletions

View File

@ -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();

View File

@ -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} />
</>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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} />
</>

View File

@ -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',

View File

@ -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 || '');
}
}}

View File

@ -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);

View File

@ -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'],

View File

@ -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"

View File

@ -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ę"