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:
Jack Westbrook 2021-09-27 18:06:47 +02:00 committed by GitHub
parent a79ee09cf5
commit 4dc556445c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 147 additions and 77 deletions

View File

@ -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) => ({

View File

@ -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>
); );

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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>

View File

@ -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));

View File

@ -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>