diff --git a/conf/defaults.ini b/conf/defaults.ini index 2a9c48bb52c..2224e9c8fcb 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -971,7 +971,7 @@ enable_alpha = false app_tls_skip_verify_insecure = false # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. allow_loading_unsigned_plugins = -# Enable or disable installing plugins directly from within Grafana. +# Enable or disable installing / uninstalling / updating plugins directly from within Grafana. plugin_admin_enabled = true plugin_admin_external_manage_enabled = false plugin_catalog_url = https://grafana.com/grafana/plugins/ diff --git a/conf/sample.ini b/conf/sample.ini index 90e689b8dfa..95928af34a4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -944,7 +944,7 @@ ;app_tls_skip_verify_insecure = false # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. ;allow_loading_unsigned_plugins = -# Enable or disable installing plugins directly from within Grafana. +# Enable or disable installing / uninstalling / updating plugins directly from within Grafana. ;plugin_admin_enabled = false ;plugin_admin_external_manage_enabled = false ;plugin_catalog_url = https://grafana.com/grafana/plugins/ diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 7a27397ebda..559f88e6638 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -1582,7 +1582,7 @@ We do _not_ recommend using this option. For more information, refer to [Plugin ### plugin_admin_enabled -Available to Grafana administrators only, the plugin admin app is set to `true` by default. Set it to `false` to disable the app. +Available to Grafana administrators only, enables installing / uninstalling / updating plugins directly from the Grafana UI. Set to `true` by default. Setting it to `false` will hide the install / uninstall / update controls. For more information, refer to [Plugin catalog]({{< relref "../plugins/catalog.md" >}}). diff --git a/docs/sources/plugins/catalog.md b/docs/sources/plugins/catalog.md index edfe3a1e298..f73b5e49017 100644 --- a/docs/sources/plugins/catalog.md +++ b/docs/sources/plugins/catalog.md @@ -23,7 +23,7 @@ The Plugin catalog allows you to browse and manage plugins from within Grafana. -Before you can use the Plugin catalog, you must enable it in the Grafana [configuration]({{< relref "../administration/configuration.md#plugin_admin_enabled" >}}) file. +In order to be able to install / uninstall / update plugins using plugin catalog, you must enable it via the `plugin_admin_enabled` flag in the [configuration]({{< relref "../administration/configuration.md#plugin_admin_enabled" >}}) file. Before following the steps below, make sure you are logged in as a Grafana administrator. diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index c0799e93ff8..c33e2819273 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -7,7 +7,7 @@ import apiKeysReducers from 'app/features/api-keys/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; import exploreReducers from 'app/features/explore/state/main'; -import pluginReducers from 'app/features/plugins/state/reducers'; +import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; import userReducers from 'app/features/profile/state/reducers'; @@ -26,7 +26,6 @@ const rootReducers = { ...foldersReducers, ...dashboardReducers, ...exploreReducers, - ...pluginReducers, ...dataSourcesReducers, ...usersReducers, ...userReducers, @@ -36,6 +35,7 @@ const rootReducers = { ...importDashboardReducers, ...panelEditorReducers, ...panelsReducers, + plugins: pluginsReducer, }; const addedReducers = {}; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index 2337d7cd75e..c2d82b330d7 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -9,7 +9,7 @@ import { } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction'; import { TransformationsEditorTransformation } from './types'; -import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo'; +import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; import { useToggle } from 'react-use'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 40880a97b1b..b88c5f9aa3e 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -33,7 +33,7 @@ import { TransformationsEditorTransformation } from './types'; import { PanelNotSupported } from '../PanelEditor/PanelNotSupported'; import { AppNotificationSeverity } from '../../../../types'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; -import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo'; +import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index e166fd37785..5c3c6021252 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -2,7 +2,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { createSuccessNotification } from 'app/core/copy/appNotification'; // Actions -import { loadPluginDashboards } from '../../plugins/state/actions'; +import { loadPluginDashboards } from '../../plugins/admin/state/actions'; import { cleanUpDashboard, loadDashboardPermissions } from './reducers'; import { notifyApp } from 'app/core/actions'; import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers'; diff --git a/public/app/features/datasources/DataSourceDashboards.tsx b/public/app/features/datasources/DataSourceDashboards.tsx index 738b663929e..425402ba3b2 100644 --- a/public/app/features/datasources/DataSourceDashboards.tsx +++ b/public/app/features/datasources/DataSourceDashboards.tsx @@ -9,7 +9,7 @@ import DashboardTable from './DashboardsTable'; // Actions & Selectors import { getNavModel } from 'app/core/selectors/navModel'; import { loadDataSource } from './state/actions'; -import { loadPluginDashboards } from '../plugins/state/actions'; +import { loadPluginDashboards } from '../plugins/admin/state/actions'; import { importDashboard, removeDashboard } from '../dashboard/state/actions'; import { getDataSource } from './state/selectors'; diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index 2b4e699430c..f07336c124b 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -10,7 +10,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions'; import { getDataSourcePlugins } from './state/selectors'; import { setDataSourceTypeSearchQuery } from './state/reducers'; import { Card } from 'app/core/components/Card/Card'; -import { PluginsErrorsInfo } from '../plugins/PluginsErrorsInfo'; +import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo'; function mapStateToProps(state: StoreState) { return { diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 14aaed49903..134a09f7598 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -24,7 +24,7 @@ import { StoreState, AccessControlAction } from 'app/types/'; import { DataSourceSettings, urlUtil } from '@grafana/data'; import { Alert, Button } from '@grafana/ui'; import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel'; -import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo'; +import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers'; import { selectors } from '@grafana/e2e-selectors'; import { CloudInfoBox } from './CloudInfoBox'; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 3899e244d0a..4f058064196 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -5,7 +5,7 @@ import { updateNavIndex } from 'app/core/actions'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; -import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache'; +import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types'; import config from '../../../core/config'; diff --git a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx index c672bce9087..fb4b4aa9f81 100644 --- a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx @@ -3,7 +3,7 @@ import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui'; import { css, cx } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; -import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo'; +import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; interface Props { isCurrent: boolean; diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index 4935cfbc1a5..12b58f90903 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -1,6 +1,6 @@ import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { loadPanelPlugin } from 'app/features/plugins/state/actions'; +import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions'; import { ThunkResult } from 'app/types'; import { panelModelAndPluginReady } from './reducers'; import { LibraryElementDTO } from 'app/features/library-panels/types'; diff --git a/public/app/features/plugins/PluginList.test.tsx b/public/app/features/plugins/PluginList.test.tsx deleted file mode 100644 index 2f145706cd7..00000000000 --- a/public/app/features/plugins/PluginList.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import PluginList from './PluginList'; -import { getMockPlugins } from './__mocks__/pluginMocks'; -import { LayoutModes } from '@grafana/data'; - -const setup = (propOverrides?: object) => { - const props = Object.assign( - { - plugins: getMockPlugins(5), - layoutMode: LayoutModes.Grid, - }, - propOverrides - ); - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx deleted file mode 100644 index 3a155481c2f..00000000000 --- a/public/app/features/plugins/PluginList.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { FC } from 'react'; -import PluginListItem from './PluginListItem'; -import { PluginMeta } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; - -interface Props { - plugins: PluginMeta[]; -} - -const PluginList: FC = (props) => { - const { plugins } = props; - - return ( -
-
    - {plugins.map((plugin, index) => { - return ; - })} -
-
- ); -}; - -export default PluginList; diff --git a/public/app/features/plugins/PluginListItem.test.tsx b/public/app/features/plugins/PluginListItem.test.tsx deleted file mode 100644 index 175911c5e05..00000000000 --- a/public/app/features/plugins/PluginListItem.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import PluginListItem from './PluginListItem'; -import { getMockPlugin } from './__mocks__/pluginMocks'; - -const setup = (propOverrides?: object) => { - const props = Object.assign( - { - plugin: getMockPlugin(), - }, - propOverrides - ); - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render has plugin section', () => { - const mockPlugin = getMockPlugin(); - mockPlugin.hasUpdate = true; - const wrapper = setup({ - plugin: mockPlugin, - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx deleted file mode 100644 index c0d92d30d96..00000000000 --- a/public/app/features/plugins/PluginListItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { FC } from 'react'; -import { PluginMeta } from '@grafana/data'; -import { PluginSignatureBadge } from '@grafana/ui'; -import { selectors } from '@grafana/e2e-selectors'; - -interface Props { - plugin: PluginMeta; -} - -const PluginListItem: FC = (props) => { - const { plugin } = props; - - return ( -
  • - -
    -
    {plugin.type}
    -
    - -
    - {plugin.hasUpdate && ( -
    - Update available! -
    - )} -
    -
    -
    - -
    -
    -
    {plugin.name}
    -
    {`By ${plugin.info.author.name}`}
    -
    -
    -
    -
  • - ); -}; - -export default PluginListItem; diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx deleted file mode 100644 index 03037cfa55c..00000000000 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { PluginListPage, Props } from './PluginListPage'; -import { NavModel, PluginErrorCode, PluginMeta } from '@grafana/data'; -import { mockToolkitActionCreator } from 'test/core/redux/mocks'; -import { setPluginsSearchQuery } from './state/reducers'; -import { render, screen, waitFor } from '@testing-library/react'; -import { selectors } from '@grafana/e2e-selectors'; -import { Provider } from 'react-redux'; -import { configureStore } from '../../store/configureStore'; -import { afterEach } from '../../../test/lib/common'; - -let errorsReturnMock: any = []; - -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - const mockedRuntime = { - ...original, - getBackendSrv: () => ({ - get: () => { - return errorsReturnMock as any; - }, - }), - }; - - mockedRuntime.config.pluginAdminEnabled = false; - - return mockedRuntime; -}); - -const setup = (propOverrides?: object) => { - const store = configureStore(); - const props: Props = { - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Plugins', - }, - } as NavModel, - plugins: [] as PluginMeta[], - searchQuery: '', - setPluginsSearchQuery: mockToolkitActionCreator(setPluginsSearchQuery), - loadPlugins: jest.fn(), - hasFetched: false, - }; - - Object.assign(props, propOverrides); - - return render( - - - - ); -}; - -describe('Render', () => { - afterEach(() => { - errorsReturnMock = []; - }); - - it('should render component', async () => { - errorsReturnMock = []; - setup(); - await waitFor(() => { - expect(screen.queryByLabelText(selectors.pages.PluginsList.page)).toBeInTheDocument(); - expect(screen.queryByLabelText(selectors.pages.PluginsList.list)).not.toBeInTheDocument(); - }); - }); - - it('should render list', async () => { - errorsReturnMock = []; - setup({ - hasFetched: true, - }); - await waitFor(() => { - expect(screen.queryByLabelText(selectors.pages.PluginsList.list)).toBeInTheDocument(); - }); - }); - - describe('Plugin signature errors', () => { - it('should render notice if there are plugins with signing errors', async () => { - errorsReturnMock = [{ pluginId: 'invalid-sig', errorCode: PluginErrorCode.invalidSignature }]; - setup({ - hasFetched: true, - }); - - await waitFor(() => - expect(screen.getByLabelText(selectors.pages.PluginsList.signatureErrorNotice)).toBeInTheDocument() - ); - }); - }); -}); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx deleted file mode 100644 index 885011ac436..00000000000 --- a/public/app/features/plugins/PluginListPage.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import Page from 'app/core/components/Page/Page'; -import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; -import PluginList from './PluginList'; -import { loadPlugins } from './state/actions'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { getPlugins, getPluginsSearchQuery } from './state/selectors'; -import { StoreState } from 'app/types'; -import { setPluginsSearchQuery } from './state/reducers'; -import { useAsync } from 'react-use'; -import { selectors } from '@grafana/e2e-selectors'; -import { PluginsErrorsInfo } from './PluginsErrorsInfo'; - -const mapStateToProps = (state: StoreState) => ({ - navModel: getNavModel(state.navIndex, 'plugins'), - plugins: getPlugins(state.plugins), - searchQuery: getPluginsSearchQuery(state.plugins), - hasFetched: state.plugins.hasFetched, -}); - -const mapDispatchToProps = { - loadPlugins, - setPluginsSearchQuery, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; - -export const PluginListPage: React.FC = ({ - hasFetched, - navModel, - plugins, - setPluginsSearchQuery, - searchQuery, - loadPlugins, -}) => { - useAsync(async () => { - loadPlugins(); - }, [loadPlugins]); - - let actionTarget: string | undefined = '_blank'; - const linkButton = { - href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', - title: 'Find more plugins on Grafana.com', - }; - - return ( - - - <> - setPluginsSearchQuery(query)} - linkButton={linkButton} - placeholder="Search by name, author, description or type" - target={actionTarget} - /> - - {hasFetched && plugins && } - - - - ); -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PluginListPage); diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx deleted file mode 100644 index a611ab8a72a..00000000000 --- a/public/app/features/plugins/PluginPage.tsx +++ /dev/null @@ -1,585 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; -import { capitalize, find } from 'lodash'; -// Types -import { - AppPlugin, - GrafanaPlugin, - GrafanaTheme2, - NavModel, - NavModelItem, - PanelPluginMeta, - PluginDependencies, - PluginInclude, - PluginIncludeType, - PluginMeta, - PluginMetaInfo, - PluginSignatureStatus, - PluginSignatureType, - PluginType, - UrlQueryMap, -} from '@grafana/data'; -import { AppNotificationSeverity } from 'app/types'; -import { Alert, Badge, Icon, LinkButton, PluginSignatureBadge, Tooltip, useStyles2 } from '@grafana/ui'; - -import Page from 'app/core/components/Page/Page'; -import { getPluginSettings } from './PluginSettingsCache'; -import { importAppPlugin, importDataSourcePlugin } from './plugin_loader'; -import { importPanelPluginFromMeta } from './importPanelPlugin'; -import { getNotFoundNav } from 'app/angular/services/nav_model_srv'; -import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; -import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper'; -import { PluginDashboards } from './PluginDashboards'; -import { appEvents } from 'app/core/core'; -import { config } from 'app/core/config'; -import { contextSrv } from '../../core/services/context_srv'; -import { css } from '@emotion/css'; -import { selectors } from '@grafana/e2e-selectors'; -import { ShowModalReactEvent } from 'app/types/events'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { UpdatePluginModal } from './UpdatePluginModal'; - -interface Props extends GrafanaRouteComponentProps<{ pluginId: string }, UrlQueryMap> {} - -interface State { - loading: boolean; - plugin?: GrafanaPlugin; - nav: NavModel; - defaultPage: string; // The first configured one or readme -} - -const PAGE_ID_README = 'readme'; -const PAGE_ID_DASHBOARDS = 'dashboards'; -const PAGE_ID_CONFIG_CTRL = 'config'; - -class PluginPage extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { - loading: true, - nav: getLoadingNav(), - defaultPage: PAGE_ID_README, - }; - } - - async componentDidMount() { - try { - const { location, queryParams } = this.props; - const { appSubUrl } = config; - const plugin = await loadPlugin(this.props.match.params.pluginId); - const { defaultPage, nav } = getPluginTabsNav( - plugin, - appSubUrl, - location.pathname, - queryParams, - contextSrv.hasRole('Admin') - ); - this.setState({ - loading: false, - plugin, - defaultPage, - nav, - }); - } catch { - this.setState({ - loading: false, - nav: getNotFoundNav(), - }); - } - } - - componentDidUpdate(prevProps: Props) { - const prevPage = prevProps.queryParams.page as string; - const page = this.props.queryParams.page as string; - - if (prevPage !== page) { - const { nav, defaultPage } = this.state; - const node = { - ...nav.node, - children: setActivePage(page, nav.node.children!, defaultPage), - }; - - this.setState({ - nav: { - node: node, - main: node, - }, - }); - } - } - - renderBody() { - const { queryParams } = this.props; - const { plugin, nav } = this.state; - - if (!plugin) { - return ; - } - - const active = nav.main.children!.find((tab) => tab.active); - if (active) { - // Find the current config tab - if (plugin.configPages) { - for (const tab of plugin.configPages) { - if (tab.id === active.id) { - return ; - } - } - } - - // Apps have some special behavior - if (plugin.meta.type === PluginType.app) { - if (active.id === PAGE_ID_DASHBOARDS) { - return ; - } - - if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) { - return ; - } - } - } - - return ; - } - - showUpdateInfo = () => { - const { id, name } = this.state.plugin!.meta; - appEvents.publish( - new ShowModalReactEvent({ - props: { - id, - name, - }, - component: UpdatePluginModal, - }) - ); - }; - - renderVersionInfo(meta: PluginMeta) { - if (!meta.info.version) { - return null; - } - - return ( -
    -

    Version

    - {meta.info.version} - {meta.hasUpdate && ( -
    - - - Update Available! - - -
    - )} -
    - ); - } - - renderSidebarIncludeBody(item: PluginInclude) { - if (item.type === PluginIncludeType.page) { - const pluginId = this.state.plugin!.meta.id; - const page = item.name.toLowerCase().replace(' ', '-'); - const url = item.path ?? `plugins/${pluginId}/page/${page}`; - return ( - - - {item.name} - - ); - } - return ( - <> - - {item.name} - - ); - } - - renderSidebarIncludes(includes?: PluginInclude[]) { - if (!includes || !includes.length) { - return null; - } - - return ( -
    -

    Includes

    -
      - {includes.map((include) => { - return ( -
    • - {this.renderSidebarIncludeBody(include)} -
    • - ); - })} -
    -
    - ); - } - - renderSidebarDependencies(dependencies?: PluginDependencies) { - if (!dependencies) { - return null; - } - - return ( -
    -

    Dependencies

    -
      -
    • - Grafana logo - Grafana {dependencies.grafanaVersion} -
    • - {dependencies.plugins && - dependencies.plugins.map((plug) => { - return ( -
    • - - {plug.name} {plug.version} -
    • - ); - })} -
    -
    - ); - } - - renderSidebarLinks(info: PluginMetaInfo) { - if (!info.links || !info.links.length) { - return null; - } - - return ( -
    -

    Links

    - -
    - ); - } - - renderPluginNotice() { - const { plugin } = this.state; - - if (!plugin) { - return null; - } - - const isSignatureValid = plugin.meta.signature === PluginSignatureStatus.valid; - - if (plugin.meta.signature === PluginSignatureStatus.internal) { - return null; - } - - return ( - -
    - - {isSignatureValid && ( - - )} -
    -
    -

    - Grafana Labs checks each plugin to verify that it has a valid digital signature. Plugin signature verification - is part of our security measures to ensure plugins are safe and trustworthy.{' '} - {!isSignatureValid && - 'Grafana Labs can’t guarantee the integrity of this unsigned plugin. Ask the plugin author to request it to be signed.'} -

    - - Read more about plugins signing. - -
    - ); - } - - render() { - const { loading, nav, plugin } = this.state; - const isAdmin = contextSrv.hasRole('Admin'); - - return ( - - - {plugin && ( -
    -
    - {plugin.loadError && ( - - <> - Check the server startup logs for more information.
    - If this plugin was loaded from git, make sure it was compiled. - -
    - )} - {this.renderPluginNotice()} - {this.renderBody()} -
    - -
    - )} -
    -
    - ); - } -} - -function getPluginTabsNav( - plugin: GrafanaPlugin, - appSubUrl: string, - path: string, - query: UrlQueryMap, - isAdmin: boolean -): { defaultPage: string; nav: NavModel } { - const { meta } = plugin; - let defaultPage: string | undefined; - const pages: NavModelItem[] = []; - - pages.push({ - text: 'Readme', - icon: 'file-alt', - url: `${appSubUrl}${path}?page=${PAGE_ID_README}`, - id: PAGE_ID_README, - }); - - // We allow non admins to see plugins but only their readme. Config is hidden - // even though the API needs to be public for plugins to work properly. - if (isAdmin) { - // Only show Config/Pages for app - if (meta.type === PluginType.app) { - // Legacy App Config - if (plugin.angularConfigCtrl) { - pages.push({ - text: 'Config', - icon: 'cog', - url: `${appSubUrl}${path}?page=${PAGE_ID_CONFIG_CTRL}`, - id: PAGE_ID_CONFIG_CTRL, - }); - defaultPage = PAGE_ID_CONFIG_CTRL; - } - - if (plugin.configPages) { - for (const page of plugin.configPages) { - pages.push({ - text: page.title, - icon: page.icon, - url: `${appSubUrl}${path}?page=${page.id}`, - id: page.id, - }); - - if (!defaultPage) { - defaultPage = page.id; - } - } - } - - // Check for the dashboard pages - if (find(meta.includes, { type: PluginIncludeType.dashboard })) { - pages.push({ - text: 'Dashboards', - icon: 'apps', - url: `${appSubUrl}${path}?page=${PAGE_ID_DASHBOARDS}`, - id: PAGE_ID_DASHBOARDS, - }); - } - } - } - - if (!defaultPage) { - defaultPage = pages[0].id; // the first tab - } - - const node = { - text: meta.name, - img: meta.info.logos.large, - subTitle: meta.info.author.name, - breadcrumbs: [{ title: 'Plugins', url: 'plugins' }], - url: `${appSubUrl}${path}`, - children: setActivePage(query.page as string, pages, defaultPage!), - }; - - return { - defaultPage: defaultPage!, - nav: { - node: node, - main: node, - }, - }; -} - -function setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] { - let found = false; - const selected = pageId || defaultPageId; - const changed = pages.map((p) => { - const active = !found && selected === p.id; - if (active) { - found = true; - } - return { ...p, active }; - }); - - if (!found) { - changed[0].active = true; - } - - return changed; -} - -function getPluginIcon(type: string) { - switch (type) { - case 'datasource': - return 'gicon gicon-datasources'; - case 'panel': - return 'icon-gf icon-gf-panel'; - case 'app': - return 'icon-gf icon-gf-apps'; - case 'page': - return 'icon-gf icon-gf-endpoint-tiny'; - case 'dashboard': - return 'gicon gicon-dashboard'; - default: - return 'icon-gf icon-gf-apps'; - } -} - -export function getLoadingNav(): NavModel { - const node = { - text: 'Loading...', - icon: 'icon-gf icon-gf-panel', - }; - return { - node: node, - main: node, - }; -} - -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; -} - -type PluginSignatureDetailsBadgeProps = { - signatureType?: PluginSignatureType; - signatureOrg?: string; -}; - -const PluginSignatureDetailsBadge: React.FC = ({ signatureType, signatureOrg }) => { - const styles = useStyles2(getDetailsBadgeStyles); - - if (!signatureType && !signatureOrg) { - return null; - } - - const signatureTypeIcon = - signatureType === PluginSignatureType.grafana - ? 'grafana' - : signatureType === PluginSignatureType.commercial || signatureType === PluginSignatureType.community - ? 'shield' - : 'shield-exclamation'; - - const signatureTypeText = signatureType === PluginSignatureType.grafana ? 'Grafana Labs' : capitalize(signatureType); - - return ( - <> - {signatureType && ( - - Level:  - -   - {signatureTypeText} - - } - /> - )} - {signatureOrg && ( - - 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. - -
    -

    - grafana logo - 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`] = ` -
  • - -
    -
    - panel -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - 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 ( +
    + This plugin is not published to{' '} + + grafana.com/plugins + {' '} + and can't be managed via the catalog. +
    + ); + } + + 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 ( -
    - This plugin is not published to{' '} - - grafana.com/plugins - {' '} - and can't be managed via the catalog. -
    - ); - } - - 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,