mirror of https://github.com/grafana/grafana
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 controlspull/41857/head
parent
98f87c4c49
commit
35c2c95fdc
@ -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'; |
||||
|
@ -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,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(), |
||||
})); |
||||
|
@ -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; |
@ -1,4 +1,4 @@ |
||||
import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster'; |
||||
import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from '../pluginCacheBuster'; |
||||
|
||||
describe('PluginCacheBuster', () => { |
||||
const now = 12345; |
@ -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; |
||||
} |
Loading…
Reference in new issue