Catalog: Provide default tab fallback in Plugin Details page (#41979)

* feat(catalog): introduce defaultTab to usePluginDetailsTabs hook

* feat(catalog): use defaultTab as fallback tab for PluginDetails

* chore(catalog): remove hardcoded page query param in list items

* refactor(catalog): prefer let over react ref when setting default tab in PluginDetails

* refactor(catalog): pass pageId to plugin details body rather than duplicate logic

* test(catalog): remove query param from List item test hrefs

* test(catalog): introduce a test for default app config page for installed app plugins
pull/42194/head
Jack Westbrook 4 years ago committed by GitHub
parent aefb2659ec
commit 694600ed04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      public/app/features/plugins/admin/components/PluginDetailsBody.tsx
  2. 4
      public/app/features/plugins/admin/components/PluginListItem.test.tsx
  3. 7
      public/app/features/plugins/admin/components/PluginListItem.tsx
  4. 19
      public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx
  5. 42
      public/app/features/plugins/admin/pages/PluginDetails.test.tsx
  6. 6
      public/app/features/plugins/admin/pages/PluginDetails.tsx

@ -13,12 +13,12 @@ import { PluginDashboards } from './PluginDashboards';
type Props = { type Props = {
plugin: CatalogPlugin; plugin: CatalogPlugin;
queryParams: UrlQueryMap; queryParams: UrlQueryMap;
pageId: string;
}; };
export function PluginDetailsBody({ plugin, queryParams }: Props): JSX.Element { export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.Element {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { value: pluginConfig } = usePluginConfig(plugin); const { value: pluginConfig } = usePluginConfig(plugin);
const pageId = queryParams.page;
if (pageId === PluginTabIds.OVERVIEW) { if (pageId === PluginTabIds.OVERVIEW) {
return ( return (

@ -59,7 +59,7 @@ describe('PluginListItem', () => {
it('renders a card with link, image, name, orgName and badges', () => { it('renders a card with link, image, name, orgName and badges', () => {
render(<PluginListItem plugin={plugin} pathName="/plugins" />); render(<PluginListItem plugin={plugin} pathName="/plugins" />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview'); expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin');
const logo = screen.getByRole('img'); const logo = screen.getByRole('img');
expect(logo).toHaveAttribute('src', plugin.info.logos.small); expect(logo).toHaveAttribute('src', plugin.info.logos.small);
@ -102,7 +102,7 @@ describe('PluginListItem', () => {
it('renders a row with link, image, name, orgName and badges', () => { it('renders a row with link, image, name, orgName and badges', () => {
render(<PluginListItem plugin={plugin} pathName="/plugins" displayMode={PluginListDisplayMode.List} />); render(<PluginListItem plugin={plugin} pathName="/plugins" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview'); expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin');
const logo = screen.getByRole('img'); const logo = screen.getByRole('img');
expect(logo).toHaveAttribute('src', plugin.info.logos.small); expect(logo).toHaveAttribute('src', plugin.info.logos.small);

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types'; import { CatalogPlugin, PluginIconName, PluginListDisplayMode } from '../types';
import { PluginListItemBadges } from './PluginListItemBadges'; import { PluginListItemBadges } from './PluginListItemBadges';
import { PluginLogo } from './PluginLogo'; import { PluginLogo } from './PluginLogo';
import { Icon, useStyles2 } from '@grafana/ui'; import { Icon, useStyles2 } from '@grafana/ui';
@ -19,10 +19,7 @@ export function PluginListItem({ plugin, pathName, displayMode = PluginListDispl
const isList = displayMode === PluginListDisplayMode.List; const isList = displayMode === PluginListDisplayMode.List;
return ( return (
<a <a href={`${pathName}/${plugin.id}`} className={cx(styles.container, { [styles.list]: isList })}>
href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`}
className={cx(styles.container, { [styles.list]: isList })}
>
<PluginLogo src={plugin.info.logos.small} className={styles.pluginLogo} height={LOGO_SIZE} alt="" /> <PluginLogo src={plugin.info.logos.small} className={styles.pluginLogo} height={LOGO_SIZE} alt="" />
<h2 className={cx(styles.name, 'plugin-name')}>{plugin.name}</h2> <h2 className={cx(styles.name, 'plugin-name')}>{plugin.name}</h2>
<div className={cx(styles.content, 'plugin-content')}> <div className={cx(styles.content, 'plugin-content')}>

@ -9,15 +9,18 @@ type ReturnType = {
error: Error | undefined; error: Error | undefined;
loading: boolean; loading: boolean;
tabs: PluginDetailsTab[]; tabs: PluginDetailsTab[];
defaultTab: string;
}; };
export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: PluginDetailsTab[] = []): ReturnType => { export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: PluginDetailsTab[] = []): ReturnType => {
const { loading, error, value: pluginConfig } = usePluginConfig(plugin); const { loading, error, value: pluginConfig } = usePluginConfig(plugin);
const isPublished = Boolean(plugin?.isPublished); const isPublished = Boolean(plugin?.isPublished);
const { pathname } = useLocation(); const { pathname } = useLocation();
const tabs = useMemo(() => {
const [tabs, defaultTab] = useMemo(() => {
const canConfigurePlugins = isOrgAdmin(); const canConfigurePlugins = isOrgAdmin();
const tabs: PluginDetailsTab[] = [...defaultTabs]; const tabs: PluginDetailsTab[] = [...defaultTabs];
let defaultTab;
if (isPublished) { if (isPublished) {
tabs.push({ tabs.push({
@ -30,7 +33,8 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
// Not extending the tabs with the config pages if the plugin is not installed // Not extending the tabs with the config pages if the plugin is not installed
if (!pluginConfig) { if (!pluginConfig) {
return tabs; defaultTab = PluginTabIds.OVERVIEW;
return [tabs, defaultTab];
} }
if (canConfigurePlugins) { if (canConfigurePlugins) {
@ -42,6 +46,7 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
id: PluginTabIds.CONFIG, id: PluginTabIds.CONFIG,
href: `${pathname}?page=${PluginTabIds.CONFIG}`, href: `${pathname}?page=${PluginTabIds.CONFIG}`,
}); });
defaultTab = PluginTabIds.CONFIG;
} }
if (pluginConfig.configPages) { if (pluginConfig.configPages) {
@ -52,6 +57,9 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
id: page.id, id: page.id,
href: `${pathname}?page=${page.id}`, href: `${pathname}?page=${page.id}`,
}); });
if (!defaultTab) {
defaultTab = page.id;
}
} }
} }
@ -66,12 +74,17 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
} }
} }
return tabs; if (!defaultTab) {
defaultTab = PluginTabIds.OVERVIEW;
}
return [tabs, defaultTab];
}, [pluginConfig, defaultTabs, pathname, isPublished]); }, [pluginConfig, defaultTabs, pathname, isPublished]);
return { return {
error, error,
loading, loading,
tabs, tabs,
defaultTab,
}; };
}; };

@ -40,7 +40,7 @@ jest.mock('../hooks/usePluginConfig.tsx', () => ({
const renderPluginDetails = ( const renderPluginDetails = (
pluginOverride: Partial<CatalogPlugin>, pluginOverride: Partial<CatalogPlugin>,
{ {
pageId = PluginTabIds.OVERVIEW, pageId,
pluginsStateOverride, pluginsStateOverride,
}: { }: {
pageId?: PluginTabIds; pageId?: PluginTabIds;
@ -55,7 +55,7 @@ const renderPluginDetails = (
location: { location: {
hash: '', hash: '',
pathname: `/plugins/${id}`, pathname: `/plugins/${id}`,
search: `?page=${pageId}`, search: pageId ? `?page=${pageId}` : '',
state: undefined, state: undefined,
}, },
}); });
@ -118,11 +118,11 @@ describe('Plugin details page', () => {
const props = getRouteComponentProps({ const props = getRouteComponentProps({
match: { params: { pluginId: id }, isExact: true, url: '', path: '' }, match: { params: { pluginId: id }, isExact: true, url: '', path: '' },
queryParams: { page: PluginTabIds.OVERVIEW }, queryParams: {},
location: { location: {
hash: '', hash: '',
pathname: `/plugins/${id}`, pathname: `/plugins/${id}`,
search: `?page=${PluginTabIds.OVERVIEW}`, search: '',
state: undefined, state: undefined,
}, },
}); });
@ -145,6 +145,40 @@ describe('Plugin details page', () => {
await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument()); await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument());
}); });
it('should display an app config page by default for installed app plugins', async () => {
const name = 'Akumuli';
// @ts-ignore
usePluginConfig.mockReturnValue({
value: {
meta: {
type: PluginType.app,
enabled: false,
pinned: false,
jsonData: {},
},
configPages: [
{
title: 'Config',
icon: 'cog',
id: 'configPage',
body: function ConfigPage() {
return <div>Custom Config Page!</div>;
},
},
],
},
});
const { queryByText } = renderPluginDetails({
name,
isInstalled: true,
type: PluginType.app,
});
await waitFor(() => expect(queryByText(/custom config page/i)).toBeInTheDocument());
});
it('should display the number of downloads in the header', async () => { it('should display the number of downloads in the header', async () => {
// depending on what locale you have the Intl.NumberFormat will return a format that contains // depending on what locale you have the Intl.NumberFormat will return a format that contains
// whitespaces. In that case we don't want testing library to remove whitespaces. // whitespaces. In that case we don't want testing library to remove whitespaces.

@ -25,7 +25,6 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen
params: { pluginId = '' }, params: { pluginId = '' },
url, url,
} = match; } = match;
const pageId = (queryParams.page as PluginTabIds) || PluginTabIds.OVERVIEW;
const parentUrl = url.substring(0, url.lastIndexOf('/')); const parentUrl = url.substring(0, url.lastIndexOf('/'));
const defaultTabs: PluginDetailsTab[] = [ const defaultTabs: PluginDetailsTab[] = [
{ {
@ -36,11 +35,12 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen
}, },
]; ];
const plugin = useGetSingle(pluginId); // fetches the localplugin settings const plugin = useGetSingle(pluginId); // fetches the localplugin settings
const { tabs } = usePluginDetailsTabs(plugin, defaultTabs); const { tabs, defaultTab } = usePluginDetailsTabs(plugin, defaultTabs);
const { isLoading: isFetchLoading } = useFetchStatus(); const { isLoading: isFetchLoading } = useFetchStatus();
const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus(); const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const prevTabs = usePrevious(tabs); const prevTabs = usePrevious(tabs);
const pageId = (queryParams.page as PluginTabIds) || defaultTab;
// If an app plugin is uninstalled we need to reset the active tab when the config / dashboards tabs are removed. // If an app plugin is uninstalled we need to reset the active tab when the config / dashboards tabs are removed.
useEffect(() => { useEffect(() => {
@ -95,7 +95,7 @@ export default function PluginDetails({ match, queryParams }: Props): JSX.Elemen
<TabContent className={styles.tabContent}> <TabContent className={styles.tabContent}>
<PluginDetailsSignature plugin={plugin} className={styles.alert} /> <PluginDetailsSignature plugin={plugin} className={styles.alert} />
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} /> <PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
<PluginDetailsBody queryParams={queryParams} plugin={plugin} /> <PluginDetailsBody queryParams={queryParams} plugin={plugin} pageId={pageId} />
</TabContent> </TabContent>
</PluginPage> </PluginPage>
</Page> </Page>

Loading…
Cancel
Save