- Signed by: {signatureOrg}
- >
- }
- />
- )}
- >
- );
-};
-
-const getDetailsBadgeStyles = (theme: GrafanaTheme2) => ({
- badge: css`
- background-color: ${theme.colors.background.canvas};
- border-color: ${theme.colors.border.strong};
- color: ${theme.colors.text.secondary};
- margin-left: ${theme.spacing()};
- `,
- strong: css`
- color: ${theme.colors.text.primary};
- `,
- icon: css`
- margin-right: ${theme.spacing(0.5)};
- `,
-});
-
-export default PluginPage;
diff --git a/public/app/features/plugins/UpdatePluginModal.tsx b/public/app/features/plugins/UpdatePluginModal.tsx
deleted file mode 100644
index 8b125842bff..00000000000
--- a/public/app/features/plugins/UpdatePluginModal.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import { Modal, useStyles2, VerticalGroup } from '@grafana/ui';
-import { GrafanaTheme2 } from '@grafana/data';
-import { css } from '@emotion/css';
-
-export interface UpdatePluginModalProps {
- onDismiss: () => void;
- id: string;
- name: string;
-}
-
-export function UpdatePluginModal({ onDismiss, id, name }: UpdatePluginModalProps): JSX.Element {
- const styles = useStyles2(getStyles);
- return (
-
-
-
- Type the following on the command line to update {name}.
-
- grafana-cli plugins update {id}
-
-
- Check out {name} on Grafana.com for README and changelog.
- If you do not have access to the command line, ask your Grafana administator.
-
-
-
-
- Pro tip : To update all plugins at once, type{' '}
- grafana-cli plugins update-all
on the command line.
-
-
-
- );
-}
-
-function getStyles(theme: GrafanaTheme2) {
- return {
- small: css`
- font-size: ${theme.typography.bodySmall.fontSize};
- font-weight: ${theme.typography.bodySmall.fontWeight};
- `,
- weak: css`
- color: ${theme.colors.text.disabled};
- font-size: ${theme.typography.bodySmall.fontSize};
- `,
- logo: css`
- vertical-align: sub;
- margin-right: ${theme.spacing(0.3)};
- width: ${theme.spacing(2)};
- `,
- codeSmall: css`
- white-space: nowrap;
- margin: 0 ${theme.spacing(0.25)};
- padding: ${theme.spacing(0.25)};
- `,
- };
-}
diff --git a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap
deleted file mode 100644
index 34cc4c7334c..00000000000
--- a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap
+++ /dev/null
@@ -1,241 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-
-`;
diff --git a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap
deleted file mode 100644
index 05fcd88c55d..00000000000
--- a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap
+++ /dev/null
@@ -1,114 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-
-
-
-
-
-
-
-
-
- pretty cool plugin 1
-
-
- By Grafana Labs
-
-
-
-
-
-`;
-
-exports[`Render should render has plugin section 1`] = `
-
-
-
-
- panel
-
-
-
-
- Update available!
-
-
-
-
-
-
-
-
-
- pretty cool plugin 1
-
-
- By Grafana Labs
-
-
-
-
-
-`;
diff --git a/public/app/features/plugins/wrappers/AppConfigWrapper.tsx b/public/app/features/plugins/admin/components/AppConfigWrapper.tsx
similarity index 100%
rename from public/app/features/plugins/wrappers/AppConfigWrapper.tsx
rename to public/app/features/plugins/admin/components/AppConfigWrapper.tsx
diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx
new file mode 100644
index 00000000000..aad623da4ab
--- /dev/null
+++ b/public/app/features/plugins/admin/components/InstallControls/InstallControls.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { css } from '@emotion/css';
+
+import { config } from '@grafana/runtime';
+import { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui';
+import { GrafanaTheme2, PluginType } from '@grafana/data';
+
+import { ExternallyManagedButton } from './ExternallyManagedButton';
+import { InstallControlsButton } from './InstallControlsButton';
+import { CatalogPlugin, PluginStatus, Version } from '../../types';
+import { getExternalManageLink, isInstallControlsEnabled } from '../../helpers';
+import { useIsRemotePluginsAvailable } from '../../state/hooks';
+import { isGrafanaAdmin } from '../../permissions';
+
+interface Props {
+ plugin: CatalogPlugin;
+ latestCompatibleVersion?: Version;
+}
+
+export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => {
+ const styles = useStyles2(getStyles);
+ const isExternallyManaged = config.pluginAdminExternalManageEnabled;
+ const hasPermission = isGrafanaAdmin();
+ const isRemotePluginsAvailable = useIsRemotePluginsAvailable();
+ const isCompatible = Boolean(latestCompatibleVersion);
+ const isInstallControlsDisabled = plugin.isCore || plugin.isDisabled || !isInstallControlsEnabled();
+
+ const pluginStatus = plugin.isInstalled
+ ? plugin.hasUpdate
+ ? PluginStatus.UPDATE
+ : PluginStatus.UNINSTALL
+ : PluginStatus.INSTALL;
+
+ if (isInstallControlsDisabled) {
+ return null;
+ }
+
+ if (plugin.type === PluginType.renderer) {
+ return Renderer plugins cannot be managed by the Plugin Catalog.
;
+ }
+
+ if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) {
+ return (
+
+ No valid Grafana Enterprise license detected.
+
+ Learn more
+
+
+ );
+ }
+
+ if (plugin.isDev) {
+ return (
+ This is a development build of the plugin and can't be uninstalled.
+ );
+ }
+
+ if (!hasPermission && !isExternallyManaged) {
+ const message = `You do not have permission to ${pluginStatus} this plugin.`;
+ return {message}
;
+ }
+
+ if (!plugin.isPublished) {
+ return (
+
+ );
+ }
+
+ if (!isCompatible) {
+ return (
+
+
+ This plugin doesn't support your version of Grafana.
+
+ );
+ }
+
+ if (isExternallyManaged) {
+ return ;
+ }
+
+ if (!isRemotePluginsAvailable) {
+ return (
+
+ The install controls have been disabled because the Grafana server cannot access grafana.com.
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ message: css`
+ color: ${theme.colors.text.secondary};
+ `,
+ };
+};
diff --git a/public/app/features/plugins/admin/components/InstallControls/index.tsx b/public/app/features/plugins/admin/components/InstallControls/index.tsx
index a65d1df7850..6d6ba2dc7c9 100644
--- a/public/app/features/plugins/admin/components/InstallControls/index.tsx
+++ b/public/app/features/plugins/admin/components/InstallControls/index.tsx
@@ -1,118 +1 @@
-import React from 'react';
-import { css } from '@emotion/css';
-
-import { config } from '@grafana/runtime';
-import { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui';
-import { GrafanaTheme2, PluginType } from '@grafana/data';
-
-import { ExternallyManagedButton } from './ExternallyManagedButton';
-import { InstallControlsButton } from './InstallControlsButton';
-import { CatalogPlugin, PluginStatus, Version } from '../../types';
-import { getExternalManageLink } from '../../helpers';
-import { useIsRemotePluginsAvailable } from '../../state/hooks';
-import { isGrafanaAdmin } from '../../permissions';
-
-interface Props {
- plugin: CatalogPlugin;
- latestCompatibleVersion?: Version;
-}
-
-export const InstallControls = ({ plugin, latestCompatibleVersion }: Props) => {
- const styles = useStyles2(getStyles);
- const isExternallyManaged = config.pluginAdminExternalManageEnabled;
- const hasPermission = isGrafanaAdmin();
- const isRemotePluginsAvailable = useIsRemotePluginsAvailable();
- const isCompatible = Boolean(latestCompatibleVersion);
-
- const pluginStatus = plugin.isInstalled
- ? plugin.hasUpdate
- ? PluginStatus.UPDATE
- : PluginStatus.UNINSTALL
- : PluginStatus.INSTALL;
-
- if (plugin.isCore || plugin.isDisabled) {
- return null;
- }
-
- if (plugin.type === PluginType.renderer) {
- return Renderer plugins cannot be managed by the Plugin Catalog.
;
- }
-
- if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) {
- return (
-
- No valid Grafana Enterprise license detected.
-
- Learn more
-
-
- );
- }
-
- if (plugin.isDev) {
- return (
- This is a development build of the plugin and can't be uninstalled.
- );
- }
-
- if (!hasPermission && !isExternallyManaged) {
- const message = `You do not have permission to ${pluginStatus} this plugin.`;
- return {message}
;
- }
-
- if (!plugin.isPublished) {
- return (
-
- );
- }
-
- if (!isCompatible) {
- return (
-
-
- This plugin doesn't support your version of Grafana.
-
- );
- }
-
- if (isExternallyManaged) {
- return ;
- }
-
- if (!isRemotePluginsAvailable) {
- return (
-
- The install controls have been disabled because the Grafana server cannot access grafana.com.
-
- );
- }
-
- return (
-
- );
-};
-
-export const getStyles = (theme: GrafanaTheme2) => {
- return {
- message: css`
- color: ${theme.colors.text.secondary};
- `,
- };
-};
+export * from './InstallControls';
diff --git a/public/app/features/plugins/PluginDashboards.tsx b/public/app/features/plugins/admin/components/PluginDashboards.tsx
similarity index 100%
rename from public/app/features/plugins/PluginDashboards.tsx
rename to public/app/features/plugins/admin/components/PluginDashboards.tsx
diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx
index 7c0e7583635..5608e976b42 100644
--- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx
+++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx
@@ -7,8 +7,8 @@ import { useStyles2 } from '@grafana/ui';
import { CatalogPlugin, PluginTabIds } from '../types';
import { VersionList } from '../components/VersionList';
import { usePluginConfig } from '../hooks/usePluginConfig';
-import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
-import { PluginDashboards } from '../../PluginDashboards';
+import { AppConfigCtrlWrapper } from './AppConfigWrapper';
+import { PluginDashboards } from './PluginDashboards';
type Props = {
plugin: CatalogPlugin;
diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts
index db942ab1b81..7c044a2e3b9 100644
--- a/public/app/features/plugins/admin/helpers.ts
+++ b/public/app/features/plugins/admin/helpers.ts
@@ -261,6 +261,8 @@ export function getLatestCompatibleVersion(versions: Version[] | undefined): Ver
return latest;
}
+export const isInstallControlsEnabled = () => config.pluginAdminEnabled;
+
export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
diff --git a/public/app/features/plugins/admin/hooks/usePluginConfig.tsx b/public/app/features/plugins/admin/hooks/usePluginConfig.tsx
index 8f8e5da594c..43a2fe47809 100644
--- a/public/app/features/plugins/admin/hooks/usePluginConfig.tsx
+++ b/public/app/features/plugins/admin/hooks/usePluginConfig.tsx
@@ -1,6 +1,6 @@
import { useAsync } from 'react-use';
import { CatalogPlugin } from '../types';
-import { loadPlugin } from '../../PluginPage';
+import { loadPlugin } from '../../utils';
export const usePluginConfig = (plugin?: CatalogPlugin) => {
return useAsync(async () => {
diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
index 6e6ba894a97..2057a28ca97 100644
--- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
+++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx
@@ -493,6 +493,25 @@ describe('Plugin details page', () => {
expect(rendered.getByText(message)).toBeInTheDocument();
});
+ it('should not display the install / uninstall / update buttons if `pluginAdminEnabled` flag is set to FALSE in the Grafana config', async () => {
+ let rendered: RenderResult;
+
+ // Disable the install controls for the plugins catalog
+ config.pluginAdminEnabled = false;
+
+ // Should not show an "Install" button
+ rendered = renderPluginDetails({ id, isInstalled: false });
+ await waitFor(() => expect(rendered.queryByRole('button', { name: /^install/i })).not.toBeInTheDocument());
+
+ // Should not show an "Uninstall" button
+ rendered = renderPluginDetails({ id, isInstalled: true });
+ await waitFor(() => expect(rendered.queryByRole('button', { name: /^uninstall/i })).not.toBeInTheDocument());
+
+ // Should not show an "Update" button
+ rendered = renderPluginDetails({ id, isInstalled: true, hasUpdate: true });
+ await waitFor(() => expect(rendered.queryByRole('button', { name: /^update/i })).not.toBeInTheDocument());
+ });
+
it('should display a "Create" button as a post installation step for installed data source plugins', async () => {
const name = 'Akumuli';
const { queryByText } = renderPluginDetails({
diff --git a/public/app/features/plugins/admin/routes.ts b/public/app/features/plugins/admin/routes.ts
new file mode 100644
index 00000000000..f2f9f6317ca
--- /dev/null
+++ b/public/app/features/plugins/admin/routes.ts
@@ -0,0 +1,48 @@
+import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
+import { RouteDescriptor } from 'app/core/navigation/types';
+import { isGrafanaAdmin } from './permissions';
+import { PluginAdminRoutes } from './types';
+
+const DEFAULT_ROUTES = [
+ {
+ path: '/plugins',
+ routeName: PluginAdminRoutes.Home,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
+ },
+ {
+ path: '/plugins/browse',
+ routeName: PluginAdminRoutes.Browse,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
+ },
+ {
+ path: '/plugins/:pluginId/',
+ routeName: PluginAdminRoutes.Details,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')),
+ },
+];
+
+const ADMIN_ROUTES = [
+ {
+ path: '/admin/plugins',
+ routeName: PluginAdminRoutes.HomeAdmin,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
+ },
+ {
+ path: '/admin/plugins/browse',
+ routeName: PluginAdminRoutes.BrowseAdmin,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
+ },
+ {
+ path: '/admin/plugins/:pluginId/',
+ routeName: PluginAdminRoutes.DetailsAdmin,
+ component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')),
+ },
+];
+
+export function getRoutes(): RouteDescriptor[] {
+ if (isGrafanaAdmin()) {
+ return [...DEFAULT_ROUTES, ...ADMIN_ROUTES];
+ }
+
+ return DEFAULT_ROUTES;
+}
diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts
index 0f7b6e2f01a..e6eaae31789 100644
--- a/public/app/features/plugins/admin/state/hooks.ts
+++ b/public/app/features/plugins/admin/state/hooks.ts
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
+import { PluginError } from '@grafana/data';
import { setDisplayMode } from './reducer';
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall } from './actions';
import { CatalogPlugin, PluginCatalogStoreState, PluginListDisplayMode } from '../types';
@@ -11,6 +12,7 @@ import {
selectRequestError,
selectIsRequestNotFetched,
selectDisplayMode,
+ selectPluginErrors,
} from './selectors';
import { sortPlugins, Sorters } from '../helpers';
@@ -53,6 +55,12 @@ export const useGetSingle = (id: string): CatalogPlugin | undefined => {
return useSelector((state: PluginCatalogStoreState) => selectById(state, id));
};
+export const useGetErrors = (): PluginError[] => {
+ useFetchAll();
+
+ return useSelector(selectPluginErrors);
+};
+
export const useInstall = () => {
const dispatch = useDispatch();
return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating }));
diff --git a/public/app/features/plugins/admin/state/reducer.ts b/public/app/features/plugins/admin/state/reducer.ts
index 74b6a03c054..565b84973f8 100644
--- a/public/app/features/plugins/admin/state/reducer.ts
+++ b/public/app/features/plugins/admin/state/reducer.ts
@@ -1,4 +1,4 @@
-import { createSlice, createEntityAdapter, AnyAction, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice, createEntityAdapter, Reducer, AnyAction, PayloadAction } from '@reduxjs/toolkit';
import { fetchAll, fetchDetails, install, uninstall, loadPluginDashboards, panelPluginLoaded } from './actions';
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
import { STATE_PREFIX } from '../constants';
@@ -97,4 +97,4 @@ const slice = createSlice({
});
export const { setDisplayMode } = slice.actions;
-export const { reducer } = slice;
+export const reducer: Reducer = slice.reducer;
diff --git a/public/app/features/plugins/admin/state/selectors.ts b/public/app/features/plugins/admin/state/selectors.ts
index 8e2fc476829..a0dae21aba9 100644
--- a/public/app/features/plugins/admin/state/selectors.ts
+++ b/public/app/features/plugins/admin/state/selectors.ts
@@ -1,4 +1,5 @@
-import { createSelector } from 'reselect';
+import { createSelector } from '@reduxjs/toolkit';
+import { PluginError, PluginErrorCode } from '@grafana/data';
import { RequestStatus, PluginCatalogStoreState } from '../types';
import { pluginsAdapter } from './reducer';
@@ -49,6 +50,20 @@ export const find = (searchBy: string, filterBy: string, filterByType: string) =
}
);
+export const selectPluginErrors = createSelector(selectAll, (plugins) =>
+ plugins
+ ? plugins
+ .filter((p) => Boolean(p.error))
+ .map(
+ (p): PluginError => ({
+ pluginId: p.id,
+ errorCode: p!.error as PluginErrorCode,
+ })
+ )
+ : []
+);
+
+// The following selectors are used to get information about the outstanding or completed plugins-related network requests.
export const selectRequest = (actionType: string) =>
createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);
diff --git a/public/app/features/plugins/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx
similarity index 93%
rename from public/app/features/plugins/AppRootPage.test.tsx
rename to public/app/features/plugins/components/AppRootPage.test.tsx
index b0cc93fa087..cf6609f3c3d 100644
--- a/public/app/features/plugins/AppRootPage.test.tsx
+++ b/public/app/features/plugins/components/AppRootPage.test.tsx
@@ -1,19 +1,19 @@
import { act, render, screen } from '@testing-library/react';
import React, { Component } from 'react';
import AppRootPage from './AppRootPage';
-import { getPluginSettings } from './PluginSettingsCache';
-import { importAppPlugin } from './plugin_loader';
-import { getMockPlugin } from './__mocks__/pluginMocks';
+import { getPluginSettings } from '../pluginSettings';
+import { importAppPlugin } from '../plugin_loader';
+import { getMockPlugin } from '../__mocks__/pluginMocks';
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
import { Route, Router } from 'react-router-dom';
import { locationService, setEchoSrv } from '@grafana/runtime';
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
import { Echo } from 'app/core/services/echo/Echo';
-jest.mock('./PluginSettingsCache', () => ({
+jest.mock('../pluginSettings', () => ({
getPluginSettings: jest.fn(),
}));
-jest.mock('./plugin_loader', () => ({
+jest.mock('../plugin_loader', () => ({
importAppPlugin: jest.fn(),
}));
diff --git a/public/app/features/plugins/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx
similarity index 96%
rename from public/app/features/plugins/AppRootPage.tsx
rename to public/app/features/plugins/components/AppRootPage.tsx
index 5d39c6e4eb6..faa63014b8f 100644
--- a/public/app/features/plugins/AppRootPage.tsx
+++ b/public/app/features/plugins/components/AppRootPage.tsx
@@ -4,8 +4,8 @@ import { AppEvents, AppPlugin, AppPluginMeta, KeyValue, NavModel, PluginType } f
import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react-reverse-portal';
import Page from 'app/core/components/Page/Page';
-import { getPluginSettings } from './PluginSettingsCache';
-import { importAppPlugin } from './plugin_loader';
+import { getPluginSettings } from '../pluginSettings';
+import { importAppPlugin } from '../plugin_loader';
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
import { appEvents } from 'app/core/core';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
diff --git a/public/app/features/plugins/PluginStateInfo.tsx b/public/app/features/plugins/components/PluginStateInfo.tsx
similarity index 100%
rename from public/app/features/plugins/PluginStateInfo.tsx
rename to public/app/features/plugins/components/PluginStateInfo.tsx
diff --git a/public/app/features/plugins/PluginsErrorsInfo.tsx b/public/app/features/plugins/components/PluginsErrorsInfo.tsx
similarity index 61%
rename from public/app/features/plugins/PluginsErrorsInfo.tsx
rename to public/app/features/plugins/components/PluginsErrorsInfo.tsx
index bbaaffce885..6be74a424e1 100644
--- a/public/app/features/plugins/PluginsErrorsInfo.tsx
+++ b/public/app/features/plugins/components/PluginsErrorsInfo.tsx
@@ -1,42 +1,19 @@
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
-import { StoreState } from '../../types';
-import { getAllPluginsErrors } from './state/selectors';
-import { loadPlugins, loadPluginsErrors } from './state/actions';
-import useAsync from 'react-use/lib/useAsync';
-import { connect, ConnectedProps } from 'react-redux';
+import { useGetErrors, useFetchStatus } from '../admin/state/hooks';
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
import { css } from '@emotion/css';
-const mapStateToProps = (state: StoreState) => ({
- errors: getAllPluginsErrors(state.plugins),
-});
-
-const mapDispatchToProps = {
- loadPluginsErrors,
-};
-
-interface OwnProps {
- children?: React.ReactNode;
-}
-const connector = connect(mapStateToProps, mapDispatchToProps);
-type PluginsErrorsInfoProps = ConnectedProps & OwnProps;
-
-export const PluginsErrorsInfoUnconnected: React.FC = ({
- loadPluginsErrors,
- errors,
- children,
-}) => {
+export function PluginsErrorsInfo(): React.ReactElement | null {
+ const errors = useGetErrors();
+ const { isLoading } = useFetchStatus();
const theme = useTheme();
- const { loading } = useAsync(async () => {
- await loadPluginsErrors();
- }, [loadPlugins]);
-
- if (loading || errors.length === 0) {
+ if (isLoading || errors.length === 0) {
return null;
}
+
return (
= ({
className={css`
list-style-type: circle;
`}
- renderItem={(e) => (
+ renderItem={(error) => (
- {e.pluginId}
+ {error.pluginId}
= ({
)}
/>
- {children}
);
-};
-
-export const PluginsErrorsInfo = connect(mapStateToProps, mapDispatchToProps)(PluginsErrorsInfoUnconnected);
+}
function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
switch (code) {
diff --git a/public/app/features/plugins/PluginSettingsCache.ts b/public/app/features/plugins/pluginSettings.ts
similarity index 100%
rename from public/app/features/plugins/PluginSettingsCache.ts
rename to public/app/features/plugins/pluginSettings.ts
diff --git a/public/app/features/plugins/routes.ts b/public/app/features/plugins/routes.ts
deleted file mode 100644
index 1bd5bfdecb6..00000000000
--- a/public/app/features/plugins/routes.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
-import { config } from 'app/core/config';
-import { RouteDescriptor } from 'app/core/navigation/types';
-import { isGrafanaAdmin } from './admin/permissions';
-import { PluginAdminRoutes } from './admin/types';
-
-const pluginAdminRoutes = [
- {
- path: '/plugins',
- routeName: PluginAdminRoutes.Home,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')),
- },
- {
- path: '/plugins/browse',
- routeName: PluginAdminRoutes.Browse,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')),
- },
- {
- path: '/plugins/:pluginId/',
- routeName: PluginAdminRoutes.Details,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './admin/pages/PluginDetails')),
- },
-];
-
-export function getPluginsAdminRoutes(cfg = config): RouteDescriptor[] {
- if (!cfg.pluginAdminEnabled) {
- return [
- {
- path: '/plugins',
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './PluginListPage')),
- },
- {
- path: '/plugins/browse',
- component: SafeDynamicImport(
- () => import(/* webpackChunkName: "PluginAdminNotEnabled" */ './admin/pages/NotEnabed')
- ),
- },
- {
- path: '/plugins/:pluginId/',
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './PluginPage')),
- },
- ];
- }
-
- if (isGrafanaAdmin()) {
- return [
- ...pluginAdminRoutes,
- {
- path: '/admin/plugins',
- routeName: PluginAdminRoutes.HomeAdmin,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')),
- },
- {
- path: '/admin/plugins/browse',
- routeName: PluginAdminRoutes.BrowseAdmin,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './admin/pages/Browse')),
- },
- {
- path: '/admin/plugins/:pluginId/',
- routeName: PluginAdminRoutes.DetailsAdmin,
- component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './admin/pages/PluginDetails')),
- },
- ];
- }
-
- return pluginAdminRoutes;
-}
diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts
deleted file mode 100644
index dcabcc58b4b..00000000000
--- a/public/app/features/plugins/state/actions.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { getBackendSrv } from '@grafana/runtime';
-import { PanelPlugin } from '@grafana/data';
-import { ThunkResult } from 'app/types';
-import { config } from 'app/core/config';
-import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
-import {
- loadPanelPlugin as loadPanelPluginNew,
- loadPluginDashboards as loadPluginDashboardsNew,
-} from '../admin/state/actions';
-import {
- pluginDashboardsLoad,
- pluginDashboardsLoaded,
- pluginsLoaded,
- panelPluginLoaded,
- pluginsErrorsLoaded,
-} from './reducers';
-
-export function loadPlugins(): ThunkResult {
- return async (dispatch) => {
- const plugins = await getBackendSrv().get('api/plugins', { embedded: 0 });
- dispatch(pluginsLoaded(plugins));
- };
-}
-
-export function loadPluginsErrors(): ThunkResult {
- return async (dispatch) => {
- const errors = await getBackendSrv().get('api/plugins/errors');
- dispatch(pluginsErrorsLoaded(errors));
- };
-}
-
-function loadPluginDashboardsOriginal(): ThunkResult {
- return async (dispatch, getStore) => {
- dispatch(pluginDashboardsLoad());
- const dataSourceType = getStore().dataSources.dataSource.type;
- const response = await getBackendSrv().get(`api/plugins/${dataSourceType}/dashboards`);
- dispatch(pluginDashboardsLoaded(response));
- };
-}
-
-function loadPanelPluginOriginal(pluginId: string): ThunkResult> {
- return async (dispatch, getStore) => {
- let plugin = getStore().plugins.panels[pluginId];
-
- if (!plugin) {
- plugin = await importPanelPlugin(pluginId);
-
- // second check to protect against raise condition
- if (!getStore().plugins.panels[pluginId]) {
- dispatch(panelPluginLoaded(plugin));
- }
- }
-
- return plugin;
- };
-}
-
-export const loadPluginDashboards = config.pluginAdminEnabled ? loadPluginDashboardsNew : loadPluginDashboardsOriginal;
-export const loadPanelPlugin = config.pluginAdminEnabled ? loadPanelPluginNew : loadPanelPluginOriginal;
diff --git a/public/app/features/plugins/state/reducers.test.ts b/public/app/features/plugins/state/reducers.test.ts
deleted file mode 100644
index 1ef517f7433..00000000000
--- a/public/app/features/plugins/state/reducers.test.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { Reducer, AnyAction } from '@reduxjs/toolkit';
-import { reducerTester } from '../../../../test/core/redux/reducerTester';
-import { PluginsState } from '../../../types';
-import {
- initialState,
- pluginDashboardsLoad,
- pluginDashboardsLoaded,
- pluginsLoaded,
- pluginsReducer,
- setPluginsSearchQuery,
-} from './reducers';
-import { PluginMetaInfo, PluginType } from '@grafana/data';
-
-// Mock the config to enable the old version of the plugins page
-jest.mock('@grafana/runtime', () => {
- const original = jest.requireActual('@grafana/runtime');
- const mockedRuntime = { ...original };
-
- mockedRuntime.config.pluginAdminEnabled = false;
-
- return mockedRuntime;
-});
-
-describe('pluginsReducer', () => {
- describe('when pluginsLoaded is dispatched', () => {
- it('then state should be correct', () => {
- reducerTester()
- .givenReducer(pluginsReducer as Reducer, { ...initialState })
- .whenActionIsDispatched(
- pluginsLoaded([
- {
- id: 'some-id',
- baseUrl: 'some-url',
- module: 'some module',
- name: 'Some Plugin',
- type: PluginType.app,
- info: {} as PluginMetaInfo,
- },
- ])
- )
- .thenStateShouldEqual({
- ...initialState,
- hasFetched: true,
- plugins: [
- {
- baseUrl: 'some-url',
- id: 'some-id',
- info: {} as PluginMetaInfo,
- module: 'some module',
- name: 'Some Plugin',
- type: PluginType.app,
- },
- ],
- errors: [],
- });
- });
- });
-
- describe('when setPluginsSearchQuery is dispatched', () => {
- it('then state should be correct', () => {
- reducerTester()
- .givenReducer(pluginsReducer as Reducer, { ...initialState })
- .whenActionIsDispatched(setPluginsSearchQuery('A query'))
- .thenStateShouldEqual({
- ...initialState,
- searchQuery: 'A query',
- });
- });
- });
-
- describe('when pluginDashboardsLoad is dispatched', () => {
- it('then state should be correct', () => {
- reducerTester()
- .givenReducer(pluginsReducer as Reducer, {
- ...initialState,
- dashboards: [
- {
- dashboardId: 1,
- title: 'Some Dash',
- description: 'Some Desc',
- folderId: 2,
- imported: false,
- importedRevision: 1,
- importedUri: 'some-uri',
- importedUrl: 'some-url',
- path: 'some/path',
- pluginId: 'some-plugin-id',
- removed: false,
- revision: 22,
- slug: 'someSlug',
- },
- ],
- })
- .whenActionIsDispatched(pluginDashboardsLoad())
- .thenStateShouldEqual({
- ...initialState,
- dashboards: [],
- isLoadingPluginDashboards: true,
- });
- });
- });
-
- describe('when pluginDashboardsLoad is dispatched', () => {
- it('then state should be correct', () => {
- reducerTester()
- .givenReducer(pluginsReducer as Reducer, {
- ...initialState,
- isLoadingPluginDashboards: true,
- })
- .whenActionIsDispatched(
- pluginDashboardsLoaded([
- {
- dashboardId: 1,
- title: 'Some Dash',
- description: 'Some Desc',
- folderId: 2,
- imported: false,
- importedRevision: 1,
- importedUri: 'some-uri',
- importedUrl: 'some-url',
- path: 'some/path',
- pluginId: 'some-plugin-id',
- removed: false,
- revision: 22,
- slug: 'someSlug',
- },
- ])
- )
- .thenStateShouldEqual({
- ...initialState,
- dashboards: [
- {
- dashboardId: 1,
- title: 'Some Dash',
- description: 'Some Desc',
- folderId: 2,
- imported: false,
- importedRevision: 1,
- importedUri: 'some-uri',
- importedUrl: 'some-url',
- path: 'some/path',
- pluginId: 'some-plugin-id',
- removed: false,
- revision: 22,
- slug: 'someSlug',
- },
- ],
- isLoadingPluginDashboards: false,
- });
- });
- });
-});
diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts
deleted file mode 100644
index 7b366ca208b..00000000000
--- a/public/app/features/plugins/state/reducers.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { AnyAction, createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit';
-import { PluginMeta, PanelPlugin, PluginError } from '@grafana/data';
-import { PluginsState } from 'app/types';
-import { config } from 'app/core/config';
-import { reducer as pluginCatalogReducer } from '../admin/state/reducer';
-import { PluginDashboard } from '../../../types/plugins';
-
-export const initialState: PluginsState = {
- plugins: [],
- errors: [],
- searchQuery: '',
- hasFetched: false,
- dashboards: [],
- isLoadingPluginDashboards: false,
- panels: {},
-};
-
-const pluginsSlice = createSlice({
- name: 'plugins',
- initialState,
- reducers: {
- pluginsLoaded: (state, action: PayloadAction) => {
- state.hasFetched = true;
- state.plugins = action.payload;
- },
- pluginsErrorsLoaded: (state, action: PayloadAction) => {
- state.errors = action.payload;
- },
- setPluginsSearchQuery: (state, action: PayloadAction) => {
- state.searchQuery = action.payload;
- },
- pluginDashboardsLoad: (state, action: PayloadAction) => {
- state.isLoadingPluginDashboards = true;
- state.dashboards = [];
- },
- pluginDashboardsLoaded: (state, action: PayloadAction) => {
- state.isLoadingPluginDashboards = false;
- state.dashboards = action.payload;
- },
- panelPluginLoaded: (state, action: PayloadAction) => {
- state.panels[action.payload.meta!.id] = action.payload;
- },
- },
-});
-
-export const {
- pluginsLoaded,
- pluginsErrorsLoaded,
- pluginDashboardsLoad,
- pluginDashboardsLoaded,
- setPluginsSearchQuery,
- panelPluginLoaded,
-} = pluginsSlice.actions;
-
-export const pluginsReducer: Reducer = config.pluginAdminEnabled
- ? ((pluginCatalogReducer as unknown) as Reducer)
- : pluginsSlice.reducer;
-
-export default {
- plugins: pluginsReducer,
-};
diff --git a/public/app/features/plugins/state/selectors.test.ts b/public/app/features/plugins/state/selectors.test.ts
deleted file mode 100644
index 4f2f77efe8a..00000000000
--- a/public/app/features/plugins/state/selectors.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { getPlugins, getPluginsSearchQuery } from './selectors';
-import { initialState } from './reducers';
-import { getMockPlugins } from '../__mocks__/pluginMocks';
-
-describe('Selectors', () => {
- const mockState = { ...initialState };
-
- it('should return search query', () => {
- mockState.searchQuery = 'test';
- const query = getPluginsSearchQuery(mockState);
-
- expect(query).toEqual(mockState.searchQuery);
- });
-
- it('should return plugins', () => {
- mockState.plugins = getMockPlugins(5);
- mockState.searchQuery = '';
-
- const plugins = getPlugins(mockState);
-
- expect(plugins).toEqual(mockState.plugins);
- });
-
- it('should filter plugins', () => {
- mockState.searchQuery = 'plugin-1';
-
- const plugins = getPlugins(mockState);
-
- expect(plugins.length).toEqual(1);
- });
-});
diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts
deleted file mode 100644
index 774844ba723..00000000000
--- a/public/app/features/plugins/state/selectors.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { PluginsState } from 'app/types/plugins';
-
-export const getPlugins = (state: PluginsState) => {
- const regex = new RegExp(state.searchQuery, 'i');
-
- return state.plugins.filter((item) => {
- return (
- regex.test(item.name) ||
- regex.test(item.info.author.name) ||
- regex.test(item.type) ||
- regex.test(item.info.description)
- );
- });
-};
-export const getAllPluginsErrors = (state: PluginsState) => {
- return state.errors;
-};
-
-export const getPluginsSearchQuery = (state: PluginsState) => state.searchQuery;
diff --git a/public/app/features/plugins/datasource_srv.test.ts b/public/app/features/plugins/tests/datasource_srv.test.ts
similarity index 99%
rename from public/app/features/plugins/datasource_srv.test.ts
rename to public/app/features/plugins/tests/datasource_srv.test.ts
index a87d5a16891..a471af2b66e 100644
--- a/public/app/features/plugins/datasource_srv.test.ts
+++ b/public/app/features/plugins/tests/datasource_srv.test.ts
@@ -30,7 +30,7 @@ class TestDataSource {
constructor(public instanceSettings: DataSourceInstanceSettings) {}
}
-jest.mock('./plugin_loader', () => ({
+jest.mock('../plugin_loader', () => ({
importDataSourcePlugin: (meta: DataSourcePluginMeta) => {
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
},
diff --git a/public/app/features/plugins/pluginCacheBuster.test.ts b/public/app/features/plugins/tests/pluginCacheBuster.test.ts
similarity index 97%
rename from public/app/features/plugins/pluginCacheBuster.test.ts
rename to public/app/features/plugins/tests/pluginCacheBuster.test.ts
index 131d5982809..7954cef0278 100644
--- a/public/app/features/plugins/pluginCacheBuster.test.ts
+++ b/public/app/features/plugins/tests/pluginCacheBuster.test.ts
@@ -1,4 +1,4 @@
-import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster';
+import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from '../pluginCacheBuster';
describe('PluginCacheBuster', () => {
const now = 12345;
diff --git a/public/app/features/plugins/plugin_loader.test.ts b/public/app/features/plugins/tests/plugin_loader.test.ts
similarity index 96%
rename from public/app/features/plugins/plugin_loader.test.ts
rename to public/app/features/plugins/tests/plugin_loader.test.ts
index 2d365233710..2ddc26bd51f 100644
--- a/public/app/features/plugins/plugin_loader.test.ts
+++ b/public/app/features/plugins/tests/plugin_loader.test.ts
@@ -17,7 +17,7 @@ import { SystemJS } from '@grafana/runtime';
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
// Loaded after the `unmock` abve
-import { importAppPlugin } from './plugin_loader';
+import { importAppPlugin } from '../plugin_loader';
class MyCustomApp extends AppPlugin {
initWasCalled = false;
diff --git a/public/app/features/plugins/utils.ts b/public/app/features/plugins/utils.ts
new file mode 100644
index 00000000000..b043b2ca45f
--- /dev/null
+++ b/public/app/features/plugins/utils.ts
@@ -0,0 +1,29 @@
+import { GrafanaPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
+import { getPluginSettings } from './pluginSettings';
+import { importAppPlugin, importDataSourcePlugin } from './plugin_loader';
+import { importPanelPluginFromMeta } from './importPanelPlugin';
+
+export async function loadPlugin(pluginId: string): Promise {
+ const info = await getPluginSettings(pluginId);
+ let result: GrafanaPlugin | undefined;
+
+ if (info.type === PluginType.app) {
+ result = await importAppPlugin(info);
+ }
+ if (info.type === PluginType.datasource) {
+ result = await importDataSourcePlugin(info);
+ }
+ if (info.type === PluginType.panel) {
+ const panelPlugin = await importPanelPluginFromMeta(info as PanelPluginMeta);
+ result = (panelPlugin as unknown) as GrafanaPlugin;
+ }
+ if (info.type === PluginType.renderer) {
+ result = { meta: info } as GrafanaPlugin;
+ }
+
+ if (!result) {
+ throw new Error('Unknown Plugin type: ' + info.type);
+ }
+
+ return result;
+}
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index d1c2c995907..4cae56ffb23 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -8,7 +8,7 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic
import { RouteDescriptor } from '../core/navigation/types';
import { Redirect } from 'react-router-dom';
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
-import { getPluginsAdminRoutes } from 'app/features/plugins/routes';
+import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
import { contextSrv } from 'app/core/services/context_srv';
import { getLiveRoutes } from 'app/features/live/pages/routes';
import { getAlertingRoutes } from 'app/features/alerting/routes';
@@ -150,7 +150,7 @@ export function getAppRoutes(): RouteDescriptor[] {
exact: false,
// Someday * and will get a ReactRouter under that path!
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/AppRootPage')
+ () => import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/components/AppRootPage')
),
},
{
@@ -372,7 +372,7 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
),
},
- ...getPluginsAdminRoutes(),
+ ...getPluginCatalogRoutes(),
...getLiveRoutes(),
...getAlertingRoutes(),
...extraRoutes,