Merge remote-tracking branch 'upstream/develop' into graph-legend-v5

This commit is contained in:
Alexander Zobnin 2017-12-07 12:45:08 +03:00
commit 9bff005faf
51 changed files with 1325 additions and 504 deletions

View File

@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
data.NavTree = append(data.NavTree, &dtos.NavLink{ data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Create", Text: "Create",
Id: "create",
Icon: "fa fa-fw fa-plus", Icon: "fa fa-fw fa-plus",
Url: "#", Url: "#",
Children: []*dtos.NavLink{ Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {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: "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{ dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true},
{Divider: true, 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: "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: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"},
} }

View File

@ -1,12 +1,10 @@
import { react2AngularDirective } from 'app/core/utils/react2angular'; import { react2AngularDirective } from 'app/core/utils/react2angular';
import { PasswordStrength } from './components/PasswordStrength'; import { PasswordStrength } from './components/PasswordStrength';
import PageHeader from './components/PageHeader'; import PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { NavModel, NavModelItem } from '../nav_model_srv'; import { NavModel, NavModelItem } from '../../nav_model_srv';
import classNames from 'classnames'; import classNames from 'classnames';
import appEvents from 'app/core/app_events';
export interface IProps { export interface IProps {
model: NavModel; model: NavModel;
@ -26,8 +27,44 @@ function TabItem(tab: NavModelItem) {
); );
} }
function Tabs({main}: {main: NavModelItem}) { function SelectOption(navItem: NavModelItem) {
return <ul className="gf-tabs">{main.children.map(TabItem)}</ul>; if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav
return (null);
}
return (
<option key={navItem.url} value={navItem.url}>
{navItem.text}
</option>
);
}
function Navigation({main}: {main: NavModelItem}) {
return (<nav>
<SelectNav customCss="page-header__select_nav" main={main} />
<Tabs customCss="page-header__tabs" main={main} />
</nav>);
}
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 (<select
className={`gf-select-nav ${customCss}`}
defaultValue={defaultSelectedItem.url}
onChange={gotoUrl}>{main.children.map(SelectOption)}</select>);
}
function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) {
return <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>;
} }
export default class PageHeader extends React.Component<IProps, any> { export default class PageHeader extends React.Component<IProps, any> {
@ -63,7 +100,7 @@ export default class PageHeader extends React.Component<IProps, any> {
<div className="page-container"> <div className="page-container">
<div className="page-header"> <div className="page-header">
{this.renderHeaderTitle(this.props.model.main)} {this.renderHeaderTitle(this.props.model.main)}
{this.props.model.main.children && <Tabs main={this.props.model.main} />} {this.props.model.main.children && <Navigation main={this.props.model.main} />}
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@ import Drop from 'tether-drop';
export class GrafanaCtrl { export class GrafanaCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv) { constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) {
$scope.init = function() { $scope.init = function() {
$scope.contextSrv = contextSrv; $scope.contextSrv = contextSrv;
@ -23,6 +23,7 @@ export class GrafanaCtrl {
profiler.init(config, $rootScope); profiler.init(config, $rootScope);
alertSrv.init(); alertSrv.init();
utilSrv.init(); utilSrv.init();
globalEventSrv.init();
$scope.dashAlerts = alertSrv; $scope.dashAlerts = alertSrv;
}; };

View File

@ -20,37 +20,12 @@
<div class="search-dropdown"> <div class="search-dropdown">
<div class="search-dropdown__col_1"> <div class="search-dropdown__col_1">
<div class="search-results-container" grafana-scrollbar> <div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && results.length">No dashboards matching your query were found.</h6> <h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
<div ng-repeat="section in ctrl.results" class="search-section"> results="ctrl.results"
<a class="search-section__header pointer" ng-hide="section.hideHeader" ng-click="ctrl.toggleFolder(section)"> on-tag-selected="ctrl.filterByTag($tag)" />
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}">
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
</a>
</div>
</div> </div>
</div>
</div> </div>
<div class="search-dropdown__col_2"> <div class="search-dropdown__col_2">

View File

@ -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({}, {}, {}, <SearchSrv>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();
});
});
});
});

View File

@ -64,18 +64,70 @@ export class SearchCtrl {
this.moveSelection(-1); this.moveSelection(-1);
} }
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
var selectedDash = this.results[this.selectedIndex]; const flattenedResult = this.getFlattenedResultForNavigation();
if (selectedDash) { const currentItem = flattenedResult[this.selectedIndex];
this.$location.search({});
this.$location.path(selectedDash.url); 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) { moveSelection(direction) {
var max = (this.results || []).length; if (this.results.length === 0) {
var newIndex = this.selectedIndex + direction; 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; 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() { searchDashboards() {
@ -84,8 +136,9 @@ export class SearchCtrl {
return this.searchSrv.search(this.query).then(results => { return this.searchSrv.search(this.query).then(results => {
if (localSearchId < this.currentSearchId) { return; } if (localSearchId < this.currentSearchId) { return; }
this.results = results; this.results = results || [];
this.isLoading = false; this.isLoading = false;
this.moveSelection(1);
}); });
} }
@ -94,13 +147,11 @@ export class SearchCtrl {
return query.query === '' && query.starred === false && query.tag.length === 0; return query.query === '' && query.starred === false && query.tag.length === 0;
} }
filterByTag(tag, evt) { filterByTag(tag) {
this.query.tag.push(tag); if (_.indexOf(this.query.tag, tag) === -1) {
this.search(); this.query.tag.push(tag);
this.giveSearchFocus = this.giveSearchFocus + 1; this.search();
if (evt) { this.giveSearchFocus = this.giveSearchFocus + 1;
evt.stopPropagation();
evt.preventDefault();
} }
} }
@ -127,12 +178,32 @@ export class SearchCtrl {
search() { search() {
this.showImport = false; this.showImport = false;
this.selectedIndex = 0; this.selectedIndex = -1;
this.searchDashboards(); this.searchDashboards();
} }
toggleFolder(section) { private getFlattenedResultForNavigation() {
this.searchSrv.toggleSection(section); 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;
});
} }
} }

View File

@ -0,0 +1,47 @@
<div ng-repeat="section in ctrl.results" class="search-section">
<a class="search-section__header pointer" ng-hide="section.hideHeader" ng-class="{'selected': section.selected}" ng-click="ctrl.toggleFolderExpand(section)">
<div ng-click="ctrl.toggleSelection(section, $event)">
<gf-form-switch
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged($event)"
checked="section.checked"
switch-class="gf-form-switch--transparent gf-form-switch--search-result__section">
</gf-form-switch>
</div>
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<div ng-show="ctrl.editable && section.id > 0 && section.expanded" ng-click="ctrl.navigateToFolder(section, $event)">
<i class="fa fa-cog search-section__header__toggle"></i>&nbsp;
</div>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
<div class="search-section__header" ng-show="section.hideHeader"></div>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}">
<div ng-click="ctrl.toggleSelection(item, $event)">
<gf-form-switch
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged()"
checked="item.checked"
switch-class="gf-form-switch--transparent gf-form-switch--search-result__item">
</gf-form-switch>
</div>
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.selectTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
</a>
</div>
</div>

View File

@ -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();
});
});
});

View File

@ -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);

View File

@ -54,6 +54,7 @@ import {profiler} from './profiler';
import {registerAngularDirectives} from './angular_wrappers'; import {registerAngularDirectives} from './angular_wrappers';
import {updateLegendValues} from './time_series2'; import {updateLegendValues} from './time_series2';
import TimeSeries from './time_series2'; import TimeSeries from './time_series2';
import {searchResultsDirective} from './components/search/search_results';
export { export {
profiler, profiler,
@ -87,5 +88,6 @@ export {
gfPageDirective, gfPageDirective,
orgSwitcher, orgSwitcher,
TimeSeries, TimeSeries,
updateLegendValues updateLegendValues,
searchResultsDirective
}; };

View File

@ -119,14 +119,6 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.openEditView('annotations') 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) { if (!dashboard.meta.isHome) {
menu.push({ menu.push({
title: 'Version history', title: 'Version history',

View File

@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
reloadOnSearch: false, reloadOnSearch: false,
pageClass: 'page-dashboard', pageClass: 'page-dashboard',
}) })
.when('/dashboard/import', {
templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html',
controller : 'DashboardImportCtrl',
controllerAs: 'ctrl',
})
.when('/datasources', { .when('/datasources', {
templateUrl: 'public/app/features/plugins/partials/ds_list.html', templateUrl: 'public/app/features/plugins/partials/ds_list.html',
controller : 'DataSourcesCtrl', controller : 'DataSourcesCtrl',
@ -68,6 +73,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controller : 'DashboardListCtrl', controller : 'DashboardListCtrl',
controllerAs: 'ctrl', controllerAs: 'ctrl',
}) })
.when('/dashboards/folder/:folderId/:type/:slug', {
templateUrl: 'public/app/features/dashboard/partials/dashboardList.html',
controller : 'DashboardListCtrl',
controllerAs: 'ctrl',
})
.when('/org', { .when('/org', {
templateUrl: 'public/app/features/org/partials/orgDetails.html', templateUrl: 'public/app/features/org/partials/orgDetails.html',
controller : 'OrgDetailsCtrl', controller : 'OrgDetailsCtrl',

View File

@ -8,5 +8,6 @@ define([
'./segment_srv', './segment_srv',
'./backend_srv', './backend_srv',
'./dynamic_directive_srv', './dynamic_directive_srv',
'./global_event_srv'
], ],
function () {}); function () {});

View File

@ -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);

View File

@ -128,14 +128,20 @@ export class SearchSrv {
}); });
} }
private browse() { private browse(options) {
let sections: any = {}; let sections: any = {};
let promises = [ let promises = [];
this.getRecentDashboards(sections),
this.getStarred(sections), if (!options.skipRecent) {
this.getDashboardsAndFolders(sections), 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 this.$q.all(promises).then(() => {
return _.sortBy(_.values(sections), 'score'); return _.sortBy(_.values(sections), 'score');
@ -148,15 +154,19 @@ export class SearchSrv {
} }
search(options) { search(options) {
if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) { if (!options.folderIds && !options.query && (!options.tag || options.tag.length === 0) && !options.starred) {
return this.browse(); return this.browse(options);
} }
let query = _.clone(options); let query = _.clone(options);
query.folderIds = []; query.folderIds = options.folderIds || [];
query.type = 'dash-db'; query.type = 'dash-db';
return this.backendSrv.search(query).then(results => { return this.backendSrv.search(query).then(results => {
if (results.length === 0) {
return results;
}
let section = { let section = {
hideHeader: true, hideHeader: true,
items: [], items: [],
@ -191,10 +201,6 @@ export class SearchSrv {
}); });
} }
toggleSection(section) {
section.toggle(section);
}
getDashboardTags() { getDashboardTags() {
return this.backendSrv.get('/api/dashboards/tags'); return this.backendSrv.get('/api/dashboards/tags');
} }

View File

@ -2,6 +2,7 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv'; import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv'; import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common';
jest.mock('app/core/store', () => { jest.mock('app/core/store', () => {
return { return {
@ -244,4 +245,43 @@ describe('SearchSrv', () => {
expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); 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();
});
});
}); });

View File

@ -1,10 +1,7 @@
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
export function react2AngularDirective(name: string, component: any, options: any) { export function react2AngularDirective(name: string, component: any, options: any) {
coreModule.directive(name, ['reactDirective', reactDirective => { coreModule.directive(name, ['reactDirective', reactDirective => {
return reactDirective(component, options); return reactDirective(component, options);
}]); }]);
} }

View File

@ -15,7 +15,6 @@ import './unsavedChangesSrv';
import './unsaved_changes_modal'; import './unsaved_changes_modal';
import './timepicker/timepicker'; import './timepicker/timepicker';
import './upload'; import './upload';
import './import/dash_import';
import './export/export_modal'; import './export/export_modal';
import './export_data/export_data_modal'; import './export_data/export_data_modal';
import './ad_hoc_filters'; import './ad_hoc_filters';
@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import {DashboardListCtrl} from './dashboard_list_ctrl'; import {DashboardListCtrl} from './dashboard_list_ctrl';
import {DashboardImportCtrl} from './dashboard_import_ctrl';
coreModule.controller('DashboardListCtrl', DashboardListCtrl); coreModule.controller('DashboardListCtrl', DashboardListCtrl);
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);

View File

@ -1,10 +1,8 @@
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import _ from 'lodash'; import _ from 'lodash';
import config from 'app/core/config';
export class DashImportCtrl { export class DashboardImportCtrl {
navModel: any;
step: number; step: number;
jsonText: string; jsonText: string;
parseError: string; parseError: string;
@ -17,7 +15,9 @@ export class DashImportCtrl {
gnetInfo: any; gnetInfo: any;
/** @ngInject */ /** @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.step = 1;
this.nameExists = false; this.nameExists = false;
@ -160,17 +160,4 @@ export class DashImportCtrl {
this.gnetError = ''; this.gnetError = '';
this.gnetInfo = ''; 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);

View File

@ -14,16 +14,29 @@ export class DashboardListCtrl {
selectAllChecked = false; selectAllChecked = false;
starredFilterOptions = [{text: 'Filter by Starred', disabled: true}, {text: 'Yes'}, {text: 'No'}]; starredFilterOptions = [{text: 'Filter by Starred', disabled: true}, {text: 'Yes'}, {text: 'No'}];
selectedStarredFilter: any; selectedStarredFilter: any;
folderTitle = null;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv, private $routeParams) {
this.navModel = navModelSrv.getNav('dashboards', 'dashboards', 0); this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
this.query = {query: '', mode: 'tree', tag: [], starred: false}; this.query = {query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true};
this.selectedStarredFilter = this.starredFilterOptions[0]; this.selectedStarredFilter = this.starredFilterOptions[0];
this.getDashboards().then(() => { if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) {
this.getTags(); 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() { getDashboards() {
@ -137,10 +150,6 @@ export class DashboardListCtrl {
}); });
} }
toggleFolder(section) {
return this.searchSrv.toggleSection(section);
}
getTags() { getTags() {
return this.searchSrv.getDashboardTags().then((results) => { return this.searchSrv.getDashboardTags().then((results) => {
this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results);
@ -148,11 +157,9 @@ export class DashboardListCtrl {
}); });
} }
filterByTag(tag, evt) { filterByTag(tag) {
this.query.tag.push(tag); if (_.indexOf(this.query.tag, tag) === -1) {
if (evt) { this.query.tag.push(tag);
evt.stopPropagation();
evt.preventDefault();
} }
return this.getDashboards(); return this.getDashboards();
@ -163,9 +170,9 @@ export class DashboardListCtrl {
} }
onTagFilterChange() { onTagFilterChange() {
this.query.tag.push(this.selectedTagFilter.term); var res = this.filterByTag(this.selectedTagFilter.term);
this.selectedTagFilter = this.tagFilterOptions[0]; this.selectedTagFilter = this.tagFilterOptions[0];
return this.getDashboards(); return res;
} }
removeTag(tag, evt) { removeTag(tag, evt) {

View File

@ -383,8 +383,8 @@ export class DashboardMigrator {
return; return;
} }
// Add special "row" panels if even one row is collapsed or has visible title // 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); const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle || row.repeat);
for (let row of old.rows) { for (let row of old.rows) {
let height: any = row.height || DEFAULT_ROW_HEIGHT; let height: any = row.height || DEFAULT_ROW_HEIGHT;
@ -398,6 +398,7 @@ export class DashboardMigrator {
rowPanel.type = 'row'; rowPanel.type = 'row';
rowPanel.title = row.title; rowPanel.title = row.title;
rowPanel.collapsed = row.collapse; rowPanel.collapsed = row.collapse;
rowPanel.repeat = row.repeat;
rowPanel.panels = []; rowPanel.panels = [];
rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight}; rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight};
rowPanelModel = new PanelModel(rowPanel); rowPanelModel = new PanelModel(rowPanel);

View File

@ -1,138 +0,0 @@
<div class="modal-header">
<h2 class="modal-header-title">
<i class="gicon gicon-dashboard-import"></i>
<span class="p-l-1">Import Dashboard</span>
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content" ng-cloak>
<div ng-if="ctrl.step === 1">
<form class="gf-form-group">
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
</form>
<h5 class="section-heading">Grafana.com Dashboard</h5>
<div class="gf-form-group">
<div class="gf-form">
<input type="text" class="gf-form-input" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.com dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
</div>
<div class="gf-form" ng-if="ctrl.gnetError">
<label class="gf-form-label text-warning">
<i class="fa fa-warning"></i>
{{ctrl.gnetError}}
</label>
</div>
</div>
<h5 class="section-heading">Or paste JSON</h5>
<div class="gf-form-group">
<div class="gf-form">
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
</div>
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
<i class="fa fa-paste"></i>
Load
</button>
<span ng-if="ctrl.parseError" class="text-error p-l-1">
<i class="fa fa-warning"></i>
{{ctrl.parseError}}
</span>
</div>
</div>
<div ng-if="ctrl.step === 2">
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
<h3 class="section-heading">
Importing Dashboard from
<a href="https://grafana.com/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.com</a>
</h3>
<div class="gf-form">
<label class="gf-form-label width-15">Published by</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-15">Updated on</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
</div>
</div>
<h3 class="section-heading">
Options
</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-15">Name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.dash.title">
<i class="fa fa-check"></i>
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.nameExists">
<div class="gf-form offset-width-15 gf-form--grow">
<label class="gf-form-label text-warning gf-form-label--grow">
<i class="fa fa-warning"></i>
A Dashboard with the same name already exists
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="!ctrl.dash.title">
<div class="gf-form offset-width-15 gf-form--grow">
<label class="gf-form-label text-warning gf-form-label--grow">
<i class="fa fa-warning"></i>
A Dashboard should have a name
</label>
</div>
</div>
<div ng-repeat="input in ctrl.inputs">
<div class="gf-form">
<label class="gf-form-label width-15">
{{input.label}}
<info-popover mode="right-normal">
{{input.info}}
</info-popover>
</label>
<!-- Data source input -->
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
<option value="" ng-hide="input.value">{{input.info}}</option>
</select>
</div>
<!-- Constant input -->
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
<label class="gf-form-label text-success" ng-show="input.value">
<i class="fa fa-check"></i>
</label>
</div>
</div>
</div>
<div class="gf-form-button-row">
<button type="button" class="btn gf-form-btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
<i class="fa fa-save"></i> Import
</button>
<button type="button" class="btn gf-form-btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
<i class="fa fa-save"></i> Import (Overwrite)
</button>
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
<a class="btn btn-link" ng-click="ctrl.back()">Back</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,126 @@
<page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body" ng-cloak>
<div ng-if="ctrl.step === 1">
<form class="page-action-bar">
<div class="page-action-bar__spacer"></div>
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
</form>
<h5 class="section-heading">Grafana.com Dashboard</h5>
<div class="gf-form-group">
<div class="gf-form gf-form--grow">
<input type="text" class="gf-form-input max-width-30" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.com dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
</div>
<div class="gf-form" ng-if="ctrl.gnetError">
<label class="gf-form-label text-warning">
<i class="fa fa-warning"></i>
{{ctrl.gnetError}}
</label>
</div>
</div>
<h5 class="section-heading">Or paste JSON</h5>
<div class="gf-form-group">
<div class="gf-form">
<textarea rows="10" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
</div>
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
<i class="fa fa-paste"></i>
Load
</button>
<span ng-if="ctrl.parseError" class="text-error p-l-1">
<i class="fa fa-warning"></i>
{{ctrl.parseError}}
</span>
</div>
</div>
<div ng-if="ctrl.step === 2">
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
<h3 class="section-heading">
Importing Dashboard from
<a href="https://grafana.com/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.com</a>
</h3>
<div class="gf-form">
<label class="gf-form-label width-15">Published by</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-15">Updated on</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
</div>
</div>
<h3 class="section-heading">
Options
</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-15">Name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.dash.title">
<i class="fa fa-check"></i>
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.nameExists">
<div class="gf-form offset-width-15 gf-form--grow">
<label class="gf-form-label text-warning gf-form-label--grow">
<i class="fa fa-warning"></i>
A Dashboard with the same name already exists
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="!ctrl.dash.title">
<div class="gf-form offset-width-15 gf-form--grow">
<label class="gf-form-label text-warning gf-form-label--grow">
<i class="fa fa-warning"></i>
A Dashboard should have a name
</label>
</div>
</div>
<div ng-repeat="input in ctrl.inputs">
<div class="gf-form">
<label class="gf-form-label width-15">
{{input.label}}
<info-popover mode="right-normal">
{{input.info}}
</info-popover>
</label>
<!-- Data source input -->
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
<option value="" ng-hide="input.value">{{input.info}}</option>
</select>
</div>
<!-- Constant input -->
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
<label class="gf-form-label text-success" ng-show="input.value">
<i class="fa fa-check"></i>
</label>
</div>
</div>
</div>
<div class="gf-form-button-row">
<button type="button" class="btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
<i class="fa fa-save"></i> Import
</button>
<button type="button" class="btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
<i class="fa fa-save"></i> Import (Overwrite)
</button>
<a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>
</div>
</div>
</div>

View File

@ -1,17 +1,35 @@
<page-header model="ctrl.navModel"></page-header> <page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body"> <div class="page-container page-body">
<div class="page-action-bar" ng-show="ctrl.folderTitle">
<div class="gf-form gf-form--grow">
<h3 class="page-sub-heading">
<i class="fa fa-folder-open"></i>&nbsp;{{ctrl.folderTitle}}
</h3>
</div>
<div class="page-action-bar__spacer"></div>
<button class="btn btn-inverse" disabled>Permissions</button>
<a class="btn btn-success" href="/dashboard/new">
<i class="fa fa-plus"></i>
Dashboard
</a>
<a class="btn btn-success" href="/dashboard/new/?editview=new-folder">
<i class="fa fa-plus"></i>
Folder
</a>
</div>
<div class="page-action-bar"> <div class="page-action-bar">
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<label class="gf-form-label">Search</label> <label class="gf-form-label">Search</label>
<input type="text" class="gf-form-input max-width-30" placeholder="Find Dashboard by name" tabindex="1" give-focus="true" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.onQueryChange()" /> <input type="text" class="gf-form-input max-width-30" placeholder="Find Dashboard by name" tabindex="1" give-focus="true" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.onQueryChange()" />
</div> </div>
<div class="page-action-bar__spacer"></div> <div class="page-action-bar__spacer"></div>
<a class="btn btn-success" href="/dashboard/new"> <a class="btn btn-success" href="/dashboard/new" ng-hide="ctrl.folderTitle">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
Dashboard Dashboard
</a> </a>
<a class="btn btn-success" href="/dashboard/new/?editview=new-folder"> <a class="btn btn-success" href="/dashboard/new/?editview=new-folder" ng-hide="ctrl.folderTitle">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
Folder Folder
</a> </a>
@ -39,29 +57,25 @@
</div> </div>
</div> </div>
<div class="gf-form-group"> <div ng-if="!ctrl.hasFilters && ctrl.sections.length === 0">
<div class="gf-form-button-row"> <empty-list-cta model="{
<button type="button" title: 'This folder doesn\'t have any dashboards yet',
class="btn gf-form-button btn-secondary" buttonIcon: 'gicon gicon-dashboard-new',
ng-disabled="!ctrl.canMove" buttonLink: '/dashboard/new',
ng-click="ctrl.moveTo()" buttonTitle: 'Create Dashboard',
bs-tooltip="ctrl.canMove ? '' : 'Select a dashboard to move (cannot move folders)'" data-placement="bottom"> proTip: 'You can bulk move dashboards into this folder from the main dashboard list.',
<i class="fa fa-exchange"></i>&nbsp;&nbsp;Move to... proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
</button> proTipLinkTitle: 'Learn more',
<button type="button" proTipTarget: '_blank'
class="btn gf-form-button btn-inverse" }" />
ng-click="ctrl.delete()"
ng-disabled="!ctrl.canDelete">
<i class="fa fa-trash"></i>&nbsp;&nbsp;Delete
</button>
</div>
</div> </div>
<div class="dashboard-list"> <div class="dashboard-list" ng-show="ctrl.sections.length > 0">
<div class="search-results-filter-row"> <div class="search-results-filter-row">
<gf-form-switch <gf-form-switch
on-change="ctrl.onSelectAllChanged()" on-change="ctrl.onSelectAllChanged()"
checked="ctrl.selectAllChecked" checked="ctrl.selectAllChecked"
switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox"
/> />
<div class="search-results-filter-row__filters"> <div class="search-results-filter-row__filters">
<select <select
@ -69,63 +83,39 @@
ng-model="ctrl.selectedStarredFilter" ng-model="ctrl.selectedStarredFilter"
ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions" ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions"
ng-change="ctrl.onStarredFilterChange()" ng-change="ctrl.onStarredFilterChange()"
ng-show="!(ctrl.canMove || ctrl.canDelete)"
/> />
<select <select
class="search-results-filter-row__filters-item gf-form-input" class="search-results-filter-row__filters-item gf-form-input"
ng-model="ctrl.selectedTagFilter" ng-model="ctrl.selectedTagFilter"
ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions" ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions"
ng-change="ctrl.onTagFilterChange()" ng-change="ctrl.onTagFilterChange()"
ng-show="!(ctrl.canMove || ctrl.canDelete)"
/> />
<div class="gf-form-button-row" ng-show="ctrl.canMove || ctrl.canDelete">
<button type="button"
class="btn gf-form-button btn-inverse"
ng-disabled="!ctrl.canMove"
ng-click="ctrl.moveTo()"
bs-tooltip="ctrl.canMove ? '' : 'Select a dashboard to move (cannot move folders)'"
data-placement="bottom">
<i class="fa fa-exchange"></i>&nbsp;&nbsp;Move
</button>
<button type="button"
class="btn gf-form-button btn-danger"
ng-click="ctrl.delete()"
ng-disabled="!ctrl.canDelete">
<i class="fa fa-trash"></i>&nbsp;&nbsp;Delete
</button>
</div>
</div> </div>
</div> </div>
<div class="search-results-container" ng-show="ctrl.sections.length > 0" grafana-scrollbar> <div class="search-results-container">
<div ng-repeat="section in ctrl.sections" class="search-section"> <dashboard-search-results
results="ctrl.sections"
<div class="search-section__header__with-checkbox" ng-hide="section.hideHeader"> editable="true"
<gf-form-switch on-selection-changed="ctrl.selectionChanged()"
on-change="ctrl.selectionChanged()" on-tag-selected="ctrl.filterByTag($tag)" />
checked="section.checked">
</gf-form-switch>
<a class="search-section__header pointer" ng-click="ctrl.toggleFolder(section)" ng-hide="section.hideHeader">
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
</div>
<div ng-if="section.expanded">
<div ng-repeat="item in section.items" class="search-item__with-checkbox" ng-class="{'selected': item.selected}">
<gf-form-switch
on-change="ctrl.selectionChanged()"
checked="item.checked" />
<a ng-href="{{::item.url}}" class="search-item">
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
<i class="fa fa-folder-o"></i>
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
<span class="search-item__actions">
<i class="fa" ng-class="{'fa-star': item.isStarred, 'fa-star-o': !item.isStarred}"></i>
</span>
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<em class="muted" ng-hide="ctrl.sections.length > 0">
No Dashboards or Folders found.
</em>

View File

@ -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'; describe('DashboardImportCtrl', function() {
import config from 'app/core/config';
describe('DashImportCtrl', function() {
var ctx: any = {}; 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) => { beforeEach(() => {
ctx.$q = $q; navModelSrv = {
ctx.scope = $rootScope.$new(); getNav: () => {}
ctx.ctrl = $controller(DashImportCtrl, { };
$scope: ctx.scope,
backendSrv: backendSrv, backendSrv = {
}); search: jest.fn().mockReturnValue(Promise.resolve([])),
})); get: jest.fn()
};
ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {});
});
describe('when uploading json', function() { describe('when uploading json', function() {
beforeEach(function() { beforeEach(function() {
@ -37,13 +36,13 @@ describe('DashImportCtrl', function() {
}); });
it('should build input model', function() { it('should build input model', function() {
expect(ctx.ctrl.inputs.length).to.eql(1); expect(ctx.ctrl.inputs.length).toBe(1);
expect(ctx.ctrl.inputs[0].name).to.eql('ds'); expect(ctx.ctrl.inputs[0].name).toBe('ds');
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source');
}); });
it('should set inputValid to false', function() { 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() { beforeEach(function() {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock // setup api mock
backendSrv.get = sinon.spy(() => { backendSrv.get = jest.fn(() => {
return Promise.resolve({ return Promise.resolve({
json: {} json: {}
}); });
@ -60,7 +59,7 @@ describe('DashImportCtrl', function() {
}); });
it('should call gnet api with correct dashboard id', 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() { beforeEach(function() {
ctx.ctrl.gnetUrl = '2342'; ctx.ctrl.gnetUrl = '2342';
// setup api mock // setup api mock
backendSrv.get = sinon.spy(() => { backendSrv.get = jest.fn(() => {
return Promise.resolve({ return Promise.resolve({
json: {} json: {}
}); });
@ -77,10 +76,8 @@ describe('DashImportCtrl', function() {
}); });
it('should call gnet api with correct dashboard id', 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');
}); });
}); });
}); });

View File

@ -537,13 +537,10 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
search: (options: any) => { search: (options: any) => {
return q.resolve(searchResponse); return q.resolve(searchResponse);
}, },
toggleSection: (section) => {
return;
},
getDashboardTags: () => { getDashboardTags: () => {
return q.resolve(tags || []); return q.resolve(tags || []);
} }
}; };
return new DashboardListCtrl({}, { getNav: () => { } }, q, <SearchSrv>searchSrvStub); return new DashboardListCtrl({}, { getNav: () => { } }, q, <SearchSrv>searchSrvStub, {});
} }

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import { DashboardModel } from '../dashboard_model'; import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
import { expect } from 'test/lib/common';
jest.mock('app/core/services/context_srv', () => ({})); jest.mock('app/core/services/context_srv', () => ({}));
@ -315,12 +316,33 @@ describe('DashboardModel', function() {
expect(panelGridPos).toEqual(expectedGrid); 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[]) { function createRow(options, panelDescriptions: any[]) {
const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; 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; height = height * PANEL_HEIGHT_STEP;
let panels = []; let panels = [];
_.each(panelDescriptions, panelDesc => { _.each(panelDescriptions, panelDesc => {
@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) {
} }
panels.push(panel); panels.push(panel);
}); });
let row = {collapse, height, showTitle, title, panels}; let row = {collapse, height, showTitle, title, panels, repeat};
return row; return row;
} }

View File

@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
var template = ` var template = `
<input type="file" id="dashupload" name="dashupload" class="hide"/> <input type="file" id="dashupload" name="dashupload" class="hide"/>
<label class="btn btn-secondary" for="dashupload"> <label class="btn btn-success" for="dashupload">
<i class="fa fa-upload"></i> <i class="fa fa-upload"></i>
Upload .json File Upload .json File
</label> </label>

View File

@ -1,6 +1,7 @@
///<reference path="../../headers/common.d.ts" /> ///<reference path="../../headers/common.d.ts" />
import coreModule from '../../core/core_module'; import coreModule from '../../core/core_module';
import {appEvents} from 'app/core/core';
export class DataSourcesCtrl { export class DataSourcesCtrl {
datasources: any; datasources: any;
@ -11,13 +12,24 @@ export class DataSourcesCtrl {
private $scope, private $scope,
private backendSrv, private backendSrv,
private datasourceSrv, private datasourceSrv,
private $location,
private navModelSrv) { private navModelSrv) {
this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0); this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0);
this.navigateToUrl = this.navigateToUrl.bind(this);
backendSrv.get('/api/datasources').then(result => { backendSrv.get('/api/datasources').then(result => {
this.datasources = result; this.datasources = result;
}); });
appEvents.on('location-change', payload => {
this.navigateToUrl(payload.href);
});
}
navigateToUrl(url) {
// debugger;
this.$location.path(url);
this.$location.replace();
} }
removeDataSourceConfirmed(ds) { removeDataSourceConfirmed(ds) {

View File

@ -49,7 +49,7 @@
buttonLink: '/datasources/new', buttonLink: '/datasources/new',
buttonTitle: 'Add data source', buttonTitle: 'Add data source',
proTip: 'You can also define data sources through configuration files.', proTip: 'You can also define data sources through configuration files.',
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources', proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
proTipLinkTitle: 'Learn more', proTipLinkTitle: 'Learn more',
proTipTarget: '_blank' proTipTarget: '_blank'
}" /> }" />

View File

@ -1,55 +1,60 @@
<navbar model="navModel"></navbar>
<div class="page-container"> <div class="page-container">
<div class="page-header"> <div class="page-header">
<h1> <div class="page-header__inner">
Page not found (404) <span class="page-header__logo">
</h1> <i class="page-header__icon fa fa-fw fa-exclamation-triangle"></i>
</div> </span>
<div class="error-row"> <div class="page-header__info-block">
<div class="dash-row-menu-grip"><i class="fa fa-ellipsis-v"></i></div> <h1 class="page-header__title">
<div class="panel-container error-row"> Page not found
<div class="error-column graph-box"> </h1>
<div class="error-row"> <div class="page-header__sub-title">
<div class="error-column error-space-between graph-percentage"> 404 Error
<p>100%</p> </div>
<p>80%</p> </div>
<p>60%</p> </div>
<p>40%</p> </div>
<p>20%</p> <div class="panel-container error-container">
<p>0%</p> <div class="error-column graph-box">
</div> <div class="error-row">
<div class="error-column image-box"> <div class="error-column error-space-between graph-percentage">
<img src="public/img/graph404.svg" width="100%"> <p>100%</p>
<div class="error-row error-space-between"> <p>80%</p>
<p class="graph-text">Then</p> <p>60%</p>
<p class="graph-text">Now</p> <p>40%</p>
</div> <p>20%</p>
</div> <p>0%</p>
</div> </div>
</div> <div class="error-column image-box">
<div class="error-column info-box"> <img src="public/img/graph404.svg" width="100%">
<div class="error-row current-box"> <div class="error-row error-space-between">
<p class="current-text">current</p> <p class="graph-text">Then</p>
</div> <p class="graph-text">Now</p>
<div class="error-row" style="flex: 1"> </div>
<i class="fa fa-minus error-minus"></i> </div>
<div class="error-column error-space-between"> </div>
<div class="error-row error-space-between"> </div>
<p>Chances you are on the page you are looking for.</p> <div class="error-column info-box">
<p class="left-margin">0%</p> <div class="error-row current-box">
</div> <p class="current-text">current</p>
<div> </div>
<h3>Sorry for the inconvenience</h3> <div class="error-row" style="flex: 1">
<p>Please go back to your <a href="{{appSubUrl}}/" class="error-link">home dashboard</a> and try again.</p> <i class="fa fa-minus error-minus"></i>
<p>If the error persists, seek help on the <a href="https://community.grafana.com" target="_blank" class="error-link">community site</a>.</p> <div class="error-column error-space-between error-full-width">
</div> <div class="error-row error-space-between">
</div> <p>Chances you are on the page you are looking for.</p>
</div> <p class="left-margin">0%</p>
</div> </div>
<span class="resize-panel-handle icon-gf icon-gf-grabber" style="cursor: default"></span> <div>
</div> <h3>Sorry for the inconvenience</h3>
</div> <p>Please go back to your
<a href="{{appSubUrl}}/" class="error-link">home dashboard</a> and try again.</p>
<p>If the error persists, seek help on the
<a href="https://community.grafana.com" target="_blank" class="error-link">community site</a>.</p>
</div>
</div>
</div>
</div>
<span class="react-resizable-handle" style="cursor: default"></span>
</div>
</div> </div>

View File

@ -139,6 +139,8 @@ $table-bg-accent: $dark-3; // for striping
$table-bg-hover: $dark-4; // for hover $table-bg-hover: $dark-4; // for hover
$table-border: $dark-3; // table and cell border $table-border: $dark-3; // table and cell border
$table-bg-odd: $dark-2;
// Buttons // Buttons
// ------------------------- // -------------------------
@ -160,6 +162,7 @@ $btn-danger-bg-hl: darken($red, 8%);
$btn-inverse-bg: $dark-3; $btn-inverse-bg: $dark-3;
$btn-inverse-bg-hl: lighten($dark-3, 4%); $btn-inverse-bg-hl: lighten($dark-3, 4%);
$btn-inverse-text-color: $link-color; $btn-inverse-text-color: $link-color;
$btn-inverse-text-shadow: 0px 1px 0 rgba(0,0,0,.1);
$btn-link-color: $gray-3; $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-box-shadow-focus: rgba(102,175,233,.6) !default;
$input-color-placeholder: $gray-1 !default; $input-color-placeholder: $gray-1 !default;
$input-label-bg: $gray-blue; $input-label-bg: $gray-blue;
$input-label-border-color: transparent; $input-label-border-color: $gray-blue;
$input-invalid-border-color: lighten($red, 5%); $input-invalid-border-color: lighten($red, 5%);
// Search // Search
@ -241,11 +244,14 @@ $navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg;
$navbarButtonBackground: $navbarBackground; $navbarButtonBackground: $navbarBackground;
$navbarButtonBackgroundHighlight: $body-bg; $navbarButtonBackgroundHighlight: $body-bg;
$navbar-button-border: #151515;
// Sidemenu // Sidemenu
// ------------------------- // -------------------------
$side-menu-bg: $black; $side-menu-bg: $black;
$side-menu-item-hover-bg: $dark-2; $side-menu-item-hover-bg: $dark-2;
$side-menu-shadow: 0 0 20px black; $side-menu-shadow: 0 0 20px black;
$side-menu-link-color: $link-color;
$breadcrumb-hover-hl: #111; $breadcrumb-hover-hl: #111;
// Menu dropdowns // Menu dropdowns
@ -261,6 +267,9 @@ $page-nav-bg: $black;
$page-nav-shadow: 5px 5px 20px -5px $black; $page-nav-shadow: 5px 5px 20px -5px $black;
$page-nav-breadcrumb-color: $gray-3; $page-nav-breadcrumb-color: $gray-3;
// Tabs
// -------------------------
$tab-border-color: $dark-4;
// Pagination // Pagination
// ------------------------- // -------------------------

View File

@ -14,24 +14,24 @@ $black: #000;
// ------------------------- // -------------------------
$black: #000; $black: #000;
$dark-1: #141414; $dark-1: #13161d;
$dark-2: #1d1d1f; $dark-2: #1e2028;
$dark-3: #262628; $dark-3: #303133;
$dark-4: #373737; $dark-4: #35373f;
$dark-5: #444444; $dark-5: #41444b;
$gray-1: #555555; $gray-1: #52545c;
$gray-2: #7B7B7B; $gray-2: #767980;
$gray-3: #b3b3b3; $gray-3: #acb6bf;
$gray-4: #D8D9DA; $gray-4: #c7d0d9;
$gray-5: #ECECEC; $gray-5: #dde4ed;
$gray-6: #f4f5f8; $gray-6: #e9edf2;
$gray-7: #fbfbfb; $gray-7: #f7f8fa;
$white: #fff; $white: #fff;
// Accent colors // Accent colors
// ------------------------- // -------------------------
$blue: #2AB2E4; $blue: #1ca4d6;
$blue-dark: #3CAAD6; $blue-dark: #3CAAD6;
$green: #3aa655; $green: #3aa655;
$red: #d44939; $red: #d44939;
@ -39,7 +39,7 @@ $yellow: #FF851B;
$orange: #Ff7941; $orange: #Ff7941;
$pink: #E671B8; $pink: #E671B8;
$purple: #9954BB; $purple: #9954BB;
$variable: #2AB2E4; $variable: $blue;
$brand-primary: $orange; $brand-primary: $orange;
$brand-success: $green; $brand-success: $green;
@ -55,22 +55,22 @@ $critical: #EC2128;
// Scaffolding // Scaffolding
// ------------------------- // -------------------------
$body-bg: $white; $body-bg: $gray-7;
$page-bg: $white; $page-bg: $gray-7;
$body-color: $gray-1; $body-color: $gray-1;
$text-color: $gray-1; $text-color: $dark-4;
$text-color-strong: $white; $text-color-strong: $white;
$text-color-weak: $gray-3; $text-color-weak: $gray-2;
$text-color-faint: $gray-4; $text-color-faint: $gray-4;
$text-color-emphasis: $dark-5; $text-color-emphasis: $dark-5;
$text-shadow-strong: none; $text-shadow-strong: none;
$text-shadow-faint: none; $text-shadow-faint: none;
$textShadow: none;
// gradients // 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%); $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-gradient: linear-gradient(-60deg, transparent 70%, $gray-7 98%);
$page-header-bg: linear-gradient(90deg, #292a2d, black);
// Links // Links
// ------------------------- // -------------------------
@ -97,7 +97,7 @@ $component-active-bg: $brand-primary !default;
// Panel // Panel
// ------------------------- // -------------------------
$panel-bg: $gray-7; $panel-bg: $white;
$panel-border-color: $gray-5; $panel-border-color: $gray-5;
$panel-border: solid 1px $panel-border-color; $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); $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; $panel-header-menu-hover-bg: $gray-4;
// Page header // Page header
$page-header-bg: linear-gradient(90deg, #292a2d, black); $page-header-bg: linear-gradient(90deg, $white, $gray-7);
$page-header-shadow: inset 0px -4px 14px $dark-2; $page-header-shadow: inset 0px -3px 10px $gray-6;
$page-header-border-color: $dark-4; $page-header-border-color: $gray-4;
$divider-border-color: $gray-2; $divider-border-color: $gray-2;
@ -122,12 +122,12 @@ $code-tag-bg: $gray-6;
$code-tag-border: darken($code-tag-bg, 3%); $code-tag-border: darken($code-tag-bg, 3%);
// cards // cards
$card-background: linear-gradient(135deg, $gray-5, $gray-6); $card-background: linear-gradient(135deg, $gray-6, $gray-5);
$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); $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); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1);
// Lists // 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-hover-bg: darken($gray-5, 5%);
$list-item-link-color: $text-color; $list-item-link-color: $text-color;
$list-item-shadow: $card-shadow; $list-item-shadow: $card-shadow;
@ -140,6 +140,8 @@ $table-bg-hover: $gray-5; // for hover
$table-bg-active: $table-bg-hover !default; $table-bg-active: $table-bg-hover !default;
$table-border: $gray-3; // table and cell border $table-border: $gray-3; // table and cell border
$table-bg-odd: $gray-5;
// Scrollbars // Scrollbars
$scrollbarBackground: $gray-5; $scrollbarBackground: $gray-5;
$scrollbarBackground2: $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: lighten($red, 3%);
$btn-danger-bg-hl: darken($red, 3%); $btn-danger-bg-hl: darken($red, 3%);
$btn-inverse-bg: $gray-5; $btn-inverse-bg: $gray-6;
$btn-inverse-bg-hl: darken($gray-5, 5%); $btn-inverse-bg-hl: darken($gray-6, 5%);
$btn-inverse-text-color: $dark-4; $btn-inverse-text-color: $gray-1;
$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
$btn-link-color: $gray-1; $btn-link-color: $gray-1;
@ -176,7 +179,7 @@ $iconContainerBackground: $white;
// Forms // Forms
// ------------------------- // -------------------------
$input-bg: $gray-7; $input-bg: $white;
$input-bg-disabled: $gray-5; $input-bg-disabled: $gray-5;
$input-color: $dark-3; $input-color: $dark-3;
@ -185,33 +188,38 @@ $input-box-shadow: none;
$input-border-focus: $blue !default; $input-border-focus: $blue !default;
$input-box-shadow-focus: $blue !default; $input-box-shadow-focus: $blue !default;
$input-color-placeholder: $gray-4 !default; $input-color-placeholder: $gray-4 !default;
$input-label-bg: #eaebee; $input-label-bg: $gray-5;
$input-label-border-color: #e3e4e7; $input-label-border-color: $gray-5;
$input-invalid-border-color: lighten($red, 5%); $input-invalid-border-color: lighten($red, 5%);
// Sidemenu // Sidemenu
// ------------------------- // -------------------------
$side-menu-bg: $body-bg; $side-menu-bg: $dark-2;
$side-menu-item-hover-bg: $gray-6; $side-menu-item-hover-bg: $gray-1;
$side-menu-shadow: 0 0 5px #c2c2c2; $side-menu-shadow: 5px 0px 10px -5px $gray-1;
$side-menu-link-color: $gray-6;
// Menu dropdowns // Menu dropdowns
// ------------------------- // -------------------------
$menu-dropdown-bg: $white; $menu-dropdown-bg: $gray-7;
$menu-dropdown-hover-bg: $gray-6; $menu-dropdown-hover-bg: $gray-6;
$menu-dropdown-border-color: $gray-4; $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 // Breadcrumb
// ------------------------- // -------------------------
$page-nav-bg: #eaebee; $page-nav-bg: $gray-5;
$page-nav-shadow: 5px 5px 20px -5px $gray-4; $page-nav-shadow: 5px 5px 20px -5px $gray-4;
$page-nav-breadcrumb-color: $black; $page-nav-breadcrumb-color: $black;
$breadcrumb-hover-hl: #d9dadd; $breadcrumb-hover-hl: #d9dadd;
// Tabs
// -------------------------
$tab-border-color: $gray-5;
// search // search
$search-shadow: 0 5px 30px 0 $gray-4; $search-shadow: 0 5px 30px 0 $gray-4;
$search-filter-box-bg: $gray-4; $search-filter-box-bg: $gray-7;
// Dropdowns // Dropdowns
// ------------------------- // -------------------------
@ -257,8 +265,8 @@ $wellBackground: $gray-3;
// ------------------------- // -------------------------
$navbarHeight: 52px; $navbarHeight: 52px;
$navbarBackgroundHighlight: #f8f8f8; $navbarBackgroundHighlight: $white;
$navbarBackground: #f2f3f7; $navbarBackground: $white;
$navbarBorder: 1px solid $gray-4; $navbarBorder: 1px solid $gray-4;
$navbarShadow: 0 0 3px #c1c1c1; $navbarShadow: 0 0 3px #c1c1c1;
@ -275,6 +283,8 @@ $navbarBrandColor: $navbarLinkColor;
$navbarButtonBackground: lighten($navbarBackground, 3%); $navbarButtonBackground: lighten($navbarBackground, 3%);
$navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%); $navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%);
$navbar-button-border: $gray-4;
// Pagination // Pagination
// ------------------------- // -------------------------
@ -318,7 +328,8 @@ $graph-tooltip-bg: $gray-5;
$checkboxImageUrl: '../img/checkbox_white.png'; $checkboxImageUrl: '../img/checkbox_white.png';
// info box // 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
$footer-link-color: $gray-3; $footer-link-color: $gray-3;

View File

@ -75,7 +75,7 @@ $container-max-widths: (
$grid-columns: 12 !default; $grid-columns: 12 !default;
$grid-gutter-width: 30px !default; $grid-gutter-width: 30px !default;
$enable-flex: false; $enable-flex: true;
// Typography // Typography
// ------------------------- // -------------------------
@ -224,7 +224,7 @@ $btn-padding-y-lg: 11px !default;
$btn-padding-x-xl: 21px !default; $btn-padding-x-xl: 21px !default;
$btn-padding-y-xl: 11px !default; $btn-padding-y-xl: 11px !default;
$btn-border-radius: 3px; $btn-border-radius: 2px;
// sidemenu // sidemenu
$side-menu-width: 60px; $side-menu-width: 60px;
@ -235,5 +235,5 @@ $dashboard-padding: $panel-margin * 2;
$panel-padding: 0px 10px 5px 10px; $panel-padding: 0px 10px 5px 10px;
// tabs // tabs
$tabs-padding: 9px 15px 9px; $tabs-padding: 10px 15px 9px;

View File

@ -15,6 +15,10 @@
background-image: url('../img/icons_#{$theme-name}_theme/icon_alert.svg'); 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 { .gicon-datasources {
background-image: url('../img/icons_#{$theme-name}_theme/icon_data_sources.svg'); background-image: url('../img/icons_#{$theme-name}_theme/icon_data_sources.svg');
} }
@ -58,3 +62,14 @@
.gicon-zoom-out { .gicon-zoom-out {
background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); 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');
}
}

View File

@ -106,7 +106,7 @@
} }
// Inverse appears as dark gray // Inverse appears as dark gray
.btn-inverse { .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; //background: $card-background;
box-shadow: $card-shadow; box-shadow: $card-shadow;
//border: 1px solid $tight-form-func-highlight-bg; //border: 1px solid $tight-form-func-highlight-bg;

View File

@ -200,9 +200,8 @@
} }
.card-item { .card-item {
border-bottom: .2rem solid $page-bg; border-bottom: 3px solid $page-bg;
border-radius: 0; border-radius: 2px;
box-shadow: none;
} }
.card-item-header { .card-item-header {

View File

@ -8,7 +8,7 @@
} }
.react-grid-item { .react-grid-item {
display: none; display: none !important;
transition-property: none !important; transition-property: none !important;
} }

View File

@ -1,19 +1,25 @@
.dashboard-list { .dashboard-list {
height: 75%;
.search-results-container { .search-results-container {
padding-left: 0; padding: 5px 0 0 0;
} }
} }
.search-results-filter-row { .search-results-filter-row {
height: 35px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.gf-form-button-row {
padding-top: 0;
button:last-child {
margin-right: 0;
}
}
} }
.search-results-filter-row__filters { .search-results-filter-row__filters {
display: flex; display: flex;
width: 300px;
} }
.search-results-filter-row__filters-item { .search-results-filter-row__filters-item {

View File

@ -17,7 +17,7 @@
tbody { tbody {
tr:nth-child(odd) { tr:nth-child(odd) {
background: $dark-2; background: $table-bg-odd;
} }
} }
@ -34,7 +34,6 @@
padding: $table-cell-padding; padding: $table-cell-padding;
line-height: 30px; line-height: 30px;
height: 30px; height: 30px;
border-bottom: 1px solid black;
white-space: nowrap; white-space: nowrap;
&.filter-table__switch-cell { &.filter-table__switch-cell {

View File

@ -74,15 +74,15 @@
} }
.navbar-button { .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; display: inline-block;
font-weight: $btn-font-weight; font-weight: $btn-font-weight;
padding: 8px 11px; padding: 8px 11px;
line-height: 16px; line-height: 16px;
color: $text-muted; color: $text-muted;
border: 1px solid #151515; border: 1px solid $navbar-button-border;
margin-right: 1px; margin-right: 3px;
white-space: nowrap; white-space: nowrap;
.gicon { .gicon {

View File

@ -72,6 +72,21 @@
text-transform: uppercase; 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 { .page-breadcrumbs {
display: flex; display: flex;
padding: 10px 0; padding: 10px 0;

View File

@ -18,7 +18,7 @@
} }
.pluginlist-image { .pluginlist-image {
width: 20px; width: 17px;
} }
.pluginlist-title { .pluginlist-title {

View File

@ -120,8 +120,9 @@
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
&:hover { &:hover, &.selected {
color: $text-color-weak; color: $link-hover-color;
.search-section__header__toggle { .search-section__header__toggle {
background: $tight-form-func-bg; background: $tight-form-func-bg;
color: $link-hover-color; color: $link-hover-color;
@ -129,12 +130,8 @@
} }
} }
.search-section__header__with-checkbox {
display: flex;
}
.search-section__header__icon { .search-section__header__icon {
padding: 5px 10px; padding: 2px 10px;
} }
.search-section__header__toggle { .search-section__header__toggle {
@ -145,14 +142,6 @@
flex-grow: 1; flex-grow: 1;
} }
.search-item__with-checkbox {
display: flex;
.search-item {
margin: 1px 3px;
}
}
.search-item { .search-item {
@include list-item(); @include list-item();
@include left-brand-border(); @include left-brand-border();
@ -163,11 +152,8 @@
white-space: nowrap; white-space: nowrap;
padding: 0px; padding: 0px;
&:hover { &:hover, &.selected {
@include left-brand-border-gradient(); @include left-brand-border-gradient();
}
&.selected {
background: $list-item-hover-bg; background: $list-item-hover-bg;
} }
} }

View File

@ -110,7 +110,7 @@
display: inline-block; display: inline-block;
.fa, .icon-gf, .gicon { .fa, .icon-gf, .gicon {
color: $link-color; color: $side-menu-link-color;
position: relative; position: relative;
opacity: .7; opacity: .7;
font-size: 130%; font-size: 130%;
@ -135,6 +135,7 @@
white-space: nowrap; white-space: nowrap;
background-color: $side-menu-item-hover-bg; background-color: $side-menu-item-hover-bg;
font-size: 17px; font-size: 17px;
color: #ebedf2;
} }
li.sidemenu-org-switcher { li.sidemenu-org-switcher {

View File

@ -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-switch[disabled] {
.gf-form-label, .gf-form-label,
.gf-form-switch input + label { .gf-form-switch input + label {

View File

@ -26,7 +26,7 @@
.tabbed-view-panel-title { .tabbed-view-panel-title {
float: left; float: left;
padding-top: 1rem; padding-top: 9px;
margin: 0 2rem 0 0; margin: 0 2rem 0 0;
} }

View File

@ -16,7 +16,7 @@
position: relative; position: relative;
display: block; display: block;
border: solid transparent; border: solid transparent;
border-width: 2px 1px 1px; border-width: 0 1px 1px;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
i { i {
@ -31,9 +31,21 @@
&.active, &.active,
&.active:hover, &.active:hover,
&.active:focus { &.active:focus {
border-color: $orange $dark-4 transparent; border-color: $orange $tab-border-color transparent;
background: $page-bg; background: $page-bg;
color: $link-color; 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%);
}
} }
} }

View File

@ -15,8 +15,12 @@
} }
.page-container { .page-container {
@extend .container; margin-left: auto;
padding: 0 $spacer * 2; margin-right: auto;
padding-left: $spacer*2;
padding-right: $spacer*2;
max-width: 980px;
@include clearfix();
} }
.scroll-canvas { .scroll-canvas {

View File

@ -3,6 +3,11 @@
// Layout // Layout
// //
.error-container {
display: flex;
flex-direction: row;
}
.error-row { .error-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -22,7 +27,7 @@
.info-box { .info-box {
width: 38%; width: 38%;
padding: 2rem 1rem 6rem; padding: 2rem 1rem 2rem;
} }
.graph-percentage {padding: 0 0 1.5rem;} .graph-percentage {padding: 0 0 1.5rem;}
@ -58,3 +63,31 @@
} }
.graph-text {margin: 0;} .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%;
}
}