mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* refactor(plugins): use routes specific to the new plugins/admin
* refactor(plugins): remove unused pages (PluginList, PluginItem)
* refactor(plugins): remove PluginPage
* refactor(plugins): remove UpdatePluginModal
* refactor(plugins): move AppConfigWrapper under plugins/admin
* refactor(plugins): move PluginDashboards under plugins/admin
* refactor(plugins): rename the "specs" folder to "tests"
* refactor(plugins): move test files to /tests folder
* refactor(plugins): move AppRootPage into a /components folder
* refactor(plugins): move PluginsErrorsInfo into a /plugins folder
* refactor(plugins): move PluginSettingsCache into a /components folder
* refactor(plugins): move PluginStateInfo into a /plugins folder
* refactor(plugins): move AppRootPage.test.tsx next to the tested component
* refactor(plugins): remove old snapshot tests
* fix(plugins): fix tests
* refactor(plugins/admin): move & rename PluginSettingsCache
* fix(plugins): fix a few rebase issues
* Plugins: remove deprecated code (state handling) (#41739)
* refactor(plugins): use the plugins/admin reducer only
* refactor(plugins): remove tests for the deprecated plugins reducer
* refactor(plugins): remove tests for the deprecated plugins selectors
* refactor(plugins/state): add a short comment note to selectors
* feat(plugins/state): add a selector for selecting errors
* feat(plugins/state): add a hook for getting plugin errors
* refactor(plugins): udpate the PluginsErrorsInfo component to use the new state selectors
* refactor(plugins/state): remove the old (deprecated) selectors
* refactor(plugins/state): use the new actions under /admin
* refactor(plugins/state): remove old (deprecated) reducers and actions
* refactor(plugins): update component definition
* fix(plugins): remove unnecessary {children} prop for PluginsErrorsInfo
* Plugins: show / hide install controls based on the `pluginAdminEnabled` flag (#41749)
* docs(plugins): update documentation for the `plugin_admin_enabled` flag
* refactor(InstallControls): move the main component to a named module
* feat(plugins): use the `pluginAdminEnable` flag to hide / show install controls in the UI
* test(plugins): add tests for enabling/disabling install controls
(cherry picked from commit 35c2c95fdc
)
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
parent
37f5946b8b
commit
2742a461f3
@ -971,7 +971,7 @@ enable_alpha = false
|
|||||||
app_tls_skip_verify_insecure = 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.
|
# 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 =
|
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_enabled = true
|
||||||
plugin_admin_external_manage_enabled = false
|
plugin_admin_external_manage_enabled = false
|
||||||
plugin_catalog_url = https://grafana.com/grafana/plugins/
|
plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||||
|
@ -944,7 +944,7 @@
|
|||||||
;app_tls_skip_verify_insecure = 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.
|
# 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 =
|
;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_enabled = false
|
||||||
;plugin_admin_external_manage_enabled = false
|
;plugin_admin_external_manage_enabled = false
|
||||||
;plugin_catalog_url = https://grafana.com/grafana/plugins/
|
;plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||||
|
@ -1582,7 +1582,7 @@ We do _not_ recommend using this option. For more information, refer to [Plugin
|
|||||||
|
|
||||||
### plugin_admin_enabled
|
### 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" >}}).
|
For more information, refer to [Plugin catalog]({{< relref "../plugins/catalog.md" >}}).
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ The Plugin catalog allows you to browse and manage plugins from within Grafana.
|
|||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
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.
|
Before following the steps below, make sure you are logged in as a Grafana administrator.
|
||||||
|
|
||||||
<a id="#plugin-catalog-entry"></a>
|
<a id="#plugin-catalog-entry"></a>
|
||||||
|
@ -7,7 +7,7 @@ import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
|||||||
import foldersReducers from 'app/features/folders/state/reducers';
|
import foldersReducers from 'app/features/folders/state/reducers';
|
||||||
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||||
import exploreReducers from 'app/features/explore/state/main';
|
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 dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||||
import usersReducers from 'app/features/users/state/reducers';
|
import usersReducers from 'app/features/users/state/reducers';
|
||||||
import userReducers from 'app/features/profile/state/reducers';
|
import userReducers from 'app/features/profile/state/reducers';
|
||||||
@ -26,7 +26,6 @@ const rootReducers = {
|
|||||||
...foldersReducers,
|
...foldersReducers,
|
||||||
...dashboardReducers,
|
...dashboardReducers,
|
||||||
...exploreReducers,
|
...exploreReducers,
|
||||||
...pluginReducers,
|
|
||||||
...dataSourcesReducers,
|
...dataSourcesReducers,
|
||||||
...usersReducers,
|
...usersReducers,
|
||||||
...userReducers,
|
...userReducers,
|
||||||
@ -36,6 +35,7 @@ const rootReducers = {
|
|||||||
...importDashboardReducers,
|
...importDashboardReducers,
|
||||||
...panelEditorReducers,
|
...panelEditorReducers,
|
||||||
...panelsReducers,
|
...panelsReducers,
|
||||||
|
plugins: pluginsReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const addedReducers = {};
|
const addedReducers = {};
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||||
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
||||||
import { TransformationsEditorTransformation } from './types';
|
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 { useToggle } from 'react-use';
|
||||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ import { TransformationsEditorTransformation } from './types';
|
|||||||
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
||||||
import { AppNotificationSeverity } from '../../../../types';
|
import { AppNotificationSeverity } from '../../../../types';
|
||||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
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';
|
const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
// Actions
|
// Actions
|
||||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
import { loadPluginDashboards } from '../../plugins/admin/state/actions';
|
||||||
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
||||||
|
@ -9,7 +9,7 @@ import DashboardTable from './DashboardsTable';
|
|||||||
// Actions & Selectors
|
// Actions & Selectors
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { loadDataSource } from './state/actions';
|
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 { importDashboard, removeDashboard } from '../dashboard/state/actions';
|
||||||
import { getDataSource } from './state/selectors';
|
import { getDataSource } from './state/selectors';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions';
|
|||||||
import { getDataSourcePlugins } from './state/selectors';
|
import { getDataSourcePlugins } from './state/selectors';
|
||||||
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
||||||
import { Card } from 'app/core/components/Card/Card';
|
import { Card } from 'app/core/components/Card/Card';
|
||||||
import { PluginsErrorsInfo } from '../plugins/PluginsErrorsInfo';
|
import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo';
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState) {
|
function mapStateToProps(state: StoreState) {
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +24,7 @@ import { StoreState, AccessControlAction } from 'app/types/';
|
|||||||
import { DataSourceSettings, urlUtil } from '@grafana/data';
|
import { DataSourceSettings, urlUtil } from '@grafana/data';
|
||||||
import { Alert, Button } from '@grafana/ui';
|
import { Alert, Button } from '@grafana/ui';
|
||||||
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel';
|
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 { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { CloudInfoBox } from './CloudInfoBox';
|
import { CloudInfoBox } from './CloudInfoBox';
|
||||||
|
@ -5,7 +5,7 @@ import { updateNavIndex } from 'app/core/actions';
|
|||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
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 { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
|
||||||
|
|
||||||
import config from '../../../core/config';
|
import config from '../../../core/config';
|
||||||
|
@ -3,7 +3,7 @@ import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState
|
|||||||
import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
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 { ThunkResult } from 'app/types';
|
||||||
import { panelModelAndPluginReady } from './reducers';
|
import { panelModelAndPluginReady } from './reducers';
|
||||||
import { LibraryElementDTO } from 'app/features/library-panels/types';
|
import { LibraryElementDTO } from 'app/features/library-panels/types';
|
||||||
|
@ -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(<PluginList {...props} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Render', () => {
|
|
||||||
it('should render component', () => {
|
|
||||||
const wrapper = setup();
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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> = (props) => {
|
|
||||||
const { plugins } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="card-section card-list-layout-list">
|
|
||||||
<ol className="card-list" aria-label={selectors.pages.PluginsList.list}>
|
|
||||||
{plugins.map((plugin, index) => {
|
|
||||||
return <PluginListItem plugin={plugin} key={`${plugin.name}-${index}`} />;
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginList;
|
|
@ -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(<PluginListItem {...props} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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> = (props) => {
|
|
||||||
const { plugin } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="card-item-wrapper" aria-label={selectors.pages.PluginsList.listItem}>
|
|
||||||
<a className="card-item" href={`plugins/${plugin.id}/`}>
|
|
||||||
<div className="card-item-header">
|
|
||||||
<div className="card-item-type">{plugin.type}</div>
|
|
||||||
<div className="card-item-badge">
|
|
||||||
<PluginSignatureBadge status={plugin.signature} />
|
|
||||||
</div>
|
|
||||||
{plugin.hasUpdate && (
|
|
||||||
<div className="card-item-notice">
|
|
||||||
<span bs-tooltip="plugin.latestVersion">Update available!</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="card-item-body">
|
|
||||||
<figure className="card-item-figure">
|
|
||||||
<img src={plugin.info.logos.small} />
|
|
||||||
</figure>
|
|
||||||
<div className="card-item-details">
|
|
||||||
<div className="card-item-name">{plugin.name}</div>
|
|
||||||
<div className="card-item-sub-name">{`By ${plugin.info.author.name}`}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PluginListItem;
|
|
@ -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(
|
|
||||||
<Provider store={store}>
|
|
||||||
<PluginListPage {...props} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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<typeof connector>;
|
|
||||||
|
|
||||||
export const PluginListPage: React.FC<Props> = ({
|
|
||||||
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 (
|
|
||||||
<Page navModel={navModel} aria-label={selectors.pages.PluginsList.page}>
|
|
||||||
<Page.Contents isLoading={!hasFetched}>
|
|
||||||
<>
|
|
||||||
<PageActionBar
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
setSearchQuery={(query) => setPluginsSearchQuery(query)}
|
|
||||||
linkButton={linkButton}
|
|
||||||
placeholder="Search by name, author, description or type"
|
|
||||||
target={actionTarget}
|
|
||||||
/>
|
|
||||||
<PluginsErrorsInfo />
|
|
||||||
{hasFetched && plugins && <PluginList plugins={plugins} />}
|
|
||||||
</>
|
|
||||||
</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PluginListPage);
|
|
@ -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<Props, State> {
|
|
||||||
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 <Alert severity={AppNotificationSeverity.Error} title="Plugin Not Found" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <tab.body plugin={plugin} query={queryParams} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apps have some special behavior
|
|
||||||
if (plugin.meta.type === PluginType.app) {
|
|
||||||
if (active.id === PAGE_ID_DASHBOARDS) {
|
|
||||||
return <PluginDashboards plugin={plugin.meta} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) {
|
|
||||||
return <AppConfigCtrlWrapper app={plugin as AppPlugin} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PluginHelp plugin={plugin.meta} type="help" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<section className="page-sidebar-section">
|
|
||||||
<h4>Version</h4>
|
|
||||||
<span>{meta.info.version}</span>
|
|
||||||
{meta.hasUpdate && (
|
|
||||||
<div>
|
|
||||||
<Tooltip content={meta.latestVersion!} theme="info" placement="top">
|
|
||||||
<LinkButton fill="text" onClick={this.showUpdateInfo}>
|
|
||||||
Update Available!
|
|
||||||
</LinkButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<a href={url}>
|
|
||||||
<i className={getPluginIcon(item.type)} />
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<i className={getPluginIcon(item.type)} />
|
|
||||||
{item.name}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebarIncludes(includes?: PluginInclude[]) {
|
|
||||||
if (!includes || !includes.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="page-sidebar-section">
|
|
||||||
<h4>Includes</h4>
|
|
||||||
<ul className="ui-list plugin-info-list">
|
|
||||||
{includes.map((include) => {
|
|
||||||
return (
|
|
||||||
<li className="plugin-info-list-item" key={include.name}>
|
|
||||||
{this.renderSidebarIncludeBody(include)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebarDependencies(dependencies?: PluginDependencies) {
|
|
||||||
if (!dependencies) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="page-sidebar-section">
|
|
||||||
<h4>Dependencies</h4>
|
|
||||||
<ul className="ui-list plugin-info-list">
|
|
||||||
<li className="plugin-info-list-item">
|
|
||||||
<img src="public/img/grafana_icon.svg" alt="Grafana logo" />
|
|
||||||
Grafana {dependencies.grafanaVersion}
|
|
||||||
</li>
|
|
||||||
{dependencies.plugins &&
|
|
||||||
dependencies.plugins.map((plug) => {
|
|
||||||
return (
|
|
||||||
<li className="plugin-info-list-item" key={plug.name}>
|
|
||||||
<i className={getPluginIcon(plug.type)} />
|
|
||||||
{plug.name} {plug.version}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebarLinks(info: PluginMetaInfo) {
|
|
||||||
if (!info.links || !info.links.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="page-sidebar-section">
|
|
||||||
<h4>Links</h4>
|
|
||||||
<ul className="ui-list">
|
|
||||||
{info.links.map((link) => {
|
|
||||||
return (
|
|
||||||
<li key={link.url}>
|
|
||||||
<a href={link.url} className="external-link" target="_blank" rel="noreferrer noopener">
|
|
||||||
{link.name}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Alert
|
|
||||||
aria-label={selectors.pages.PluginPage.signatureInfo}
|
|
||||||
severity={isSignatureValid ? 'info' : 'warning'}
|
|
||||||
title="Plugin signature"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
display: flex;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<PluginSignatureBadge
|
|
||||||
status={plugin.meta.signature}
|
|
||||||
className={css`
|
|
||||||
margin-top: 0;
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
{isSignatureValid && (
|
|
||||||
<PluginSignatureDetailsBadge
|
|
||||||
signatureType={plugin.meta.signatureType}
|
|
||||||
signatureOrg={plugin.meta.signatureOrg}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
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.'}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://grafana.com/docs/grafana/latest/plugins/plugin-signatures/"
|
|
||||||
className="external-link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Read more about plugins signing.
|
|
||||||
</a>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, nav, plugin } = this.state;
|
|
||||||
const isAdmin = contextSrv.hasRole('Admin');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navModel={nav} aria-label={selectors.pages.PluginPage.page}>
|
|
||||||
<Page.Contents isLoading={loading}>
|
|
||||||
{plugin && (
|
|
||||||
<div className="sidebar-container">
|
|
||||||
<div className="sidebar-content">
|
|
||||||
{plugin.loadError && (
|
|
||||||
<Alert severity={AppNotificationSeverity.Error} title="Error Loading Plugin">
|
|
||||||
<>
|
|
||||||
Check the server startup logs for more information. <br />
|
|
||||||
If this plugin was loaded from git, make sure it was compiled.
|
|
||||||
</>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{this.renderPluginNotice()}
|
|
||||||
{this.renderBody()}
|
|
||||||
</div>
|
|
||||||
<aside className="page-sidebar">
|
|
||||||
<section className="page-sidebar-section">
|
|
||||||
{this.renderVersionInfo(plugin.meta)}
|
|
||||||
{isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
|
|
||||||
{this.renderSidebarDependencies(plugin.meta.dependencies)}
|
|
||||||
{this.renderSidebarLinks(plugin.meta.info)}
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<GrafanaPlugin> {
|
|
||||||
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<PluginSignatureDetailsBadgeProps> = ({ 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 && (
|
|
||||||
<Badge
|
|
||||||
color="green"
|
|
||||||
className={styles.badge}
|
|
||||||
text={
|
|
||||||
<>
|
|
||||||
<strong className={styles.strong}>Level: </strong>
|
|
||||||
<Icon size="xs" name={signatureTypeIcon} />
|
|
||||||
|
|
||||||
{signatureTypeText}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{signatureOrg && (
|
|
||||||
<Badge
|
|
||||||
color="green"
|
|
||||||
className={styles.badge}
|
|
||||||
text={
|
|
||||||
<>
|
|
||||||
<strong className={styles.strong}>Signed by:</strong> {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;
|
|
@ -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 (
|
|
||||||
<Modal title="Update Plugin" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
|
|
||||||
<VerticalGroup spacing="md">
|
|
||||||
<VerticalGroup spacing="sm">
|
|
||||||
<p>Type the following on the command line to update {name}.</p>
|
|
||||||
<pre>
|
|
||||||
<code>grafana-cli plugins update {id}</code>
|
|
||||||
</pre>
|
|
||||||
<span className={styles.small}>
|
|
||||||
Check out {name} on <a href={`https://grafana.com/plugins/${id}`}>Grafana.com</a> for README and changelog.
|
|
||||||
If you do not have access to the command line, ask your Grafana administator.
|
|
||||||
</span>
|
|
||||||
</VerticalGroup>
|
|
||||||
<p className={styles.weak}>
|
|
||||||
<img className={styles.logo} src="public/img/grafana_icon.svg" alt="grafana logo" />
|
|
||||||
<strong>Pro tip</strong>: To update all plugins at once, type{' '}
|
|
||||||
<code className={styles.codeSmall}>grafana-cli plugins update-all</code> on the command line.
|
|
||||||
</p>
|
|
||||||
</VerticalGroup>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,241 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
<section
|
|
||||||
className="card-section card-list-layout-list"
|
|
||||||
>
|
|
||||||
<ol
|
|
||||||
aria-label="Plugins list"
|
|
||||||
className="card-list"
|
|
||||||
>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-0-0"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "0",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.0",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-0",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-1-1"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "1",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.1",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-1",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-2-2"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "2",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.2",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-2",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-3-3"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "3",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.3",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-3",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-4-4"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "4",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.4",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-4",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginListItem
|
|
||||||
key="pretty cool plugin-5-5"
|
|
||||||
plugin={
|
|
||||||
Object {
|
|
||||||
"defaultNavUrl": "some/url",
|
|
||||||
"enabled": false,
|
|
||||||
"hasUpdate": false,
|
|
||||||
"id": "5",
|
|
||||||
"info": Object {
|
|
||||||
"author": Object {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "url/to/GrafanaLabs",
|
|
||||||
},
|
|
||||||
"description": "pretty decent plugin",
|
|
||||||
"links": Array [
|
|
||||||
"one link",
|
|
||||||
],
|
|
||||||
"logos": Object {
|
|
||||||
"large": "large/logo",
|
|
||||||
"small": "small/logo",
|
|
||||||
},
|
|
||||||
"screenshots": Array [
|
|
||||||
Object {
|
|
||||||
"path": "screenshot/5",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"updated": "2018-09-26",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
"latestVersion": "1.5",
|
|
||||||
"module": Object {},
|
|
||||||
"name": "pretty cool plugin-5",
|
|
||||||
"pinned": false,
|
|
||||||
"state": "",
|
|
||||||
"type": "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
`;
|
|
@ -1,114 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
<li
|
|
||||||
aria-label="Plugins list item"
|
|
||||||
className="card-item-wrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="card-item"
|
|
||||||
href="plugins/1/"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-header"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-type"
|
|
||||||
>
|
|
||||||
panel
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-badge"
|
|
||||||
>
|
|
||||||
<PluginSignatureBadge />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-body"
|
|
||||||
>
|
|
||||||
<figure
|
|
||||||
className="card-item-figure"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="small/logo"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<div
|
|
||||||
className="card-item-details"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-name"
|
|
||||||
>
|
|
||||||
pretty cool plugin 1
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-sub-name"
|
|
||||||
>
|
|
||||||
By Grafana Labs
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render should render has plugin section 1`] = `
|
|
||||||
<li
|
|
||||||
aria-label="Plugins list item"
|
|
||||||
className="card-item-wrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="card-item"
|
|
||||||
href="plugins/1/"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-header"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-type"
|
|
||||||
>
|
|
||||||
panel
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-badge"
|
|
||||||
>
|
|
||||||
<PluginSignatureBadge />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-notice"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
bs-tooltip="plugin.latestVersion"
|
|
||||||
>
|
|
||||||
Update available!
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-body"
|
|
||||||
>
|
|
||||||
<figure
|
|
||||||
className="card-item-figure"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="small/logo"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<div
|
|
||||||
className="card-item-details"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card-item-name"
|
|
||||||
>
|
|
||||||
pretty cool plugin 1
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-item-sub-name"
|
|
||||||
>
|
|
||||||
By Grafana Labs
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
`;
|
|
@ -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 <div className={styles.message}>Renderer plugins cannot be managed by the Plugin Catalog.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup height="auto" align="center">
|
||||||
|
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
|
||||||
|
<LinkButton
|
||||||
|
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="sm"
|
||||||
|
fill="text"
|
||||||
|
icon="external-link-alt"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isDev) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>This is a development build of the plugin and can't be uninstalled.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission && !isExternallyManaged) {
|
||||||
|
const message = `You do not have permission to ${pluginStatus} this plugin.`;
|
||||||
|
return <div className={styles.message}>{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plugin.isPublished) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<Icon name="exclamation-triangle" /> This plugin is not published to{' '}
|
||||||
|
<a href="https://www.grafana.com/plugins" target="__blank" rel="noreferrer">
|
||||||
|
grafana.com/plugins
|
||||||
|
</a>{' '}
|
||||||
|
and can't be managed via the catalog.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCompatible) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<Icon name="exclamation-triangle" />
|
||||||
|
This plugin doesn't support your version of Grafana.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExternallyManaged) {
|
||||||
|
return <ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRemotePluginsAvailable) {
|
||||||
|
return (
|
||||||
|
<div className={styles.message}>
|
||||||
|
The install controls have been disabled because the Grafana server cannot access grafana.com.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstallControlsButton
|
||||||
|
plugin={plugin}
|
||||||
|
pluginStatus={pluginStatus}
|
||||||
|
latestCompatibleVersion={latestCompatibleVersion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
message: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -1,118 +1 @@
|
|||||||
import React from 'react';
|
export * from './InstallControls';
|
||||||
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 <div className={styles.message}>Renderer plugins cannot be managed by the Plugin Catalog.</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.isEnterprise && !config.licenseInfo?.hasValidLicense) {
|
|
||||||
return (
|
|
||||||
<HorizontalGroup height="auto" align="center">
|
|
||||||
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
|
|
||||||
<LinkButton
|
|
||||||
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
size="sm"
|
|
||||||
fill="text"
|
|
||||||
icon="external-link-alt"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.isDev) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>This is a development build of the plugin and can't be uninstalled.</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission && !isExternallyManaged) {
|
|
||||||
const message = `You do not have permission to ${pluginStatus} this plugin.`;
|
|
||||||
return <div className={styles.message}>{message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plugin.isPublished) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>
|
|
||||||
<Icon name="exclamation-triangle" /> This plugin is not published to{' '}
|
|
||||||
<a href="https://www.grafana.com/plugins" target="__blank" rel="noreferrer">
|
|
||||||
grafana.com/plugins
|
|
||||||
</a>{' '}
|
|
||||||
and can't be managed via the catalog.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCompatible) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>
|
|
||||||
<Icon name="exclamation-triangle" />
|
|
||||||
This plugin doesn't support your version of Grafana.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExternallyManaged) {
|
|
||||||
return <ExternallyManagedButton pluginId={plugin.id} pluginStatus={pluginStatus} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRemotePluginsAvailable) {
|
|
||||||
return (
|
|
||||||
<div className={styles.message}>
|
|
||||||
The install controls have been disabled because the Grafana server cannot access grafana.com.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InstallControlsButton
|
|
||||||
plugin={plugin}
|
|
||||||
pluginStatus={pluginStatus}
|
|
||||||
latestCompatibleVersion={latestCompatibleVersion}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStyles = (theme: GrafanaTheme2) => {
|
|
||||||
return {
|
|
||||||
message: css`
|
|
||||||
color: ${theme.colors.text.secondary};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -7,8 +7,8 @@ import { useStyles2 } from '@grafana/ui';
|
|||||||
import { CatalogPlugin, PluginTabIds } from '../types';
|
import { CatalogPlugin, PluginTabIds } from '../types';
|
||||||
import { VersionList } from '../components/VersionList';
|
import { VersionList } from '../components/VersionList';
|
||||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||||
import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
|
import { AppConfigCtrlWrapper } from './AppConfigWrapper';
|
||||||
import { PluginDashboards } from '../../PluginDashboards';
|
import { PluginDashboards } from './PluginDashboards';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugin: CatalogPlugin;
|
plugin: CatalogPlugin;
|
||||||
|
@ -261,6 +261,8 @@ export function getLatestCompatibleVersion(versions: Version[] | undefined): Ver
|
|||||||
return latest;
|
return latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isInstallControlsEnabled = () => config.pluginAdminEnabled;
|
||||||
|
|
||||||
export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
|
export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
|
||||||
|
|
||||||
export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
|
export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
import { CatalogPlugin } from '../types';
|
import { CatalogPlugin } from '../types';
|
||||||
import { loadPlugin } from '../../PluginPage';
|
import { loadPlugin } from '../../utils';
|
||||||
|
|
||||||
export const usePluginConfig = (plugin?: CatalogPlugin) => {
|
export const usePluginConfig = (plugin?: CatalogPlugin) => {
|
||||||
return useAsync(async () => {
|
return useAsync(async () => {
|
||||||
|
@ -493,6 +493,25 @@ describe('Plugin details page', () => {
|
|||||||
expect(rendered.getByText(message)).toBeInTheDocument();
|
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 () => {
|
it('should display a "Create" button as a post installation step for installed data source plugins', async () => {
|
||||||
const name = 'Akumuli';
|
const name = 'Akumuli';
|
||||||
const { queryByText } = renderPluginDetails({
|
const { queryByText } = renderPluginDetails({
|
||||||
|
48
public/app/features/plugins/admin/routes.ts
Normal file
48
public/app/features/plugins/admin/routes.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { PluginError } from '@grafana/data';
|
||||||
import { setDisplayMode } from './reducer';
|
import { setDisplayMode } from './reducer';
|
||||||
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall } from './actions';
|
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall } from './actions';
|
||||||
import { CatalogPlugin, PluginCatalogStoreState, PluginListDisplayMode } from '../types';
|
import { CatalogPlugin, PluginCatalogStoreState, PluginListDisplayMode } from '../types';
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
selectRequestError,
|
selectRequestError,
|
||||||
selectIsRequestNotFetched,
|
selectIsRequestNotFetched,
|
||||||
selectDisplayMode,
|
selectDisplayMode,
|
||||||
|
selectPluginErrors,
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { sortPlugins, Sorters } from '../helpers';
|
import { sortPlugins, Sorters } from '../helpers';
|
||||||
|
|
||||||
@ -53,6 +55,12 @@ export const useGetSingle = (id: string): CatalogPlugin | undefined => {
|
|||||||
return useSelector((state: PluginCatalogStoreState) => selectById(state, id));
|
return useSelector((state: PluginCatalogStoreState) => selectById(state, id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetErrors = (): PluginError[] => {
|
||||||
|
useFetchAll();
|
||||||
|
|
||||||
|
return useSelector(selectPluginErrors);
|
||||||
|
};
|
||||||
|
|
||||||
export const useInstall = () => {
|
export const useInstall = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating }));
|
return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating }));
|
||||||
|
@ -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 { fetchAll, fetchDetails, install, uninstall, loadPluginDashboards, panelPluginLoaded } from './actions';
|
||||||
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
|
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
|
||||||
import { STATE_PREFIX } from '../constants';
|
import { STATE_PREFIX } from '../constants';
|
||||||
@ -97,4 +97,4 @@ const slice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const { setDisplayMode } = slice.actions;
|
export const { setDisplayMode } = slice.actions;
|
||||||
export const { reducer } = slice;
|
export const reducer: Reducer<ReducerState, AnyAction> = slice.reducer;
|
||||||
|
@ -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 { RequestStatus, PluginCatalogStoreState } from '../types';
|
||||||
import { pluginsAdapter } from './reducer';
|
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) =>
|
export const selectRequest = (actionType: string) =>
|
||||||
createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);
|
createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AppRootPage from './AppRootPage';
|
import AppRootPage from './AppRootPage';
|
||||||
import { getPluginSettings } from './PluginSettingsCache';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
import { importAppPlugin } from './plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
import { getMockPlugin } from './__mocks__/pluginMocks';
|
import { getMockPlugin } from '../__mocks__/pluginMocks';
|
||||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
||||||
import { Route, Router } from 'react-router-dom';
|
import { Route, Router } from 'react-router-dom';
|
||||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
||||||
import { Echo } from 'app/core/services/echo/Echo';
|
import { Echo } from 'app/core/services/echo/Echo';
|
||||||
|
|
||||||
jest.mock('./PluginSettingsCache', () => ({
|
jest.mock('../pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn(),
|
getPluginSettings: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./plugin_loader', () => ({
|
jest.mock('../plugin_loader', () => ({
|
||||||
importAppPlugin: jest.fn(),
|
importAppPlugin: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -4,8 +4,8 @@ import { AppEvents, AppPlugin, AppPluginMeta, KeyValue, NavModel, PluginType } f
|
|||||||
import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react-reverse-portal';
|
import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react-reverse-portal';
|
||||||
|
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { getPluginSettings } from './PluginSettingsCache';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
import { importAppPlugin } from './plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
|
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
@ -1,42 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
|
import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
|
||||||
import { StoreState } from '../../types';
|
import { useGetErrors, useFetchStatus } from '../admin/state/hooks';
|
||||||
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 { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
|
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
export function PluginsErrorsInfo(): React.ReactElement | null {
|
||||||
errors: getAllPluginsErrors(state.plugins),
|
const errors = useGetErrors();
|
||||||
});
|
const { isLoading } = useFetchStatus();
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
loadPluginsErrors,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface OwnProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
type PluginsErrorsInfoProps = ConnectedProps<typeof connector> & OwnProps;
|
|
||||||
|
|
||||||
export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
|
||||||
loadPluginsErrors,
|
|
||||||
errors,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { loading } = useAsync(async () => {
|
if (isLoading || errors.length === 0) {
|
||||||
await loadPluginsErrors();
|
|
||||||
}, [loadPlugins]);
|
|
||||||
|
|
||||||
if (loading || errors.length === 0) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
aria-label={selectors.pages.PluginsList.signatureErrorNotice}
|
aria-label={selectors.pages.PluginsList.signatureErrorNotice}
|
||||||
@ -55,16 +32,16 @@ export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
|||||||
className={css`
|
className={css`
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
`}
|
`}
|
||||||
renderItem={(e) => (
|
renderItem={(error) => (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
margin-top: ${theme.spacing.sm};
|
margin-top: ${theme.spacing.sm};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<HorizontalGroup spacing="sm" justify="flex-start" align="center">
|
<HorizontalGroup spacing="sm" justify="flex-start" align="center">
|
||||||
<strong>{e.pluginId}</strong>
|
<strong>{error.pluginId}</strong>
|
||||||
<PluginSignatureBadge
|
<PluginSignatureBadge
|
||||||
status={mapPluginErrorCodeToSignatureStatus(e.errorCode)}
|
status={mapPluginErrorCodeToSignatureStatus(error.errorCode)}
|
||||||
className={css`
|
className={css`
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
`}
|
`}
|
||||||
@ -73,13 +50,10 @@ export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const PluginsErrorsInfo = connect(mapStateToProps, mapDispatchToProps)(PluginsErrorsInfoUnconnected);
|
|
||||||
|
|
||||||
function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
|
function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
|
||||||
switch (code) {
|
switch (code) {
|
@ -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;
|
|
||||||
}
|
|
@ -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<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const plugins = await getBackendSrv().get('api/plugins', { embedded: 0 });
|
|
||||||
dispatch(pluginsLoaded(plugins));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadPluginsErrors(): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const errors = await getBackendSrv().get('api/plugins/errors');
|
|
||||||
dispatch(pluginsErrorsLoaded(errors));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginDashboardsOriginal(): ThunkResult<void> {
|
|
||||||
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<Promise<PanelPlugin>> {
|
|
||||||
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;
|
|
@ -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<PluginsState>()
|
|
||||||
.givenReducer(pluginsReducer as Reducer<PluginsState, AnyAction>, { ...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<PluginsState>()
|
|
||||||
.givenReducer(pluginsReducer as Reducer<PluginsState, AnyAction>, { ...initialState })
|
|
||||||
.whenActionIsDispatched(setPluginsSearchQuery('A query'))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...initialState,
|
|
||||||
searchQuery: 'A query',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when pluginDashboardsLoad is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<PluginsState>()
|
|
||||||
.givenReducer(pluginsReducer as Reducer<PluginsState, AnyAction>, {
|
|
||||||
...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<PluginsState>()
|
|
||||||
.givenReducer(pluginsReducer as Reducer<PluginsState, AnyAction>, {
|
|
||||||
...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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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<PluginMeta[]>) => {
|
|
||||||
state.hasFetched = true;
|
|
||||||
state.plugins = action.payload;
|
|
||||||
},
|
|
||||||
pluginsErrorsLoaded: (state, action: PayloadAction<PluginError[]>) => {
|
|
||||||
state.errors = action.payload;
|
|
||||||
},
|
|
||||||
setPluginsSearchQuery: (state, action: PayloadAction<string>) => {
|
|
||||||
state.searchQuery = action.payload;
|
|
||||||
},
|
|
||||||
pluginDashboardsLoad: (state, action: PayloadAction<undefined>) => {
|
|
||||||
state.isLoadingPluginDashboards = true;
|
|
||||||
state.dashboards = [];
|
|
||||||
},
|
|
||||||
pluginDashboardsLoaded: (state, action: PayloadAction<PluginDashboard[]>) => {
|
|
||||||
state.isLoadingPluginDashboards = false;
|
|
||||||
state.dashboards = action.payload;
|
|
||||||
},
|
|
||||||
panelPluginLoaded: (state, action: PayloadAction<PanelPlugin>) => {
|
|
||||||
state.panels[action.payload.meta!.id] = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
pluginsLoaded,
|
|
||||||
pluginsErrorsLoaded,
|
|
||||||
pluginDashboardsLoad,
|
|
||||||
pluginDashboardsLoaded,
|
|
||||||
setPluginsSearchQuery,
|
|
||||||
panelPluginLoaded,
|
|
||||||
} = pluginsSlice.actions;
|
|
||||||
|
|
||||||
export const pluginsReducer: Reducer<PluginsState, AnyAction> = config.pluginAdminEnabled
|
|
||||||
? ((pluginCatalogReducer as unknown) as Reducer<PluginsState, AnyAction>)
|
|
||||||
: pluginsSlice.reducer;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
plugins: pluginsReducer,
|
|
||||||
};
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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;
|
|
@ -30,7 +30,7 @@ class TestDataSource {
|
|||||||
constructor(public instanceSettings: DataSourceInstanceSettings) {}
|
constructor(public instanceSettings: DataSourceInstanceSettings) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
jest.mock('./plugin_loader', () => ({
|
jest.mock('../plugin_loader', () => ({
|
||||||
importDataSourcePlugin: (meta: DataSourcePluginMeta) => {
|
importDataSourcePlugin: (meta: DataSourcePluginMeta) => {
|
||||||
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
|
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
|
||||||
},
|
},
|
@ -1,4 +1,4 @@
|
|||||||
import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster';
|
import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from '../pluginCacheBuster';
|
||||||
|
|
||||||
describe('PluginCacheBuster', () => {
|
describe('PluginCacheBuster', () => {
|
||||||
const now = 12345;
|
const now = 12345;
|
@ -17,7 +17,7 @@ import { SystemJS } from '@grafana/runtime';
|
|||||||
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
|
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
|
||||||
|
|
||||||
// Loaded after the `unmock` abve
|
// Loaded after the `unmock` abve
|
||||||
import { importAppPlugin } from './plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
|
|
||||||
class MyCustomApp extends AppPlugin {
|
class MyCustomApp extends AppPlugin {
|
||||||
initWasCalled = false;
|
initWasCalled = false;
|
29
public/app/features/plugins/utils.ts
Normal file
29
public/app/features/plugins/utils.ts
Normal file
@ -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<GrafanaPlugin> {
|
||||||
|
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;
|
||||||
|
}
|
@ -8,7 +8,7 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic
|
|||||||
import { RouteDescriptor } from '../core/navigation/types';
|
import { RouteDescriptor } from '../core/navigation/types';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
||||||
import { getAlertingRoutes } from 'app/features/alerting/routes';
|
import { getAlertingRoutes } from 'app/features/alerting/routes';
|
||||||
@ -150,7 +150,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
exact: false,
|
exact: false,
|
||||||
// Someday * and will get a ReactRouter under that path!
|
// Someday * and will get a ReactRouter under that path!
|
||||||
component: SafeDynamicImport(
|
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')
|
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...getPluginsAdminRoutes(),
|
...getPluginCatalogRoutes(),
|
||||||
...getLiveRoutes(),
|
...getLiveRoutes(),
|
||||||
...getAlertingRoutes(),
|
...getAlertingRoutes(),
|
||||||
...extraRoutes,
|
...extraRoutes,
|
||||||
|
Loading…
Reference in New Issue
Block a user