mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/develop' into graph-legend-v5
This commit is contained in:
commit
9bff005faf
@ -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"},
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import { PasswordStrength } from './components/PasswordStrength';
|
||||
import PageHeader from './components/PageHeader';
|
||||
import PageHeader from './components/PageHeader/PageHeader';
|
||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
|
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||
react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]);
|
||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { NavModel, NavModelItem } from '../nav_model_srv';
|
||||
import { NavModel, NavModelItem } from '../../nav_model_srv';
|
||||
import classNames from 'classnames';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export interface IProps {
|
||||
model: NavModel;
|
||||
@ -26,8 +27,44 @@ function TabItem(tab: NavModelItem) {
|
||||
);
|
||||
}
|
||||
|
||||
function Tabs({main}: {main: NavModelItem}) {
|
||||
return <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;
|
||||
};
|
||||
|
@ -21,35 +21,10 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
335
public/app/core/components/search/search.jest.ts
Normal file
335
public/app/core/components/search/search.jest.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -64,18 +64,70 @@ export class SearchCtrl {
|
||||
this.moveSelection(-1);
|
||||
}
|
||||
if (evt.keyCode === 13) {
|
||||
var selectedDash = this.results[this.selectedIndex];
|
||||
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) {
|
||||
filterByTag(tag) {
|
||||
if (_.indexOf(this.query.tag, tag) === -1) {
|
||||
this.query.tag.push(tag);
|
||||
this.search();
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
47
public/app/core/components/search/search_results.html
Normal file
47
public/app/core/components/search/search_results.html
Normal 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>
|
||||
</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>
|
75
public/app/core/components/search/search_results.jest.ts
Normal file
75
public/app/core/components/search/search_results.jest.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
70
public/app/core/components/search/search_results.ts
Normal file
70
public/app/core/components/search/search_results.ts
Normal 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);
|
@ -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 () {});
|
||||
|
21
public/app/core/services/global_event_srv.ts
Normal file
21
public/app/core/services/global_event_srv.ts
Normal 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);
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,7 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export function react2AngularDirective(name: string, component: any, options: any) {
|
||||
|
||||
coreModule.directive(name, ['reactDirective', reactDirective => {
|
||||
return reactDirective(component, options);
|
||||
}]);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 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];
|
||||
|
||||
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) {
|
||||
filterByTag(tag) {
|
||||
if (_.indexOf(this.query.tag, tag) === -1) {
|
||||
this.query.tag.push(tag);
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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>
|
||||
|
126
public/app/features/dashboard/partials/dashboardImport.html
Normal file
126
public/app/features/dashboard/partials/dashboardImport.html
Normal 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>
|
@ -1,17 +1,35 @@
|
||||
<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> {{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> 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> 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 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> 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> 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>
|
||||
|
@ -1,25 +1,24 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
import {DashboardImportCtrl} from '../dashboard_import_ctrl';
|
||||
import config from '../../../core/config';
|
||||
|
||||
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
|
||||
import config from 'app/core/config';
|
||||
|
||||
describe('DashImportCtrl', function() {
|
||||
describe('DashboardImportCtrl', function() {
|
||||
var ctx: any = {};
|
||||
var backendSrv = {
|
||||
search: sinon.stub().returns(Promise.resolve([])),
|
||||
get: sinon.stub()
|
||||
|
||||
let navModelSrv;
|
||||
let backendSrv;
|
||||
|
||||
beforeEach(() => {
|
||||
navModelSrv = {
|
||||
getNav: () => {}
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
backendSrv = {
|
||||
search: jest.fn().mockReturnValue(Promise.resolve([])),
|
||||
get: jest.fn()
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
||||
ctx.$q = $q;
|
||||
ctx.scope = $rootScope.$new();
|
||||
ctx.ctrl = $controller(DashImportCtrl, {
|
||||
$scope: ctx.scope,
|
||||
backendSrv: backendSrv,
|
||||
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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
///<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'
|
||||
}" />
|
||||
|
@ -1,15 +1,20 @@
|
||||
<navbar model="navModel"></navbar>
|
||||
|
||||
<div class="page-container">
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Page not found (404)
|
||||
<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 class="error-row">
|
||||
<div class="dash-row-menu-grip"><i class="fa fa-ellipsis-v"></i></div>
|
||||
<div class="panel-container error-row">
|
||||
</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">
|
||||
@ -35,21 +40,21 @@
|
||||
</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-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>
|
||||
<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>
|
||||
<span class="react-resizable-handle" style="cursor: default"></span>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,25 @@
|
||||
.dashboard-list {
|
||||
height: 75%;
|
||||
|
||||
.search-results-container {
|
||||
padding-left: 0;
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-filter-row {
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.gf-form-button-row {
|
||||
padding-top: 0;
|
||||
|
||||
button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-filter-row__filters {
|
||||
display: flex;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-results-filter-row__filters-item {
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user