mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
Plugins: Make the Plugin Details page reusable (#58741)
* refactor(PluginDetails): use react-router hooks instead of props * Wip * refactor: remove unnecessary constant * feat: use the original plugin details page under connections * chore: use better wording in the not-found warning Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> * chore: use the renderer utility everywhere in the test * chore: don't show a title while loading a plugin Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
parent
5e53582bb1
commit
778fb07464
@ -4298,6 +4298,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/PluginDetailsBody.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/SearchField.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -4316,9 +4319,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/features/plugins/admin/pages/PluginDetails.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/state/actions.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
@ -1,6 +1,3 @@
|
||||
// The ID of the app plugin that we render under that "Cloud Integrations" tab
|
||||
export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app';
|
||||
|
||||
// The ID of the main nav-tree item (the main item in the NavIndex)
|
||||
export const ROUTE_BASE_ID = 'connections';
|
||||
|
||||
|
@ -1,24 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
import { Alert, Badge } from '@grafana/ui';
|
||||
import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage';
|
||||
import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
|
||||
|
||||
import { ROUTES } from '../constants';
|
||||
|
||||
export function DataSourceDetailsPage() {
|
||||
const overrideNavId = 'standalone-plugin-page-/connections/connect-data';
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
||||
const isConnectDataPageOverriden = Boolean(navIndex[overrideNavId]);
|
||||
const navId = isConnectDataPageOverriden ? overrideNavId : 'connections-connect-data'; // The nav id changes (gets a prefix) if it is overriden by a plugin
|
||||
|
||||
return (
|
||||
<Page
|
||||
<PluginDetailsPage
|
||||
pluginId={id}
|
||||
navId={navId}
|
||||
pageNav={{
|
||||
text: 'Datasource details',
|
||||
subTitle: 'This is going to be the details page for a datasource',
|
||||
notFoundComponent={<NotFoundDatasource />}
|
||||
notFoundNavModel={{
|
||||
text: 'Unknown datasource',
|
||||
subTitle: 'No datasource with this ID could be found.',
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<Page.Contents>Data Source Details (no exposed component from plugins yet)</Page.Contents>
|
||||
</Page>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NotFoundDatasource() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<Alert severity={AppNotificationSeverity.Warning} title="">
|
||||
Maybe you mistyped the URL or the plugin with the id <Badge text={id} color="orange" /> is unavailable.
|
||||
<br />
|
||||
To see a list of available datasources please <a href={ROUTES.ConnectData}>click here</a>.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { useStyles2, TabContent, Alert } from '@grafana/ui';
|
||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
|
||||
import { Loader } from '../components/Loader';
|
||||
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
||||
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
|
||||
import { PluginDetailsSignature } from '../components/PluginDetailsSignature';
|
||||
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
||||
import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';
|
||||
import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';
|
||||
import { PluginTabIds } from '../types';
|
||||
|
||||
export type Props = {
|
||||
// The ID of the plugin
|
||||
pluginId: string;
|
||||
// The navigation ID used for displaying the sidebar navigation
|
||||
navId?: string;
|
||||
// Can be used to customise the title & subtitle for the not found page
|
||||
notFoundNavModel?: NavModelItem;
|
||||
// Can be used to customise the content shown when a plugin with the given ID cannot be found
|
||||
notFoundComponent?: React.ReactElement;
|
||||
};
|
||||
|
||||
export function PluginDetailsPage({
|
||||
pluginId,
|
||||
navId = 'plugins',
|
||||
notFoundComponent = <NotFoundPlugin />,
|
||||
notFoundNavModel = {
|
||||
text: 'Unknown plugin',
|
||||
subTitle: 'The requested ID does not belong to any plugin',
|
||||
active: true,
|
||||
},
|
||||
}: Props) {
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const plugin = useGetSingle(pluginId); // fetches the plugin settings for this Grafana instance
|
||||
const { navModel, activePageId } = usePluginDetailsTabs(plugin, queryParams.get('page') as PluginTabIds);
|
||||
const { actions, info, subtitle } = usePluginPageExtensions(plugin);
|
||||
const { isLoading: isFetchLoading } = useFetchStatus();
|
||||
const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (isFetchLoading || isFetchDetailsLoading) {
|
||||
return (
|
||||
<Page
|
||||
navId={navId}
|
||||
pageNav={{
|
||||
text: '',
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (!plugin) {
|
||||
return (
|
||||
<Page navId={navId} pageNav={notFoundNavModel}>
|
||||
{notFoundComponent}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navId={navId} pageNav={navModel} actions={actions} subTitle={subtitle} info={info}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} />
|
||||
</TabContent>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
alert: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
subtitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
// Needed due to block formatting context
|
||||
tabContent: css`
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
function NotFoundPlugin() {
|
||||
return (
|
||||
<Layout justify="center" align="center">
|
||||
<Alert severity={AppNotificationSeverity.Warning} title="Plugin not found">
|
||||
That plugin cannot be found. Please check the url is correct or <br />
|
||||
go to the <a href="/plugins">plugin catalog</a>.
|
||||
</Alert>
|
||||
</Layout>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ import { getDefaultNormalizer, render, RenderResult, SelectorMatcherOptions, wai
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
PluginErrorCode,
|
||||
@ -13,7 +13,6 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { mockPluginApis, getCatalogPluginMock, getPluginsStateMock, mockUserPermissions } from '../__mocks__';
|
||||
@ -70,24 +69,14 @@ const renderPluginDetails = (
|
||||
): RenderResult => {
|
||||
const plugin = getCatalogPluginMock(pluginOverride);
|
||||
const { id } = plugin;
|
||||
const props = getRouteComponentProps({
|
||||
match: { params: { pluginId: id }, isExact: true, url: '', path: '' },
|
||||
queryParams: { page: pageId },
|
||||
location: {
|
||||
hash: '',
|
||||
pathname: `/plugins/${id}`,
|
||||
search: pageId ? `?page=${pageId}` : '',
|
||||
state: undefined,
|
||||
},
|
||||
});
|
||||
const store = configureStore({
|
||||
plugins: pluginsStateOverride || getPluginsStateMock([plugin]),
|
||||
});
|
||||
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MemoryRouter initialEntries={[{ pathname: `/plugins/${id}`, search: pageId ? `?page=${pageId}` : '' }]}>
|
||||
<Provider store={store}>
|
||||
<PluginDetailsPage {...props} />
|
||||
<Route path="/plugins/:pluginId" component={PluginDetailsPage} />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -137,24 +126,7 @@ describe('Plugin details page', () => {
|
||||
local: { id },
|
||||
});
|
||||
|
||||
const props = getRouteComponentProps({
|
||||
match: { params: { pluginId: id }, isExact: true, url: '', path: '' },
|
||||
queryParams: {},
|
||||
location: {
|
||||
hash: '',
|
||||
pathname: `/plugins/${id}`,
|
||||
search: '',
|
||||
state: undefined,
|
||||
},
|
||||
});
|
||||
const store = configureStore();
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<PluginDetailsPage {...props} />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const { queryByText } = renderPluginDetails({ id });
|
||||
|
||||
await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument());
|
||||
});
|
||||
|
@ -1,84 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, TabContent, Alert } from '@grafana/ui';
|
||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
import { PluginDetailsPage } from '../components/PluginDetailsPage';
|
||||
|
||||
import { Loader } from '../components/Loader';
|
||||
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
||||
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
|
||||
import { PluginDetailsSignature } from '../components/PluginDetailsSignature';
|
||||
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
||||
import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';
|
||||
import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';
|
||||
import { PluginTabIds } from '../types';
|
||||
export default function PluginDetails(): JSX.Element {
|
||||
const { pluginId } = useParams<{ pluginId: string }>();
|
||||
|
||||
type Props = GrafanaRouteComponentProps<{ pluginId?: string }>;
|
||||
|
||||
export default function PluginDetails({ match, queryParams }: Props): JSX.Element | null {
|
||||
const {
|
||||
params: { pluginId = '' },
|
||||
url,
|
||||
} = match;
|
||||
const parentUrl = url.substring(0, url.lastIndexOf('/'));
|
||||
|
||||
const plugin = useGetSingle(pluginId); // fetches the localplugin settings
|
||||
const { navModel, activePageId } = usePluginDetailsTabs(plugin, queryParams.page as PluginTabIds);
|
||||
const { actions, info, subtitle } = usePluginPageExtensions(plugin);
|
||||
const { isLoading: isFetchLoading } = useFetchStatus();
|
||||
const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (isFetchLoading || isFetchDetailsLoading) {
|
||||
return (
|
||||
<Page navId="plugins">
|
||||
<Loader />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (!plugin) {
|
||||
return (
|
||||
<Layout justify="center" align="center">
|
||||
<Alert severity={AppNotificationSeverity.Warning} title="Plugin not found">
|
||||
That plugin cannot be found. Please check the url is correct or <br />
|
||||
go to the <a href={parentUrl}>plugin catalog</a>.
|
||||
</Alert>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navId="plugins" pageNav={navModel} actions={actions} subTitle={subtitle} info={info}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsBody queryParams={queryParams} plugin={plugin} pageId={activePageId} />
|
||||
</TabContent>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
return <PluginDetailsPage pluginId={pluginId} />;
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
alert: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
subtitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
// Needed due to block formatting context
|
||||
tabContent: css`
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user