Merge branch 'develop' into develop_searchresults

This commit is contained in:
Marcus Efraimsson 2017-12-05 14:25:20 +01:00
commit 8e8c3d2eeb
23 changed files with 380 additions and 241 deletions

View File

@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
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"},
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

@ -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',

View File

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

View File

@ -1,10 +1,8 @@
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import _ from 'lodash';
import 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);

View File

@ -17,7 +17,7 @@ export class DashboardListCtrl {
/** @ngInject */
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, skipRecent: true, skipStarred: true};
this.selectedStarredFilter = this.starredFilterOptions[0];

View File

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

View File

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

View File

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

View File

@ -1,25 +1,24 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {DashboardImportCtrl} from '../dashboard_import_ctrl';
import config from '../../../core/config';
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
import config from 'app/core/config';
describe('DashImportCtrl', function() {
describe('DashboardImportCtrl', function() {
var ctx: any = {};
var backendSrv = {
search: sinon.stub().returns(Promise.resolve([])),
get: sinon.stub()
};
beforeEach(angularMocks.module('grafana.core'));
let navModelSrv;
let backendSrv;
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.ctrl = $controller(DashImportCtrl, {
$scope: ctx.scope,
backendSrv: backendSrv,
});
}));
beforeEach(() => {
navModelSrv = {
getNav: () => {}
};
backendSrv = {
search: jest.fn().mockReturnValue(Promise.resolve([])),
get: jest.fn()
};
ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {});
});
describe('when uploading json', function() {
beforeEach(function() {
@ -37,13 +36,13 @@ describe('DashImportCtrl', function() {
});
it('should build input model', function() {
expect(ctx.ctrl.inputs.length).to.eql(1);
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
expect(ctx.ctrl.inputs.length).toBe(1);
expect(ctx.ctrl.inputs[0].name).toBe('ds');
expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source');
});
it('should set inputValid to false', function() {
expect(ctx.ctrl.inputsValid).to.eql(false);
expect(ctx.ctrl.inputsValid).toBe(false);
});
});
@ -51,7 +50,7 @@ describe('DashImportCtrl', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock
backendSrv.get = sinon.spy(() => {
backendSrv.get = jest.fn(() => {
return Promise.resolve({
json: {}
});
@ -60,7 +59,7 @@ describe('DashImportCtrl', function() {
});
it('should call gnet api with correct dashboard id', function() {
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
});
});
@ -68,7 +67,7 @@ describe('DashImportCtrl', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = '2342';
// setup api mock
backendSrv.get = sinon.spy(() => {
backendSrv.get = jest.fn(() => {
return Promise.resolve({
json: {}
});
@ -77,10 +76,8 @@ describe('DashImportCtrl', function() {
});
it('should call gnet api with correct dashboard id', function() {
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
});
});
});

View File

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

View File

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

View File

@ -1,46 +1,57 @@
<page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body">
<div class="page-action-bar">
<div class="page-action-bar__spacer"></div>
<a class="page-header__cta btn btn-success" href="datasources/new">
<i class="fa fa-plus"></i>
Add data source
</a>
</div>
<div ng-if="ctrl.datasources.length">
<div class="page-action-bar">
<div class="page-action-bar__spacer"></div>
<a class="page-header__cta btn btn-success" href="datasources/new">
<i class="fa fa-plus"></i>
Add data source
</a>
</div>
<section class="card-section" layout-mode>
<layout-selector></layout-selector>
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
<a class="card-item" href="datasources/edit/{{ds.id}}/">
<div class="card-item-header">
<div class="card-item-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}}
<section class="card-section" layout-mode>
<layout-selector></layout-selector>
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
<a class="card-item" href="datasources/edit/{{ds.id}}/">
<div class="card-item-header">
<div class="card-item-type">
{{ds.type}}
</div>
</div>
</div>
</a>
</li>
</ol>
</section>
<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>
</a>
</li>
</ol>
</section>
</div>
<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>

View File

@ -86,6 +86,7 @@
@import "components/dashboard_grid";
@import "components/dashboard_list";
@import "components/page_header";
@import "components/empty_list_cta";
// PAGES

View File

@ -218,8 +218,11 @@ $btn-font-weight: 500 !default;
$btn-padding-x-sm: .5rem !default;
$btn-padding-y-sm: .25rem !default;
$btn-padding-x-lg: 1.5rem !default;
$btn-padding-y-lg: .75rem !default;
$btn-padding-x-lg: 21px !default;
$btn-padding-y-lg: 11px !default;
$btn-padding-x-xl: 21px !default;
$btn-padding-y-xl: 11px !default;
$btn-border-radius: 3px;

View File

@ -48,6 +48,8 @@ a.text-success:hover,
a.text-success:focus { color: darken($success-text-color, 10%); }
a { cursor: pointer; }
.text-link { text-decoration: underline; }
a:focus {
outline:0 none !important;
}

View File

@ -51,10 +51,21 @@
// 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
.btn-large {
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius);
font-weight: normal;
}
.btn-small {

View File

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

View File

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