Connections: hook up search to url params to match cloud behaviour (#80166)

hook up connections search to url params to match cloud behaviour
This commit is contained in:
Ashley Harrison
2024-01-10 09:57:17 +00:00
committed by GitHub
parent 2d6ad9f7c5
commit 1162c28a55
4 changed files with 54 additions and 68 deletions
-8
View File
@@ -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"],
@@ -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(
<Provider store={store}>
<TestProvider storeState={{ plugins: getPluginsStateMock(plugins) }}>
<AddNewConnection />
</Provider>
</TestProvider>
);
};
@@ -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();
});
});
@@ -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<CardGridItem | null>(null);
const styles = useStyles2(getStyles);
const canCreateDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const handleSearchChange = (e: React.FormEvent<HTMLInputElement>) => {
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 && <NoAccessModal item={focusedItem} isOpen={isNoAccessModalOpen} onDismiss={closeModal} />}
<Search onChange={handleSearchChange} />
<Search onChange={handleSearchChange} value={searchTerm} />
{/* We need this extra spacing when there are no filters */}
<div className={styles.spacer} />
<CategoryHeader iconName="database" label={categoryHeaderLabel} />
@@ -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<HTMLInputElement>) => void }> = ({ onChange }) => {
export interface Props {
onChange: (e: React.FormEvent<HTMLInputElement>) => void;
value: string | undefined;
}
export const Search = ({ onChange, value }: Props) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.searchContainer}>
<Input onChange={onChange} prefix={<Icon name="search" />} placeholder={placeholder} aria-label="Search all" />
<Input
value={value}
onChange={onChange}
prefix={<Icon name="search" />}
placeholder={placeholder}
aria-label="Search all"
/>
</div>
);
};