Plugins: Remove cards and list display options functionality from plugin catalogue (#91840)

* remove displayMode functionality from plugin catalogue

* remove test for removed functionality

* fix linting
This commit is contained in:
Yulia Shanyrova
2024-08-14 10:24:10 +02:00
committed by GitHub
parent 427dad26a2
commit d1b0e70f8d
11 changed files with 18 additions and 270 deletions
@@ -2,15 +2,7 @@ import { setBackendSrv } from '@grafana/runtime';
import { API_ROOT, GCOM_API_ROOT } from '../constants';
import * as permissions from '../permissions';
import {
CatalogPlugin,
LocalPlugin,
RemotePlugin,
Version,
ReducerState,
RequestStatus,
PluginListDisplayMode,
} from '../types';
import { CatalogPlugin, LocalPlugin, RemotePlugin, Version, ReducerState, RequestStatus } from '../types';
import catalogPluginMock from './catalogPlugin.mock';
import localPluginMock from './localPlugin.mock';
@@ -40,9 +32,6 @@ export const getPluginsStateMock = (plugins: CatalogPlugin[] = []): ReducerState
status: RequestStatus.Fulfilled,
},
},
settings: {
displayMode: PluginListDisplayMode.Grid,
},
// Backward compatibility
plugins: [],
errors: [],
@@ -1,79 +0,0 @@
import { render } from '@testing-library/react';
import { useLocation } from 'react-router-dom';
import { PluginSignatureStatus } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { PluginList } from './PluginList';
jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
appSubUrl: '',
},
}));
const useLocationMock = useLocation as jest.Mock;
const getMockPlugin = (id: string): CatalogPlugin => {
return {
description: 'The test plugin',
downloads: 5,
id,
info: {
logos: {
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
},
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',
popularity: 0,
signature: PluginSignatureStatus.valid,
publishedAt: '2020-09-01',
updatedAt: '2021-06-28',
hasUpdate: false,
isInstalled: false,
isCore: false,
isDev: false,
isEnterprise: false,
isDisabled: false,
isDeprecated: false,
isPublished: true,
isManaged: false,
};
};
const plugins = [getMockPlugin('test1'), getMockPlugin('test2'), getMockPlugin('test3')];
describe('PluginList', () => {
beforeAll(() => {
useLocationMock.mockImplementation(() => ({
pathname: '/plugins',
}));
});
it('renders a plugin list', () => {
const result = render(<PluginList plugins={plugins} displayMode={PluginListDisplayMode.List} />);
expect(result.getByTestId('plugin-list')).toBeTruthy();
const links = result.getAllByRole('link');
for (const link of links) {
expect(link).toHaveAttribute('href', expect.stringMatching(/^\/plugins\/test\d/));
}
});
it('renders a plugin list with a subAppUrl', () => {
config.appSubUrl = 'test-sub-url';
const result = render(<PluginList plugins={plugins} displayMode={PluginListDisplayMode.List} />);
expect(result.getByTestId('plugin-list')).toBeTruthy();
const links = result.getAllByRole('link');
for (const link of links) {
expect(link).toHaveAttribute('href', expect.stringMatching(/^test-sub-url\/plugins\/test\d/));
}
});
});
@@ -4,18 +4,16 @@ import { config } from '@grafana/runtime';
import { EmptyState, Grid } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { CatalogPlugin } from '../types';
import { PluginListItem } from './PluginListItem';
interface Props {
plugins: CatalogPlugin[];
displayMode: PluginListDisplayMode;
isLoading?: boolean;
}
export const PluginList = ({ plugins, displayMode, isLoading }: Props) => {
const isList = displayMode === PluginListDisplayMode.List;
export const PluginList = ({ plugins, isLoading }: Props) => {
const { pathname } = useLocation();
const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname);
@@ -24,12 +22,10 @@ export const PluginList = ({ plugins, displayMode, isLoading }: Props) => {
}
return (
<Grid gap={3} {...(isList ? { columns: 1 } : { minColumnWidth: 34 })} data-testid="plugin-list">
<Grid gap={3} {...{ minColumnWidth: 34 }} data-testid="plugin-list">
{isLoading
? new Array(50).fill(null).map((_, index) => <PluginListItem.Skeleton key={index} displayMode={displayMode} />)
: plugins.map((plugin) => (
<PluginListItem key={plugin.id} plugin={plugin} pathName={pathName} displayMode={displayMode} />
))}
? new Array(50).fill(null).map((_, index) => <PluginListItem.Skeleton key={index} />)
: plugins.map((plugin) => <PluginListItem key={plugin.id} plugin={plugin} pathName={pathName} />)}
</Grid>
);
};
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/data';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { CatalogPlugin } from '../types';
import { PluginListItem } from './PluginListItem';
@@ -102,47 +102,4 @@ describe('PluginListItem', () => {
expect(screen.getByText(/disabled/i)).toBeVisible();
});
/** As List */
it('renders a row with link, image, name, orgName and badges', () => {
render(<PluginListItem plugin={plugin} pathName="/plugins" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin');
const logo = screen.getByRole('presentation');
expect(logo).toHaveAttribute('src', plugin.info.logos.small);
expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
expect(screen.getByText(/signed/i)).toBeVisible();
expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
});
it('renders a datasource plugin with correct icon', () => {
const datasourcePlugin = { ...plugin, type: PluginType.datasource };
render(<PluginListItem plugin={datasourcePlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument();
});
it('renders a panel plugin with correct icon', () => {
const panelPlugin = { ...plugin, type: PluginType.panel };
render(<PluginListItem plugin={panelPlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument();
});
it('renders an app plugin with correct icon', () => {
const appPlugin = { ...plugin, type: PluginType.app };
render(<PluginListItem plugin={appPlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument();
});
it('renders a disabled plugin with a badge to indicate its error', () => {
const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature };
render(<PluginListItem plugin={pluginWithError} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByText(/disabled/i)).toBeVisible();
});
});
@@ -6,7 +6,7 @@ import { locationService, reportInteraction } from '@grafana/runtime';
import { Badge, Icon, Stack, useStyles2 } from '@grafana/ui';
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
import { CatalogPlugin, PluginIconName, PluginListDisplayMode } from '../types';
import { CatalogPlugin, PluginIconName } from '../types';
import { PluginListItemBadges } from './PluginListItemBadges';
import { PluginLogo } from './PluginLogo';
@@ -16,12 +16,10 @@ export const LOGO_SIZE = '48px';
type Props = {
plugin: CatalogPlugin;
pathName: string;
displayMode?: PluginListDisplayMode;
};
function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDisplayMode.Grid }: Props) {
function PluginListItemComponent({ plugin, pathName }: Props) {
const styles = useStyles2(getStyles);
const isList = displayMode === PluginListDisplayMode.List;
const reportUserClickInteraction = () => {
if (locationService.getSearchObject()?.q) {
@@ -29,11 +27,7 @@ function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDis
}
};
return (
<a
href={`${pathName}/${plugin.id}`}
className={cx(styles.container, { [styles.list]: isList })}
onClick={reportUserClickInteraction}
>
<a href={`${pathName}/${plugin.id}`} className={cx(styles.container)} onClick={reportUserClickInteraction}>
<PluginLogo src={plugin.info.logos.small} className={styles.pluginLogo} height={LOGO_SIZE} alt="" />
<h2 className={cx(styles.name, 'plugin-name')}>{plugin.name}</h2>
<div className={cx(styles.content, 'plugin-content')}>
@@ -47,15 +41,11 @@ function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDis
);
}
const PluginListItemSkeleton: SkeletonComponent<Pick<Props, 'displayMode'>> = ({
displayMode = PluginListDisplayMode.Grid,
rootProps,
}) => {
const PluginListItemSkeleton: SkeletonComponent = ({ rootProps }) => {
const styles = useStyles2(getStyles);
const isList = displayMode === PluginListDisplayMode.List;
return (
<div className={cx(styles.container, { [styles.list]: isList })} {...rootProps}>
<div className={cx(styles.container)} {...rootProps}>
<Skeleton
containerClassName={cx(
styles.pluginLogo,
@@ -109,27 +99,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
},
}),
list: css({
rowGap: 0,
'> img': {
alignSelf: 'start',
},
'> .plugin-content': {
minHeight: 0,
gridArea: '2 / 2 / 4 / 3',
'> p': {
margin: theme.spacing(0, 0, 0.5, 0),
},
},
'> .plugin-name': {
alignSelf: 'center',
gridArea: '1 / 2 / 2 / 3',
},
}),
pluginType: css({
gridArea: '1 / 3 / 2 / 4',
color: theme.colors.text.secondary,
@@ -1,5 +1,4 @@
import { render, RenderResult, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { PluginType, escapeStringForRegex } from '@grafana/data';
@@ -406,39 +405,4 @@ describe('Browse list of plugins', () => {
await waitFor(() => expect(getByRole('radio', { name: 'Installed' })).toBeDisabled());
});
});
it('should be possible to switch between display modes', async () => {
const { findByTestId, getByRole, getByTitle, queryByText } = renderBrowse('/plugins?filterBy=all', [
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1' }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }),
]);
await findByTestId('plugin-list');
const listOptionTitle = 'Display plugins in list';
const gridOptionTitle = 'Display plugins in a grid layout';
const listOption = getByRole('radio', { name: listOptionTitle });
const listOptionLabel = getByTitle(listOptionTitle);
const gridOption = getByRole('radio', { name: gridOptionTitle });
const gridOptionLabel = getByTitle(gridOptionTitle);
// All options should be visible
expect(listOptionLabel).toBeVisible();
expect(gridOptionLabel).toBeVisible();
// The default display mode should be "grid"
expect(gridOption).toBeChecked();
expect(listOption).not.toBeChecked();
// Switch to "list" view
await userEvent.click(listOption);
expect(gridOption).not.toBeChecked();
expect(listOption).toBeChecked();
// All plugins are still visible
expect(queryByText('Plugin 1')).toBeInTheDocument();
expect(queryByText('Plugin 2')).toBeInTheDocument();
expect(queryByText('Plugin 3')).toBeInTheDocument();
});
});
@@ -17,14 +17,12 @@ import { RoadmapLinks } from '../components/RoadmapLinks';
import { SearchField } from '../components/SearchField';
import { Sorters } from '../helpers';
import { useHistory } from '../hooks/useHistory';
import { useGetAll, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks';
import { PluginListDisplayMode } from '../types';
import { useGetAll, useIsRemotePluginsAvailable } from '../state/hooks';
export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
const location = useLocation();
const locationSearch = locationSearchToObject(location.search);
const navModel = useSelector((state) => getNavModel(state.navIndex, 'plugins'));
const { displayMode, setDisplayMode } = useDisplayMode();
const styles = useStyles2(getStyles);
const history = useHistory();
const remotePluginsAvailable = useIsRemotePluginsAvailable();
@@ -143,27 +141,10 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
]}
/>
</Field>
{/* Display mode */}
<Field label="View">
<RadioButtonGroup<PluginListDisplayMode>
className={styles.displayAs}
value={displayMode}
onChange={setDisplayMode}
options={[
{
value: PluginListDisplayMode.Grid,
icon: 'table',
description: 'Display plugins in a grid layout',
},
{ value: PluginListDisplayMode.List, icon: 'list-ul', description: 'Display plugins in list' },
]}
/>
</Field>
</HorizontalGroup>
</HorizontalGroup>
<div className={styles.listWrap}>
<PluginList plugins={plugins} displayMode={displayMode} isLoading={isLoading} />
<PluginList plugins={plugins} isLoading={isLoading} />
</div>
<RoadmapLinks />
</Page.Contents>
@@ -4,17 +4,15 @@ import { PluginError, PluginType } from '@grafana/data';
import { useDispatch, useSelector } from 'app/types';
import { sortPlugins, Sorters } from '../helpers';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { CatalogPlugin } from '../types';
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions';
import { setDisplayMode } from './reducer';
import {
selectPlugins,
selectById,
selectIsRequestPending,
selectRequestError,
selectIsRequestNotFetched,
selectDisplayMode,
selectPluginErrors,
type PluginFilters,
} from './selectors';
@@ -150,13 +148,3 @@ export const useFetchDetailsLazy = () => {
return (id: string) => dispatch(fetchDetails(id));
};
export const useDisplayMode = () => {
const dispatch = useDispatch();
const displayMode = useSelector(selectDisplayMode);
return {
displayMode,
setDisplayMode: (v: PluginListDisplayMode) => dispatch(setDisplayMode(v)),
};
};
@@ -3,7 +3,7 @@ import { createSlice, createEntityAdapter, Reducer, AnyAction, PayloadAction } f
import { PanelPlugin } from '@grafana/data';
import { STATE_PREFIX } from '../constants';
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
import { CatalogPlugin, ReducerState, RequestStatus } from '../types';
import {
fetchDetails,
@@ -33,9 +33,7 @@ const getOriginalActionType = (type: string) => {
export const initialState: ReducerState = {
items: pluginsAdapter.getInitialState(),
requests: {},
settings: {
displayMode: PluginListDisplayMode.Grid,
},
// Backwards compatibility
// (we need to have the following fields in the store as well to be backwards compatible with other parts of Grafana)
// TODO<remove once the "plugin_admin_enabled" feature flag is removed>
@@ -51,11 +49,7 @@ export const initialState: ReducerState = {
const slice = createSlice({
name: 'plugins',
initialState,
reducers: {
setDisplayMode(state, action: PayloadAction<PluginListDisplayMode>) {
state.settings.displayMode = action.payload;
},
},
reducers: {},
extraReducers: (builder) =>
builder
.addCase(addPlugins, (state, action: PayloadAction<CatalogPlugin[]>) => {
@@ -113,5 +107,4 @@ const slice = createSlice({
}),
});
export const { setDisplayMode } = slice.actions;
export const reducer: Reducer<ReducerState, AnyAction> = slice.reducer;
@@ -12,8 +12,6 @@ export const selectRoot = (state: PluginCatalogStoreState) => state.plugins;
export const selectItems = createSelector(selectRoot, ({ items }) => items);
export const selectDisplayMode = createSelector(selectRoot, ({ settings }) => settings.displayMode);
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
export type PluginFilters = {
@@ -13,11 +13,6 @@ import { StoreState, PluginsState } from 'app/types';
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
export enum PluginListDisplayMode {
Grid = 'grid',
List = 'list',
}
export enum PluginAdminRoutes {
Home = 'plugins-home',
Browse = 'plugins-browse',
@@ -296,9 +291,6 @@ export type PluginDetailsTab = {
export type ReducerState = PluginsState & {
items: EntityState<CatalogPlugin, string>;
requests: Record<string, RequestInfo>;
settings: {
displayMode: PluginListDisplayMode;
};
};
// TODO<remove when the "plugin_admin_enabled" feature flag is removed>