Commit 7f1214ac by Andrej Ocenas Committed by GitHub

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.
parent 3ba2388a
......@@ -308,6 +308,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)
}
if c.IsGrafanaAdmin {
......
......@@ -29,6 +29,7 @@ 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';
export function getLoadingNav(): NavModel {
const node = {
......@@ -69,6 +70,7 @@ interface Props {
pluginId: string;
query: UrlQueryMap;
path: string; // the URL path
$contextSrv: ContextSrv;
}
interface State {
......@@ -93,7 +95,7 @@ class PluginPage extends PureComponent<Props, State> {
}
async componentDidMount() {
const { pluginId, path, query } = this.props;
const { pluginId, path, query, $contextSrv } = this.props;
const { appSubUrl } = config;
const plugin = await loadPlugin(pluginId);
......@@ -105,97 +107,16 @@ class PluginPage extends PureComponent<Props, State> {
return; // 404
}
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,
});
}
// 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),
};
const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin'));
this.setState({
loading: false,
plugin,
defaultPage,
nav: {
node: node,
main: node,
},
nav,
});
}
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) {
const prevPage = prevProps.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 node = {
...nav.node,
children: this.setActivePage(page, nav.node.children, defaultPage),
children: setActivePage(page, nav.node.children, defaultPage),
};
this.setState({
nav: {
......@@ -369,6 +290,8 @@ class PluginPage extends PureComponent<Props, State> {
render() {
const { loading, nav, plugin } = this.state;
const { $contextSrv } = this.props;
const isAdmin = $contextSrv.hasRole('Admin');
return (
<Page navModel={nav}>
<Page.Contents isLoading={loading}>
......@@ -379,7 +302,7 @@ class PluginPage extends PureComponent<Props, State> {
{plugin && (
<section className="page-sidebar-section">
{this.renderVersionInfo(plugin.meta)}
{this.renderSidebarIncludes(plugin.meta.includes)}
{isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
{this.renderSidebarDependencies(plugin.meta.dependencies)}
{this.renderSidebarLinks(plugin.meta.info)}
</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) {
switch (type) {
case 'datasource':
......
......@@ -10,6 +10,7 @@
<span class="pluginlist-title">{{plugin.name}}</span>
<span class="pluginlist-version">v{{plugin.info.version}}</span>
</span>
<span ng-if="ctrl.isAdmin">
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
Update available!
</span>
......@@ -19,6 +20,7 @@
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
Up to date
</span>
</span>
</a>
</div>
<div class="pluginlist-item" ng-show="category.list.length === 0">
......
......@@ -2,6 +2,7 @@ import _ from 'lodash';
import { PanelCtrl } from '../../../features/panel/panel_ctrl';
import { auto } from 'angular';
import { BackendSrv } from '@grafana/runtime';
import { ContextSrv } from '../../../core/services/context_srv';
class PluginListCtrl extends PanelCtrl {
static templateUrl = 'module.html';
......@@ -9,16 +10,18 @@ class PluginListCtrl extends PanelCtrl {
pluginList: any[];
viewModel: any;
isAdmin: boolean;
// Set and populate defaults
panelDefaults = {};
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) {
constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv, contextSrv: ContextSrv) {
super($scope, $injector);
_.defaults(this.panel, this.panelDefaults);
this.isAdmin = contextSrv.hasRole('Admin');
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.pluginList = [];
this.viewModel = [
......
......@@ -41,6 +41,7 @@ export function reactContainer($route: any, $location: any, $injector: any, $roo
$injector: $injector,
$rootScope: $rootScope,
$scope: scope,
$contextSrv: contextSrv,
routeInfo: $route.current.$$route.routeInfo,
};
......
......@@ -35,6 +35,9 @@ import { DashboardRouteInfo } from 'app/types';
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
$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
.when('/', {
template: '<react-container />',
......
......@@ -77,29 +77,8 @@
},
"timepicker": {
"hidden": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"],
"type": "timepicker"
},
"timezone": "browser",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment