mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin Catalog: Use routing for PluginDetails Tabs (#39555)
* feat(catalog): introduce id and href to PluginDetailsTabs * feat(catalog): add hrefs and ids to PluginDetails tabs. Pass queryParams to PluginDetailsBody * feat(catalog): pass queryParams to PluginsDetailsBody and add page param to PluginListCard * fix(catalog): prevent flicker of content by waiting for fetch details to finish loading * feat(catalog): add tab icons to PluginDetails page * feat(catalog): make breadcrumbs in PluginDetailsHeader aware of page queryparam * fix(catalog): fix deeplinking to PluginDetails by comparing tabs length * test(catalog): update tests with correct props and wrap in router
This commit is contained in:
parent
a79ee09cf5
commit
4dc556445c
@ -1,25 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
import { AppPlugin, GrafanaTheme2 } from '@grafana/data';
|
import { AppPlugin, GrafanaTheme2, UrlQueryMap } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { CatalogPlugin, PluginTabLabels } from '../types';
|
import { CatalogPlugin, PluginTabIds } from '../types';
|
||||||
import { VersionList } from '../components/VersionList';
|
import { VersionList } from '../components/VersionList';
|
||||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||||
import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
|
import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
|
||||||
import { PluginDashboards } from '../../PluginDashboards';
|
import { PluginDashboards } from '../../PluginDashboards';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tab: { label: string };
|
|
||||||
plugin: CatalogPlugin;
|
plugin: CatalogPlugin;
|
||||||
|
queryParams: UrlQueryMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PluginDetailsBody({ tab, plugin }: Props): JSX.Element | null {
|
export function PluginDetailsBody({ plugin, queryParams }: 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 (tab?.label === PluginTabLabels.OVERVIEW) {
|
if (pageId === PluginTabIds.OVERVIEW) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(styles.readme, styles.container)}
|
className={cx(styles.readme, styles.container)}
|
||||||
@ -30,7 +31,7 @@ export function PluginDetailsBody({ tab, plugin }: Props): JSX.Element | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab?.label === PluginTabLabels.VERSIONS) {
|
if (pageId === PluginTabIds.VERSIONS) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<VersionList versions={plugin.details?.versions} />
|
<VersionList versions={plugin.details?.versions} />
|
||||||
@ -38,7 +39,7 @@ export function PluginDetailsBody({ tab, plugin }: Props): JSX.Element | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab?.label === PluginTabLabels.CONFIG && pluginConfig?.angularConfigCtrl) {
|
if (pageId === PluginTabIds.CONFIG && pluginConfig?.angularConfigCtrl) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<AppConfigCtrlWrapper app={pluginConfig as AppPlugin} />
|
<AppConfigCtrlWrapper app={pluginConfig as AppPlugin} />
|
||||||
@ -48,18 +49,17 @@ export function PluginDetailsBody({ tab, plugin }: Props): JSX.Element | null {
|
|||||||
|
|
||||||
if (pluginConfig?.configPages) {
|
if (pluginConfig?.configPages) {
|
||||||
for (const configPage of pluginConfig.configPages) {
|
for (const configPage of pluginConfig.configPages) {
|
||||||
if (tab?.label === configPage.title) {
|
if (pageId === configPage.id) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{/* TODO: we should pass the query params down */}
|
<configPage.body plugin={pluginConfig} query={queryParams} />
|
||||||
<configPage.body plugin={pluginConfig} query={{}} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab?.label === PluginTabLabels.DASHBOARDS && pluginConfig) {
|
if (pageId === PluginTabIds.DASHBOARDS && pluginConfig) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<PluginDashboards plugin={pluginConfig?.meta} />
|
<PluginDashboards plugin={pluginConfig?.meta} />
|
||||||
@ -67,7 +67,11 @@ export function PluginDetailsBody({ tab, plugin }: Props): JSX.Element | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>Page not found.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2, Icon } from '@grafana/ui';
|
import { useStyles2, Icon } from '@grafana/ui';
|
||||||
import { CatalogPlugin, IconName } from '../types';
|
import { CatalogPlugin, PluginIconName } from '../types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugin: CatalogPlugin;
|
plugin: CatalogPlugin;
|
||||||
@ -37,7 +37,7 @@ export function PluginDetailsHeaderDependencies({ plugin, className }: Props): R
|
|||||||
{pluginDependencies.map((p) => {
|
{pluginDependencies.map((p) => {
|
||||||
return (
|
return (
|
||||||
<span key={p.name}>
|
<span key={p.name}>
|
||||||
<Icon name={IconName[p.type]} className={styles.icon} />
|
<Icon name={PluginIconName[p.type]} className={styles.icon} />
|
||||||
{p.name} {p.version}
|
{p.name} {p.version}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -33,7 +33,7 @@ describe('PluginCard', () => {
|
|||||||
it('renders a card with link, image, name, orgName and badges', () => {
|
it('renders a card with link, image, name, orgName and badges', () => {
|
||||||
render(<PluginListCard plugin={plugin} pathName="/plugins" />);
|
render(<PluginListCard plugin={plugin} pathName="/plugins" />);
|
||||||
|
|
||||||
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin');
|
expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview');
|
||||||
|
|
||||||
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);
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Icon, useStyles2, CardContainer, HorizontalGroup, VerticalGroup, Tooltip } from '@grafana/ui';
|
import { Icon, useStyles2, CardContainer, HorizontalGroup, VerticalGroup, Tooltip } from '@grafana/ui';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { CatalogPlugin, IconName } from '../types';
|
import { CatalogPlugin, PluginIconName, PluginTabIds } from '../types';
|
||||||
import { PluginLogo } from './PluginLogo';
|
import { PluginLogo } from './PluginLogo';
|
||||||
import { PluginListBadges } from './PluginListBadges';
|
import { PluginListBadges } from './PluginListBadges';
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export function PluginListCard({ plugin, pathName }: PluginListCardProps) {
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer href={`${pathName}/${plugin.id}`} className={styles.cardContainer}>
|
<CardContainer href={`${pathName}/${plugin.id}?page=${PluginTabIds.OVERVIEW}`} className={styles.cardContainer}>
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
<div className={styles.headerWrap}>
|
<div className={styles.headerWrap}>
|
||||||
<PluginLogo
|
<PluginLogo
|
||||||
@ -29,7 +29,7 @@ export function PluginListCard({ plugin, pathName }: PluginListCardProps) {
|
|||||||
<h2 className={styles.name}>{plugin.name}</h2>
|
<h2 className={styles.name}>{plugin.name}</h2>
|
||||||
{plugin.type && (
|
{plugin.type && (
|
||||||
<div className={styles.icon} data-testid={`${plugin.type} plugin icon`}>
|
<div className={styles.icon} data-testid={`${plugin.type} plugin icon`}>
|
||||||
<Icon name={IconName[plugin.type]} />
|
<Icon name={PluginIconName[plugin.type]} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { PluginIncludeType, PluginType } from '@grafana/data';
|
import { PluginIncludeType, PluginType } from '@grafana/data';
|
||||||
import { CatalogPlugin, PluginDetailsTab } from '../types';
|
import { CatalogPlugin, PluginDetailsTab, PluginTabIds } from '../types';
|
||||||
import { isOrgAdmin } from '../helpers';
|
import { isOrgAdmin } from '../helpers';
|
||||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ type ReturnType = {
|
|||||||
|
|
||||||
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 { pathname } = useLocation();
|
||||||
const tabs = useMemo(() => {
|
const tabs = useMemo(() => {
|
||||||
const canConfigurePlugins = isOrgAdmin();
|
const canConfigurePlugins = isOrgAdmin();
|
||||||
const tabs: PluginDetailsTab[] = [...defaultTabs];
|
const tabs: PluginDetailsTab[] = [...defaultTabs];
|
||||||
@ -26,6 +28,9 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
|
|||||||
if (pluginConfig.angularConfigCtrl) {
|
if (pluginConfig.angularConfigCtrl) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
label: 'Config',
|
label: 'Config',
|
||||||
|
icon: 'cog',
|
||||||
|
id: PluginTabIds.CONFIG,
|
||||||
|
href: `${pathname}?page=${PluginTabIds.CONFIG}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +38,9 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
|
|||||||
for (const page of pluginConfig.configPages) {
|
for (const page of pluginConfig.configPages) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
label: page.title,
|
label: page.title,
|
||||||
|
icon: page.icon,
|
||||||
|
id: page.id,
|
||||||
|
href: `${pathname}?page=${page.id}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,13 +48,16 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
|
|||||||
if (pluginConfig.meta.includes?.find((include) => include.type === PluginIncludeType.dashboard)) {
|
if (pluginConfig.meta.includes?.find((include) => include.type === PluginIncludeType.dashboard)) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
label: 'Dashboards',
|
label: 'Dashboards',
|
||||||
|
icon: 'apps',
|
||||||
|
id: PluginTabIds.DASHBOARDS,
|
||||||
|
href: `${pathname}?page=${PluginTabIds.DASHBOARDS}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
}, [pluginConfig, defaultTabs]);
|
}, [pluginConfig, defaultTabs, pathname]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { render, RenderResult, waitFor } from '@testing-library/react';
|
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import PluginDetailsPage from './PluginDetails';
|
import PluginDetailsPage from './PluginDetails';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { CatalogPlugin } from '../types';
|
import { CatalogPlugin, PluginTabIds } from '../types';
|
||||||
import * as api from '../api';
|
import * as api from '../api';
|
||||||
import { mockPluginApis, getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
|
import { mockPluginApis, getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
|
||||||
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
|
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
|
||||||
@ -24,10 +25,19 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
return mockedRuntime;
|
return mockedRuntime;
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderPluginDetails = (pluginOverride: Partial<CatalogPlugin>): RenderResult => {
|
const renderPluginDetails = (pluginOverride: Partial<CatalogPlugin>, pageId = PluginTabIds.OVERVIEW): RenderResult => {
|
||||||
const plugin = getCatalogPluginMock(pluginOverride);
|
const plugin = getCatalogPluginMock(pluginOverride);
|
||||||
const { id } = plugin;
|
const { id } = plugin;
|
||||||
const props = getRouteComponentProps({ match: { params: { pluginId: id }, isExact: true, url: '', path: '' } });
|
const props = getRouteComponentProps({
|
||||||
|
match: { params: { pluginId: id }, isExact: true, url: '', path: '' },
|
||||||
|
queryParams: { page: pageId },
|
||||||
|
location: {
|
||||||
|
hash: '',
|
||||||
|
pathname: `/plugins/${id}`,
|
||||||
|
search: `?page=${pageId}`,
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
plugins: getPluginsStateMock([plugin]),
|
plugins: getPluginsStateMock([plugin]),
|
||||||
});
|
});
|
||||||
@ -35,7 +45,8 @@ const renderPluginDetails = (pluginOverride: Partial<CatalogPlugin>): RenderResu
|
|||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PluginDetailsPage {...props} />
|
<PluginDetailsPage {...props} />
|
||||||
</Provider>
|
</Provider>,
|
||||||
|
{ wrapper: MemoryRouter }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,12 +77,22 @@ describe('Plugin details page', () => {
|
|||||||
local: { id },
|
local: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = getRouteComponentProps({ match: { params: { pluginId: id }, isExact: true, url: '', path: '' } });
|
const props = getRouteComponentProps({
|
||||||
|
match: { params: { pluginId: id }, isExact: true, url: '', path: '' },
|
||||||
|
queryParams: { page: PluginTabIds.OVERVIEW },
|
||||||
|
location: {
|
||||||
|
hash: '',
|
||||||
|
pathname: `/plugins/${id}`,
|
||||||
|
search: `?page=${PluginTabIds.OVERVIEW}`,
|
||||||
|
state: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
const { queryByText } = render(
|
const { queryByText } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PluginDetailsPage {...props} />
|
<PluginDetailsPage {...props} />
|
||||||
</Provider>
|
</Provider>,
|
||||||
|
{ wrapper: MemoryRouter }
|
||||||
);
|
);
|
||||||
|
|
||||||
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());
|
||||||
@ -129,24 +150,25 @@ describe('Plugin details page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display version history in case it is available', async () => {
|
it('should display version history in case it is available', async () => {
|
||||||
const { queryByText, getByText, getByRole } = renderPluginDetails({
|
const { queryByText, getByRole } = renderPluginDetails(
|
||||||
id,
|
{
|
||||||
details: {
|
id,
|
||||||
links: [],
|
details: {
|
||||||
versions: [
|
links: [],
|
||||||
{
|
versions: [
|
||||||
version: '1.0.0',
|
{
|
||||||
createdAt: '2016-04-06T20:23:41.000Z',
|
version: '1.0.0',
|
||||||
},
|
createdAt: '2016-04-06T20:23:41.000Z',
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
PluginTabIds.VERSIONS
|
||||||
|
);
|
||||||
|
|
||||||
// Check if version information is available
|
// Check if version information is available
|
||||||
await waitFor(() => expect(queryByText(/version history/i)).toBeInTheDocument());
|
await waitFor(() => expect(queryByText(/version history/i)).toBeInTheDocument());
|
||||||
|
|
||||||
// Go to the versions tab
|
|
||||||
userEvent.click(getByText(/version history/i));
|
|
||||||
expect(
|
expect(
|
||||||
getByRole('columnheader', {
|
getByRole('columnheader', {
|
||||||
name: /version/i,
|
name: /version/i,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { usePrevious } from 'react-use';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2, TabsBar, TabContent, Tab, Alert } from '@grafana/ui';
|
import { useStyles2, TabsBar, TabContent, Tab, Alert, IconName } from '@grafana/ui';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
@ -10,43 +12,53 @@ import { PluginDetailsHeader } from '../components/PluginDetailsHeader';
|
|||||||
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
||||||
import { Page as PluginPage } from '../components/Page';
|
import { Page as PluginPage } from '../components/Page';
|
||||||
import { Loader } from '../components/Loader';
|
import { Loader } from '../components/Loader';
|
||||||
import { PluginTabLabels, PluginDetailsTab } from '../types';
|
import { PluginTabLabels, PluginTabIds, PluginDetailsTab } from '../types';
|
||||||
import { useGetSingle, useFetchStatus } from '../state/hooks';
|
import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';
|
||||||
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
||||||
import { AppNotificationSeverity } from 'app/types';
|
import { AppNotificationSeverity } from 'app/types';
|
||||||
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
|
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
|
||||||
|
|
||||||
type Props = GrafanaRouteComponentProps<{ pluginId?: string }>;
|
type Props = GrafanaRouteComponentProps<{ pluginId?: string }>;
|
||||||
|
|
||||||
type State = {
|
export default function PluginDetails({ match, queryParams }: Props): JSX.Element | null {
|
||||||
tabs: PluginDetailsTab[];
|
const {
|
||||||
activeTabIndex: number;
|
params: { pluginId = '' },
|
||||||
};
|
url,
|
||||||
|
} = match;
|
||||||
const DefaultState = {
|
const pageId = (queryParams.page as PluginTabIds) || PluginTabIds.OVERVIEW;
|
||||||
tabs: [{ label: PluginTabLabels.OVERVIEW }, { label: PluginTabLabels.VERSIONS }],
|
const parentUrl = url.substring(0, url.lastIndexOf('/'));
|
||||||
activeTabIndex: 0,
|
const defaultTabs: PluginDetailsTab[] = [
|
||||||
};
|
{
|
||||||
|
label: PluginTabLabels.OVERVIEW,
|
||||||
export default function PluginDetails({ match }: Props): JSX.Element | null {
|
icon: 'file-alt',
|
||||||
const { pluginId = '' } = match.params;
|
id: PluginTabIds.OVERVIEW,
|
||||||
const [state, setState] = useState<State>(DefaultState);
|
href: `${url}?page=${PluginTabIds.OVERVIEW}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: PluginTabLabels.VERSIONS,
|
||||||
|
icon: 'history',
|
||||||
|
id: PluginTabIds.VERSIONS,
|
||||||
|
href: `${url}?page=${PluginTabIds.VERSIONS}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
const plugin = useGetSingle(pluginId); // fetches the localplugin settings
|
const plugin = useGetSingle(pluginId); // fetches the localplugin settings
|
||||||
const { tabs } = usePluginDetailsTabs(plugin, DefaultState.tabs);
|
const { tabs } = usePluginDetailsTabs(plugin, defaultTabs);
|
||||||
const { activeTabIndex } = state;
|
const { isLoading: isFetchLoading } = useFetchStatus();
|
||||||
const { isLoading } = useFetchStatus();
|
const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const setActiveTab = useCallback((activeTabIndex: number) => setState({ ...state, activeTabIndex }), [state]);
|
const prevTabs = usePrevious(tabs);
|
||||||
const parentUrl = match.url.substring(0, match.url.lastIndexOf('/'));
|
|
||||||
|
|
||||||
// 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(() => {
|
||||||
if (activeTabIndex > tabs.length - 1) {
|
const hasUninstalledWithConfigPages = prevTabs && prevTabs.length > tabs.length;
|
||||||
setActiveTab(0);
|
const isViewingAConfigPage = pageId !== PluginTabIds.OVERVIEW && pageId !== PluginTabIds.VERSIONS;
|
||||||
}
|
|
||||||
}, [setActiveTab, activeTabIndex, tabs]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (hasUninstalledWithConfigPages && isViewingAConfigPage) {
|
||||||
|
locationService.replace(`${url}?page=${PluginTabIds.OVERVIEW}`);
|
||||||
|
}
|
||||||
|
}, [pageId, url, tabs, prevTabs]);
|
||||||
|
|
||||||
|
if (isFetchLoading || isFetchDetailsLoading) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Loader />
|
<Loader />
|
||||||
@ -68,25 +80,28 @@ export default function PluginDetails({ match }: Props): JSX.Element | null {
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PluginPage>
|
<PluginPage>
|
||||||
<PluginDetailsHeader currentUrl={match.url} parentUrl={parentUrl} plugin={plugin} />
|
<PluginDetailsHeader currentUrl={`${url}?page=${pageId}`} parentUrl={parentUrl} plugin={plugin} />
|
||||||
|
|
||||||
{/* Tab navigation */}
|
{/* Tab navigation */}
|
||||||
<TabsBar>
|
<TabsBar>
|
||||||
{tabs.map((tab: PluginDetailsTab, idx: number) => (
|
{tabs.map((tab: PluginDetailsTab) => {
|
||||||
<Tab
|
return (
|
||||||
key={tab.label}
|
<Tab
|
||||||
label={tab.label}
|
key={tab.label}
|
||||||
active={idx === activeTabIndex}
|
label={tab.label}
|
||||||
onChangeTab={() => setActiveTab(idx)}
|
href={tab.href}
|
||||||
/>
|
icon={tab.icon as IconName}
|
||||||
))}
|
active={tab.id === pageId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TabsBar>
|
</TabsBar>
|
||||||
|
|
||||||
{/* Active tab */}
|
{/* Active tab */}
|
||||||
<TabContent className={styles.tabContent}>
|
<TabContent className={styles.tabContent}>
|
||||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||||
<PluginDetailsBody tab={tabs[activeTabIndex]} plugin={plugin} />
|
<PluginDetailsBody queryParams={queryParams} plugin={plugin} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</PluginPage>
|
</PluginPage>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -70,6 +70,13 @@ export const useFetchStatus = () => {
|
|||||||
return { isLoading, error };
|
return { isLoading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useFetchDetailsStatus = () => {
|
||||||
|
const isLoading = useSelector(selectIsRequestPending(fetchDetails.typePrefix));
|
||||||
|
const error = useSelector(selectRequestError(fetchDetails.typePrefix));
|
||||||
|
|
||||||
|
return { isLoading, error };
|
||||||
|
};
|
||||||
|
|
||||||
export const useInstallStatus = () => {
|
export const useInstallStatus = () => {
|
||||||
const isInstalling = useSelector(selectIsRequestPending(install.typePrefix));
|
const isInstalling = useSelector(selectIsRequestPending(install.typePrefix));
|
||||||
const error = useSelector(selectRequestError(install.typePrefix));
|
const error = useSelector(selectRequestError(install.typePrefix));
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
PluginDependencies,
|
PluginDependencies,
|
||||||
PluginErrorCode,
|
PluginErrorCode,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { IconName } from '@grafana/ui';
|
||||||
import { StoreState, PluginsState } from 'app/types';
|
import { StoreState, PluginsState } from 'app/types';
|
||||||
|
|
||||||
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
|
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
|
||||||
@ -19,7 +20,7 @@ export enum PluginAdminRoutes {
|
|||||||
DetailsAdmin = 'plugins-details-admin',
|
DetailsAdmin = 'plugins-details-admin',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IconName {
|
export enum PluginIconName {
|
||||||
app = 'apps',
|
app = 'apps',
|
||||||
datasource = 'database',
|
datasource = 'database',
|
||||||
panel = 'credit-card',
|
panel = 'credit-card',
|
||||||
@ -203,6 +204,13 @@ export enum PluginTabLabels {
|
|||||||
DASHBOARDS = 'Dashboards',
|
DASHBOARDS = 'Dashboards',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PluginTabIds {
|
||||||
|
OVERVIEW = 'overview',
|
||||||
|
VERSIONS = 'version-history',
|
||||||
|
CONFIG = 'config',
|
||||||
|
DASHBOARDS = 'dashboards',
|
||||||
|
}
|
||||||
|
|
||||||
export enum RequestStatus {
|
export enum RequestStatus {
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
Fulfilled = 'Fulfilled',
|
Fulfilled = 'Fulfilled',
|
||||||
@ -219,6 +227,9 @@ export type RequestInfo = {
|
|||||||
|
|
||||||
export type PluginDetailsTab = {
|
export type PluginDetailsTab = {
|
||||||
label: PluginTabLabels | string;
|
label: PluginTabLabels | string;
|
||||||
|
icon?: IconName | string;
|
||||||
|
id: PluginTabIds | string;
|
||||||
|
href?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO<remove `PluginsState &` when the "plugin_admin_enabled" feature flag is removed>
|
// TODO<remove `PluginsState &` when the "plugin_admin_enabled" feature flag is removed>
|
||||||
|
Loading…
Reference in New Issue
Block a user