Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Dashboard Panel Title #7336

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="panel panel-default" ng-switch on="panel.type" ng-if="savedObj || error">
<div class="panel-heading">
<span class="panel-title" title="{{::savedObj.title}}">
{{::savedObj.title}}
<span class="panel-title" title="{{savedObj.title}}">
<div initial-val="{{savedObj.title}}" kbn-edit-field="dashboardPanelTitle"></div>
</span>
<div class="btn-group">
<div class="btn-group action-items">
<a aria-label="Edit" ng-show="chrome.getVisible() && editUrl" ng-href="{{::editUrl}}">
<i aria-hidden="true" class="fa fa-pencil"></i>
</a>
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/kibana/public/dashboard/components/panel/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ uiModules
// create child ui state from the savedObj
const uiState = panelConfig.uiState || {};
$scope.uiState = $scope.parentUiState.createChild(getPanelId(panelConfig.panel), uiState, true);
$scope.dashboardPanelTitle = $scope.uiState.get('panel-title') || $scope.savedObj.title;

$scope.$watch('dashboardPanelTitle', (newVal, oldVal) => {
if (newVal === oldVal) { return false; }
$scope.uiState.set('panel-title', newVal);
});
const panelSavedVis = _.get(panelConfig, 'savedObj.vis'); // Sometimes this will be a search, and undef
if (panelSavedVis) {
panelSavedVis.setUiState($scope.uiState);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/kibana/public/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'ui/notify';
import 'ui/typeahead';
import 'ui/navbar_extensions';
import 'ui/share';
import 'ui/directives/kbn_editable_field';
import 'plugins/kibana/dashboard/directives/grid';
import 'plugins/kibana/dashboard/components/panel/panel';
import 'plugins/kibana/dashboard/services/saved_dashboards';
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/kibana/public/dashboard/styles/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ dashboard-grid {
.visualize-show-spy {
visibility: visible;
}
.panel .panel-heading .btn-group {
.panel .panel-heading .btn-group.action-items {
display: block !important;
}
}
Expand Down Expand Up @@ -85,7 +85,7 @@ dashboard-grid {
background-color: @white;
border: none;

.btn-group {
.btn-group.action-items {
a {
color: inherit;
}
Expand Down
120 changes: 120 additions & 0 deletions src/ui/public/directives/__tests__/kbn_editable_field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import angular from 'angular';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import $ from 'jquery';
import _ from 'lodash';
import 'ui/directives/kbn_editable_field';

describe('kbnEditableField Directive', function () {
let $compile;
let $rootScope;
let $timeout;
let element;
let $el;
let selectedEl;
let selectedText;
const testObj = {
a: {
b: {
c: {
val: 'Initialvalue',
reset: 'Resetvalue'
}
}
}
};

beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_, _$timeout_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$timeout = _$timeout_;

$el = $('<div>');
$el.appendTo('body');
}));

afterEach(function () {
$el.remove();
$el = null;
});

function renderEl(html, scopeObj = testObj) {
_.assign($rootScope, scopeObj);
const angularElem = angular.element(html);
element = $compile(angularElem)($rootScope);
element.appendTo($el);
$rootScope.$digest();
return [element, angularElem.controller('kbnEditField')];
}


it('should make the proper markup with proper values', function () {
const el = renderEl('<div kbn-edit-field="a.b.c.val" initial-val"{{a.b.c.initial}}"></div>')[0];
const children = el.children();
expect(children.length).to.equal(4);
const $input = children.filter('input');
expect($input.length).to.equal(1);
expect($input.val()).to.equal(testObj.a.b.c.val);
});

describe('it should have a controller with proper functions', function () {
it('it should set and get the input value', function () {
const ret = renderEl('<div kbn-edit-field="a.b.c.val" initial-val"{{a.b.c.initial}}"></div>');
const ctrl = ret[1];
const $input = ret[0].find('input');
expect(ctrl.input()).to.equal($input.val());
expect(ctrl.input()).to.equal(testObj.a.b.c.val);
const diffVal = 'new value';
ctrl.input(diffVal);
expect(ctrl.input()).to.equal(diffVal);
expect(ctrl.input()).to.equal($input.val());
});

it('set and get the model value', function () {
const ret = renderEl('<div kbn-edit-field="a.b.c.val" initial-val"{{a.b.c.initial}}"></div>');
const ctrl = ret[1];
expect(ctrl.model()).to.equal(testObj.a.b.c.val);
expect(ctrl.model()).to.equal($rootScope.a.b.c.val);
const newVal = 'test value';
ctrl.model(newVal);
expect(ctrl.model()).to.equal($rootScope.a.b.c.val);
expect(ctrl.model()).to.equal(newVal);
});

it('set and get the model value with non nested values', function () {
const scopeObj = { val: 'foo', initial: 'first' };
const ret = renderEl('<div kbn-edit-field="val" initial-val"{{initial}}"></div>', scopeObj);
const ctrl = ret[1];
expect(ctrl.model()).to.equal(scopeObj.val);
expect(ctrl.model()).to.equal($rootScope.val);
const newVal = 'test value';
ctrl.model(newVal);
expect(ctrl.model()).to.equal($rootScope.val);
expect(ctrl.model()).to.equal(newVal);
});

it('should add an editing class when the the input val is different from the model', function () {
const ret = renderEl('<div kbn-edit-field="a.b.c.val" initial-val"{{a.b.c.initial}}"></div>');
const ctrl = ret[1];
const $el = ret[0];
const $input = $el.children('input');
expect($el.hasClass('editing')).to.be(false);
const newVal = 'another one';
$input.val(newVal);
ctrl.toggleEditClass();
expect($el.hasClass('editing')).to.be(true);
});
it('should blur the input on command', function () {
const ret = renderEl('<div kbn-edit-field="a.b.c.val" initial-val"{{a.b.c.initial}}"></div>');
const ctrl = ret[1];
const $el = ret[0];
const $input = $el.children('input');
expect(document.activeElement).to.be(document.body);
$input.focus();
expect(document.activeElement).to.be($input[0]);
ctrl.blurInput();
expect(document.activeElement).to.be(document.body);
});
});
});
113 changes: 113 additions & 0 deletions src/ui/public/directives/kbn_editable_field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import $ from 'jquery';
import uiModules from 'ui/modules';

const module = uiModules.get('kibana');

// Take any value and add a can
module.directive('kbnEditField', ['$parse', function ($parse) {
function trimWhiteSpace(str) {
let returnVal = str;
while (returnVal[0] === ' ') {
returnVal = returnVal.substr(1);
}
while (returnVal[returnVal.length - 1] === ' ') {
returnVal = returnVal.substr(0, returnVal.length - 1);
}
return returnVal;
}
return {
restrict: 'A',
template: `<input type="text" class="form-control" />
<span><i class="fa fa-check"></i></span>
<span><i class="fa fa-remove"></i></span>
<span><i class="fa fa-undo"></i></span>`,
controller: function ($scope, $element, $attrs) {
const fieldKey = $attrs.kbnEditField;
// On external change show the user
$scope.$watch(fieldKey, function (newVal) { setInput(newVal); });

const $inputEl = $element.children('input');
// Inspired by
// https://github.com/angular/angular.js/blob/master/src/ng/directive/ngModel.js#L245
const parsedModel = $parse(fieldKey);
const getVal = parsedModel;
const setVal = parsedModel.assign;

// Get the val
function getModelVal() { return getVal($scope); }
// Sync the veiwVal, and what is.
function setInput(val) { $inputEl.val(val); }
// Set the model value
function setModel(val) { setVal($scope, val); }

this.model = function (val) {
const retVal = getModelVal();
if (val) { setModel(val); }
return retVal;
};
this.input = function (val) {
const retVal = $inputEl.val();
if (val) { setInput(val); }
return retVal;
};
this.blurInput = function () { $inputEl.blur(); };
this.toggleEditClass = function () { $element.toggleClass('editing', getModelVal() !== $inputEl.val()); };
const initialValue = $attrs.initialVal || getModelVal();
this.getInitialVal = function () { return initialValue; };
},
link: function ($scope, $element, attrs, kbnEditFieldCtrl) {
// Add necessary classesto the markup
$element.addClass('kbn-edit-field');
const $inputEl = $element.children('input');
const ctrl = kbnEditFieldCtrl;

$inputEl.on('keyup', evt => {
const isEnterKey = evt.keyCode === 13;
const isEscKey = evt.keyCode === 27;
if (isEnterKey) {
// This is much slower in setting than the click, not really sure why honestly
safeSetModel();
} else if (isEscKey) { // reset the input
ctrl.input(ctrl.model());
}

if (isEnterKey || isEscKey) {
ctrl.blurInput();
}
ctrl.toggleEditClass();
});
// click on the check button
$element.find('.fa-check').click(() => {
safeSetModel();
ctrl.toggleEditClass();
ctrl.blurInput();
});

// Click on the undo button
$element.find('.fa-undo').click(() => {
safeSetModel(ctrl.getInitialVal());
ctrl.input(ctrl.getInitialVal());
ctrl.toggleEditClass();
});

// click on the x, button; same as ESC
$element.find('.fa-remove').click(() => {
ctrl.input(ctrl.model());
ctrl.toggleEditClass();
});

function safeSetModel(val = ctrl.input()) {
const safeVal = trimWhiteSpace(val);
if (safeVal.length) {
ctrl.model(safeVal);
if (safeVal !== val) { // If trimWhiteSpace had to, you know, trimWhiteSpace
ctrl.input(safeVal);
}
} else {
ctrl.input(ctrl.model());
}
}

}
};
}]);
9 changes: 9 additions & 0 deletions src/ui/public/styles/dark-theme.less
Original file line number Diff line number Diff line change
Expand Up @@ -586,4 +586,13 @@
background-color: @config-bg;
color: @dark-button-font;
}
.kbn-edit-field {
input {
background-color: @panel-bg;
border-color: transparent;
&:focus {
border-color: @input-border;
}
}
}
}
42 changes: 42 additions & 0 deletions src/ui/public/styles/input.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "~ui/styles/variables";
@import (reference) "~ui/styles/mixins.less";

i.input-error {
position: absolute;
Expand All @@ -12,3 +13,44 @@ select {
color: @input-color;
background-color: @input-bg;
}

.kbn-edit-field {
.real-flex-parent(row nowrap);
&.editing {
input {
border-color: @brand-warning !important;
}
}
input {
cursor: pointer;
border-color: transparent;
padding-left: 0;
display: inline-block;
flex: 1 1 auto;
margin-right: 5px;
&:focus {
outline: none;
border-color: #999;
+ span {
opacity: 100;
+ span {
opacity: 100;
+ span { opacity: 100; }
}
}
}
}
span {
cursor: pointer;
flex: 0 1 auto;
line-height: 32px;
text-align: center;
width: 25px;
display: inline-block;
opacity: 0;
&:active,
&:hover {
opacity: 100;
}
}
}