mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Permissions: Show plugins in nav for non admin users but hide plugin configuration (#18234)
Allow non admins to see plugins list but only with readme. Any config tabs are hidden from the plugin page. Also plugin panel does not show action buttons (like Enable) for non admins.
This commit is contained in:
parent
3ba2388af7
commit
7f1214ac46
@ -307,6 +307,25 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, cfgNode)
|
||||||
|
} else {
|
||||||
|
cfgNode := &dtos.NavLink{
|
||||||
|
Id: "cfg",
|
||||||
|
Text: "Configuration",
|
||||||
|
SubTitle: "Organization: " + c.OrgName,
|
||||||
|
Icon: "gicon gicon-cog",
|
||||||
|
Url: setting.AppSubUrl + "/plugins",
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{
|
||||||
|
Text: "Plugins",
|
||||||
|
Id: "plugins",
|
||||||
|
Description: "View and configure plugins",
|
||||||
|
Icon: "gicon gicon-plugins",
|
||||||
|
Url: setting.AppSubUrl + "/plugins",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
data.NavTree = append(data.NavTree, cfgNode)
|
data.NavTree = append(data.NavTree, cfgNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper';
|
|||||||
import { PluginDashboards } from './PluginDashboards';
|
import { PluginDashboards } from './PluginDashboards';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
import { ContextSrv } from '../../core/services/context_srv';
|
||||||
|
|
||||||
export function getLoadingNav(): NavModel {
|
export function getLoadingNav(): NavModel {
|
||||||
const node = {
|
const node = {
|
||||||
@ -69,6 +70,7 @@ interface Props {
|
|||||||
pluginId: string;
|
pluginId: string;
|
||||||
query: UrlQueryMap;
|
query: UrlQueryMap;
|
||||||
path: string; // the URL path
|
path: string; // the URL path
|
||||||
|
$contextSrv: ContextSrv;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -93,7 +95,7 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { pluginId, path, query } = this.props;
|
const { pluginId, path, query, $contextSrv } = this.props;
|
||||||
const { appSubUrl } = config;
|
const { appSubUrl } = config;
|
||||||
|
|
||||||
const plugin = await loadPlugin(pluginId);
|
const plugin = await loadPlugin(pluginId);
|
||||||
@ -105,97 +107,16 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
return; // 404
|
return; // 404
|
||||||
}
|
}
|
||||||
|
|
||||||
const { meta } = plugin;
|
const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin'));
|
||||||
let defaultPage: string;
|
|
||||||
const pages: NavModelItem[] = [];
|
|
||||||
|
|
||||||
if (true) {
|
|
||||||
pages.push({
|
|
||||||
text: 'Readme',
|
|
||||||
icon: 'fa fa-fw fa-file-text-o',
|
|
||||||
url: `${appSubUrl}${path}?page=${PAGE_ID_README}`,
|
|
||||||
id: PAGE_ID_README,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show Config/Pages for app
|
|
||||||
if (meta.type === PluginType.app) {
|
|
||||||
// Legacy App Config
|
|
||||||
if (plugin.angularConfigCtrl) {
|
|
||||||
pages.push({
|
|
||||||
text: 'Config',
|
|
||||||
icon: 'gicon gicon-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: path + '?page=' + page.id,
|
|
||||||
id: page.id,
|
|
||||||
});
|
|
||||||
if (!defaultPage) {
|
|
||||||
defaultPage = page.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the dashboard pages
|
|
||||||
if (find(meta.includes, { type: 'dashboard' })) {
|
|
||||||
pages.push({
|
|
||||||
text: 'Dashboards',
|
|
||||||
icon: 'gicon gicon-dashboard',
|
|
||||||
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: this.setActivePage(query.page as string, pages, defaultPage),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
plugin,
|
plugin,
|
||||||
defaultPage,
|
defaultPage,
|
||||||
nav: {
|
nav,
|
||||||
node: node,
|
|
||||||
main: node,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
const prevPage = prevProps.query.page as string;
|
const prevPage = prevProps.query.page as string;
|
||||||
const page = this.props.query.page as string;
|
const page = this.props.query.page as string;
|
||||||
@ -203,7 +124,7 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
const { nav, defaultPage } = this.state;
|
const { nav, defaultPage } = this.state;
|
||||||
const node = {
|
const node = {
|
||||||
...nav.node,
|
...nav.node,
|
||||||
children: this.setActivePage(page, nav.node.children, defaultPage),
|
children: setActivePage(page, nav.node.children, defaultPage),
|
||||||
};
|
};
|
||||||
this.setState({
|
this.setState({
|
||||||
nav: {
|
nav: {
|
||||||
@ -369,6 +290,8 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, nav, plugin } = this.state;
|
const { loading, nav, plugin } = this.state;
|
||||||
|
const { $contextSrv } = this.props;
|
||||||
|
const isAdmin = $contextSrv.hasRole('Admin');
|
||||||
return (
|
return (
|
||||||
<Page navModel={nav}>
|
<Page navModel={nav}>
|
||||||
<Page.Contents isLoading={loading}>
|
<Page.Contents isLoading={loading}>
|
||||||
@ -379,7 +302,7 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
{plugin && (
|
{plugin && (
|
||||||
<section className="page-sidebar-section">
|
<section className="page-sidebar-section">
|
||||||
{this.renderVersionInfo(plugin.meta)}
|
{this.renderVersionInfo(plugin.meta)}
|
||||||
{this.renderSidebarIncludes(plugin.meta.includes)}
|
{isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
|
||||||
{this.renderSidebarDependencies(plugin.meta.dependencies)}
|
{this.renderSidebarDependencies(plugin.meta.dependencies)}
|
||||||
{this.renderSidebarLinks(plugin.meta.info)}
|
{this.renderSidebarLinks(plugin.meta.info)}
|
||||||
</section>
|
</section>
|
||||||
@ -393,6 +316,106 @@ class PluginPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPluginTabsNav(
|
||||||
|
plugin: GrafanaPlugin,
|
||||||
|
appSubUrl: string,
|
||||||
|
path: string,
|
||||||
|
query: UrlQueryMap,
|
||||||
|
isAdmin: boolean
|
||||||
|
): { defaultPage: string; nav: NavModel } {
|
||||||
|
const { meta } = plugin;
|
||||||
|
let defaultPage: string;
|
||||||
|
const pages: NavModelItem[] = [];
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
pages.push({
|
||||||
|
text: 'Readme',
|
||||||
|
icon: 'fa fa-fw fa-file-text-o',
|
||||||
|
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: 'gicon gicon-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: path + '?page=' + page.id,
|
||||||
|
id: page.id,
|
||||||
|
});
|
||||||
|
if (!defaultPage) {
|
||||||
|
defaultPage = page.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the dashboard pages
|
||||||
|
if (find(meta.includes, { type: 'dashboard' })) {
|
||||||
|
pages.push({
|
||||||
|
text: 'Dashboards',
|
||||||
|
icon: 'gicon gicon-dashboard',
|
||||||
|
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,
|
||||||
|
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) {
|
function getPluginIcon(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'datasource':
|
case 'datasource':
|
||||||
|
@ -10,14 +10,16 @@
|
|||||||
<span class="pluginlist-title">{{plugin.name}}</span>
|
<span class="pluginlist-title">{{plugin.name}}</span>
|
||||||
<span class="pluginlist-version">v{{plugin.info.version}}</span>
|
<span class="pluginlist-version">v{{plugin.info.version}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
|
<span ng-if="ctrl.isAdmin">
|
||||||
Update available!
|
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
|
||||||
</span>
|
Update available!
|
||||||
<span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
|
</span>
|
||||||
Enable now
|
<span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
|
||||||
</span>
|
Enable now
|
||||||
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
|
</span>
|
||||||
Up to date
|
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
|
||||||
|
Up to date
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
import { PanelCtrl } from '../../../features/panel/panel_ctrl';
|
import { PanelCtrl } from '../../../features/panel/panel_ctrl';
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { BackendSrv } from '@grafana/runtime';
|
import { BackendSrv } from '@grafana/runtime';
|
||||||
|
import { ContextSrv } from '../../../core/services/context_srv';
|
||||||
|
|
||||||
class PluginListCtrl extends PanelCtrl {
|
class PluginListCtrl extends PanelCtrl {
|
||||||
static templateUrl = 'module.html';
|
static templateUrl = 'module.html';
|
||||||
@ -9,16 +10,18 @@ class PluginListCtrl extends PanelCtrl {
|
|||||||
|
|
||||||
pluginList: any[];
|
pluginList: any[];
|
||||||
viewModel: any;
|
viewModel: any;
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
// Set and populate defaults
|
// Set and populate defaults
|
||||||
panelDefaults = {};
|
panelDefaults = {};
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) {
|
constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv, contextSrv: ContextSrv) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
|
|
||||||
_.defaults(this.panel, this.panelDefaults);
|
_.defaults(this.panel, this.panelDefaults);
|
||||||
|
|
||||||
|
this.isAdmin = contextSrv.hasRole('Admin');
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||||
this.pluginList = [];
|
this.pluginList = [];
|
||||||
this.viewModel = [
|
this.viewModel = [
|
||||||
|
@ -41,6 +41,7 @@ export function reactContainer($route: any, $location: any, $injector: any, $roo
|
|||||||
$injector: $injector,
|
$injector: $injector,
|
||||||
$rootScope: $rootScope,
|
$rootScope: $rootScope,
|
||||||
$scope: scope,
|
$scope: scope,
|
||||||
|
$contextSrv: contextSrv,
|
||||||
routeInfo: $route.current.$$route.routeInfo,
|
routeInfo: $route.current.$$route.routeInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ import { DashboardRouteInfo } from 'app/types';
|
|||||||
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
|
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
|
||||||
$locationProvider.html5Mode(true);
|
$locationProvider.html5Mode(true);
|
||||||
|
|
||||||
|
// Routes here are guarded both here and server side for react-container routes or just on the server for angular
|
||||||
|
// ones. That means angular ones could be navigated to in case there is a client side link some where.
|
||||||
|
|
||||||
$routeProvider
|
$routeProvider
|
||||||
.when('/', {
|
.when('/', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
|
@ -77,29 +77,8 @@
|
|||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"refresh_intervals": [
|
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||||
"5s",
|
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"],
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
],
|
|
||||||
"type": "timepicker"
|
"type": "timepicker"
|
||||||
},
|
},
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
|
Loading…
Reference in New Issue
Block a user