mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: remove deprecated code (components) (#41686)
* 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
This commit is contained in:
parent
98f87c4c49
commit
35c2c95fdc
conf
docs/sources
public/app
core/reducers
features
dashboard
components/TransformationsEditor
state
datasources
panel
plugins
PluginList.test.tsxPluginList.tsxPluginListItem.test.tsxPluginListItem.tsxPluginListPage.test.tsxPluginListPage.tsxPluginPage.tsxUpdatePluginModal.tsx
__snapshots__
admin
components
helpers.tshooks
pages
routes.tsstate
components
pluginSettings.tsroutes.tsstate
tests
utils.tsroutes
@ -968,7 +968,7 @@ enable_alpha = false
|
||||
app_tls_skip_verify_insecure = false
|
||||
# Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded.
|
||||
allow_loading_unsigned_plugins =
|
||||
# Enable or disable installing plugins directly from within Grafana.
|
||||
# Enable or disable installing / uninstalling / updating plugins directly from within Grafana.
|
||||
plugin_admin_enabled = true
|
||||
plugin_admin_external_manage_enabled = false
|
||||
plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||
|
@ -941,7 +941,7 @@
|
||||
;app_tls_skip_verify_insecure = false
|
||||
# Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded.
|
||||
;allow_loading_unsigned_plugins =
|
||||
# Enable or disable installing plugins directly from within Grafana.
|
||||
# Enable or disable installing / uninstalling / updating plugins directly from within Grafana.
|
||||
;plugin_admin_enabled = false
|
||||
;plugin_admin_external_manage_enabled = false
|
||||
;plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||
|
@ -1582,7 +1582,7 @@ We do _not_ recommend using this option. For more information, refer to [Plugin
|
||||
|
||||
### plugin_admin_enabled
|
||||
|
||||
Available to Grafana administrators only, the plugin admin app is set to `true` by default. Set it to `false` to disable the app.
|
||||
Available to Grafana administrators only, enables installing / uninstalling / updating plugins directly from the Grafana UI. Set to `true` by default. Setting it to `false` will hide the install / uninstall / update controls.
|
||||
|
||||
For more information, refer to [Plugin catalog]({{< relref "../plugins/catalog.md" >}}).
|
||||
|
||||
|
@ -23,7 +23,7 @@ The Plugin catalog allows you to browse and manage plugins from within Grafana.
|
||||
</video>
|
||||
</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.
|
||||
|
||||
<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 dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||
import exploreReducers from 'app/features/explore/state/main';
|
||||
import pluginReducers from 'app/features/plugins/state/reducers';
|
||||
import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
import usersReducers from 'app/features/users/state/reducers';
|
||||
import userReducers from 'app/features/profile/state/reducers';
|
||||
@ -26,7 +26,6 @@ const rootReducers = {
|
||||
...foldersReducers,
|
||||
...dashboardReducers,
|
||||
...exploreReducers,
|
||||
...pluginReducers,
|
||||
...dataSourcesReducers,
|
||||
...usersReducers,
|
||||
...userReducers,
|
||||
@ -36,6 +35,7 @@ const rootReducers = {
|
||||
...importDashboardReducers,
|
||||
...panelEditorReducers,
|
||||
...panelsReducers,
|
||||
plugins: pluginsReducer,
|
||||
};
|
||||
|
||||
const addedReducers = {};
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
||||
import { TransformationsEditorTransformation } from './types';
|
||||
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
import { useToggle } from 'react-use';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
|
||||
|
@ -33,7 +33,7 @@ import { TransformationsEditorTransformation } from './types';
|
||||
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
||||
import { AppNotificationSeverity } from '../../../../types';
|
||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
||||
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed';
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { loadPluginDashboards } from '../../plugins/admin/state/actions';
|
||||
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
||||
|
@ -9,7 +9,7 @@ import DashboardTable from './DashboardsTable';
|
||||
// Actions & Selectors
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { loadDataSource } from './state/actions';
|
||||
import { loadPluginDashboards } from '../plugins/state/actions';
|
||||
import { loadPluginDashboards } from '../plugins/admin/state/actions';
|
||||
import { importDashboard, removeDashboard } from '../dashboard/state/actions';
|
||||
import { getDataSource } from './state/selectors';
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions';
|
||||
import { getDataSourcePlugins } from './state/selectors';
|
||||
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
||||
import { Card } from 'app/core/components/Card/Card';
|
||||
import { PluginsErrorsInfo } from '../plugins/PluginsErrorsInfo';
|
||||
import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo';
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
|
@ -24,7 +24,7 @@ import { StoreState, AccessControlAction } from 'app/types/';
|
||||
import { DataSourceSettings, urlUtil } from '@grafana/data';
|
||||
import { Alert, Button } from '@grafana/ui';
|
||||
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel';
|
||||
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { CloudInfoBox } from './CloudInfoBox';
|
||||
|
@ -5,7 +5,7 @@ import { updateNavIndex } from 'app/core/actions';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
|
||||
import config from '../../../core/config';
|
||||
|
@ -3,7 +3,7 @@ import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState
|
||||
import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
|
||||
interface Props {
|
||||
isCurrent: boolean;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { panelModelAndPluginReady } from './reducers';
|
||||
import { LibraryElementDTO } from 'app/features/library-panels/types';
|
||||
|
@ -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';
|
||||
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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
export * from './InstallControls';
|
||||
|
@ -7,8 +7,8 @@ import { useStyles2 } from '@grafana/ui';
|
||||
import { CatalogPlugin, PluginTabIds } from '../types';
|
||||
import { VersionList } from '../components/VersionList';
|
||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||
import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
|
||||
import { PluginDashboards } from '../../PluginDashboards';
|
||||
import { AppConfigCtrlWrapper } from './AppConfigWrapper';
|
||||
import { PluginDashboards } from './PluginDashboards';
|
||||
|
||||
type Props = {
|
||||
plugin: CatalogPlugin;
|
||||
|
@ -261,6 +261,8 @@ export function getLatestCompatibleVersion(versions: Version[] | undefined): Ver
|
||||
return latest;
|
||||
}
|
||||
|
||||
export const isInstallControlsEnabled = () => config.pluginAdminEnabled;
|
||||
|
||||
export const isLocalPluginVisible = (p: LocalPlugin) => isPluginVisible(p.id);
|
||||
|
||||
export const isRemotePluginVisible = (p: RemotePlugin) => isPluginVisible(p.slug);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAsync } from 'react-use';
|
||||
import { CatalogPlugin } from '../types';
|
||||
import { loadPlugin } from '../../PluginPage';
|
||||
import { loadPlugin } from '../../utils';
|
||||
|
||||
export const usePluginConfig = (plugin?: CatalogPlugin) => {
|
||||
return useAsync(async () => {
|
||||
|
@ -459,6 +459,25 @@ describe('Plugin details page', () => {
|
||||
expect(rendered.getByText(message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display the install / uninstall / update buttons if `pluginAdminEnabled` flag is set to FALSE in the Grafana config', async () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
// Disable the install controls for the plugins catalog
|
||||
config.pluginAdminEnabled = false;
|
||||
|
||||
// Should not show an "Install" button
|
||||
rendered = renderPluginDetails({ id, isInstalled: false });
|
||||
await waitFor(() => expect(rendered.queryByRole('button', { name: /^install/i })).not.toBeInTheDocument());
|
||||
|
||||
// Should not show an "Uninstall" button
|
||||
rendered = renderPluginDetails({ id, isInstalled: true });
|
||||
await waitFor(() => expect(rendered.queryByRole('button', { name: /^uninstall/i })).not.toBeInTheDocument());
|
||||
|
||||
// Should not show an "Update" button
|
||||
rendered = renderPluginDetails({ id, isInstalled: true, hasUpdate: true });
|
||||
await waitFor(() => expect(rendered.queryByRole('button', { name: /^update/i })).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should display a "Create" button as a post installation step for installed data source plugins', async () => {
|
||||
const name = 'Akumuli';
|
||||
const { queryByText } = renderPluginDetails({
|
||||
|
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 { useDispatch, useSelector } from 'react-redux';
|
||||
import { PluginError } from '@grafana/data';
|
||||
import { setDisplayMode } from './reducer';
|
||||
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall } from './actions';
|
||||
import { CatalogPlugin, PluginCatalogStoreState, PluginListDisplayMode } from '../types';
|
||||
@ -11,6 +12,7 @@ import {
|
||||
selectRequestError,
|
||||
selectIsRequestNotFetched,
|
||||
selectDisplayMode,
|
||||
selectPluginErrors,
|
||||
} from './selectors';
|
||||
import { sortPlugins, Sorters } from '../helpers';
|
||||
|
||||
@ -53,6 +55,12 @@ export const useGetSingle = (id: string): CatalogPlugin | undefined => {
|
||||
return useSelector((state: PluginCatalogStoreState) => selectById(state, id));
|
||||
};
|
||||
|
||||
export const useGetErrors = (): PluginError[] => {
|
||||
useFetchAll();
|
||||
|
||||
return useSelector(selectPluginErrors);
|
||||
};
|
||||
|
||||
export const useInstall = () => {
|
||||
const dispatch = useDispatch();
|
||||
return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating }));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createSlice, createEntityAdapter, AnyAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, createEntityAdapter, Reducer, AnyAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { fetchAll, fetchDetails, install, uninstall, loadPluginDashboards, panelPluginLoaded } from './actions';
|
||||
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
|
||||
import { STATE_PREFIX } from '../constants';
|
||||
@ -97,4 +97,4 @@ const slice = createSlice({
|
||||
});
|
||||
|
||||
export const { setDisplayMode } = slice.actions;
|
||||
export const { reducer } = slice;
|
||||
export const reducer: Reducer<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 { pluginsAdapter } from './reducer';
|
||||
|
||||
@ -49,6 +50,20 @@ export const find = (searchBy: string, filterBy: string, filterByType: string) =
|
||||
}
|
||||
);
|
||||
|
||||
export const selectPluginErrors = createSelector(selectAll, (plugins) =>
|
||||
plugins
|
||||
? plugins
|
||||
.filter((p) => Boolean(p.error))
|
||||
.map(
|
||||
(p): PluginError => ({
|
||||
pluginId: p.id,
|
||||
errorCode: p!.error as PluginErrorCode,
|
||||
})
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
// The following selectors are used to get information about the outstanding or completed plugins-related network requests.
|
||||
export const selectRequest = (actionType: string) =>
|
||||
createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import React, { Component } from 'react';
|
||||
import AppRootPage from './AppRootPage';
|
||||
import { getPluginSettings } from './PluginSettingsCache';
|
||||
import { importAppPlugin } from './plugin_loader';
|
||||
import { getMockPlugin } from './__mocks__/pluginMocks';
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
import { getMockPlugin } from '../__mocks__/pluginMocks';
|
||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
||||
import { Route, Router } from 'react-router-dom';
|
||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
|
||||
jest.mock('./PluginSettingsCache', () => ({
|
||||
jest.mock('../pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('./plugin_loader', () => ({
|
||||
jest.mock('../plugin_loader', () => ({
|
||||
importAppPlugin: jest.fn(),
|
||||
}));
|
||||
|
@ -4,8 +4,8 @@ import { AppEvents, AppPlugin, AppPluginMeta, KeyValue, NavModel, PluginType } f
|
||||
import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react-reverse-portal';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { getPluginSettings } from './PluginSettingsCache';
|
||||
import { importAppPlugin } from './plugin_loader';
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
@ -1,42 +1,19 @@
|
||||
import React from 'react';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
|
||||
import { StoreState } from '../../types';
|
||||
import { getAllPluginsErrors } from './state/selectors';
|
||||
import { loadPlugins, loadPluginsErrors } from './state/actions';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useGetErrors, useFetchStatus } from '../admin/state/hooks';
|
||||
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
errors: getAllPluginsErrors(state.plugins),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadPluginsErrors,
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
type PluginsErrorsInfoProps = ConnectedProps<typeof connector> & OwnProps;
|
||||
|
||||
export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
||||
loadPluginsErrors,
|
||||
errors,
|
||||
children,
|
||||
}) => {
|
||||
export function PluginsErrorsInfo(): React.ReactElement | null {
|
||||
const errors = useGetErrors();
|
||||
const { isLoading } = useFetchStatus();
|
||||
const theme = useTheme();
|
||||
|
||||
const { loading } = useAsync(async () => {
|
||||
await loadPluginsErrors();
|
||||
}, [loadPlugins]);
|
||||
|
||||
if (loading || errors.length === 0) {
|
||||
if (isLoading || errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
aria-label={selectors.pages.PluginsList.signatureErrorNotice}
|
||||
@ -55,16 +32,16 @@ export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
||||
className={css`
|
||||
list-style-type: circle;
|
||||
`}
|
||||
renderItem={(e) => (
|
||||
renderItem={(error) => (
|
||||
<div
|
||||
className={css`
|
||||
margin-top: ${theme.spacing.sm};
|
||||
`}
|
||||
>
|
||||
<HorizontalGroup spacing="sm" justify="flex-start" align="center">
|
||||
<strong>{e.pluginId}</strong>
|
||||
<strong>{error.pluginId}</strong>
|
||||
<PluginSignatureBadge
|
||||
status={mapPluginErrorCodeToSignatureStatus(e.errorCode)}
|
||||
status={mapPluginErrorCodeToSignatureStatus(error.errorCode)}
|
||||
className={css`
|
||||
margin-top: 0;
|
||||
`}
|
||||
@ -73,13 +50,10 @@ export const PluginsErrorsInfoUnconnected: React.FC<PluginsErrorsInfoProps> = ({
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</InfoBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const PluginsErrorsInfo = connect(mapStateToProps, mapDispatchToProps)(PluginsErrorsInfoUnconnected);
|
||||
}
|
||||
|
||||
function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
|
||||
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) {}
|
||||
}
|
||||
|
||||
jest.mock('./plugin_loader', () => ({
|
||||
jest.mock('../plugin_loader', () => ({
|
||||
importDataSourcePlugin: (meta: DataSourcePluginMeta) => {
|
||||
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', () => {
|
||||
const now = 12345;
|
@ -17,7 +17,7 @@ import { SystemJS } from '@grafana/runtime';
|
||||
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
|
||||
|
||||
// Loaded after the `unmock` abve
|
||||
import { importAppPlugin } from './plugin_loader';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
|
||||
class MyCustomApp extends AppPlugin {
|
||||
initWasCalled = false;
|
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 { Redirect } from 'react-router-dom';
|
||||
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
||||
import { getPluginsAdminRoutes } from 'app/features/plugins/routes';
|
||||
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
||||
import { getAlertingRoutes } from 'app/features/alerting/routes';
|
||||
@ -150,7 +150,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
exact: false,
|
||||
// Someday * and will get a ReactRouter under that path!
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/AppRootPage')
|
||||
() => import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/components/AppRootPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -372,7 +372,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
|
||||
),
|
||||
},
|
||||
...getPluginsAdminRoutes(),
|
||||
...getPluginCatalogRoutes(),
|
||||
...getLiveRoutes(),
|
||||
...getAlertingRoutes(),
|
||||
...extraRoutes,
|
||||
|
Loading…
Reference in New Issue
Block a user