diff --git a/pkg/api/index.go b/pkg/api/index.go index d4103b0aa52..7a597ba90ae 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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) } diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx index e7a751d7882..da8e654e998 100644 --- a/public/app/features/plugins/PluginPage.tsx +++ b/public/app/features/plugins/PluginPage.tsx @@ -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 { } 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 { 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 { 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 { render() { const { loading, nav, plugin } = this.state; + const { $contextSrv } = this.props; + const isAdmin = $contextSrv.hasRole('Admin'); return ( @@ -379,7 +302,7 @@ class PluginPage extends PureComponent { {plugin && (
{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)}
@@ -393,6 +316,106 @@ class PluginPage extends PureComponent { } } +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': diff --git a/public/app/plugins/panel/pluginlist/module.html b/public/app/plugins/panel/pluginlist/module.html index d182699f4bd..1ab69bbc62a 100644 --- a/public/app/plugins/panel/pluginlist/module.html +++ b/public/app/plugins/panel/pluginlist/module.html @@ -10,14 +10,16 @@ {{plugin.name}} v{{plugin.info.version}} - - Update available! - - - Enable now - - - Up to date + + + Update available! + + + Enable now + + + Up to date + diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 3aecced2ef1..c6ca83f248a 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -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 = [ diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 75b5831bf92..117df53b8b1 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -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, }; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 26746f82168..a854b7e1ce6 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -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: '', diff --git a/public/dashboards/home.json b/public/dashboards/home.json index f2c441053bb..88709a767ef 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -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",