Commit 9bff005f by Alexander Zobnin

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

parents 58d40eb6 58fb35c5
......@@ -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"},
}
......
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']);
}
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 <ul className="gf-tabs">{main.children.map(TabItem)}</ul>;
function SelectOption(navItem: NavModelItem) {
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> {
......@@ -63,7 +100,7 @@ export default class PageHeader extends React.Component<IProps, any> {
<div className="page-container">
<div className="page-header">
{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>
......
......@@ -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;
};
......
......@@ -20,37 +20,12 @@
<div class="search-dropdown">
<div class="search-dropdown__col_1">
<div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && results.length">No dashboards matching your query were found.</h6>
<div ng-repeat="section in ctrl.results" class="search-section">
<a class="search-section__header pointer" ng-hide="section.hideHeader" ng-click="ctrl.toggleFolder(section)">
<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 class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
results="ctrl.results"
on-tag-selected="ctrl.filterByTag($tag)" />
</div>
</div>
</div>
<div class="search-dropdown__col_2">
......
......@@ -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;
});
}
}
......
<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>
\ No newline at end of file
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();
});
});
});
// 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);
......@@ -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
};
......@@ -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',
......
......@@ -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',
......
......@@ -8,5 +8,6 @@ define([
'./segment_srv',
'./backend_srv',
'./dynamic_directive_srv',
'./global_event_srv'
],
function () {});
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);
......@@ -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');
}
......
......@@ -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();
});
});
});
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);
}]);
}
......@@ -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);
///<reference path="../../../headers/common.d.ts" />
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);
......@@ -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) {
......
......@@ -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);
......
<page-header model="ctrl.navModel"></page-header>
<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="gf-form gf-form--grow">
<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()" />
</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>
Dashboard
</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>
Folder
</a>
......@@ -39,29 +57,25 @@
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-button-row">
<button type="button"
class="btn gf-form-button btn-secondary"
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 to...
</button>
<button type="button"
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 ng-if="!ctrl.hasFilters && ctrl.sections.length === 0">
<empty-list-cta model="{
title: 'This folder doesn\'t have any dashboards yet',
buttonIcon: 'gicon gicon-dashboard-new',
buttonLink: '/dashboard/new',
buttonTitle: 'Create Dashboard',
proTip: 'You can bulk move dashboards into this folder from the main dashboard list.',
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
proTipLinkTitle: 'Learn more',
proTipTarget: '_blank'
}" />
</div>
<div class="dashboard-list">
<div class="dashboard-list" ng-show="ctrl.sections.length > 0">
<div class="search-results-filter-row">
<gf-form-switch
on-change="ctrl.onSelectAllChanged()"
checked="ctrl.selectAllChecked"
switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox"
/>
<div class="search-results-filter-row__filters">
<select
......@@ -69,63 +83,39 @@
ng-model="ctrl.selectedStarredFilter"
ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions"
ng-change="ctrl.onStarredFilterChange()"
ng-show="!(ctrl.canMove || ctrl.canDelete)"
/>
<select
class="search-results-filter-row__filters-item gf-form-input"
ng-model="ctrl.selectedTagFilter"
ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions"
ng-change="ctrl.onTagFilterChange()"
ng-show="!(ctrl.canMove || ctrl.canDelete)"
/>
</div>
</div>
<div class="search-results-container" ng-show="ctrl.sections.length > 0" grafana-scrollbar>
<div ng-repeat="section in ctrl.sections" class="search-section">
<div class="search-section__header__with-checkbox" ng-hide="section.hideHeader">
<gf-form-switch
on-change="ctrl.selectionChanged()"
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 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 class="search-results-container">
<dashboard-search-results
results="ctrl.sections"
editable="true"
on-selection-changed="ctrl.selectionChanged()"
on-tag-selected="ctrl.filterByTag($tag)" />
</div>
</div>
</div>
<em class="muted" ng-hide="ctrl.sections.length > 0">
No Dashboards or Folders found.
</em>
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');
});
});
});
......@@ -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, <SearchSrv>searchSrvStub);
return new DashboardListCtrl({}, { getNav: () => { } }, q, <SearchSrv>searchSrvStub, {});
}
......@@ -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;
}
......
......@@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
var template = `
<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>
Upload .json File
</label>
......
///<reference path="../../headers/common.d.ts" />
import coreModule from '../../core/core_module';
import {appEvents} from 'app/core/core';
export class DataSourcesCtrl {
datasources: any;
......@@ -11,13 +12,24 @@ export class DataSourcesCtrl {
private $scope,
private backendSrv,
private datasourceSrv,
private $location,
private navModelSrv) {
this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0);
this.navigateToUrl = this.navigateToUrl.bind(this);
backendSrv.get('/api/datasources').then(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) {
......
......@@ -49,7 +49,7 @@
buttonLink: '/datasources/new',
buttonTitle: 'Add data source',
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',
proTipTarget: '_blank'
}" />
......
<navbar model="navModel"></navbar>
<div class="page-container">
<div class="page-header">
<h1>
Page not found (404)
</h1>
</div>
<div class="error-row">
<div class="dash-row-menu-grip"><i class="fa fa-ellipsis-v"></i></div>
<div class="panel-container error-row">
<div class="error-column graph-box">
<div class="error-row">
<div class="error-column error-space-between graph-percentage">
<p>100%</p>
<p>80%</p>
<p>60%</p>
<p>40%</p>
<p>20%</p>
<p>0%</p>
</div>
<div class="error-column image-box">
<img src="public/img/graph404.svg" width="100%">
<div class="error-row error-space-between">
<p class="graph-text">Then</p>
<p class="graph-text">Now</p>
</div>
</div>
</div>
</div>
<div class="error-column info-box">
<div class="error-row current-box">
<p class="current-text">current</p>
</div>
<div class="error-row" style="flex: 1">
<i class="fa fa-minus error-minus"></i>
<div class="error-column error-space-between">
<div class="error-row error-space-between">
<p>Chances you are on the page you are looking for.</p>
<p class="left-margin">0%</p>
</div>
<div>
<h3>Sorry for the inconvenience</h3>
<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="resize-panel-handle icon-gf icon-gf-grabber" style="cursor: default"></span>
</div>
</div>
<div class="page-header__inner">
<span class="page-header__logo">
<i class="page-header__icon fa fa-fw fa-exclamation-triangle"></i>
</span>
<div class="page-header__info-block">
<h1 class="page-header__title">
Page not found
</h1>
<div class="page-header__sub-title">
404 Error
</div>
</div>
</div>
</div>
<div class="panel-container error-container">
<div class="error-column graph-box">
<div class="error-row">
<div class="error-column error-space-between graph-percentage">
<p>100%</p>
<p>80%</p>
<p>60%</p>
<p>40%</p>
<p>20%</p>
<p>0%</p>
</div>
<div class="error-column image-box">
<img src="public/img/graph404.svg" width="100%">
<div class="error-row error-space-between">
<p class="graph-text">Then</p>
<p class="graph-text">Now</p>
</div>
</div>
</div>
</div>
<div class="error-column info-box">
<div class="error-row current-box">
<p class="current-text">current</p>
</div>
<div class="error-row" style="flex: 1">
<i class="fa fa-minus error-minus"></i>
<div class="error-column error-space-between error-full-width">
<div class="error-row error-space-between">
<p>Chances you are on the page you are looking for.</p>
<p class="left-margin">0%</p>
</div>
<div>
<h3>Sorry for the inconvenience</h3>
<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>
......@@ -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
// -------------------------
......
......@@ -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;
......
......@@ -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;
......@@ -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');
}
}
......@@ -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;
......
......@@ -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 {
......
......@@ -8,7 +8,7 @@
}
.react-grid-item {
display: none;
display: none !important;
transition-property: none !important;
}
......
.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 {
......
......@@ -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 {
......
......@@ -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 {
......
......@@ -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;
......
......@@ -18,7 +18,7 @@
}
.pluginlist-image {
width: 20px;
width: 17px;
}
.pluginlist-title {
......
......@@ -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;
}
}
......
......@@ -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 {
......
......@@ -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 {
......
......@@ -26,7 +26,7 @@
.tabbed-view-panel-title {
float: left;
padding-top: 1rem;
padding-top: 9px;
margin: 0 2rem 0 0;
}
......
......@@ -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%);
}
}
}
......@@ -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 {
......
......@@ -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%;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment