diff --git a/.betterer.results b/.betterer.results
index cb77689214a..b903dc1d5e6 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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"],
diff --git a/public/app/features/connections/constants.ts b/public/app/features/connections/constants.ts
index 561e44eed99..29d9c00ab3b 100644
--- a/public/app/features/connections/constants.ts
+++ b/public/app/features/connections/constants.ts
@@ -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';
diff --git a/public/app/features/connections/pages/DataSourceDetailsPage.tsx b/public/app/features/connections/pages/DataSourceDetailsPage.tsx
index e39e0cafb8e..09e7a1795b5 100644
--- a/public/app/features/connections/pages/DataSourceDetailsPage.tsx
+++ b/public/app/features/connections/pages/DataSourceDetailsPage.tsx
@@ -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 (
- }
+ notFoundNavModel={{
+ text: 'Unknown datasource',
+ subTitle: 'No datasource with this ID could be found.',
active: true,
}}
- >
- Data Source Details (no exposed component from plugins yet)
-
+ />
+ );
+}
+
+function NotFoundDatasource() {
+ const { id } = useParams<{ id: string }>();
+
+ return (
+
+ Maybe you mistyped the URL or the plugin with the id is unavailable.
+
+ To see a list of available datasources please click here.
+
);
}
diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx
new file mode 100644
index 00000000000..ad7b6ab7160
--- /dev/null
+++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx
@@ -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 = ,
+ 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 (
+
+
+
+ );
+ }
+
+ if (!plugin) {
+ return (
+
+ {notFoundComponent}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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 (
+
+
+ That plugin cannot be found. Please check the url is correct or
+ go to the plugin catalog.
+
+
+ );
+}
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
index f1b21bb99ac..16892cb07b4 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
@@ -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(
-
+
-
+
);
@@ -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(
-
-
-
-
-
- );
+ const { queryByText } = renderPluginDetails({ id });
await waitFor(() => expect(queryByText(/licensed under the apache 2.0 license/i)).toBeInTheDocument());
});
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx
index 7b30a496878..575aac998f4 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx
@@ -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 (
-
-
-
- );
- }
-
- if (!plugin) {
- return (
-
-
- That plugin cannot be found. Please check the url is correct or
- go to the plugin catalog.
-
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
- );
+ return ;
}
-
-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%;
- `,
- };
-};