From 1162c28a5588b9c7ee39236eaafcafd1f094da40 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 10 Jan 2024 09:57:17 +0000 Subject: [PATCH] Connections: hook up search to url params to match cloud behaviour (#80166) hook up connections search to url params to match cloud behaviour --- .betterer.results | 8 ---- .../tabs/ConnectData/ConnectData.test.tsx | 45 ++++++------------- .../tabs/ConnectData/ConnectData.tsx | 32 +++++++------ .../tabs/ConnectData/Search/Search.tsx | 37 +++++++++------ 4 files changed, 54 insertions(+), 68 deletions(-) diff --git a/.betterer.results b/.betterer.results index e2d8f86b1dd..c75b69a702d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2380,11 +2380,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/connections/tabs/ConnectData/ConnectData.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], "public/app/features/connections/tabs/ConnectData/NoAccessModal/NoAccessModal.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -2399,9 +2394,6 @@ exports[`better eslint`] = { "public/app/features/connections/tabs/ConnectData/NoResults/NoResults.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/features/connections/tabs/ConnectData/Search/Search.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/correlations/CorrelationsPage.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx index 4d8b2ad7012..44e8ff024d9 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx @@ -1,12 +1,12 @@ -import { fireEvent, render, RenderResult, screen, waitFor } from '@testing-library/react'; +import { render, RenderResult, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; -import { Provider } from 'react-redux'; +import { TestProvider } from 'test/helpers/TestProvider'; import { PluginType } from '@grafana/data'; import { contextSrv } from 'app/core/core'; import { getCatalogPluginMock, getPluginsStateMock } from 'app/features/plugins/admin/__mocks__'; import { CatalogPlugin } from 'app/features/plugins/admin/types'; -import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import { AddNewConnection } from './ConnectData'; @@ -14,13 +14,10 @@ import { AddNewConnection } from './ConnectData'; jest.mock('app/features/datasources/api'); const renderPage = (plugins: CatalogPlugin[] = []): RenderResult => { - // @ts-ignore - const store = configureStore({ plugins: getPluginsStateMock(plugins) }); - return render( - + - + ); }; @@ -30,8 +27,6 @@ const mockCatalogDataSourcePlugin = getCatalogPluginMock({ id: 'sample-data-source', }); -const originalHasPermission = contextSrv.hasPermission; - describe('Angular badge', () => { test('does not show angular badge for non-angular plugins', async () => { renderPage([ @@ -65,10 +60,6 @@ describe('Angular badge', () => { }); describe('Add new connection', () => { - beforeEach(() => { - contextSrv.hasPermission = originalHasPermission; - }); - test('renders no results if the plugins list is empty', async () => { renderPage(); @@ -91,15 +82,19 @@ describe('Add new connection', () => { renderPage([getCatalogPluginMock(), mockCatalogDataSourcePlugin]); const searchField = await screen.findByRole('textbox'); - fireEvent.change(searchField, { target: { value: 'ampl' } }); + await userEvent.type(searchField, 'ampl'); expect(await screen.findByText('Sample data source')).toBeVisible(); - fireEvent.change(searchField, { target: { value: 'cramp' } }); + await userEvent.clear(searchField); + await userEvent.type(searchField, 'cramp'); expect(screen.queryByText('No results matching your query were found.')).toBeInTheDocument(); + + await userEvent.clear(searchField); + expect(await screen.findByText('Sample data source')).toBeVisible(); }); test('shows a "No access" modal if the user does not have permissions to create datasources', async () => { - (contextSrv.hasPermission as jest.Mock) = jest.fn().mockImplementation((permission: string) => { + jest.spyOn(contextSrv, 'hasPermission').mockImplementation((permission: string) => { if (permission === AccessControlAction.DataSourcesCreate) { return false; } @@ -114,21 +109,7 @@ describe('Add new connection', () => { expect(screen.queryByText(new RegExp(exampleSentenceInModal))).not.toBeInTheDocument(); // Should show the modal if the user has no permissions - fireEvent.click(await screen.findByText('Sample data source')); + await userEvent.click(await screen.findByText('Sample data source')); expect(screen.queryByText(new RegExp(exampleSentenceInModal))).toBeInTheDocument(); }); - - test('does not show a "No access" modal but displays the details page if the user has the right permissions', async () => { - (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); - - renderPage([getCatalogPluginMock(), mockCatalogDataSourcePlugin]); - const exampleSentenceInModal = 'Editors cannot add new connections.'; - - // Should not show the modal by default - expect(screen.queryByText(new RegExp(exampleSentenceInModal))).not.toBeInTheDocument(); - - // Should not show the modal when clicking a card - fireEvent.click(await screen.findByText('Sample data source')); - expect(screen.queryByText(new RegExp(exampleSentenceInModal))).not.toBeInTheDocument(); - }); }); diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx index a9335668c6c..edd64e78f5e 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx @@ -1,9 +1,10 @@ import { css } from '@emotion/css'; import React, { useMemo, useState } from 'react'; -import { PluginType } from '@grafana/data'; +import { GrafanaTheme2, PluginType } from '@grafana/data'; import { useStyles2, LoadingPlaceholder } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; +import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { t } from 'app/core/internationalization'; import { useGetAll } from 'app/features/plugins/admin/state/hooks'; import { AccessControlAction } from 'app/types'; @@ -16,27 +17,30 @@ import { NoAccessModal } from './NoAccessModal'; import { NoResults } from './NoResults'; import { Search } from './Search'; -const getStyles = () => ({ - spacer: css` - height: 16px; - `, - modal: css` - width: 500px; - `, - modalContent: css` - overflow: visible; - `, +const getStyles = (theme: GrafanaTheme2) => ({ + spacer: css({ + height: theme.spacing(2), + }), + modal: css({ + width: '500px', + }), + modalContent: css({ + overflow: 'visible', + }), }); export function AddNewConnection() { - const [searchTerm, setSearchTerm] = useState(''); + const [queryParams, setQueryParams] = useQueryParams(); + const searchTerm = queryParams.search ? String(queryParams.search) : ''; const [isNoAccessModalOpen, setIsNoAccessModalOpen] = useState(false); const [focusedItem, setFocusedItem] = useState(null); const styles = useStyles2(getStyles); const canCreateDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); const handleSearchChange = (e: React.FormEvent) => { - setSearchTerm(e.currentTarget.value.toLowerCase()); + setQueryParams({ + search: e.currentTarget.value.toLowerCase(), + }); }; const { error, plugins, isLoading } = useGetAll({ @@ -82,7 +86,7 @@ export function AddNewConnection() { return ( <> {focusedItem && } - + {/* We need this extra spacing when there are no filters */}
diff --git a/public/app/features/connections/tabs/ConnectData/Search/Search.tsx b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx index 626f3b14350..bddb9f0579f 100644 --- a/public/app/features/connections/tabs/ConnectData/Search/Search.tsx +++ b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx @@ -1,33 +1,42 @@ import { css } from '@emotion/css'; -import React, { FC } from 'react'; +import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, Input, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; const getStyles = (theme: GrafanaTheme2) => ({ - searchContainer: css` - display: flex; - margin: 16px 0; - justify-content: space-between; + searchContainer: css({ + display: 'flex', + justifyContent: 'space-between', - position: sticky; - top: 0; - background-color: ${theme.colors.background.primary}; - z-index: 2; - padding: ${theme.spacing(2)}; - margin: 0 -${theme.spacing(2)}; - `, + position: 'sticky', + top: 0, + backgroundColor: theme.colors.background.primary, + zIndex: 2, + padding: theme.spacing(2, 0), + }), }); const placeholder = t('connections.search.placeholder', 'Search all'); -export const Search: FC<{ onChange: (e: React.FormEvent) => void }> = ({ onChange }) => { +export interface Props { + onChange: (e: React.FormEvent) => void; + value: string | undefined; +} + +export const Search = ({ onChange, value }: Props) => { const styles = useStyles2(getStyles); return (
- } placeholder={placeholder} aria-label="Search all" /> + } + placeholder={placeholder} + aria-label="Search all" + />
); };