diff --git a/pkg/api/index.go b/pkg/api/index.go index eb9e70bb404..f3e7af0e4b9 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Create", + Id: "create", Icon: "fa fa-fw fa-plus", Url: "#", Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, - {Text: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}, + {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } @@ -103,7 +104,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { dashboardChildNavs := []*dtos.NavLink{ {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: "Manage", Id: "manage-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"}, } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 3880be7de8d..4cb3ca513cc 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,12 +1,10 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; -import PageHeader from './components/PageHeader'; +import PageHeader from './components/PageHeader/PageHeader'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; export function registerAngularDirectives() { - react2AngularDirective('passwordStrength', PasswordStrength, ['password']); - react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]); + react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); - } diff --git a/public/app/core/components/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx similarity index 54% rename from public/app/core/components/PageHeader.tsx rename to public/app/core/components/PageHeader/PageHeader.tsx index e0accc03fd4..5371b464ec3 100644 --- a/public/app/core/components/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { NavModel, NavModelItem } from '../nav_model_srv'; +import { NavModel, NavModelItem } from '../../nav_model_srv'; import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; export interface IProps { model: NavModel; @@ -26,8 +27,44 @@ function TabItem(tab: NavModelItem) { ); } -function Tabs({main}: {main: NavModelItem}) { - return ; +function SelectOption(navItem: NavModelItem) { + if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav + return (null); + } + + return ( + + ); +} + +function Navigation({main}: {main: NavModelItem}) { + return (); +} + +function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) { + const defaultSelectedItem = main.children.find(navItem => { + return navItem.active === true; + }); + + const gotoUrl = evt => { + var element = evt.target; + var url = element.options[element.selectedIndex].value; + appEvents.emit('location-change', {href: url}); + }; + + return (); +} + +function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) { + return ; } export default class PageHeader extends React.Component { @@ -63,7 +100,7 @@ export default class PageHeader extends React.Component {
{this.renderHeaderTitle(this.props.model.main)} - {this.props.model.main.children && } + {this.props.model.main.children && }
diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index c60c8a20a4c..77089b14ddc 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -12,7 +12,7 @@ import Drop from 'tether-drop'; export class GrafanaCtrl { /** @ngInject */ - constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv) { + constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) { $scope.init = function() { $scope.contextSrv = contextSrv; @@ -23,6 +23,7 @@ export class GrafanaCtrl { profiler.init(config, $rootScope); alertSrv.init(); utilSrv.init(); + globalEventSrv.init(); $scope.dashAlerts = alertSrv; }; diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 811e21bc64a..3b0d9c003dd 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -20,37 +20,12 @@
-
-
No dashboards matching your query were found.
- -
diff --git a/public/app/core/components/search/search.jest.ts b/public/app/core/components/search/search.jest.ts new file mode 100644 index 00000000000..68b1292f9d4 --- /dev/null +++ b/public/app/core/components/search/search.jest.ts @@ -0,0 +1,335 @@ +import { SearchCtrl } from './search'; +import { SearchSrv } from 'app/core/services/search_srv'; + +describe('SearchCtrl', () => { + const searchSrvStub = { + search: (options: any) => {}, + getDashboardTags: () => {} + }; + let ctrl = new SearchCtrl({}, {}, {}, searchSrvStub, { onAppEvent: () => { } }); + + describe('Given an empty result', () => { + beforeEach(() => { + ctrl.results = []; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [ + { id: 2, selected: false }, + { id: 4, selected: false } + ], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of a search with 2 dashboards where the first is selected', () => { + beforeEach(() => { + ctrl.results = [ + { + hideHeader: true, + items: [ + { id: 3, selected: true }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + }); +}); diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 728b6a3ee18..08d050c81bf 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -64,18 +64,70 @@ export class SearchCtrl { this.moveSelection(-1); } if (evt.keyCode === 13) { - var selectedDash = this.results[this.selectedIndex]; - if (selectedDash) { - this.$location.search({}); - this.$location.path(selectedDash.url); + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex]; + + if (selectedDash) { + this.$location.search({}); + this.$location.path(selectedDash.url); + } + } else { + const selectedFolder = this.results[currentItem.folderIndex]; + + if (selectedFolder) { + selectedFolder.toggle(selectedFolder); + } + } } } } moveSelection(direction) { - var max = (this.results || []).length; - var newIndex = this.selectedIndex + direction; + if (this.results.length === 0) { + return; + } + + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false; + } else { + this.results[currentItem.folderIndex].selected = false; + } + } + + const max = flattenedResult.length; + let newIndex = this.selectedIndex + direction; this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex; + const selectedItem = flattenedResult[this.selectedIndex]; + + if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { + this.moveSelection(direction); + return; + } + + if (selectedItem.dashboardIndex !== undefined) { + if (!this.results[selectedItem.folderIndex].expanded) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true; + return; + } + + if (this.results[selectedItem.folderIndex].hideHeader) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].selected = true; } searchDashboards() { @@ -84,8 +136,9 @@ export class SearchCtrl { return this.searchSrv.search(this.query).then(results => { if (localSearchId < this.currentSearchId) { return; } - this.results = results; + this.results = results || []; this.isLoading = false; + this.moveSelection(1); }); } @@ -94,13 +147,11 @@ export class SearchCtrl { return query.query === '' && query.starred === false && query.tag.length === 0; } - filterByTag(tag, evt) { - this.query.tag.push(tag); - this.search(); - this.giveSearchFocus = this.giveSearchFocus + 1; - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); + this.search(); + this.giveSearchFocus = this.giveSearchFocus + 1; } } @@ -127,12 +178,32 @@ export class SearchCtrl { search() { this.showImport = false; - this.selectedIndex = 0; + this.selectedIndex = -1; this.searchDashboards(); } - toggleFolder(section) { - this.searchSrv.toggleSection(section); + private getFlattenedResultForNavigation() { + let folderIndex = 0; + + return _.flatMap(this.results, (s) => { + let result = []; + + result.push({ + folderIndex: folderIndex + }); + + let dashboardIndex = 0; + + result = result.concat(_.map(s.items || [], (i) => { + return { + folderIndex: folderIndex, + dashboardIndex: dashboardIndex++ + }; + })); + + folderIndex++; + return result; + }); } } diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html new file mode 100644 index 00000000000..2008910f53a --- /dev/null +++ b/public/app/core/components/search/search_results.html @@ -0,0 +1,47 @@ + \ No newline at end of file diff --git a/public/app/core/components/search/search_results.jest.ts b/public/app/core/components/search/search_results.jest.ts new file mode 100644 index 00000000000..fc661dc8472 --- /dev/null +++ b/public/app/core/components/search/search_results.jest.ts @@ -0,0 +1,75 @@ +import { SearchResultsCtrl } from './search_results'; + +describe('SearchResultsCtrl', () => { + let ctrl; + + describe('when checking an item that is not checked', () => { + let item = {checked: false}; + let selectionChanged = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onSelectionChanged = () => selectionChanged = true; + ctrl.toggleSelection(item); + }); + + it('should set checked to true', () => { + expect(item.checked).toBeTruthy(); + }); + + it('should trigger selection changed callback', () => { + expect(selectionChanged).toBeTruthy(); + }); + }); + + describe('when checking an item that is checked', () => { + let item = {checked: true}; + let selectionChanged = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onSelectionChanged = () => selectionChanged = true; + ctrl.toggleSelection(item); + }); + + it('should set checked to false', () => { + expect(item.checked).toBeFalsy(); + }); + + it('should trigger selection changed callback', () => { + expect(selectionChanged).toBeTruthy(); + }); + }); + + describe('when selecting a tag', () => { + let selectedTag = null; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onTagSelected = (tag) => selectedTag = tag; + ctrl.selectTag('tag-test'); + }); + + it('should trigger tag selected callback', () => { + expect(selectedTag["$tag"]).toBe('tag-test'); + }); + }); + + describe('when toggle a folder', () => { + let folderToggled = false; + let folder = { + toggle: () => { + folderToggled = true; + } + }; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.toggleFolderExpand(folder); + }); + + it('should trigger folder toggle callback', () => { + expect(folderToggled).toBeTruthy(); + }); + }); +}); diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts new file mode 100644 index 00000000000..0757a53a517 --- /dev/null +++ b/public/app/core/components/search/search_results.ts @@ -0,0 +1,70 @@ +// import _ from 'lodash'; +import coreModule from '../../core_module'; + +export class SearchResultsCtrl { + results: any; + onSelectionChanged: any; + onTagSelected: any; + + /** @ngInject */ + constructor(private $location) { + + } + + toggleFolderExpand(section) { + if (section.toggle) { + section.toggle(section); + } + } + + navigateToFolder(section, evt) { + this.$location.path('/dashboards/folder/' + section.id + '/' + section.uri); + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + toggleSelection(item, evt) { + item.checked = !item.checked; + + if (this.onSelectionChanged) { + this.onSelectionChanged(); + } + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + selectTag(tag, evt) { + if (this.onTagSelected) { + this.onTagSelected({$tag: tag}); + } + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } +} + +export function searchResultsDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/search/search_results.html', + controller: SearchResultsCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + editable: '@', + results: '=', + onSelectionChanged: '&', + onTagSelected: '&' + }, + }; +} + +coreModule.directive('dashboardSearchResults', searchResultsDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 4bb1e43cc25..c88e1f21136 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -54,6 +54,7 @@ import {profiler} from './profiler'; import {registerAngularDirectives} from './angular_wrappers'; import {updateLegendValues} from './time_series2'; import TimeSeries from './time_series2'; +import {searchResultsDirective} from './components/search/search_results'; export { profiler, @@ -87,5 +88,6 @@ export { gfPageDirective, orgSwitcher, TimeSeries, - updateLegendValues + updateLegendValues, + searchResultsDirective }; diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index 81a82cd4c5f..2be95aca0da 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -119,14 +119,6 @@ export class NavModelSrv { clickHandler: () => dashNavCtrl.openEditView('annotations') }); - if (dashboard.meta.canAdmin) { - menu.push({ - title: 'Permissions...', - icon: 'fa fa-fw fa-lock', - clickHandler: () => dashNavCtrl.openEditView('permissions') - }); - } - if (!dashboard.meta.isHome) { menu.push({ title: 'Version history', diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 68595da4296..f4132400c7d 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/dashboard/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/datasources', { templateUrl: 'public/app/features/plugins/partials/ds_list.html', controller : 'DataSourcesCtrl', @@ -68,6 +73,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/folder/:folderId/:type/:slug', { + templateUrl: 'public/app/features/dashboard/partials/dashboardList.html', + controller : 'DashboardListCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/core/services/all.js b/public/app/core/services/all.js index a308febb219..0053d789cbe 100644 --- a/public/app/core/services/all.js +++ b/public/app/core/services/all.js @@ -8,5 +8,6 @@ define([ './segment_srv', './backend_srv', './dynamic_directive_srv', + './global_event_srv' ], function () {}); diff --git a/public/app/core/services/global_event_srv.ts b/public/app/core/services/global_event_srv.ts new file mode 100644 index 00000000000..a4d5865eb63 --- /dev/null +++ b/public/app/core/services/global_event_srv.ts @@ -0,0 +1,21 @@ +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +// This service is for registering global events. +// Good for communication react > angular and vice verse +export class GlobalEventSrv { + + /** @ngInject */ + constructor(private $location, private $timeout) { + } + + init() { + appEvents.on('location-change', payload => { + this.$timeout(() => { // A hack to use timeout when we're changing things (in this case the url) from outside of Angular. + this.$location.path(payload.href); + }); + }); + } +} + +coreModule.service('globalEventSrv', GlobalEventSrv); diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index d7c0e75de5b..38aeea47b79 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -128,14 +128,20 @@ export class SearchSrv { }); } - private browse() { + private browse(options) { let sections: any = {}; - let promises = [ - this.getRecentDashboards(sections), - this.getStarred(sections), - this.getDashboardsAndFolders(sections), - ]; + let promises = []; + + if (!options.skipRecent) { + promises.push(this.getRecentDashboards(sections)); + } + + if (!options.skipStarred) { + promises.push(this.getStarred(sections)); + } + + promises.push(this.getDashboardsAndFolders(sections)); return this.$q.all(promises).then(() => { return _.sortBy(_.values(sections), 'score'); @@ -148,15 +154,19 @@ export class SearchSrv { } search(options) { - if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) { - return this.browse(); + if (!options.folderIds && !options.query && (!options.tag || options.tag.length === 0) && !options.starred) { + return this.browse(options); } let query = _.clone(options); - query.folderIds = []; + query.folderIds = options.folderIds || []; query.type = 'dash-db'; return this.backendSrv.search(query).then(results => { + if (results.length === 0) { + return results; + } + let section = { hideHeader: true, items: [], @@ -191,10 +201,6 @@ export class SearchSrv { }); } - toggleSection(section) { - section.toggle(section); - } - getDashboardTags() { return this.backendSrv.get('/api/dashboards/tags'); } diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts index 0624618a224..649cfbf00dd 100644 --- a/public/app/core/specs/search_srv.jest.ts +++ b/public/app/core/specs/search_srv.jest.ts @@ -2,6 +2,7 @@ import { SearchSrv } from 'app/core/services/search_srv'; import { BackendSrvMock } from 'test/mocks/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import { contextSrv } from 'app/core/services/context_srv'; +import { beforeEach } from 'test/lib/common'; jest.mock('app/core/store', () => { return { @@ -244,4 +245,43 @@ describe('SearchSrv', () => { expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); }); }); + + describe('when skipping recent dashboards', () => { + let getRecentDashboardsCalled = false; + + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + + searchSrv.getRecentDashboards = () => { + getRecentDashboardsCalled = true; + }; + + return searchSrv.search({ skipRecent: true }).then(() => {}); + }); + + it('should not fetch recent dashboards', () => { + expect(getRecentDashboardsCalled).toBeFalsy(); + }); + }); + + describe('when skipping starred dashboards', () => { + let getStarredCalled = false; + + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); + + searchSrv.getStarred = () => { + getStarredCalled = true; + }; + + return searchSrv.search({ skipStarred: true }).then(() => {}); + }); + + it('should not fetch starred dashboards', () => { + expect(getStarredCalled).toBeFalsy(); + }); + }); }); diff --git a/public/app/core/utils/react2angular.ts b/public/app/core/utils/react2angular.ts index ad6f7476d6a..e7ad3502f88 100644 --- a/public/app/core/utils/react2angular.ts +++ b/public/app/core/utils/react2angular.ts @@ -1,10 +1,7 @@ import coreModule from 'app/core/core_module'; export function react2AngularDirective(name: string, component: any, options: any) { - coreModule.directive(name, ['reactDirective', reactDirective => { return reactDirective(component, options); }]); - } - diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index b4f1f4e77f0..f2f2087a24e 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -15,7 +15,6 @@ import './unsavedChangesSrv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; import './upload'; -import './import/dash_import'; import './export/export_modal'; import './export_data/export_data_modal'; import './ad_hoc_filters'; @@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder'; import coreModule from 'app/core/core_module'; import {DashboardListCtrl} from './dashboard_list_ctrl'; +import {DashboardImportCtrl} from './dashboard_import_ctrl'; coreModule.controller('DashboardListCtrl', DashboardListCtrl); +coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); diff --git a/public/app/features/dashboard/import/dash_import.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts similarity index 87% rename from public/app/features/dashboard/import/dash_import.ts rename to public/app/features/dashboard/dashboard_import_ctrl.ts index 8f3de6adc60..9a630681145 100644 --- a/public/app/features/dashboard/import/dash_import.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -1,10 +1,8 @@ -/// - -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; import _ from 'lodash'; +import config from 'app/core/config'; -export class DashImportCtrl { +export class DashboardImportCtrl { + navModel: any; step: number; jsonText: string; parseError: string; @@ -17,7 +15,9 @@ export class DashImportCtrl { gnetInfo: any; /** @ngInject */ - constructor(private backendSrv, private $location, private $scope, $routeParams) { + constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { + this.navModel = navModelSrv.getNav('create', 'import'); + this.step = 1; this.nameExists = false; @@ -160,17 +160,4 @@ export class DashImportCtrl { this.gnetError = ''; this.gnetInfo = ''; } - } - -export function dashImportDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/import/dash_import.html', - controller: DashImportCtrl, - bindToController: true, - controllerAs: 'ctrl', - }; -} - -coreModule.directive('dashImport', dashImportDirective); diff --git a/public/app/features/dashboard/dashboard_list_ctrl.ts b/public/app/features/dashboard/dashboard_list_ctrl.ts index 93e9df84c15..e2b3ee3227d 100644 --- a/public/app/features/dashboard/dashboard_list_ctrl.ts +++ b/public/app/features/dashboard/dashboard_list_ctrl.ts @@ -14,16 +14,29 @@ export class DashboardListCtrl { selectAllChecked = false; starredFilterOptions = [{text: 'Filter by Starred', disabled: true}, {text: 'Yes'}, {text: 'No'}]; selectedStarredFilter: any; + folderTitle = null; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { - this.navModel = navModelSrv.getNav('dashboards', 'dashboards', 0); - this.query = {query: '', mode: 'tree', tag: [], starred: false}; + constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv, private $routeParams) { + this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); + this.query = {query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true}; + this.selectedStarredFilter = this.starredFilterOptions[0]; - this.getDashboards().then(() => { - this.getTags(); - }); + if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { + backendSrv.getDashboard(this.$routeParams.type, this.$routeParams.slug).then(result => { + this.folderTitle = result.dashboard.title; + this.query.folderIds = [result.dashboard.id]; + + this.getDashboards().then(() => { + this.getTags(); + }); + }); + } else { + this.getDashboards().then(() => { + this.getTags(); + }); + } } getDashboards() { @@ -137,10 +150,6 @@ export class DashboardListCtrl { }); } - toggleFolder(section) { - return this.searchSrv.toggleSection(section); - } - getTags() { return this.searchSrv.getDashboardTags().then((results) => { this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); @@ -148,11 +157,9 @@ export class DashboardListCtrl { }); } - filterByTag(tag, evt) { - this.query.tag.push(tag); - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); } return this.getDashboards(); @@ -163,9 +170,9 @@ export class DashboardListCtrl { } onTagFilterChange() { - this.query.tag.push(this.selectedTagFilter.term); + var res = this.filterByTag(this.selectedTagFilter.term); this.selectedTagFilter = this.tagFilterOptions[0]; - return this.getDashboards(); + return res; } removeTag(tag, evt) { diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 6cc3e37f2f8..95a70cbfcab 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -383,8 +383,8 @@ export class DashboardMigrator { return; } - // Add special "row" panels if even one row is collapsed or has visible title - const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle); + // Add special "row" panels if even one row is collapsed, repeated or has visible title + const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle || row.repeat); for (let row of old.rows) { let height: any = row.height || DEFAULT_ROW_HEIGHT; @@ -398,6 +398,7 @@ export class DashboardMigrator { rowPanel.type = 'row'; rowPanel.title = row.title; rowPanel.collapsed = row.collapse; + rowPanel.repeat = row.repeat; rowPanel.panels = []; rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight}; rowPanelModel = new PanelModel(rowPanel); diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html deleted file mode 100644 index c617c752366..00000000000 --- a/public/app/features/dashboard/import/dash_import.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - -
- diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html new file mode 100644 index 00000000000..b740b8f38bc --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -0,0 +1,126 @@ + + +
+
+ +
+
+ +
+ +
Grafana.com Dashboard
+ +
+
+ +
+
+ +
+
+ +
Or paste JSON
+ +
+
+ +
+ + + + {{ctrl.parseError}} + +
+
+ +
+
+

+ Importing Dashboard from + Grafana.com +

+ +
+ + +
+
+ + +
+
+ +

+ Options +

+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+ + + Cancel +
+ +
+
diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html index 8bb46ebd127..cecd52aa891 100644 --- a/public/app/features/dashboard/partials/dashboardList.html +++ b/public/app/features/dashboard/partials/dashboardList.html @@ -1,17 +1,35 @@
+
+
+

+  {{ctrl.folderTitle}} +

+
+
+ + + + Dashboard + + + + Folder + +
+
- + Dashboard - + Folder @@ -39,29 +57,25 @@
-
-
- - -
+
+
-
+ - - - No Dashboards or Folders found. - diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts similarity index 52% rename from public/app/features/dashboard/specs/dash_import_ctrl_specs.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index c541aca34b2..3270bb5a732 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -1,25 +1,24 @@ -import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import {DashboardImportCtrl} from '../dashboard_import_ctrl'; +import config from '../../../core/config'; -import {DashImportCtrl} from 'app/features/dashboard/import/dash_import'; -import config from 'app/core/config'; - -describe('DashImportCtrl', function() { +describe('DashboardImportCtrl', function() { var ctx: any = {}; - var backendSrv = { - search: sinon.stub().returns(Promise.resolve([])), - get: sinon.stub() - }; - beforeEach(angularMocks.module('grafana.core')); + let navModelSrv; + let backendSrv; - beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { - ctx.$q = $q; - ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashImportCtrl, { - $scope: ctx.scope, - backendSrv: backendSrv, - }); - })); + beforeEach(() => { + navModelSrv = { + getNav: () => {} + }; + + backendSrv = { + search: jest.fn().mockReturnValue(Promise.resolve([])), + get: jest.fn() + }; + + ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {}); + }); describe('when uploading json', function() { beforeEach(function() { @@ -37,13 +36,13 @@ describe('DashImportCtrl', function() { }); it('should build input model', function() { - expect(ctx.ctrl.inputs.length).to.eql(1); - expect(ctx.ctrl.inputs[0].name).to.eql('ds'); - expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); + expect(ctx.ctrl.inputs.length).toBe(1); + expect(ctx.ctrl.inputs[0].name).toBe('ds'); + expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); it('should set inputValid to false', function() { - expect(ctx.ctrl.inputsValid).to.eql(false); + expect(ctx.ctrl.inputsValid).toBe(false); }); }); @@ -51,7 +50,7 @@ describe('DashImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -60,7 +59,7 @@ describe('DashImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); @@ -68,7 +67,7 @@ describe('DashImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = '2342'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -77,10 +76,8 @@ describe('DashImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); }); - - diff --git a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts index c159b7eeb2b..1c13d138b6d 100644 --- a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts @@ -537,13 +537,10 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { search: (options: any) => { return q.resolve(searchResponse); }, - toggleSection: (section) => { - return; - }, getDashboardTags: () => { return q.resolve(tags || []); } }; - return new DashboardListCtrl({}, { getNav: () => { } }, q, searchSrvStub); + return new DashboardListCtrl({}, { getNav: () => { } }, q, searchSrvStub, {}); } diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 2c8b5ebcd50..dfdcb7601a4 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { DashboardModel } from '../dashboard_model'; import { PanelModel } from '../panel_model'; import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); @@ -315,12 +316,33 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); + + it('should add repeated row if repeat set', function() { + model.rows = [ + createRow({showTitle: true, title: "Row", height: 8, repeat: "server"}, [[6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, + {x: 0, y: 1, w: 12, h: 8}, + {x: 0, y: 9, w: 24, h: 8}, + {x: 0, y: 10, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + expect(dashboard.panels[0].repeat).toBe("server"); + expect(dashboard.panels[1].repeat).toBeUndefined(); + expect(dashboard.panels[2].repeat).toBeUndefined(); + expect(dashboard.panels[3].repeat).toBeUndefined(); + }); }); }); function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; - let {collapse, height, showTitle, title} = options; + let {collapse, height, showTitle, title, repeat} = options; height = height * PANEL_HEIGHT_STEP; let panels = []; _.each(panelDescriptions, panelDesc => { @@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) { } panels.push(panel); }); - let row = {collapse, height, showTitle, title, panels}; + let row = {collapse, height, showTitle, title, panels, repeat}; return row; } diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 45a1f1b4a0c..61cca0d4216 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 1567d8c5d61..c0d1757dae7 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -139,6 +139,8 @@ $table-bg-accent: $dark-3; // for striping $table-bg-hover: $dark-4; // for hover $table-border: $dark-3; // table and cell border +$table-bg-odd: $dark-2; + // Buttons // ------------------------- @@ -160,6 +162,7 @@ $btn-danger-bg-hl: darken($red, 8%); $btn-inverse-bg: $dark-3; $btn-inverse-bg-hl: lighten($dark-3, 4%); $btn-inverse-text-color: $link-color; +$btn-inverse-text-shadow: 0px 1px 0 rgba(0,0,0,.1); $btn-link-color: $gray-3; @@ -182,7 +185,7 @@ $input-border-focus: $input-border-color !default; $input-box-shadow-focus: rgba(102,175,233,.6) !default; $input-color-placeholder: $gray-1 !default; $input-label-bg: $gray-blue; -$input-label-border-color: transparent; +$input-label-border-color: $gray-blue; $input-invalid-border-color: lighten($red, 5%); // Search @@ -241,11 +244,14 @@ $navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg; $navbarButtonBackground: $navbarBackground; $navbarButtonBackgroundHighlight: $body-bg; +$navbar-button-border: #151515; + // Sidemenu // ------------------------- $side-menu-bg: $black; $side-menu-item-hover-bg: $dark-2; $side-menu-shadow: 0 0 20px black; +$side-menu-link-color: $link-color; $breadcrumb-hover-hl: #111; // Menu dropdowns @@ -261,6 +267,9 @@ $page-nav-bg: $black; $page-nav-shadow: 5px 5px 20px -5px $black; $page-nav-breadcrumb-color: $gray-3; +// Tabs +// ------------------------- +$tab-border-color: $dark-4; // Pagination // ------------------------- diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 78c0ba3bf61..f6512aed3c4 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -14,24 +14,24 @@ $black: #000; // ------------------------- $black: #000; -$dark-1: #141414; -$dark-2: #1d1d1f; -$dark-3: #262628; -$dark-4: #373737; -$dark-5: #444444; -$gray-1: #555555; -$gray-2: #7B7B7B; -$gray-3: #b3b3b3; -$gray-4: #D8D9DA; -$gray-5: #ECECEC; -$gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$dark-1: #13161d; +$dark-2: #1e2028; +$dark-3: #303133; +$dark-4: #35373f; +$dark-5: #41444b; +$gray-1: #52545c; +$gray-2: #767980; +$gray-3: #acb6bf; +$gray-4: #c7d0d9; +$gray-5: #dde4ed; +$gray-6: #e9edf2; +$gray-7: #f7f8fa; $white: #fff; // Accent colors // ------------------------- -$blue: #2AB2E4; +$blue: #1ca4d6; $blue-dark: #3CAAD6; $green: #3aa655; $red: #d44939; @@ -39,7 +39,7 @@ $yellow: #FF851B; $orange: #Ff7941; $pink: #E671B8; $purple: #9954BB; -$variable: #2AB2E4; +$variable: $blue; $brand-primary: $orange; $brand-success: $green; @@ -55,22 +55,22 @@ $critical: #EC2128; // Scaffolding // ------------------------- -$body-bg: $white; -$page-bg: $white; +$body-bg: $gray-7; +$page-bg: $gray-7; $body-color: $gray-1; -$text-color: $gray-1; +$text-color: $dark-4; $text-color-strong: $white; -$text-color-weak: $gray-3; +$text-color-weak: $gray-2; $text-color-faint: $gray-4; $text-color-emphasis: $dark-5; $text-shadow-strong: none; $text-shadow-faint: none; +$textShadow: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); -$page-gradient: linear-gradient(-60deg, transparent 70%, darken($page-bg, 4%) 98%); -$page-header-bg: linear-gradient(90deg, #292a2d, black); +$page-gradient: linear-gradient(-60deg, transparent 70%, $gray-7 98%); // Links // ------------------------- @@ -97,7 +97,7 @@ $component-active-bg: $brand-primary !default; // Panel // ------------------------- -$panel-bg: $gray-7; +$panel-bg: $white; $panel-border-color: $gray-5; $panel-border: solid 1px $panel-border-color; $panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px); @@ -105,9 +105,9 @@ $panel-header-hover-bg: $gray-6; $panel-header-menu-hover-bg: $gray-4; // Page header -$page-header-bg: linear-gradient(90deg, #292a2d, black); -$page-header-shadow: inset 0px -4px 14px $dark-2; -$page-header-border-color: $dark-4; +$page-header-bg: linear-gradient(90deg, $white, $gray-7); +$page-header-shadow: inset 0px -3px 10px $gray-6; +$page-header-border-color: $gray-4; $divider-border-color: $gray-2; @@ -122,12 +122,12 @@ $code-tag-bg: $gray-6; $code-tag-border: darken($code-tag-bg, 3%); // cards -$card-background: linear-gradient(135deg, $gray-5, $gray-6); -$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); +$card-background: linear-gradient(135deg, $gray-6, $gray-5); +$card-background-hover: linear-gradient(135deg, $gray-5, $gray-6); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); // Lists -$list-item-bg: $card-background; +$list-item-bg: linear-gradient(135deg, $gray-5, $gray-6);//$card-background; $list-item-hover-bg: darken($gray-5, 5%); $list-item-link-color: $text-color; $list-item-shadow: $card-shadow; @@ -140,6 +140,8 @@ $table-bg-hover: $gray-5; // for hover $table-bg-active: $table-bg-hover !default; $table-border: $gray-3; // table and cell border +$table-bg-odd: $gray-5; + // Scrollbars $scrollbarBackground: $gray-5; $scrollbarBackground2: $gray-5; @@ -162,9 +164,10 @@ $btn-warning-bg-hl: darken($orange, 3%); $btn-danger-bg: lighten($red, 3%); $btn-danger-bg-hl: darken($red, 3%); -$btn-inverse-bg: $gray-5; -$btn-inverse-bg-hl: darken($gray-5, 5%); -$btn-inverse-text-color: $dark-4; +$btn-inverse-bg: $gray-6; +$btn-inverse-bg-hl: darken($gray-6, 5%); +$btn-inverse-text-color: $gray-1; +$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, .4); $btn-link-color: $gray-1; @@ -176,7 +179,7 @@ $iconContainerBackground: $white; // Forms // ------------------------- -$input-bg: $gray-7; +$input-bg: $white; $input-bg-disabled: $gray-5; $input-color: $dark-3; @@ -185,33 +188,38 @@ $input-box-shadow: none; $input-border-focus: $blue !default; $input-box-shadow-focus: $blue !default; $input-color-placeholder: $gray-4 !default; -$input-label-bg: #eaebee; -$input-label-border-color: #e3e4e7; +$input-label-bg: $gray-5; +$input-label-border-color: $gray-5; $input-invalid-border-color: lighten($red, 5%); // Sidemenu // ------------------------- -$side-menu-bg: $body-bg; -$side-menu-item-hover-bg: $gray-6; -$side-menu-shadow: 0 0 5px #c2c2c2; +$side-menu-bg: $dark-2; +$side-menu-item-hover-bg: $gray-1; +$side-menu-shadow: 5px 0px 10px -5px $gray-1; +$side-menu-link-color: $gray-6; // Menu dropdowns // ------------------------- -$menu-dropdown-bg: $white; +$menu-dropdown-bg: $gray-7; $menu-dropdown-hover-bg: $gray-6; $menu-dropdown-border-color: $gray-4; -$menu-dropdown-shadow: 5px 5px 20px -5px $gray-4; +$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1; // Breadcrumb // ------------------------- -$page-nav-bg: #eaebee; +$page-nav-bg: $gray-5; $page-nav-shadow: 5px 5px 20px -5px $gray-4; $page-nav-breadcrumb-color: $black; $breadcrumb-hover-hl: #d9dadd; +// Tabs +// ------------------------- +$tab-border-color: $gray-5; + // search $search-shadow: 0 5px 30px 0 $gray-4; -$search-filter-box-bg: $gray-4; +$search-filter-box-bg: $gray-7; // Dropdowns // ------------------------- @@ -257,8 +265,8 @@ $wellBackground: $gray-3; // ------------------------- $navbarHeight: 52px; -$navbarBackgroundHighlight: #f8f8f8; -$navbarBackground: #f2f3f7; +$navbarBackgroundHighlight: $white; +$navbarBackground: $white; $navbarBorder: 1px solid $gray-4; $navbarShadow: 0 0 3px #c1c1c1; @@ -275,6 +283,8 @@ $navbarBrandColor: $navbarLinkColor; $navbarButtonBackground: lighten($navbarBackground, 3%); $navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%); +$navbar-button-border: $gray-4; + // Pagination // ------------------------- @@ -318,7 +328,8 @@ $graph-tooltip-bg: $gray-5; $checkboxImageUrl: '../img/checkbox_white.png'; // info box -$info-box-background: linear-gradient(135deg, #f1fbff, #d7ebff); +// $info-box-background: linear-gradient(135deg, #f1fbff, #d7ebff); +$info-box-background: linear-gradient(135deg, $blue, $blue-dark); // footer $footer-link-color: $gray-3; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 62091acdab5..9a1e42995b3 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -75,7 +75,7 @@ $container-max-widths: ( $grid-columns: 12 !default; $grid-gutter-width: 30px !default; -$enable-flex: false; +$enable-flex: true; // Typography // ------------------------- @@ -224,7 +224,7 @@ $btn-padding-y-lg: 11px !default; $btn-padding-x-xl: 21px !default; $btn-padding-y-xl: 11px !default; -$btn-border-radius: 3px; +$btn-border-radius: 2px; // sidemenu $side-menu-width: 60px; @@ -235,5 +235,5 @@ $dashboard-padding: $panel-margin * 2; $panel-padding: 0px 10px 5px 10px; // tabs -$tabs-padding: 9px 15px 9px; +$tabs-padding: 10px 15px 9px; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index 538733be2ef..e7f76de5f69 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -15,6 +15,10 @@ background-image: url('../img/icons_#{$theme-name}_theme/icon_alert.svg'); } +.gicon-alert-alt { + background-image: url('../img/icons_#{$theme-name}_theme/icon_alert_alt.svg'); +} + .gicon-datasources { background-image: url('../img/icons_#{$theme-name}_theme/icon_data_sources.svg'); } @@ -58,3 +62,14 @@ .gicon-zoom-out { background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); } + +.sidemenu { + .gicon-dashboard { + background-image: url('../img/icons_dark_theme/icon_dashboard.svg'); + } + .gicon-alert { + background-image: url('../img/icons_dark_theme/icon_alert.svg'); + } +} + + diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 391acdc5e87..a53d35fc1da 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -106,7 +106,7 @@ } // Inverse appears as dark gray .btn-inverse { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); //background: $card-background; box-shadow: $card-shadow; //border: 1px solid $tight-form-func-highlight-bg; diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index c5979842206..9257db6c7dd 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -200,9 +200,8 @@ } .card-item { - border-bottom: .2rem solid $page-bg; - border-radius: 0; - box-shadow: none; + border-bottom: 3px solid $page-bg; + border-radius: 2px; } .card-item-header { diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a6dd09bcd5a..b7886a3a2dc 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -8,7 +8,7 @@ } .react-grid-item { - display: none; + display: none !important; transition-property: none !important; } diff --git a/public/sass/components/_dashboard_list.scss b/public/sass/components/_dashboard_list.scss index 6863dfa785b..e1574f46da6 100644 --- a/public/sass/components/_dashboard_list.scss +++ b/public/sass/components/_dashboard_list.scss @@ -1,19 +1,25 @@ .dashboard-list { - height: 75%; - .search-results-container { - padding-left: 0; + padding: 5px 0 0 0; } } .search-results-filter-row { + height: 35px; display: flex; justify-content: space-between; + + .gf-form-button-row { + padding-top: 0; + + button:last-child { + margin-right: 0; + } + } } .search-results-filter-row__filters { display: flex; - width: 300px; } .search-results-filter-row__filters-item { diff --git a/public/sass/components/_filter-table.scss b/public/sass/components/_filter-table.scss index 5b6f8695a31..074286fad80 100644 --- a/public/sass/components/_filter-table.scss +++ b/public/sass/components/_filter-table.scss @@ -17,7 +17,7 @@ tbody { tr:nth-child(odd) { - background: $dark-2; + background: $table-bg-odd; } } @@ -34,7 +34,6 @@ padding: $table-cell-padding; line-height: 30px; height: 30px; - border-bottom: 1px solid black; white-space: nowrap; &.filter-table__switch-cell { diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index c6416421be4..25d7c6ffa82 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -74,15 +74,15 @@ } .navbar-button { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); display: inline-block; font-weight: $btn-font-weight; padding: 8px 11px; line-height: 16px; color: $text-muted; - border: 1px solid #151515; - margin-right: 1px; + border: 1px solid $navbar-button-border; + margin-right: 3px; white-space: nowrap; .gicon { diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss index b6f44be1c18..dc570e8f13c 100644 --- a/public/sass/components/_page_header.scss +++ b/public/sass/components/_page_header.scss @@ -72,6 +72,21 @@ text-transform: uppercase; } +.page-header__select_nav { + margin-bottom: 10px; + + @include media-breakpoint-up(lg) { + display: none; + } +} + +.page-header__tabs { + display: none; + @include media-breakpoint-up(lg) { + display: block; + } +} + .page-breadcrumbs { display: flex; padding: 10px 0; diff --git a/public/sass/components/_panel_pluginlist.scss b/public/sass/components/_panel_pluginlist.scss index f000b795dc1..2c1b074898b 100644 --- a/public/sass/components/_panel_pluginlist.scss +++ b/public/sass/components/_panel_pluginlist.scss @@ -18,7 +18,7 @@ } .pluginlist-image { - width: 20px; + width: 17px; } .pluginlist-title { diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 240cfe80e5d..caf6084fb9e 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -120,8 +120,9 @@ display: flex; flex-grow: 1; - &:hover { - color: $text-color-weak; + &:hover, &.selected { + color: $link-hover-color; + .search-section__header__toggle { background: $tight-form-func-bg; color: $link-hover-color; @@ -129,12 +130,8 @@ } } -.search-section__header__with-checkbox { - display: flex; -} - .search-section__header__icon { - padding: 5px 10px; + padding: 2px 10px; } .search-section__header__toggle { @@ -145,14 +142,6 @@ flex-grow: 1; } -.search-item__with-checkbox { - display: flex; - - .search-item { - margin: 1px 3px; - } -} - .search-item { @include list-item(); @include left-brand-border(); @@ -163,11 +152,8 @@ white-space: nowrap; padding: 0px; - &:hover { + &:hover, &.selected { @include left-brand-border-gradient(); - } - - &.selected { background: $list-item-hover-bg; } } diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index ee5ee39f7d5..605b15dd5bc 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -110,7 +110,7 @@ display: inline-block; .fa, .icon-gf, .gicon { - color: $link-color; + color: $side-menu-link-color; position: relative; opacity: .7; font-size: 130%; @@ -135,6 +135,7 @@ white-space: nowrap; background-color: $side-menu-item-hover-bg; font-size: 17px; + color: #ebedf2; } li.sidemenu-org-switcher { diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index f09e83f8908..b612091d171 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -102,6 +102,51 @@ $switch-height: 1.5rem; } } +.gf-form-switch--transparent { + input + label { + background: transparent; + } + + input + label::before, input + label::after { + background: transparent; + } + + &:hover { + input + label::before { + background: transparent; + } + + input + label::after { + background: transparent; + } + } +} + +.gf-form-switch--search-result__section { + min-width: 3.3rem; + margin-right: -0.3rem; + + input + label { + height: 1.7rem; + } +} + +.gf-form-switch--search-result__item { + min-width: 2.6rem; + + input + label { + height: 2.7rem; + } +} + +.gf-form-switch--search-result-filter-row__checkbox { + min-width: 4.7rem; + + input + label { + height: 2.5rem; + } +} + gf-form-switch[disabled] { .gf-form-label, .gf-form-switch input + label { diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index dac7408d70f..d2c9b24d2a0 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -26,7 +26,7 @@ .tabbed-view-panel-title { float: left; - padding-top: 1rem; + padding-top: 9px; margin: 0 2rem 0 0; } diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index fee6d2cb7b8..396c3f98880 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -16,7 +16,7 @@ position: relative; display: block; border: solid transparent; - border-width: 2px 1px 1px; + border-width: 0 1px 1px; border-radius: 3px 3px 0 0; i { @@ -31,9 +31,21 @@ &.active, &.active:hover, &.active:focus { - border-color: $orange $dark-4 transparent; + border-color: $orange $tab-border-color transparent; background: $page-bg; color: $link-color; + overflow: hidden; + + &::before { + display: block; + content: ' '; + position: absolute; + left: 0; + right: 0; + height: 2px; + top: 0; + background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); + } } } diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 19a4fb1b8e1..35371cc35a9 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -15,8 +15,12 @@ } .page-container { - @extend .container; - padding: 0 $spacer * 2; + margin-left: auto; + margin-right: auto; + padding-left: $spacer*2; + padding-right: $spacer*2; + max-width: 980px; + @include clearfix(); } .scroll-canvas { diff --git a/public/sass/pages/_errorpage.scss b/public/sass/pages/_errorpage.scss index e18306ea05a..dac2768e89b 100644 --- a/public/sass/pages/_errorpage.scss +++ b/public/sass/pages/_errorpage.scss @@ -3,6 +3,11 @@ // Layout // +.error-container { + display: flex; + flex-direction: row; +} + .error-row { display: flex; flex-direction: row; @@ -22,7 +27,7 @@ .info-box { width: 38%; - padding: 2rem 1rem 6rem; + padding: 2rem 1rem 2rem; } .graph-percentage {padding: 0 0 1.5rem;} @@ -58,3 +63,31 @@ } .graph-text {margin: 0;} + +@include media-breakpoint-down(sm) { + .graph-box { + width: 50%; + } + + .info-box { + width: 50%; + } +} + +@include media-breakpoint-down(xs) { + .error-container { + flex-direction: column; + } + + .graph-box { + width: 100%; + } + + .info-box { + width: 100%; + } + + .error-full-width { + width: 100%; + } +}