diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 5661ae1df91..8c7f505277d 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -24,11 +24,13 @@ type NavLink struct { Id string `json:"id,omitempty"` Text string `json:"text,omitempty"` Description string `json:"description,omitempty"` + SubTitle string `json:"subTitle,omitempty"` Icon string `json:"icon,omitempty"` Img string `json:"img,omitempty"` Url string `json:"url,omitempty"` Target string `json:"target,omitempty"` Divider bool `json:"divider,omitempty"` HideFromMenu bool `json:"hideFromMenu,omitempty"` + HideFromTabs bool `json:"hideFromTabs,omitempty"` Children []*NavLink `json:"children,omitempty"` } diff --git a/pkg/api/index.go b/pkg/api/index.go index bf5a128a03f..eb9e70bb404 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -101,15 +101,17 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } dashboardChildNavs := []*dtos.NavLink{ - {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home"}, + {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, + {Divider: true, HideFromTabs: true}, + {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"}, - {Text: "Dashboard List", Description: "Manage Dashboards And Folders", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-bars"}, } data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Dashboards", Id: "dashboards", + SubTitle: "Manage dashboards & folders", Icon: "gicon gicon-dashboard", Url: setting.AppSubUrl + "/", Children: dashboardChildNavs, @@ -117,22 +119,23 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.IsSignedIn { profileNode := &dtos.NavLink{ - Text: c.SignedInUser.Login, + Text: c.SignedInUser.Name, + SubTitle: c.SignedInUser.Login, Id: "profile", Img: data.User.GravatarUrl, Url: setting.AppSubUrl + "/profile", HideFromMenu: true, Children: []*dtos.NavLink{ - {Text: "Your profile", Url: setting.AppSubUrl + "/profile", Icon: "fa fa-fw fa-sliders"}, + {Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "fa fa-fw fa-sliders"}, {Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true}, }, } if !setting.DisableSignoutMenu { // add sign out first - profileNode.Children = append([]*dtos.NavLink{ - {Text: "Sign out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self"}, - }, profileNode.Children...) + profileNode.Children = append(profileNode.Children, &dtos.NavLink{ + Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self", + }) } data.NavTree = append(data.NavTree, profileNode) @@ -140,12 +143,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { alertChildNavs := []*dtos.NavLink{ - {Text: "Alert List", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"}, - {Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"}, + {Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"}, + {Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"}, } data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Alerting", + SubTitle: "Alert rules & notifications", Id: "alerting", Icon: "gicon gicon-alert", Url: setting.AppSubUrl + "/alerting/list", @@ -202,10 +206,11 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN { cfgNode := &dtos.NavLink{ - Id: "cfg", - Text: "Configuration", - Icon: "fa fa-fw fa-cogs", - Url: setting.AppSubUrl + "/configuration", + Id: "cfg", + Text: "Configuration", + SubTitle: "Organization: " + c.OrgName, + Icon: "fa fa-fw fa-cog", + Url: setting.AppSubUrl + "/datasources", Children: []*dtos.NavLink{ { Text: "Data Sources", @@ -213,29 +218,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Description: "Add and configure data sources", Id: "datasources", Url: setting.AppSubUrl + "/datasources", - Children: []*dtos.NavLink{ - {Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "gicon gicon-datasources"}, - {Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"}, - }, - }, - { - Text: "Preferences", - Id: "org", - Description: "Organization preferences", - Icon: "fa fa-fw fa-sliders", - Url: setting.AppSubUrl + "/org", - }, - { - Text: "Plugins", - Id: "plugins", - Description: "View and configure plugins", - Icon: "icon-gf icon-gf-fw icon-gf-apps", - Url: setting.AppSubUrl + "/plugins", - Children: []*dtos.NavLink{ - {Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"}, - {Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"}, - {Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"}, - }, }, { Text: "Members", @@ -245,12 +227,27 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Url: setting.AppSubUrl + "/org/users", }, { - Text: "Groups", - Id: "users", + Text: "Teams", + Id: "teams", Description: "Manage org groups", - Icon: "fa fa-fw fa-users", + Icon: "gicon gicon-user-group", Url: setting.AppSubUrl + "/org/user-groups", }, + { + Text: "Plugins", + Id: "plugins", + Description: "View and configure plugins", + Icon: "icon-gf icon-gf-fw icon-gf-apps", + Url: setting.AppSubUrl + "/plugins", + }, + { + Text: "Preferences", + Id: "org-settings", + Description: "Organization preferences", + Icon: "fa fa-fw fa-sliders", + Url: setting.AppSubUrl + "/org", + }, + { Text: "API Keys", Id: "apikeys", @@ -263,16 +260,21 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.IsGrafanaAdmin { cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ - Text: "Server Admin", - Id: "admin", - Icon: "fa fa-fw fa-shield", - Url: setting.AppSubUrl + "/admin", + Divider: true, HideFromTabs: true, + }) + cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ + Text: "Server Admin", + HideFromTabs: true, + SubTitle: "Manage all users & orgs", + Id: "admin", + Icon: "fa fa-fw fa-shield", + Url: setting.AppSubUrl + "/admin/users", Children: []*dtos.NavLink{ - {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats"}, - {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide"}, + {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "icon-gf icon-gf-fw icon-gf-users"}, + {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"}, + {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "fa fa-fw fa-sliders"}, + {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"}, + {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"}, }, }) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index eadf4cd15e5..1d84d8250a6 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -68,6 +68,15 @@ func NewDashboard(title string) *Dashboard { return dash } +// NewDashboardFolder creates a new dashboard folder +func NewDashboardFolder(title string) *Dashboard { + folder := NewDashboard(title) + folder.Data.Set("schemaVersion", 16) + folder.Data.Set("editable", true) + folder.Data.Set("hideControls", true) + return folder +} + // GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { return dash.Data.Get("tags").MustStringArray() diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 874b4ac4a0e..bf516818e3c 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -49,10 +49,6 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil { return err } - - if err = createDashboardFolderForPlugin(cmd, dashboard); err != nil { - return err - } } else { dashboard = m.NewDashboardFromJson(cmd.Dashboard) } @@ -93,63 +89,6 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { return nil } -func createDashboardFolderForPlugin(cmd *ImportDashboardCommand, dashboard *m.Dashboard) error { - var err error - var plugin *PluginBase - - if plugin, err = getPlugin(cmd.PluginId); err != nil { - return err - } - - var pluginType string - - if plugin.Type == "datasource" { - pluginType = "Datasource" - } else if plugin.Type == "app" { - pluginType = "App" - } - - folderTitle := fmt.Sprint(pluginType, ": ", plugin.Name) - - folderDash := simplejson.NewFromAny(map[string]interface{}{ - "schemaVersion": 16, - "title": folderTitle, - "editable": true, - "hideControls": true, - }) - - saveCmd := m.SaveDashboardCommand{ - Dashboard: folderDash, - OrgId: cmd.OrgId, - UserId: cmd.UserId, - PluginId: cmd.PluginId, - IsFolder: true, - } - - dashModel := saveCmd.GetDashboardModel() - - getDashboardQuery := m.GetDashboardQuery{ - OrgId: cmd.OrgId, - Slug: dashModel.Slug, - } - - if err := bus.Dispatch(&getDashboardQuery); err != nil { - return err - } - - if getDashboardQuery.Result != nil { - dashboard.FolderId = getDashboardQuery.Result.Id - return nil - } - - if err := bus.Dispatch(&saveCmd); err != nil { - return err - } - - dashboard.FolderId = saveCmd.Result.Id - return nil -} - type DashTemplateEvaluator struct { template *simplejson.Json inputs []ImportDashboardInput diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 26b5c7ab072..78df94309f8 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -13,32 +13,12 @@ import ( ) func TestDashboardImport(t *testing.T) { - - Convey("When importing plugin dashboard", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.test-app") - sec.NewKey("path", "../../tests/test-app") - err := Init() - - So(err, ShouldBeNil) - - folderId := int64(1000) + pluginScenario("When importing a plugin dashboard", t, func() { var importedDash *m.Dashboard - var createdFolder *m.Dashboard + bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { - if cmd.IsFolder { - createdFolder = cmd.GetDashboardModel() - createdFolder.Id = folderId - cmd.Result = createdFolder - } else { - importedDash = cmd.GetDashboardModel() - cmd.Result = importedDash - } - - return nil - }) - - bus.AddHandler("test", func(cmd *m.GetDashboardQuery) error { + importedDash = cmd.GetDashboardModel() + cmd.Result = importedDash return nil }) @@ -52,7 +32,7 @@ func TestDashboardImport(t *testing.T) { }, } - err = ImportDashboard(&cmd) + err := ImportDashboard(&cmd) So(err, ShouldBeNil) Convey("should install dashboard", func() { @@ -67,78 +47,6 @@ func TestDashboardImport(t *testing.T) { panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0) So(panel.Get("datasource").MustString(), ShouldEqual, "graphite") - - So(importedDash.FolderId, ShouldEqual, folderId) - }) - - Convey("should create app folder", func() { - So(createdFolder.Title, ShouldEqual, "App: Test App") - So(createdFolder.Id, ShouldEqual, folderId) - }) - }) - - Convey("When re-importing plugin dashboard", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.test-app") - sec.NewKey("path", "../../tests/test-app") - err := Init() - - So(err, ShouldBeNil) - - folderId := int64(1000) - var importedDash *m.Dashboard - var createdFolder *m.Dashboard - bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { - if cmd.IsFolder { - cmd.Result = cmd.GetDashboardModel() - } else { - importedDash = cmd.GetDashboardModel() - cmd.Result = importedDash - } - - return nil - }) - - bus.AddHandler("test", func(cmd *m.GetDashboardQuery) error { - cmd.Result = &m.Dashboard{ - Id: 1000, - Title: "Something", - } - - return nil - }) - - cmd := ImportDashboardCommand{ - PluginId: "test-app", - Path: "dashboards/connections.json", - OrgId: 1, - UserId: 1, - Inputs: []ImportDashboardInput{ - {Name: "*", Type: "datasource", Value: "graphite"}, - }, - } - - err = ImportDashboard(&cmd) - So(err, ShouldBeNil) - - Convey("should install dashboard", func() { - So(importedDash, ShouldNotBeNil) - - resultStr, _ := importedDash.Data.EncodePretty() - expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json") - expectedJson, _ := simplejson.NewJson(expectedBytes) - expectedStr, _ := expectedJson.EncodePretty() - - So(string(resultStr), ShouldEqual, string(expectedStr)) - - panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0) - So(panel.Get("datasource").MustString(), ShouldEqual, "graphite") - - So(importedDash.FolderId, ShouldEqual, folderId) - }) - - Convey("should not create app folder", func() { - So(createdFolder, ShouldBeNil) }) }) @@ -177,3 +85,16 @@ func TestDashboardImport(t *testing.T) { }) } + +func pluginScenario(desc string, t *testing.T, fn func()) { + Convey("Given a plugin", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.test-app") + sec.NewKey("path", "../../tests/test-app") + err := Init() + + So(err, ShouldBeNil) + + Convey(desc, fn) + }) +} diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 2214ad6e213..37e3d8c0076 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -108,13 +108,3 @@ func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) { return m.NewDashboardFromJson(data), nil } - -func getPlugin(pluginId string) (*PluginBase, error) { - plugin, exists := Plugins[pluginId] - - if !exists { - return nil, PluginNotFoundError{pluginId} - } - - return plugin, nil -} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 7acdc79d55a..00181736119 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,8 +1,10 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; +import PageHeader from './components/PageHeader'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); + react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]); } diff --git a/public/app/core/components/PageHeader.tsx b/public/app/core/components/PageHeader.tsx new file mode 100644 index 00000000000..e0accc03fd4 --- /dev/null +++ b/public/app/core/components/PageHeader.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { NavModel, NavModelItem } from '../nav_model_srv'; +import classNames from 'classnames'; + +export interface IProps { + model: NavModel; +} + +function TabItem(tab: NavModelItem) { + if (tab.hideFromTabs) { + return (null); + } + + let tabClasses = classNames({ + 'gf-tabs-link': true, + active: tab.active, + }); + + return ( +
  • + + + {tab.text} + +
  • + ); +} + +function Tabs({main}: {main: NavModelItem}) { + return ; +} + +export default class PageHeader extends React.Component { + constructor(props) { + super(props); + } + + renderHeaderTitle(main) { + return ( +
    + + {main.icon && } + {main.img && } + + +
    +

    {main.text}

    + {main.subTitle &&
    {main.subTitle}
    } + {main.subType && ( +
    + + {main.subType.text} +
    + )} +
    +
    + ); + } + + render() { + return ( +
    +
    +
    + {this.renderHeaderTitle(this.props.model.main)} + {this.props.model.main.children && } +
    +
    +
    + ); + } +} diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index d13c6a6a00e..c60c8a20a4c 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -85,6 +85,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } }); + let sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', sidemenuOpenSmallBreakpoint); + + scope.$watch('contextSrv.sidemenuSmallBreakpoint', newVal => { + if (sidemenuOpenSmallBreakpoint !== scope.contextSrv.sidemenuSmallBreakpoint) { + sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', scope.contextSrv.sidemenuSmallBreakpoint); + } + }); + // tooltip removal fix // manage page classes var pageClass; diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index aafd806e47d..6d611692efc 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -1,13 +1,11 @@
    -
    - +
    +
    +
    +
    No dashboards matching your query were found.
    -
    -
    No dashboards matching your query were found.
    + +
    +
    +
    - -
    +
    +
    +
    + + Filter by: + + Clear + +
    + +
    + + +
    + +
    + + + +
    +
    + + +
    +
    diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index c7ac381c7ce..728b6a3ee18 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -9,16 +9,18 @@ export class SearchCtrl { selectedIndex: number; results: any; currentSearchId: number; - tagsMode: boolean; showImport: boolean; dismiss: any; ignoreClose: any; isLoading: boolean; + initialFolderFilterTitle: string; /** @ngInject */ constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv, $rootScope) { $rootScope.onAppEvent('show-dash-search', this.openSearch.bind(this), $scope); $rootScope.onAppEvent('hide-dash-search', this.closeSearch.bind(this), $scope); + + this.initialFolderFilterTitle = "All"; } closeSearch() { @@ -44,14 +46,6 @@ export class SearchCtrl { this.query.starred = true; } - if (payload && payload.tagsMode) { - return this.$timeout(() => { - this.ignoreClose = false; - this.giveSearchFocus = this.giveSearchFocus + 1; - this.getTags(); - }, 100); - } - this.$timeout(() => { this.ignoreClose = false; this.giveSearchFocus = this.giveSearchFocus + 1; @@ -70,14 +64,6 @@ export class SearchCtrl { this.moveSelection(-1); } if (evt.keyCode === 13) { - if (this.tagsMode) { - var tag = this.results[this.selectedIndex]; - if (tag) { - this.filterByTag(tag.term, null); - } - return; - } - var selectedDash = this.results[this.selectedIndex]; if (selectedDash) { this.$location.search({}); @@ -93,7 +79,6 @@ export class SearchCtrl { } searchDashboards() { - this.tagsMode = false; this.currentSearchId = this.currentSearchId + 1; var localSearchId = this.currentSearchId; @@ -129,12 +114,8 @@ export class SearchCtrl { getTags() { return this.searchSrv.getDashboardTags().then((results) => { - this.tagsMode = !this.tagsMode; this.results = results; this.giveSearchFocus = this.giveSearchFocus + 1; - if ( !this.tagsMode ) { - this.search(); - } }); } diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 2869562c5b1..b47b7fc8265 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -2,6 +2,12 @@ + + +

     Close

    +
    + +