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%; - `, - }; -};