Merge branch 'develop' into develop-light-theme
This commit is contained in:
+3
-2
@@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
Text: "Create",
|
Text: "Create",
|
||||||
|
Id: "create",
|
||||||
Icon: "fa fa-fw fa-plus",
|
Icon: "fa fa-fw fa-plus",
|
||||||
Url: "#",
|
Url: "#",
|
||||||
Children: []*dtos.NavLink{
|
Children: []*dtos.NavLink{
|
||||||
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
||||||
{Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
|
{Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
|
||||||
{Text: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"},
|
{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -103,7 +104,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
dashboardChildNavs := []*dtos.NavLink{
|
dashboardChildNavs := []*dtos.NavLink{
|
||||||
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true},
|
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true},
|
||||||
{Divider: true, HideFromTabs: true},
|
{Divider: true, HideFromTabs: true},
|
||||||
{Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"},
|
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"},
|
||||||
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
|
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
|
||||||
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"},
|
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
import { PasswordStrength } from './components/PasswordStrength';
|
import { PasswordStrength } from './components/PasswordStrength';
|
||||||
import PageHeader from './components/PageHeader';
|
import PageHeader from './components/PageHeader';
|
||||||
|
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
|
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]);
|
react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]);
|
||||||
|
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import EmptyListCTA from './EmptyListCTA';
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
title: 'Title',
|
||||||
|
buttonIcon: 'ga css class',
|
||||||
|
buttonLink: 'http://url/to/destination',
|
||||||
|
buttonTitle: 'Click me',
|
||||||
|
proTip: 'This is a tip',
|
||||||
|
proTipLink: 'http://url/to/tip/destination',
|
||||||
|
proTipLinkTitle: 'Learn more',
|
||||||
|
proTipTarget: '_blank'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CollorPalette', () => {
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const tree = renderer.create(<EmptyListCTA model={model} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export interface IProps {
|
||||||
|
model: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyListCTA extends Component<IProps, any> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
buttonIcon,
|
||||||
|
buttonLink,
|
||||||
|
buttonTitle,
|
||||||
|
proTip,
|
||||||
|
proTipLink,
|
||||||
|
proTipLinkTitle,
|
||||||
|
proTipTarget
|
||||||
|
} = this.props.model;
|
||||||
|
return (
|
||||||
|
<div className="empty-list-cta p-t-2 p-b-1">
|
||||||
|
<div className="empty-list-cta__title">{title}</div>
|
||||||
|
<a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success"><i className={buttonIcon} />{buttonTitle}</a>
|
||||||
|
<div className="empty-list-cta__pro-tip">
|
||||||
|
<i className="fa fa-rocket" /> ProTip: {proTip}
|
||||||
|
<a className="text-link empty-list-cta__pro-tip-link"
|
||||||
|
href={proTipLink}
|
||||||
|
target={proTipTarget}>{proTipLinkTitle}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyListCTA;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CollorPalette renders correctly 1`] = `
|
||||||
|
<div
|
||||||
|
className="empty-list-cta p-t-2 p-b-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="empty-list-cta__title"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
className="empty-list-cta__button btn btn-xlarge btn-success"
|
||||||
|
href="http://url/to/destination"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="ga css class"
|
||||||
|
/>
|
||||||
|
Click me
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
className="empty-list-cta__pro-tip"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-rocket"
|
||||||
|
/>
|
||||||
|
ProTip:
|
||||||
|
This is a tip
|
||||||
|
<a
|
||||||
|
className="text-link empty-list-cta__pro-tip-link"
|
||||||
|
href="http://url/to/tip/destination"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -119,14 +119,6 @@ export class NavModelSrv {
|
|||||||
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dashboard.meta.canAdmin) {
|
|
||||||
menu.push({
|
|
||||||
title: 'Permissions...',
|
|
||||||
icon: 'fa fa-fw fa-lock',
|
|
||||||
clickHandler: () => dashNavCtrl.openEditView('permissions')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dashboard.meta.isHome) {
|
if (!dashboard.meta.isHome) {
|
||||||
menu.push({
|
menu.push({
|
||||||
title: 'Version history',
|
title: 'Version history',
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
reloadOnSearch: false,
|
reloadOnSearch: false,
|
||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
})
|
})
|
||||||
|
.when('/dashboard/import', {
|
||||||
|
templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html',
|
||||||
|
controller : 'DashboardImportCtrl',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
})
|
||||||
.when('/datasources', {
|
.when('/datasources', {
|
||||||
templateUrl: 'public/app/features/plugins/partials/ds_list.html',
|
templateUrl: 'public/app/features/plugins/partials/ds_list.html',
|
||||||
controller : 'DataSourcesCtrl',
|
controller : 'DataSourcesCtrl',
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import './unsavedChangesSrv';
|
|||||||
import './unsaved_changes_modal';
|
import './unsaved_changes_modal';
|
||||||
import './timepicker/timepicker';
|
import './timepicker/timepicker';
|
||||||
import './upload';
|
import './upload';
|
||||||
import './import/dash_import';
|
|
||||||
import './export/export_modal';
|
import './export/export_modal';
|
||||||
import './export_data/export_data_modal';
|
import './export_data/export_data_modal';
|
||||||
import './ad_hoc_filters';
|
import './ad_hoc_filters';
|
||||||
@@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder';
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
import {DashboardListCtrl} from './dashboard_list_ctrl';
|
import {DashboardListCtrl} from './dashboard_list_ctrl';
|
||||||
|
import {DashboardImportCtrl} from './dashboard_import_ctrl';
|
||||||
|
|
||||||
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
|
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
|
||||||
|
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||||
|
|||||||
+6
-19
@@ -1,10 +1,8 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export class DashImportCtrl {
|
export class DashboardImportCtrl {
|
||||||
|
navModel: any;
|
||||||
step: number;
|
step: number;
|
||||||
jsonText: string;
|
jsonText: string;
|
||||||
parseError: string;
|
parseError: string;
|
||||||
@@ -17,7 +15,9 @@ export class DashImportCtrl {
|
|||||||
gnetInfo: any;
|
gnetInfo: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private backendSrv, private $location, private $scope, $routeParams) {
|
constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) {
|
||||||
|
this.navModel = navModelSrv.getNav('create', 'import');
|
||||||
|
|
||||||
this.step = 1;
|
this.step = 1;
|
||||||
this.nameExists = false;
|
this.nameExists = false;
|
||||||
|
|
||||||
@@ -160,17 +160,4 @@ export class DashImportCtrl {
|
|||||||
this.gnetError = '';
|
this.gnetError = '';
|
||||||
this.gnetInfo = '';
|
this.gnetInfo = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dashImportDirective() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
templateUrl: 'public/app/features/dashboard/import/dash_import.html',
|
|
||||||
controller: DashImportCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('dashImport', dashImportDirective);
|
|
||||||
@@ -17,7 +17,7 @@ export class DashboardListCtrl {
|
|||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) {
|
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) {
|
||||||
this.navModel = navModelSrv.getNav('dashboards', 'dashboards', 0);
|
this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
|
||||||
this.query = {query: '', mode: 'tree', tag: [], starred: false};
|
this.query = {query: '', mode: 'tree', tag: [], starred: false};
|
||||||
this.selectedStarredFilter = this.starredFilterOptions[0];
|
this.selectedStarredFilter = this.starredFilterOptions[0];
|
||||||
|
|
||||||
|
|||||||
@@ -383,8 +383,8 @@ export class DashboardMigrator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add special "row" panels if even one row is collapsed or has visible title
|
// Add special "row" panels if even one row is collapsed, repeated or has visible title
|
||||||
const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle);
|
const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle || row.repeat);
|
||||||
|
|
||||||
for (let row of old.rows) {
|
for (let row of old.rows) {
|
||||||
let height: any = row.height || DEFAULT_ROW_HEIGHT;
|
let height: any = row.height || DEFAULT_ROW_HEIGHT;
|
||||||
@@ -398,6 +398,7 @@ export class DashboardMigrator {
|
|||||||
rowPanel.type = 'row';
|
rowPanel.type = 'row';
|
||||||
rowPanel.title = row.title;
|
rowPanel.title = row.title;
|
||||||
rowPanel.collapsed = row.collapse;
|
rowPanel.collapsed = row.collapse;
|
||||||
|
rowPanel.repeat = row.repeat;
|
||||||
rowPanel.panels = [];
|
rowPanel.panels = [];
|
||||||
rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight};
|
rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight};
|
||||||
rowPanelModel = new PanelModel(rowPanel);
|
rowPanelModel = new PanelModel(rowPanel);
|
||||||
|
|||||||
@@ -181,6 +181,14 @@ export class DashboardModel {
|
|||||||
if (panel.id > max) {
|
if (panel.id > max) {
|
||||||
max = panel.id;
|
max = panel.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panel.collapsed) {
|
||||||
|
for (let rowPanel of panel.panels) {
|
||||||
|
if (rowPanel.id > max) {
|
||||||
|
max = rowPanel.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return max + 1;
|
return max + 1;
|
||||||
@@ -251,16 +259,6 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for (let panel of this.panels) {
|
|
||||||
// if (panel.repeat) {
|
|
||||||
// if (!cleanUpOnly) {
|
|
||||||
// this.repeatPanel(panel);
|
|
||||||
// }
|
|
||||||
// } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
|
||||||
// panelsToRemove.push(panel);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// remove panels
|
// remove panels
|
||||||
_.pull(this.panels, ...panelsToRemove);
|
_.pull(this.panels, ...panelsToRemove);
|
||||||
|
|
||||||
@@ -274,21 +272,11 @@ export class DashboardModel {
|
|||||||
return sourcePanel;
|
return sourcePanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
var clone = new PanelModel(sourcePanel.getSaveModel());
|
let clone = new PanelModel(sourcePanel.getSaveModel());
|
||||||
clone.id = this.getNextPanelId();
|
clone.id = this.getNextPanelId();
|
||||||
|
|
||||||
if (sourcePanel.type === 'row') {
|
// insert after source panel + value index
|
||||||
// for row clones we need to figure out panels under row to clone and where to insert clone
|
this.panels.splice(sourcePanelIndex+valueIndex, 0, clone);
|
||||||
let rowPanels = this.getRowPanels(sourcePanelIndex);
|
|
||||||
clone.panels = _.map(rowPanels, panel => panel.getSaveModel());
|
|
||||||
|
|
||||||
// insert after preceding row's panels
|
|
||||||
let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex);
|
|
||||||
this.panels.splice(insertPos, 0, clone);
|
|
||||||
} else {
|
|
||||||
// insert after source panel + value index
|
|
||||||
this.panels.splice(sourcePanelIndex+valueIndex, 0, clone);
|
|
||||||
}
|
|
||||||
|
|
||||||
clone.repeatIteration = this.iteration;
|
clone.repeatIteration = this.iteration;
|
||||||
clone.repeatPanelId = sourcePanel.id;
|
clone.repeatPanelId = sourcePanel.id;
|
||||||
@@ -296,37 +284,60 @@ export class DashboardModel {
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBottomYForRow() {
|
getRowRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) {
|
||||||
|
// if first clone return source
|
||||||
|
if (valueIndex === 0) {
|
||||||
|
if (!sourcePanel.collapsed) {
|
||||||
|
let rowPanels = this.getRowPanels(sourcePanelIndex);
|
||||||
|
sourcePanel.panels = rowPanels;
|
||||||
|
}
|
||||||
|
return sourcePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clone = new PanelModel(sourcePanel.getSaveModel());
|
||||||
|
// for row clones we need to figure out panels under row to clone and where to insert clone
|
||||||
|
let rowPanels, insertPos;
|
||||||
|
if (sourcePanel.collapsed) {
|
||||||
|
rowPanels = _.cloneDeep(sourcePanel.panels);
|
||||||
|
clone.panels = rowPanels;
|
||||||
|
// insert copied row after preceding row
|
||||||
|
insertPos = sourcePanelIndex + valueIndex;
|
||||||
|
} else {
|
||||||
|
rowPanels = this.getRowPanels(sourcePanelIndex);
|
||||||
|
clone.panels = _.map(rowPanels, panel => panel.getSaveModel());
|
||||||
|
// insert copied row after preceding row's panels
|
||||||
|
insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex);
|
||||||
|
}
|
||||||
|
this.panels.splice(insertPos, 0, clone);
|
||||||
|
|
||||||
|
this.updateRepeatedPanelIds(clone);
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
repeatPanel(panel: PanelModel, panelIndex: number) {
|
repeatPanel(panel: PanelModel, panelIndex: number) {
|
||||||
var variable = _.find(this.templating.list, {name: panel.repeat});
|
let variable = _.find(this.templating.list, {name: panel.repeat});
|
||||||
if (!variable) {
|
if (!variable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected;
|
if (panel.type === 'row') {
|
||||||
if (variable.current.text === 'All') {
|
this.repeatRow(panel, panelIndex, variable);
|
||||||
selected = variable.options.slice(1, variable.options.length);
|
return;
|
||||||
} else {
|
|
||||||
selected = _.filter(variable.options, {selected: true});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selectedOptions = this.getSelectedVariableOptions(variable);
|
||||||
let minWidth = panel.minSpan || 6;
|
let minWidth = panel.minSpan || 6;
|
||||||
let xPos = 0;
|
let xPos = 0;
|
||||||
let yPos = panel.gridPos.y;
|
let yPos = panel.gridPos.y;
|
||||||
|
|
||||||
for (let index = 0; index < selected.length; index++) {
|
for (let index = 0; index < selectedOptions.length; index++) {
|
||||||
var option = selected[index];
|
let option = selectedOptions[index];
|
||||||
var copy = this.getPanelRepeatClone(panel, index, panelIndex);
|
let copy;
|
||||||
|
|
||||||
|
copy = this.getPanelRepeatClone(panel, index, panelIndex);
|
||||||
copy.scopedVars = {};
|
copy.scopedVars = {};
|
||||||
copy.scopedVars[variable.name] = option;
|
copy.scopedVars[variable.name] = option;
|
||||||
|
|
||||||
if (copy.type === 'row') {
|
|
||||||
// place row below row panels
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
|
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
|
||||||
copy.gridPos.y = yPos;
|
copy.gridPos.y = yPos;
|
||||||
yPos += copy.gridPos.h;
|
yPos += copy.gridPos.h;
|
||||||
@@ -334,7 +345,7 @@ export class DashboardModel {
|
|||||||
// set width based on how many are selected
|
// set width based on how many are selected
|
||||||
// assumed the repeated panels should take up full row width
|
// assumed the repeated panels should take up full row width
|
||||||
|
|
||||||
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth);
|
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
|
||||||
copy.gridPos.x = xPos;
|
copy.gridPos.x = xPos;
|
||||||
copy.gridPos.y = yPos;
|
copy.gridPos.y = yPos;
|
||||||
|
|
||||||
@@ -349,6 +360,90 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repeatRow(panel: PanelModel, panelIndex: number, variable) {
|
||||||
|
let selectedOptions = this.getSelectedVariableOptions(variable);
|
||||||
|
let yPos = panel.gridPos.y;
|
||||||
|
|
||||||
|
function setScopedVars(panel, variableOption) {
|
||||||
|
panel.scopedVars = {};
|
||||||
|
panel.scopedVars[variable.name] = variableOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
|
||||||
|
let option = selectedOptions[optionIndex];
|
||||||
|
let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
|
||||||
|
setScopedVars(rowCopy, option);
|
||||||
|
|
||||||
|
let rowHeight = this.getRowHeight(rowCopy);
|
||||||
|
let rowPanels = rowCopy.panels || [];
|
||||||
|
let panelBelowIndex;
|
||||||
|
|
||||||
|
if (panel.collapsed) {
|
||||||
|
// For collapsed row just copy its panels and set scoped vars and proper IDs
|
||||||
|
_.each(rowPanels, (rowPanel, i) => {
|
||||||
|
setScopedVars(rowPanel, option);
|
||||||
|
if (optionIndex > 0) {
|
||||||
|
this.updateRepeatedPanelIds(rowPanel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rowCopy.gridPos.y += optionIndex;
|
||||||
|
yPos += optionIndex;
|
||||||
|
panelBelowIndex = panelIndex + optionIndex + 1;
|
||||||
|
} else {
|
||||||
|
// insert after 'row' panel
|
||||||
|
let insertPos = panelIndex + ((rowPanels.length + 1) * optionIndex) + 1;
|
||||||
|
_.each(rowPanels, (rowPanel, i) => {
|
||||||
|
setScopedVars(rowPanel, option);
|
||||||
|
if (optionIndex > 0) {
|
||||||
|
let cloneRowPanel = new PanelModel(rowPanel);
|
||||||
|
this.updateRepeatedPanelIds(cloneRowPanel);
|
||||||
|
// For exposed row additionally set proper Y grid position and add it to dashboard panels
|
||||||
|
cloneRowPanel.gridPos.y += rowHeight * optionIndex;
|
||||||
|
this.panels.splice(insertPos+i, 0, cloneRowPanel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rowCopy.panels = [];
|
||||||
|
rowCopy.gridPos.y += rowHeight * optionIndex;
|
||||||
|
yPos += rowHeight;
|
||||||
|
panelBelowIndex = insertPos+rowPanels.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update gridPos for panels below
|
||||||
|
for (let i = panelBelowIndex; i< this.panels.length; i++) {
|
||||||
|
this.panels[i].gridPos.y += yPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRepeatedPanelIds(panel: PanelModel) {
|
||||||
|
panel.repeatPanelId = panel.id;
|
||||||
|
panel.id = this.getNextPanelId();
|
||||||
|
panel.repeatIteration = this.iteration;
|
||||||
|
panel.repeat = null;
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedVariableOptions(variable) {
|
||||||
|
let selectedOptions;
|
||||||
|
if (variable.current.text === 'All') {
|
||||||
|
selectedOptions = variable.options.slice(1, variable.options.length);
|
||||||
|
} else {
|
||||||
|
selectedOptions = _.filter(variable.options, {selected: true});
|
||||||
|
}
|
||||||
|
return selectedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowHeight(rowPanel: PanelModel): number {
|
||||||
|
if (!rowPanel.panels || rowPanel.panels.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const positions = _.map(rowPanel.panels, 'gridPos');
|
||||||
|
const maxPos = _.maxBy(positions, (pos) => {
|
||||||
|
return pos.y + pos.h;
|
||||||
|
});
|
||||||
|
return maxPos.h + 1;
|
||||||
|
}
|
||||||
|
|
||||||
removePanel(panel: PanelModel) {
|
removePanel(panel: PanelModel) {
|
||||||
var index = _.indexOf(this.panels, panel);
|
var index = _.indexOf(this.panels, panel);
|
||||||
this.panels.splice(index, 1);
|
this.panels.splice(index, 1);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
+25
-28
@@ -1,25 +1,24 @@
|
|||||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
import {DashboardImportCtrl} from '../dashboard_import_ctrl';
|
||||||
|
import config from '../../../core/config';
|
||||||
|
|
||||||
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
|
describe('DashboardImportCtrl', function() {
|
||||||
import config from 'app/core/config';
|
|
||||||
|
|
||||||
describe('DashImportCtrl', function() {
|
|
||||||
var ctx: any = {};
|
var ctx: any = {};
|
||||||
var backendSrv = {
|
|
||||||
search: sinon.stub().returns(Promise.resolve([])),
|
|
||||||
get: sinon.stub()
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(angularMocks.module('grafana.core'));
|
let navModelSrv;
|
||||||
|
let backendSrv;
|
||||||
|
|
||||||
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
beforeEach(() => {
|
||||||
ctx.$q = $q;
|
navModelSrv = {
|
||||||
ctx.scope = $rootScope.$new();
|
getNav: () => {}
|
||||||
ctx.ctrl = $controller(DashImportCtrl, {
|
};
|
||||||
$scope: ctx.scope,
|
|
||||||
backendSrv: backendSrv,
|
backendSrv = {
|
||||||
});
|
search: jest.fn().mockReturnValue(Promise.resolve([])),
|
||||||
}));
|
get: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when uploading json', function() {
|
describe('when uploading json', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
@@ -37,13 +36,13 @@ describe('DashImportCtrl', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should build input model', function() {
|
it('should build input model', function() {
|
||||||
expect(ctx.ctrl.inputs.length).to.eql(1);
|
expect(ctx.ctrl.inputs.length).toBe(1);
|
||||||
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
|
expect(ctx.ctrl.inputs[0].name).toBe('ds');
|
||||||
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
|
expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set inputValid to false', function() {
|
it('should set inputValid to false', function() {
|
||||||
expect(ctx.ctrl.inputsValid).to.eql(false);
|
expect(ctx.ctrl.inputsValid).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ describe('DashImportCtrl', function() {
|
|||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
|
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
|
||||||
// setup api mock
|
// setup api mock
|
||||||
backendSrv.get = sinon.spy(() => {
|
backendSrv.get = jest.fn(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
json: {}
|
json: {}
|
||||||
});
|
});
|
||||||
@@ -60,7 +59,7 @@ describe('DashImportCtrl', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call gnet api with correct dashboard id', function() {
|
it('should call gnet api with correct dashboard id', function() {
|
||||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
|
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ describe('DashImportCtrl', function() {
|
|||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.ctrl.gnetUrl = '2342';
|
ctx.ctrl.gnetUrl = '2342';
|
||||||
// setup api mock
|
// setup api mock
|
||||||
backendSrv.get = sinon.spy(() => {
|
backendSrv.get = jest.fn(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
json: {}
|
json: {}
|
||||||
});
|
});
|
||||||
@@ -77,10 +76,8 @@ describe('DashImportCtrl', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call gnet api with correct dashboard id', function() {
|
it('should call gnet api with correct dashboard id', function() {
|
||||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
|
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
import { DashboardModel } from '../dashboard_model';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
|
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
|
||||||
|
import { expect } from 'test/lib/common';
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({}));
|
jest.mock('app/core/services/context_srv', () => ({}));
|
||||||
|
|
||||||
@@ -315,12 +316,33 @@ describe('DashboardModel', function() {
|
|||||||
|
|
||||||
expect(panelGridPos).toEqual(expectedGrid);
|
expect(panelGridPos).toEqual(expectedGrid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add repeated row if repeat set', function() {
|
||||||
|
model.rows = [
|
||||||
|
createRow({showTitle: true, title: "Row", height: 8, repeat: "server"}, [[6]]),
|
||||||
|
createRow({height: 8}, [[12]])
|
||||||
|
];
|
||||||
|
let dashboard = new DashboardModel(model);
|
||||||
|
let panelGridPos = getGridPositions(dashboard);
|
||||||
|
let expectedGrid = [
|
||||||
|
{x: 0, y: 0, w: 24, h: 8},
|
||||||
|
{x: 0, y: 1, w: 12, h: 8},
|
||||||
|
{x: 0, y: 9, w: 24, h: 8},
|
||||||
|
{x: 0, y: 10, w: 24, h: 8}
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(panelGridPos).toEqual(expectedGrid);
|
||||||
|
expect(dashboard.panels[0].repeat).toBe("server");
|
||||||
|
expect(dashboard.panels[1].repeat).toBeUndefined();
|
||||||
|
expect(dashboard.panels[2].repeat).toBeUndefined();
|
||||||
|
expect(dashboard.panels[3].repeat).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createRow(options, panelDescriptions: any[]) {
|
function createRow(options, panelDescriptions: any[]) {
|
||||||
const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN;
|
const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN;
|
||||||
let {collapse, height, showTitle, title} = options;
|
let {collapse, height, showTitle, title, repeat} = options;
|
||||||
height = height * PANEL_HEIGHT_STEP;
|
height = height * PANEL_HEIGHT_STEP;
|
||||||
let panels = [];
|
let panels = [];
|
||||||
_.each(panelDescriptions, panelDesc => {
|
_.each(panelDescriptions, panelDesc => {
|
||||||
@@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) {
|
|||||||
}
|
}
|
||||||
panels.push(panel);
|
panels.push(panel);
|
||||||
});
|
});
|
||||||
let row = {collapse, height, showTitle, title, panels};
|
let row = {collapse, height, showTitle, title, panels, repeat};
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import {DashboardModel} from '../dashboard_model';
|
import {DashboardModel} from '../dashboard_model';
|
||||||
|
import { expect } from 'test/lib/common';
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
|
|
||||||
@@ -146,19 +148,19 @@ describe('given dashboard with panel repeat in vertical direction', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip('given dashboard with row repeat', function() {
|
describe('given dashboard with row repeat', function() {
|
||||||
var dashboard;
|
let dashboard, dashboardJSON;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
dashboard = new DashboardModel({
|
dashboardJSON = {
|
||||||
panels: [
|
panels: [
|
||||||
{id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}},
|
{id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'},
|
||||||
{id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
{id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
||||||
{id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
{id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
||||||
{id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}},
|
{id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}},
|
||||||
{id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
|
{id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
|
||||||
],
|
],
|
||||||
templating: {
|
templating: {
|
||||||
list: [{
|
list: [{
|
||||||
name: 'apps',
|
name: 'apps',
|
||||||
current: {
|
current: {
|
||||||
@@ -172,33 +174,137 @@ describe.skip('given dashboard with row repeat', function() {
|
|||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
dashboard = new DashboardModel(dashboardJSON);
|
||||||
dashboard.processRepeats();
|
dashboard.processRepeats();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not repeat only row', function() {
|
it('should not repeat only row', function() {
|
||||||
expect(dashboard.panels[1].type).toBe('graph');
|
const panel_types = _.map(dashboard.panels, 'type');
|
||||||
|
expect(panel_types).toEqual([
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
'row', 'graph'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set scopedVars for each panel', function() {
|
||||||
|
dashboardJSON.templating.list[0].options[2].selected = true;
|
||||||
|
dashboard = new DashboardModel(dashboardJSON);
|
||||||
|
dashboard.processRepeats();
|
||||||
|
|
||||||
|
expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}});
|
||||||
|
expect(dashboard.panels[4].scopedVars).toMatchObject({apps: {text: 'se2', value: 'se2'}});
|
||||||
|
|
||||||
|
const scopedVars = _.compact(_.map(dashboard.panels, (panel) => {
|
||||||
|
return panel.scopedVars ? panel.scopedVars.apps.value : null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(scopedVars).toEqual([
|
||||||
|
'se1', 'se1', 'se1',
|
||||||
|
'se2', 'se2', 'se2',
|
||||||
|
'se3', 'se3', 'se3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat only configured row', function() {
|
||||||
|
expect(dashboard.panels[6].id).toBe(4);
|
||||||
|
expect(dashboard.panels[7].id).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat only row if it is collapsed', function() {
|
||||||
|
dashboardJSON.panels = [
|
||||||
|
{
|
||||||
|
id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24},
|
||||||
|
panels: [
|
||||||
|
{id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
||||||
|
{id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}},
|
||||||
|
{id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}},
|
||||||
|
];
|
||||||
|
dashboard = new DashboardModel(dashboardJSON);
|
||||||
|
dashboard.processRepeats();
|
||||||
|
|
||||||
|
const panel_types = _.map(dashboard.panels, 'type');
|
||||||
|
expect(panel_types).toEqual([
|
||||||
|
'row', 'row', 'row', 'graph'
|
||||||
|
]);
|
||||||
|
expect(dashboard.panels[0].panels).toHaveLength(2);
|
||||||
|
expect(dashboard.panels[1].panels).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly repeat multiple rows', function() {
|
||||||
|
dashboardJSON.panels = [
|
||||||
|
{id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, // repeat
|
||||||
|
{id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
||||||
|
{id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
||||||
|
{id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, // don't touch
|
||||||
|
{id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
|
||||||
|
{id: 6, type: 'row', gridPos: {x: 0, y: 4, h: 1 , w: 24}, repeat: 'hosts'}, // repeat
|
||||||
|
{id: 7, type: 'graph', gridPos: {x: 0, y: 5, h: 1 , w: 6}},
|
||||||
|
{id: 8, type: 'graph', gridPos: {x: 6, y: 5, h: 1 , w: 6}}
|
||||||
|
];
|
||||||
|
dashboardJSON.templating.list.push({
|
||||||
|
name: 'hosts',
|
||||||
|
current: {
|
||||||
|
text: 'backend01, backend02',
|
||||||
|
value: ['backend01', 'backend02']
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{text: 'backend01', value: 'backend01', selected: true},
|
||||||
|
{text: 'backend02', value: 'backend02', selected: true},
|
||||||
|
{text: 'backend03', value: 'backend03', selected: false}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
dashboard = new DashboardModel(dashboardJSON);
|
||||||
|
dashboard.processRepeats();
|
||||||
|
|
||||||
|
const panel_types = _.map(dashboard.panels, 'type');
|
||||||
|
expect(panel_types).toEqual([
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
'row', 'graph',
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
'row', 'graph', 'graph',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(dashboard.panels[0].scopedVars['apps'].value).toBe('se1');
|
||||||
|
expect(dashboard.panels[1].scopedVars['apps'].value).toBe('se1');
|
||||||
|
expect(dashboard.panels[3].scopedVars['apps'].value).toBe('se2');
|
||||||
|
expect(dashboard.panels[4].scopedVars['apps'].value).toBe('se2');
|
||||||
|
expect(dashboard.panels[8].scopedVars['hosts'].value).toBe('backend01');
|
||||||
|
expect(dashboard.panels[9].scopedVars['hosts'].value).toBe('backend01');
|
||||||
|
expect(dashboard.panels[11].scopedVars['hosts'].value).toBe('backend02');
|
||||||
|
expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assign unique ids for repeated panels', function() {
|
||||||
|
dashboardJSON.panels = [
|
||||||
|
{
|
||||||
|
id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24},
|
||||||
|
panels: [
|
||||||
|
{id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
||||||
|
{id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}},
|
||||||
|
{id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}},
|
||||||
|
];
|
||||||
|
dashboard = new DashboardModel(dashboardJSON);
|
||||||
|
dashboard.processRepeats();
|
||||||
|
|
||||||
|
const panel_ids = _.flattenDeep(_.map(dashboard.panels, (panel) => {
|
||||||
|
let ids = [];
|
||||||
|
if (panel.panels && panel.panels.length) {
|
||||||
|
ids = _.map(panel.panels, 'id');
|
||||||
|
}
|
||||||
|
ids.push(panel.id);
|
||||||
|
return ids;
|
||||||
|
}));
|
||||||
|
expect(panel_ids.length).toEqual(_.uniq(panel_ids).length);
|
||||||
});
|
});
|
||||||
//
|
|
||||||
// it('should set scopedVars on panels', function() {
|
|
||||||
// expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}})
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it.skip('should repeat row and panels below two times', function() {
|
|
||||||
// expect(dashboard.panels).toMatchObject([
|
|
||||||
// // first (original row)
|
|
||||||
// {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}},
|
|
||||||
// {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
|
||||||
// {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
|
||||||
// // repeated row
|
|
||||||
// {id: 1, type: 'row', repeatPanelId: 1, gridPos: {x: 0, y: 0, h: 1 , w: 24}},
|
|
||||||
// {id: 2, type: 'graph', repeatPanelId: 1, gridPos: {x: 0, y: 1, h: 1 , w: 6}},
|
|
||||||
// {id: 3, type: 'graph', repeatPanelId: 1, gridPos: {x: 6, y: 1, h: 1 , w: 6}},
|
|
||||||
// // row below dont touch
|
|
||||||
// {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}},
|
|
||||||
// {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
|
|
||||||
// ]);
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
|
|||||||
|
|
||||||
var template = `
|
var template = `
|
||||||
<input type="file" id="dashupload" name="dashupload" class="hide"/>
|
<input type="file" id="dashupload" name="dashupload" class="hide"/>
|
||||||
<label class="btn btn-secondary" for="dashupload">
|
<label class="btn btn-success" for="dashupload">
|
||||||
<i class="fa fa-upload"></i>
|
<i class="fa fa-upload"></i>
|
||||||
Upload .json File
|
Upload .json File
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,46 +1,57 @@
|
|||||||
<page-header model="ctrl.navModel"></page-header>
|
<page-header model="ctrl.navModel"></page-header>
|
||||||
|
|
||||||
<div class="page-container page-body">
|
<div class="page-container page-body">
|
||||||
<div class="page-action-bar">
|
<div ng-if="ctrl.datasources.length">
|
||||||
<div class="page-action-bar__spacer"></div>
|
<div class="page-action-bar">
|
||||||
<a class="page-header__cta btn btn-success" href="datasources/new">
|
<div class="page-action-bar__spacer"></div>
|
||||||
<i class="fa fa-plus"></i>
|
<a class="page-header__cta btn btn-success" href="datasources/new">
|
||||||
Add data source
|
<i class="fa fa-plus"></i>
|
||||||
</a>
|
Add data source
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="card-section" layout-mode>
|
<section class="card-section" layout-mode>
|
||||||
<layout-selector></layout-selector>
|
<layout-selector></layout-selector>
|
||||||
<ol class="card-list">
|
<ol class="card-list">
|
||||||
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
|
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
|
||||||
<a class="card-item" href="datasources/edit/{{ds.id}}/">
|
<a class="card-item" href="datasources/edit/{{ds.id}}/">
|
||||||
<div class="card-item-header">
|
<div class="card-item-header">
|
||||||
<div class="card-item-type">
|
<div class="card-item-type">
|
||||||
{{ds.type}}
|
{{ds.type}}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-item-body">
|
|
||||||
<figure class="card-item-figure">
|
|
||||||
<img ng-src="{{ds.typeLogoUrl}}">
|
|
||||||
</figure>
|
|
||||||
<div class="card-item-details">
|
|
||||||
<div class="card-item-name">
|
|
||||||
{{ds.name}}
|
|
||||||
<span ng-if="ds.isDefault">
|
|
||||||
<span class="btn btn-secondary btn-mini">default</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-item-sub-name">
|
|
||||||
{{ds.url}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card-item-body">
|
||||||
</a>
|
<figure class="card-item-figure">
|
||||||
</li>
|
<img ng-src="{{ds.typeLogoUrl}}">
|
||||||
</ol>
|
</figure>
|
||||||
</section>
|
<div class="card-item-details">
|
||||||
|
<div class="card-item-name">
|
||||||
|
{{ds.name}}
|
||||||
|
<span ng-if="ds.isDefault">
|
||||||
|
<span class="btn btn-secondary btn-mini">default</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-item-sub-name">
|
||||||
|
{{ds.url}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.datasources.length === 0">
|
<div ng-if="ctrl.datasources.length === 0">
|
||||||
<em>No data sources defined</em>
|
<empty-list-cta model="{
|
||||||
|
title: 'There are no data sources defined yet',
|
||||||
|
buttonIcon: 'gicon gicon-dashboard-new',
|
||||||
|
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?utm_source=grafana_ds_list',
|
||||||
|
proTipLinkTitle: 'Learn more',
|
||||||
|
proTipTarget: '_blank'
|
||||||
|
}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
@import "components/dashboard_grid";
|
@import "components/dashboard_grid";
|
||||||
@import "components/dashboard_list";
|
@import "components/dashboard_list";
|
||||||
@import "components/page_header";
|
@import "components/page_header";
|
||||||
|
@import "components/empty_list_cta";
|
||||||
|
|
||||||
|
|
||||||
// PAGES
|
// PAGES
|
||||||
|
|||||||
@@ -218,8 +218,11 @@ $btn-font-weight: 500 !default;
|
|||||||
$btn-padding-x-sm: .5rem !default;
|
$btn-padding-x-sm: .5rem !default;
|
||||||
$btn-padding-y-sm: .25rem !default;
|
$btn-padding-y-sm: .25rem !default;
|
||||||
|
|
||||||
$btn-padding-x-lg: 1.5rem !default;
|
$btn-padding-x-lg: 21px !default;
|
||||||
$btn-padding-y-lg: .75rem !default;
|
$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: 3px;
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ a.text-success:hover,
|
|||||||
a.text-success:focus { color: darken($success-text-color, 10%); }
|
a.text-success:focus { color: darken($success-text-color, 10%); }
|
||||||
a { cursor: pointer; }
|
a { cursor: pointer; }
|
||||||
|
|
||||||
|
.text-link { text-decoration: underline; }
|
||||||
|
|
||||||
a:focus {
|
a:focus {
|
||||||
outline:0 none !important;
|
outline:0 none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,21 @@
|
|||||||
|
|
||||||
// Button Sizes
|
// Button Sizes
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
// XLarge
|
||||||
|
.btn-xlarge {
|
||||||
|
@include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $btn-border-radius);
|
||||||
|
font-weight: normal;
|
||||||
|
padding-bottom: $btn-padding-y-xl - 3;
|
||||||
|
.gicon {
|
||||||
|
font-size: 31px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Large
|
// Large
|
||||||
.btn-large {
|
.btn-large {
|
||||||
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius);
|
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius);
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small {
|
.btn-small {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.empty-list-cta {
|
||||||
|
background-color: $search-filter-box-bg;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-list-cta__title {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-list-cta__button {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-list-cta__pro-tip {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-list-cta__pro-tip-link {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
.tabbed-view-panel-title {
|
.tabbed-view-panel-title {
|
||||||
float: left;
|
float: left;
|
||||||
padding-top: 1rem;
|
padding-top: 9px;
|
||||||
margin: 0 2rem 0 0;
|
margin: 0 2rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user