Plugins: Unifying alpha state & options for all plugins (#16530)

* app pages

* app pages

* workign example

* started alpha support

* remove app stuff

* show warning on alpha/beta panels

* put app back on plugin file

* fix go

* add enum for PluginType and PluginIncludeType

* Refactoring and moving settings to plugins section

fixes #16529
This commit is contained in:
Ryan McKinley 2019-04-12 04:46:42 -07:00 committed by Torkel Ödegaard
parent 30dcf0f6c5
commit 3c21a121eb
18 changed files with 154 additions and 114 deletions

View File

@ -613,8 +613,13 @@ server_url =
callback_url = callback_url =
[panels] [panels]
# here for to support old env variables, can remove after a few months
enable_alpha = false enable_alpha = false
disable_sanitize_html = false disable_sanitize_html = false
[plugins]
enable_alpha = false
app_tls_skip_verify_insecure = false
[enterprise] [enterprise]
license_path = license_path =

View File

@ -540,7 +540,10 @@ log_queries =
;license_path = ;license_path =
[panels] [panels]
;enable_alpha = false
# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. # If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
;disable_sanitize_html = false ;disable_sanitize_html = false
[plugins]
;enable_alpha = false
;app_tls_skip_verify_insecure = false

View File

@ -666,11 +666,14 @@ Default setting for max attempts to sending alert notifications. Default value i
## [panels] ## [panels]
### enable_alpha
Set to true if you want to test panels that are not yet ready for general usage.
### disable_sanitize_html ### disable_sanitize_html
If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. Default If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. Default
is false. This settings was introduced in Grafana v6.0. is false. This settings was introduced in Grafana v6.0.
## [plugins]
### enable_alpha
Set to true if you want to test alpha plugins that are not yet ready for general usage.

View File

@ -1,10 +1,25 @@
export enum PluginState {
alpha = 'alpha', // Only included it `enable_alpha` is true
beta = 'beta', // Will show a warning banner
}
export enum PluginType {
panel = 'panel',
datasource = 'datasource',
app = 'app',
}
export interface PluginMeta { export interface PluginMeta {
id: string; id: string;
name: string; name: string;
info: PluginMetaInfo; info: PluginMetaInfo;
includes: PluginInclude[];
module: string; module: string;
baseUrl: string; includes?: PluginInclude[];
baseUrl?: string;
type: PluginType;
enabled?: boolean;
state?: PluginState;
// Datasource-specific // Datasource-specific
builtIn?: boolean; builtIn?: boolean;
@ -24,8 +39,17 @@ interface PluginMetaQueryOptions {
minInterval?: boolean; minInterval?: boolean;
} }
export enum PluginIncludeType {
dashboard = 'dashboard',
page = 'page',
// Only valid for apps
panel = 'panel',
datasource = 'datasource',
}
export interface PluginInclude { export interface PluginInclude {
type: string; type: PluginIncludeType;
name: string; name: string;
path: string; path: string;
} }

View File

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1" macaron "gopkg.in/macaron.v1"
) )
@ -21,7 +20,7 @@ var pluginProxyTransport *http.Transport
func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) { func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
pluginProxyTransport = &http.Transport{ pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS, InsecureSkipVerify: hs.Cfg.PluginsAppsSkipVerifyTLS,
Renegotiation: tls.RenegotiateFreelyAsClient, Renegotiation: tls.RenegotiateFreelyAsClient,
}, },
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,

View File

@ -145,7 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
panels := map[string]interface{}{} panels := map[string]interface{}{}
for _, panel := range enabledPlugins.Panels { for _, panel := range enabledPlugins.Panels {
if panel.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels { if panel.State == plugins.PluginStateAlpha && !hs.Cfg.PluginsEnableAlpha {
continue continue
} }
@ -162,6 +162,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"hideFromList": panel.HideFromList, "hideFromList": panel.HideFromList,
"sort": getPanelSort(panel.Id), "sort": getPanelSort(panel.Id),
"dataFormats": panel.DataFormats, "dataFormats": panel.DataFormats,
"state": panel.State,
} }
} }

View File

@ -39,7 +39,7 @@ func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
continue continue
} }
if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels { if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.PluginsEnableAlpha {
continue continue
} }

View File

@ -142,9 +142,6 @@ var (
// Basic Auth // Basic Auth
BasicAuthEnabled bool BasicAuthEnabled bool
// Plugin settings
PluginAppsSkipVerifyTLS bool
// Session settings. // Session settings.
SessionOptions session.Options SessionOptions session.Options
SessionConnMaxLifetime int64 SessionConnMaxLifetime int64
@ -233,7 +230,8 @@ type Cfg struct {
MetricsEndpointEnabled bool MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool PluginsEnableAlpha bool
PluginsAppsSkipVerifyTLS bool
DisableSanitizeHtml bool DisableSanitizeHtml bool
EnterpriseLicensePath string EnterpriseLicensePath string
@ -721,9 +719,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
authBasic := iniFile.Section("auth.basic") authBasic := iniFile.Section("auth.basic")
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
// global plugin settings
PluginAppsSkipVerifyTLS = iniFile.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false)
// Rendering // Rendering
renderSec := iniFile.Section("rendering") renderSec := iniFile.Section("rendering")
cfg.RendererUrl = renderSec.Key("server_url").String() cfg.RendererUrl = renderSec.Key("server_url").String()
@ -771,9 +766,17 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
explore := iniFile.Section("explore") explore := iniFile.Section("explore")
ExploreEnabled = explore.Key("enabled").MustBool(true) ExploreEnabled = explore.Key("enabled").MustBool(true)
panels := iniFile.Section("panels") panelsSection := iniFile.Section("panels")
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false) cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false)
cfg.DisableSanitizeHtml = panels.Key("disable_sanitize_html").MustBool(false)
pluginsSection := iniFile.Section("plugins")
cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false)
cfg.PluginsAppsSkipVerifyTLS = iniFile.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false)
// check old location for this option
if panelsSection.Key("enable_alpha").MustBool(false) {
cfg.PluginsEnableAlpha = true
}
cfg.readSessionConfig() cfg.readSessionConfig()
cfg.readSmtpSettings() cfg.readSmtpSettings()

View File

@ -7,7 +7,7 @@ import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
// Types // Types
import { PanelPlugin, AppNotificationSeverity } from 'app/types'; import { PanelPlugin, AppNotificationSeverity } from 'app/types';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; import { PanelProps, ReactPanelPlugin, PluginType } from '@grafana/ui';
interface Props { interface Props {
pluginId: string; pluginId: string;
@ -45,6 +45,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
id: id, id: id,
name: id, name: id,
sort: 100, sort: 100,
type: PluginType.panel,
module: '', module: '',
baseUrl: '', baseUrl: '',
dataFormats: [], dataFormats: [],

View File

@ -18,6 +18,7 @@ import { PanelModel } from '../state';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
import { PanelPlugin } from 'app/types/plugins'; import { PanelPlugin } from 'app/types/plugins';
import { VizPickerSearch } from './VizPickerSearch'; import { VizPickerSearch } from './VizPickerSearch';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
@ -238,6 +239,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
onClose={this.onCloseVizPicker} onClose={this.onCloseVizPicker}
/> />
</FadeIn> </FadeIn>
<PluginStateinfo state={plugin.state} />
{this.renderPanelOptions()} {this.renderPanelOptions()}
</> </>
</EditorTabBody> </EditorTabBody>

View File

@ -24,6 +24,7 @@ import { getRouteParamsId } from 'app/core/selectors/location';
import { NavModel, Plugin, StoreState } from 'app/types/'; import { NavModel, Plugin, StoreState } from 'app/types/';
import { DataSourceSettings } from '@grafana/ui/src/types/'; import { DataSourceSettings } from '@grafana/ui/src/types/';
import { getDataSourceLoadingNav } from '../state/navModel'; import { getDataSourceLoadingNav } from '../state/navModel';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
export interface Props { export interface Props {
navModel: NavModel; navModel: NavModel;
@ -44,11 +45,6 @@ interface State {
testingStatus?: string; testingStatus?: string;
} }
enum DataSourceStates {
Alpha = 'alpha',
Beta = 'beta',
}
export class DataSourceSettingsPage extends PureComponent<Props, State> { export class DataSourceSettingsPage extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -110,32 +106,6 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
return this.props.dataSource.readOnly === true; return this.props.dataSource.readOnly === true;
} }
shouldRenderInfoBox() {
const { state } = this.props.dataSourceMeta;
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
}
getInfoText() {
const { dataSourceMeta } = this.props;
switch (dataSourceMeta.state) {
case DataSourceStates.Alpha:
return (
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
' will include breaking changes.'
);
case DataSourceStates.Beta:
return (
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
' development and could be missing important features.'
);
}
return null;
}
renderIsReadOnlyMessage() { renderIsReadOnlyMessage() {
return ( return (
<div className="grafana-info-box span8"> <div className="grafana-info-box span8">
@ -196,7 +166,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
<div> <div>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
{this.isReadOnly() && this.renderIsReadOnlyMessage()} {this.isReadOnly() && this.renderIsReadOnlyMessage()}
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>} <PluginStateinfo state={dataSourceMeta.state} />
<BasicSettings <BasicSettings
dataSourceName={dataSource.name} dataSourceName={dataSource.name}

View File

@ -11,11 +11,9 @@ exports[`Render should render alpha info text 1`] = `
<form <form
onSubmit={[Function]} onSubmit={[Function]}
> >
<div <PluginStateinfo
className="grafana-info-box" state="alpha"
> />
This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes.
</div>
<BasicSettings <BasicSettings
dataSourceName="gdev-cloudwatch" dataSourceName="gdev-cloudwatch"
isDefault={false} isDefault={false}
@ -49,6 +47,7 @@ exports[`Render should render alpha info text 1`] = `
} }
dataSourceMeta={ dataSourceMeta={
Object { Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url", "defaultNavUrl": "some/url",
"enabled": false, "enabled": false,
"hasUpdate": false, "hasUpdate": false,
@ -78,11 +77,11 @@ exports[`Render should render alpha info text 1`] = `
"version": "1", "version": "1",
}, },
"latestVersion": "1", "latestVersion": "1",
"module": Object {}, "module": "path/to/module",
"name": "pretty cool plugin 1", "name": "pretty cool plugin 1",
"pinned": false, "pinned": false,
"state": "alpha", "state": "alpha",
"type": "", "type": "panel",
} }
} }
onModelChange={[Function]} onModelChange={[Function]}
@ -113,11 +112,9 @@ exports[`Render should render beta info text 1`] = `
<form <form
onSubmit={[Function]} onSubmit={[Function]}
> >
<div <PluginStateinfo
className="grafana-info-box" state="beta"
> />
This plugin is marked as being in a beta development state. This means it is in currently in active development and could be missing important features.
</div>
<BasicSettings <BasicSettings
dataSourceName="gdev-cloudwatch" dataSourceName="gdev-cloudwatch"
isDefault={false} isDefault={false}
@ -151,6 +148,7 @@ exports[`Render should render beta info text 1`] = `
} }
dataSourceMeta={ dataSourceMeta={
Object { Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url", "defaultNavUrl": "some/url",
"enabled": false, "enabled": false,
"hasUpdate": false, "hasUpdate": false,
@ -180,11 +178,11 @@ exports[`Render should render beta info text 1`] = `
"version": "1", "version": "1",
}, },
"latestVersion": "1", "latestVersion": "1",
"module": Object {}, "module": "path/to/module",
"name": "pretty cool plugin 1", "name": "pretty cool plugin 1",
"pinned": false, "pinned": false,
"state": "beta", "state": "beta",
"type": "", "type": "panel",
} }
} }
onModelChange={[Function]} onModelChange={[Function]}
@ -215,6 +213,7 @@ exports[`Render should render component 1`] = `
<form <form
onSubmit={[Function]} onSubmit={[Function]}
> >
<PluginStateinfo />
<BasicSettings <BasicSettings
dataSourceName="gdev-cloudwatch" dataSourceName="gdev-cloudwatch"
isDefault={false} isDefault={false}
@ -248,6 +247,7 @@ exports[`Render should render component 1`] = `
} }
dataSourceMeta={ dataSourceMeta={
Object { Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url", "defaultNavUrl": "some/url",
"enabled": false, "enabled": false,
"hasUpdate": false, "hasUpdate": false,
@ -277,11 +277,10 @@ exports[`Render should render component 1`] = `
"version": "1", "version": "1",
}, },
"latestVersion": "1", "latestVersion": "1",
"module": Object {}, "module": "path/to/module",
"name": "pretty cool plugin 1", "name": "pretty cool plugin 1",
"pinned": false, "pinned": false,
"state": "", "type": "panel",
"type": "",
} }
} }
onModelChange={[Function]} onModelChange={[Function]}
@ -317,6 +316,7 @@ exports[`Render should render is ready only message 1`] = `
> >
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource. This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
</div> </div>
<PluginStateinfo />
<BasicSettings <BasicSettings
dataSourceName="gdev-cloudwatch" dataSourceName="gdev-cloudwatch"
isDefault={false} isDefault={false}
@ -350,6 +350,7 @@ exports[`Render should render is ready only message 1`] = `
} }
dataSourceMeta={ dataSourceMeta={
Object { Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url", "defaultNavUrl": "some/url",
"enabled": false, "enabled": false,
"hasUpdate": false, "hasUpdate": false,
@ -379,11 +380,10 @@ exports[`Render should render is ready only message 1`] = `
"version": "1", "version": "1",
}, },
"latestVersion": "1", "latestVersion": "1",
"module": Object {}, "module": "path/to/module",
"name": "pretty cool plugin 1", "name": "pretty cool plugin 1",
"pinned": false, "pinned": false,
"state": "", "type": "panel",
"type": "",
} }
} }
onModelChange={[Function]} onModelChange={[Function]}

View File

@ -1,5 +1,5 @@
import { NavModel, NavModelItem } from 'app/types'; import { NavModel, NavModelItem } from 'app/types';
import { PluginMeta, DataSourceSettings } from '@grafana/ui/src/types'; import { PluginMeta, DataSourceSettings, PluginType } from '@grafana/ui/src/types';
import config from 'app/core/config'; import config from 'app/core/config';
export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: PluginMeta): NavModelItem { export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: PluginMeta): NavModelItem {
@ -67,6 +67,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
}, },
{ {
id: '1', id: '1',
type: PluginType.datasource,
name: '', name: '',
info: { info: {
author: { author: {
@ -83,7 +84,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
updated: '', updated: '',
version: '', version: '',
}, },
includes: [{ type: '', name: '', path: '' }], includes: [],
module: '', module: '',
baseUrl: '', baseUrl: '',
} }

View File

@ -14,10 +14,11 @@ import {
} from './actions'; } from './actions';
import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks'; import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks';
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector'; import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
import { DataSourcesState } from 'app/types'; import { DataSourcesState, Plugin } from 'app/types';
import { PluginMetaInfo } from '@grafana/ui'; import { PluginMetaInfo, PluginType } from '@grafana/ui';
const mockPlugin = () => ({ const mockPlugin = () =>
({
defaultNavUrl: 'defaultNavUrl', defaultNavUrl: 'defaultNavUrl',
enabled: true, enabled: true,
hasUpdate: true, hasUpdate: true,
@ -26,10 +27,9 @@ const mockPlugin = () => ({
latestVersion: 'latestVersion', latestVersion: 'latestVersion',
name: 'name', name: 'name',
pinned: true, pinned: true,
state: 'state', type: PluginType.datasource,
type: 'type', module: 'path/to/module',
module: {}, } as Plugin);
});
describe('dataSourcesReducer', () => { describe('dataSourcesReducer', () => {
describe('when dataSourcesLoaded is dispatched', () => { describe('when dataSourcesLoaded is dispatched', () => {

View File

@ -0,0 +1,34 @@
import React, { FC } from 'react';
import { PluginState } from '@grafana/ui';
interface Props {
state?: PluginState;
}
function getPluginStateInfoText(state?: PluginState): string | null {
switch (state) {
case PluginState.alpha:
return (
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
' will include breaking changes.'
);
case PluginState.beta:
return (
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
' development and could be missing important features.'
);
}
return null;
}
const PluginStateinfo: FC<Props> = props => {
const text = getPluginStateInfoText(props.state);
if (!text) {
return null;
}
return <div className="grafana-info-box">{text}</div>;
};
export default PluginStateinfo;

View File

@ -1,4 +1,5 @@
import { Plugin, PanelPlugin, PanelDataFormat } from 'app/types'; import { Plugin, PanelPlugin, PanelDataFormat } from 'app/types';
import { PluginType } from '@grafana/ui';
export const getMockPlugins = (amount: number): Plugin[] => { export const getMockPlugins = (amount: number): Plugin[] => {
const plugins = []; const plugins = [];
@ -36,6 +37,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
export const getPanelPlugin = (options: Partial<PanelPlugin>): PanelPlugin => { export const getPanelPlugin = (options: Partial<PanelPlugin>): PanelPlugin => {
return { return {
id: options.id, id: options.id,
type: PluginType.panel,
name: options.id, name: options.id,
sort: options.sort || 1, sort: options.sort || 1,
dataFormats: [PanelDataFormat.TimeSeries], dataFormats: [PanelDataFormat.TimeSeries],
@ -81,9 +83,9 @@ export const getMockPlugin = () => {
}, },
latestVersion: '1', latestVersion: '1',
name: 'pretty cool plugin 1', name: 'pretty cool plugin 1',
baseUrl: 'path/to/plugin',
pinned: false, pinned: false,
state: '', type: PluginType.panel,
type: '', module: 'path/to/module',
module: {}, } as Plugin;
};
}; };

View File

@ -15,8 +15,9 @@ exports[`Render should render component 1`] = `
className="card-item-type" className="card-item-type"
> >
<i <i
className="icon-gf icon-gf-" className="icon-gf icon-gf-panel"
/> />
panel
</div> </div>
</div> </div>
<div <div
@ -63,8 +64,9 @@ exports[`Render should render has plugin section 1`] = `
className="card-item-type" className="card-item-type"
> >
<i <i
className="icon-gf icon-gf-" className="icon-gf icon-gf-panel"
/> />
panel
</div> </div>
<div <div
className="card-item-notice" className="card-item-notice"

View File

@ -1,10 +1,7 @@
import { AngularPanelPlugin, ReactPanelPlugin, PluginMetaInfo } from '@grafana/ui/src/types'; import { AngularPanelPlugin, ReactPanelPlugin, PluginMetaInfo, PluginMeta } from '@grafana/ui/src/types';
export interface PanelPlugin { export interface PanelPlugin extends PluginMeta {
id: string;
name: string;
hideFromList?: boolean; hideFromList?: boolean;
module: string;
baseUrl: string; baseUrl: string;
info: PluginMetaInfo; info: PluginMetaInfo;
sort: number; sort: number;
@ -19,18 +16,11 @@ export enum PanelDataFormat {
TimeSeries = 'time_series', TimeSeries = 'time_series',
} }
export interface Plugin { export interface Plugin extends PluginMeta {
defaultNavUrl: string; defaultNavUrl: string;
enabled: boolean;
hasUpdate: boolean; hasUpdate: boolean;
id: string;
info: PluginMetaInfo;
latestVersion: string; latestVersion: string;
name: string;
pinned: boolean; pinned: boolean;
state: string;
type: string;
module: any;
} }
export interface PluginDashboard { export interface PluginDashboard {