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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user