From 44ea0ff71d83ec16b543865fad1ecf4dea1a2b99 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 15:55:37 +0300 Subject: [PATCH 01/10] Move import dashboard from modal to the page --- pkg/api/index.go | 2 +- public/app/core/routes/routes.ts | 5 + public/app/features/dashboard/all.ts | 3 +- .../dashboard/dashboard_import_ctrl.ts | 163 ++++++++++++++++++ .../dashboard/partials/dashboardImport.html | 125 ++++++++++++++ ...ctrl_specs.ts => dashboard_import_ctrl.ts} | 6 +- 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 public/app/features/dashboard/dashboard_import_ctrl.ts create mode 100644 public/app/features/dashboard/partials/dashboardImport.html rename public/app/features/dashboard/specs/{dash_import_ctrl_specs.ts => dashboard_import_ctrl.ts} (93%) diff --git a/pkg/api/index.go b/pkg/api/index.go index eb9e70bb404..d2069441b81 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -95,7 +95,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { 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"}, }, }) } @@ -104,6 +103,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {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: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {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"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 68595da4296..a965cd406d3 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -68,6 +68,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index b4f1f4e77f0..f2f2087a24e 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -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); diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts new file mode 100644 index 00000000000..f2b3fb90fc9 --- /dev/null +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -0,0 +1,163 @@ +import _ from 'lodash'; +import config from 'app/core/config'; + +export class DashboardImportCtrl { + navModel: any; + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + inputs: any[]; + inputsValid: boolean; + gnetUrl: string; + gnetError: string; + gnetInfo: any; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { + this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + + this.step = 1; + this.nameExists = false; + + // check gnetId in url + if ($routeParams.gnetId) { + this.gnetUrl = $routeParams.gnetId ; + this.checkGnetDashboard(); + } + } + + onUpload(dash) { + this.dash = dash; + this.dash.id = null; + this.step = 2; + this.inputs = []; + + if (this.dash.__inputs) { + for (let input of this.dash.__inputs) { + var inputModel = { + name: input.name, + label: input.label, + info: input.description, + value: input.value, + type: input.type, + pluginId: input.pluginId, + options: [] + }; + + if (input.type === 'datasource') { + this.setDatasourceOptions(input, inputModel); + } else if (!inputModel.info) { + inputModel.info = 'Specify a string constant'; + } + + this.inputs.push(inputModel); + } + } + + this.inputsValid = this.inputs.length === 0; + this.titleChanged(); + } + + setDatasourceOptions(input, inputModel) { + var sources = _.filter(config.datasources, val => { + return val.type === input.pluginId; + }); + + if (sources.length === 0) { + inputModel.info = "No data sources of type " + input.pluginName + " found"; + } else if (!inputModel.info) { + inputModel.info = "Select a " + input.pluginName + " data source"; + } + + inputModel.options = sources.map(val => { + return {text: val.name, value: val.name}; + }); + } + + inputValueChanged() { + this.inputsValid = true; + for (let input of this.inputs) { + if (!input.value) { + this.inputsValid = false; + } + } + } + + titleChanged() { + this.backendSrv.search({query: this.dash.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.dash.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } + + saveDashboard() { + var inputs = this.inputs.map(input => { + return { + name: input.name, + type: input.type, + pluginId: input.pluginId, + value: input.value + }; + }); + + return this.backendSrv.post('api/dashboards/import', { + dashboard: this.dash, + overwrite: true, + inputs: inputs + }).then(res => { + this.$location.url('dashboard/' + res.importedUri); + this.$scope.dismiss(); + }); + } + + loadJsonText() { + try { + this.parseError = ''; + var dash = JSON.parse(this.jsonText); + this.onUpload(dash); + } catch (err) { + console.log(err); + this.parseError = err.message; + return; + } + } + + checkGnetDashboard() { + this.gnetError = ''; + + var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + var dashboardId; + + if (match && match[1]) { + dashboardId = match[1]; + } else if (match && match[2]) { + dashboardId = match[2]; + } else { + this.gnetError = 'Could not find dashboard'; + } + + return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { + this.gnetInfo = res; + // store reference to grafana.com + res.json.gnetId = res.id; + this.onUpload(res.json); + }).catch(err => { + err.isHandled = true; + this.gnetError = err.data.message || err; + }); + } + + back() { + this.gnetUrl = ''; + this.step = 1; + this.gnetError = ''; + this.gnetInfo = ''; + } +} diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html new file mode 100644 index 00000000000..be43078d32a --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -0,0 +1,125 @@ + + +
+
+ +
+ +
+ +
Grafana.com Dashboard
+ +
+
+ +
+
+ +
+
+ +
Or paste JSON
+ +
+
+ +
+ + + + {{ctrl.parseError}} + +
+
+ +
+
+

+ Importing Dashboard from + Grafana.com +

+ +
+ + +
+
+ + +
+
+ +

+ Options +

+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+ + + Cancel +
+ +
+
diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts similarity index 93% rename from public/app/features/dashboard/specs/dash_import_ctrl_specs.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.ts index c541aca34b2..e78109b01a0 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts @@ -1,9 +1,9 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; -import {DashImportCtrl} from 'app/features/dashboard/import/dash_import'; +import {DashboardImportCtrl} from '../dashboard_import_ctrl'; import config from 'app/core/config'; -describe('DashImportCtrl', function() { +describe('DashboardImportCtrl', function() { var ctx: any = {}; var backendSrv = { search: sinon.stub().returns(Promise.resolve([])), @@ -15,7 +15,7 @@ describe('DashImportCtrl', function() { beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { ctx.$q = $q; ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashImportCtrl, { + ctx.ctrl = $controller(DashboardImportCtrl, { $scope: ctx.scope, backendSrv: backendSrv, }); From 4403d919c10457fb5d85a52303341255297fe18f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 16:51:37 +0300 Subject: [PATCH 02/10] move DashboardImportCtrl tests to jest --- ..._ctrl.ts => dashboard_import_ctrl.jest.ts} | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) rename public/app/features/dashboard/specs/{dashboard_import_ctrl.ts => dashboard_import_ctrl.jest.ts} (57%) diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts similarity index 57% rename from public/app/features/dashboard/specs/dashboard_import_ctrl.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index e78109b01a0..3270bb5a732 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -1,25 +1,24 @@ -import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; - import {DashboardImportCtrl} from '../dashboard_import_ctrl'; -import config from 'app/core/config'; +import config from '../../../core/config'; 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(DashboardImportCtrl, { - $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('DashboardImportCtrl', 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('DashboardImportCtrl', 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('DashboardImportCtrl', 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('DashboardImportCtrl', 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('DashboardImportCtrl', 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'); }); }); }); - - From 4d7ff4de150a0cafe8349ff9d5ea72ef26c9c9d5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 18:00:22 +0300 Subject: [PATCH 03/10] move import menu item to the original place --- pkg/api/index.go | 3 ++- public/app/core/routes/routes.ts | 10 +++++----- public/app/features/dashboard/dashboard_import_ctrl.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index d2069441b81..c1945906624 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -90,11 +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", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } @@ -103,7 +105,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {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: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {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"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index a965cd406d3..d3ef3d51477 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -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,11 +73,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/import', { - templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', - controller : 'DashboardImportCtrl', - controllerAs: 'ctrl', - }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index f2b3fb90fc9..9a630681145 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -16,7 +16,7 @@ export class DashboardImportCtrl { /** @ngInject */ constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { - this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + this.navModel = navModelSrv.getNav('create', 'import'); this.step = 1; this.nameExists = false; From ba3a81aba5f50234dbdf1de62c211ac19cd08ad9 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Dec 2017 16:18:46 +0100 Subject: [PATCH 04/10] ux: Add CTA for empty lists --- public/app/core/angular_wrappers.ts | 2 + .../EmptyListCTA/EmptyListCTA.jest.tsx | 22 +++++ .../components/EmptyListCTA/EmptyListCTA.tsx | 34 ++++++++ .../__snapshots__/EmptyListCTA.jest.tsx.snap | 38 +++++++++ .../features/plugins/partials/ds_list.html | 83 +++++++++++-------- public/sass/_grafana.scss | 1 + public/sass/_variables.scss | 7 +- public/sass/base/_type.scss | 2 + public/sass/components/_buttons.scss | 11 +++ public/sass/components/_empty_list_cta.scss | 21 +++++ 10 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx create mode 100644 public/app/core/components/EmptyListCTA/EmptyListCTA.tsx create mode 100644 public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap create mode 100644 public/sass/components/_empty_list_cta.scss diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 00181736119..3880be7de8d 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -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']); } diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx new file mode 100644 index 00000000000..d62ae892a0a --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx @@ -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().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx new file mode 100644 index 00000000000..a5a78a63bf1 --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; + +export interface IProps { + model: any; +} + +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget + } = this.props.model; + return ( +
+
{title}
+ {buttonTitle} +
+ ProTip: {proTip} + {proTipLinkTitle} +
+
+ ); + } +} + +export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap new file mode 100644 index 00000000000..0598d7ecd4e --- /dev/null +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollorPalette renders correctly 1`] = ` +
+
+ Title +
+ + + Click me + +
+ + ProTip: + This is a tip + + Learn more + +
+
+`; diff --git a/public/app/features/plugins/partials/ds_list.html b/public/app/features/plugins/partials/ds_list.html index 0669beb4129..91d156de317 100644 --- a/public/app/features/plugins/partials/ds_list.html +++ b/public/app/features/plugins/partials/ds_list.html @@ -1,46 +1,57 @@
- +
- No data sources defined +
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index af4a97dc1e6..614089d3121 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -86,6 +86,7 @@ @import "components/dashboard_grid"; @import "components/dashboard_list"; @import "components/page_header"; +@import "components/empty_list_cta"; // PAGES diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index ec1460ac584..62091acdab5 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -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; diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index fa6539a7175..1d2fed48830 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -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; } diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index cea04c28cd7..391acdc5e87 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -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 { diff --git a/public/sass/components/_empty_list_cta.scss b/public/sass/components/_empty_list_cta.scss new file mode 100644 index 00000000000..a8c235d83db --- /dev/null +++ b/public/sass/components/_empty_list_cta.scss @@ -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; +} \ No newline at end of file From 9be6f9734d1efcc0880866fc4bc654416aea0cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Dec 2017 16:29:46 +0100 Subject: [PATCH 05/10] ux: updated padding --- public/sass/components/_tabbed_view.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index dac7408d70f..d2c9b24d2a0 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -26,7 +26,7 @@ .tabbed-view-panel-title { float: left; - padding-top: 1rem; + padding-top: 9px; margin: 0 2rem 0 0; } From afc036492c62518f883c0b424b696b8cec893fba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Dec 2017 13:46:58 +0300 Subject: [PATCH 06/10] dashboard: migrations for repeat rows (#10070) --- .../features/dashboard/dashboard_migration.ts | 5 ++-- .../specs/dashboard_migration.jest.ts | 26 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 6cc3e37f2f8..95a70cbfcab 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -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); diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 2c8b5ebcd50..dfdcb7601a4 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -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; } From 90da964a144abc4a601fdca053d2f43bd146a57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Dec 2017 12:19:01 +0100 Subject: [PATCH 07/10] refactoring PR #10068 --- pkg/api/index.go | 2 +- .../dashboard/import/dash_import.html | 138 -------------- .../features/dashboard/import/dash_import.ts | 176 ------------------ .../dashboard/partials/dashboardImport.html | 7 +- public/app/features/dashboard/upload.ts | 2 +- 5 files changed, 6 insertions(+), 319 deletions(-) delete mode 100644 public/app/features/dashboard/import/dash_import.html delete mode 100644 public/app/features/dashboard/import/dash_import.ts diff --git a/pkg/api/index.go b/pkg/api/index.go index c1945906624..31e413a7ca3 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -96,7 +96,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { 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", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, + {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html deleted file mode 100644 index c617c752366..00000000000 --- a/public/app/features/dashboard/import/dash_import.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - diff --git a/public/app/features/dashboard/import/dash_import.ts b/public/app/features/dashboard/import/dash_import.ts deleted file mode 100644 index 8f3de6adc60..00000000000 --- a/public/app/features/dashboard/import/dash_import.ts +++ /dev/null @@ -1,176 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; -import _ from 'lodash'; - -export class DashImportCtrl { - step: number; - jsonText: string; - parseError: string; - nameExists: boolean; - dash: any; - inputs: any[]; - inputsValid: boolean; - gnetUrl: string; - gnetError: string; - gnetInfo: any; - - /** @ngInject */ - constructor(private backendSrv, private $location, private $scope, $routeParams) { - this.step = 1; - this.nameExists = false; - - // check gnetId in url - if ($routeParams.gnetId) { - this.gnetUrl = $routeParams.gnetId ; - this.checkGnetDashboard(); - } - } - - onUpload(dash) { - this.dash = dash; - this.dash.id = null; - this.step = 2; - this.inputs = []; - - if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { - var inputModel = { - name: input.name, - label: input.label, - info: input.description, - value: input.value, - type: input.type, - pluginId: input.pluginId, - options: [] - }; - - if (input.type === 'datasource') { - this.setDatasourceOptions(input, inputModel); - } else if (!inputModel.info) { - inputModel.info = 'Specify a string constant'; - } - - this.inputs.push(inputModel); - } - } - - this.inputsValid = this.inputs.length === 0; - this.titleChanged(); - } - - setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { - return val.type === input.pluginId; - }); - - if (sources.length === 0) { - inputModel.info = "No data sources of type " + input.pluginName + " found"; - } else if (!inputModel.info) { - inputModel.info = "Select a " + input.pluginName + " data source"; - } - - inputModel.options = sources.map(val => { - return {text: val.name, value: val.name}; - }); - } - - inputValueChanged() { - this.inputsValid = true; - for (let input of this.inputs) { - if (!input.value) { - this.inputsValid = false; - } - } - } - - titleChanged() { - this.backendSrv.search({query: this.dash.title}).then(res => { - this.nameExists = false; - for (let hit of res) { - if (this.dash.title === hit.title) { - this.nameExists = true; - break; - } - } - }); - } - - saveDashboard() { - var inputs = this.inputs.map(input => { - return { - name: input.name, - type: input.type, - pluginId: input.pluginId, - value: input.value - }; - }); - - return this.backendSrv.post('api/dashboards/import', { - dashboard: this.dash, - overwrite: true, - inputs: inputs - }).then(res => { - this.$location.url('dashboard/' + res.importedUri); - this.$scope.dismiss(); - }); - } - - loadJsonText() { - try { - this.parseError = ''; - var dash = JSON.parse(this.jsonText); - this.onUpload(dash); - } catch (err) { - console.log(err); - this.parseError = err.message; - return; - } - } - - checkGnetDashboard() { - this.gnetError = ''; - - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; - - if (match && match[1]) { - dashboardId = match[1]; - } else if (match && match[2]) { - dashboardId = match[2]; - } else { - this.gnetError = 'Could not find dashboard'; - } - - return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { - this.gnetInfo = res; - // store reference to grafana.com - res.json.gnetId = res.id; - this.onUpload(res.json); - }).catch(err => { - err.isHandled = true; - this.gnetError = err.data.message || err; - }); - } - - back() { - this.gnetUrl = ''; - this.step = 1; - 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); diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html index be43078d32a..b740b8f38bc 100644 --- a/public/app/features/dashboard/partials/dashboardImport.html +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -3,7 +3,8 @@
-
+ +
@@ -112,10 +113,10 @@
- - Cancel diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 45a1f1b4a0c..61cca0d4216 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -