本文之前已经拆分成章节发布在我的 《Kibana 权威指南》电子书上。欢迎移步观看全书其他章节。
Kibana 3 作为 ELKstack 风靡世界的最大推动力,其与优美的界面配套的简洁的代码同样功不可没。事实上,graphite 社区就通过移植 kibana 3 代码框架的方式,启动了 grafana 项目。至今你还能在 grafana 源码找到二十多处 “kbn” 字样。
巧合的是,在 Kibana 重构 v4 版的同时,grafana 的 v2 版也到了 Alpha 阶段,从目前的预览效果看,主体 dashboard 沿用了 Kibana 3 的风格,不过添加了额外的菜单栏,供用户权限设置等使用 —— 这意味着 grafana 2 跟 kibana 4 一样需要一个单独的 server 端。
笔者并非专业的前端工程师,对 angularjs 也处于一本入门指南都没看过的水准。所以本节内容,只会抽取一些个人经验中会有涉及到的地方提出一些”私货”。欢迎方家指正。
下面是 kibana 源码的全部文件的 tree 图:
.
├── app
│ ├── app.js
│ ├── components
│ │ ├── extend-jquery.js
│ │ ├── kbn.js
│ │ ├── lodash.extended.js
│ │ ├── require.config.js
│ │ └── settings.js
│ ├── controllers
│ │ ├── all.js
│ │ ├── dash.js
│ │ ├── dashLoader.js
│ │ ├── pulldown.js
│ │ └── row.js
│ ├── dashboards
│ │ ├── blank.json
│ │ ├── default.json
│ │ ├── guided.json
│ │ ├── logstash.js
│ │ ├── logstash.json
│ │ ├── noted.json
│ │ ├── panel.js
│ │ └── test.json
│ ├── directives
│ │ ├── addPanel.js
│ │ ├── all.js
│ │ ├── arrayJoin.js
│ │ ├── configModal.js
│ │ ├── confirmClick.js
│ │ ├── dashUpload.js
│ │ ├── esVersion.js
│ │ ├── kibanaPanel.js
│ │ ├── kibanaSimplePanel.js
│ │ ├── ngBlur.js
│ │ ├── ngModelOnBlur.js
│ │ ├── resizable.js
│ │ └── tip.js
│ ├── factories
│ │ └── store.js
│ ├── filters
│ │ └── all.js
│ ├── panels
│ │ ├── bettermap
│ │ │ ├── editor.html
│ │ │ ├── leaflet
│ │ │ │ ├── images
│ │ │ │ │ ├── layers-2x.png
│ │ │ │ │ ├── layers.png
│ │ │ │ │ ├── marker-icon-2x.png
│ │ │ │ │ ├── marker-icon.png
│ │ │ │ │ └── marker-shadow.png
│ │ │ │ ├── leaflet-src.js
│ │ │ │ ├── leaflet.css
│ │ │ │ ├── leaflet.ie.css
│ │ │ │ ├── leaflet.js
│ │ │ │ ├── plugins.css
│ │ │ │ ├── plugins.js
│ │ │ │ └── providers.js
│ │ │ ├── module.css
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── column
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ └── panelgeneral.html
│ │ ├── dashcontrol
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── derivequeries
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── fields
│ │ │ ├── editor.html
│ │ │ ├── micropanel.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── filtering
│ │ │ ├── editor.html
│ │ │ ├── meta.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── force
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── goal
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── histogram
│ │ │ ├── editor.html
│ │ │ ├── interval.js
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ ├── queriesEditor.html
│ │ │ ├── styleEditor.html
│ │ │ └── timeSeries.js
│ │ ├── hits
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── map
│ │ │ ├── editor.html
│ │ │ ├── lib
│ │ │ │ ├── jquery.jvectormap.min.js
│ │ │ │ ├── map.cn.js
│ │ │ │ ├── map.europe.js
│ │ │ │ ├── map.usa.js
│ │ │ │ └── map.world.js
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── multifieldhistogram
│ │ │ ├── editor.html
│ │ │ ├── interval.js
│ │ │ ├── markersEditor.html
│ │ │ ├── meta.html
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ ├── styleEditor.html
│ │ │ └── timeSeries.js
│ │ ├── percentiles
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── query
│ │ │ ├── editor.html
│ │ │ ├── editors
│ │ │ │ ├── lucene.html
│ │ │ │ ├── regex.html
│ │ │ │ └── topN.html
│ │ │ ├── help
│ │ │ │ ├── lucene.html
│ │ │ │ ├── regex.html
│ │ │ │ └── topN.html
│ │ │ ├── helpModal.html
│ │ │ ├── meta.html
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ └── query.css
│ │ ├── ranges
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── sparklines
│ │ │ ├── editor.html
│ │ │ ├── interval.js
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ └── timeSeries.js
│ │ ├── statisticstrend
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── stats
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── table
│ │ │ ├── editor.html
│ │ │ ├── export.html
│ │ │ ├── micropanel.html
│ │ │ ├── modal.html
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ └── pagination.html
│ │ ├── terms
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── text
│ │ │ ├── editor.html
│ │ │ ├── lib
│ │ │ │ └── showdown.js
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ ├── timepicker
│ │ │ ├── custom.html
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ ├── module.js
│ │ │ └── refreshctrl.html
│ │ ├── trends
│ │ │ ├── editor.html
│ │ │ ├── module.html
│ │ │ └── module.js
│ │ └── valuehistogram
│ │ ├── editor.html
│ │ ├── module.html
│ │ ├── module.js
│ │ ├── queriesEditor.html
│ │ └── styleEditor.html
│ ├── partials
│ │ ├── connectionFailed.html
│ │ ├── dashLoader.html
│ │ ├── dashLoaderShare.html
│ │ ├── dashboard.html
│ │ ├── dasheditor.html
│ │ ├── inspector.html
│ │ ├── load.html
│ │ ├── modal.html
│ │ ├── paneladd.html
│ │ ├── paneleditor.html
│ │ ├── panelgeneral.html
│ │ ├── querySelect.html
│ │ └── roweditor.html
│ └── services
│ ├── alertSrv.js
│ ├── all.js
│ ├── dashboard.js
│ ├── esVersion.js
│ ├── fields.js
│ ├── filterSrv.js
│ ├── kbnIndex.js
│ ├── monitor.js
│ ├── panelMove.js
│ ├── querySrv.js
│ └── timer.js
├── config.js
├── css
│ ├── angular-multi-select.css
│ ├── animate.min.css
│ ├── bootstrap-responsive.min.css
│ ├── bootstrap.dark.min.css
│ ├── bootstrap.light.min.css
│ ├── font-awesome.min.css
│ ├── jquery-ui.css
│ ├── jquery.multiselect.css
│ ├── normalize.min.css
│ └── timepicker.css
├── favicon.ico
├── font
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ └── fontawesome-webfont.woff
├── img
│ ├── annotation-icon.png
│ ├── cubes.png
│ ├── glyphicons-halflings-white.png
│ ├── glyphicons-halflings.png
│ ├── kibana.png
│ ├── light.png
│ ├── load.gif
│ ├── load_big.gif
│ ├── small.png
│ └── ui-icons_222222_256x240.png
├── index.html
└── vendor
├── LICENSE.json
├── angular
│ ├── angular-animate.js
│ ├── angular-cookies.js
│ ├── angular-dragdrop.js
│ ├── angular-loader.js
│ ├── angular-resource.js
│ ├── angular-route.js
│ ├── angular-sanitize.js
│ ├── angular-scenario.js
│ ├── angular-strap.js
│ ├── angular.js
│ ├── bindonce.js
│ ├── datepicker.js
│ └── timepicker.js
├── blob.js
├── bootstrap
│ ├── bootstrap.js
│ └── less
│ ├── accordion.less
│ ├── alerts.less
│ ├── bak
│ │ ├── bootswatch.dark.less
│ │ └── variables.dark.less
│ ├── bootstrap.dark.less
│ ├── bootstrap.less
│ ├── bootstrap.light.less
│ ├── bootswatch.dark.less
│ ├── bootswatch.light.less
│ ├── breadcrumbs.less
│ ├── button-groups.less
│ ├── buttons.less
│ ├── carousel.less
│ ├── close.less
│ ├── code.less
│ ├── component-animations.less
│ ├── dropdowns.less
│ ├── forms.less
│ ├── grid.less
│ ├── hero-unit.less
│ ├── labels-badges.less
│ ├── layouts.less
│ ├── media.less
│ ├── mixins.less
│ ├── modals.less
│ ├── navbar.less
│ ├── navs.less
│ ├── overrides.less
│ ├── pager.less
│ ├── pagination.less
│ ├── popovers.less
│ ├── progress-bars.less
│ ├── reset.less
│ ├── responsive-1200px-min.less
│ ├── responsive-767px-max.less
│ ├── responsive-768px-979px.less
│ ├── responsive-navbar.less
│ ├── responsive-utilities.less
│ ├── responsive.less
│ ├── scaffolding.less
│ ├── sprites.less
│ ├── tables.less
│ ├── tests
│ │ ├── buttons.html
│ │ ├── css-tests.css
│ │ ├── css-tests.html
│ │ ├── forms-responsive.html
│ │ ├── forms.html
│ │ ├── navbar-fixed-top.html
│ │ ├── navbar-static-top.html
│ │ └── navbar.html
│ ├── thumbnails.less
│ ├── tooltip.less
│ ├── type.less
│ ├── utilities.less
│ ├── variables.dark.less
│ ├── variables.less
│ ├── variables.light.less
│ └── wells.less
├── chromath.js
├── elasticjs
│ ├── elastic-angular-client.js
│ └── elastic.js
├── elasticsearch.angular.js
├── filesaver.js
├── jquery
│ ├── jquery-1.8.0.js
│ ├── jquery-ui-1.10.3.js
│ ├── jquery.flot.byte.js
│ ├── jquery.flot.events.js
│ ├── jquery.flot.js
│ ├── jquery.flot.pie.js
│ ├── jquery.flot.selection.js
│ ├── jquery.flot.stack.js
│ ├── jquery.flot.stackpercent.js
│ ├── jquery.flot.threshold.js
│ ├── jquery.flot.time.js
│ ├── jquery.multiselect.filter.js
│ └── jquery.multiselect.js
├── jsonpath.js
├── lodash.js
├── modernizr-2.6.1.js
├── moment.js
├── numeral.js
├── require
│ ├── css-build.js
│ ├── css.js
│ ├── require.js
│ ├── text.js
│ └── tmpl.js
├── simple_statistics.js
├── timezone.js
└── underscore.string.js
一目了然,我们可以归纳出下面几类主要文件:
这一部分是网页项目的基础。从 index.html 里就可以学到 angularjs 最基础的常用模板语法了。出现的指令有:ng-repeat
, ng-controller
, ng-include
, ng-view
, ng-slow
, ng-click
, ng-href
,以及变量绑定的语法:\{\{ dashboard.current.xxx }}
。
index.html 中,需要注意 js 的加载次序,先 require.js
,然后再 require.config.js
,最后 app
。整个 kibana 项目都是通过 requrie 方式加载的。而具体的模块,和模块的依赖关系,则定义在 require.config.js
里。这些全部加载完成后,才是启动 app 模块,也就是项目本身的代码。
require.config.js 中,主要分成两部分配置,一个是 paths
,一个是 shim
。paths 用来指定依赖模块的导出名称和模块 js 文件的具体路径。而 shim 用来指定依赖模块之间的依赖关系。比方说:绘制图表的 js,kibana3 里用的是 jquery.flot 库。这个就首先依赖于 jquery 库。(通俗的说,就是原先普通的 HTML 写法里,要先加载 jquery.js 再加载 jquery.flot.js)
在整个 paths 中,需要单独提一下的是 elasticjs:'../vendor/elasticjs/elastic-angular-client'
。这是串联 elastic.js 和 angular.js 的文件。这里面实际是定义了一个 angular.module 的 factory,名叫 ejsResource
。后续我们在 kibana 3 里用到的跟 Elasticsearch 交互的所有方法,都在这个 ejsResource
里了。
factory 是 angular 的一个单例对象,创建之后会持续到你关闭浏览器。Kibana 3 就是通过这种方式来控制你所有的图表是从同一个 Elasticsearch 获取的数据
app.js 中,定义了整个应用的 routes,加载了 controller, directives 和 filters 里的全部内容。就是在这里,加载了主页面 app/partials/dashboard.html
。当然,这个页面其实没啥看头,因为里面就是提供 pulldown 和 row 的 div,然后绑定到对应的 controller 上。
controller 里没太多可讲的。kibana 3 里,pulldown 其实跟 row 差别不大,看这简单的几行代码里,最关键的就是几个注入:
define(['angular','app','lodash'], function (angular, app, _) {
'use strict';
angular.module('kibana.controllers').controller('RowCtrl', function($scope, $rootScope, $timeout,ejsResource, querySrv) {
var _d = {
title: "Row",
height: "150px",
collapse: false,
collapsable: true,
editable: true,
panels: [],
notice: false
};
_.defaults($scope.row,_d);
$scope.init = function() {
$scope.querySrv = querySrv;
$scope.reset_panel();
};
$scope.init();
}
);
});
这里面,注入了 $scope
, ejsResource
和 querySrv
。$scope
是控制器作用域内的模型数据对象,这是 angular 提供的一个特殊变量。ejsResource
是一个 factory ,前面已经讲过。querySrv
是一个 service,下面说一下。
service 跟 factory 的概念非常类似,一般来说,可能 factory 偏向用来共享一个类,而 service 用来共享一组函数功能。
kibana 3 里,比较有用和常用的 services 包括:
dashboard.js 里提供了关于 Kibana 3 仪表板的读写操作。其中主要的几个是提供了三种读取仪表板布局纲要的方式,也就是读取文件,读取存在 .kibana-int
索引里的数据,读取 js 脚本。下面是读取 js 脚本的相关函数:
this.script_load = function(file) {
return $http({
url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"),
method: "GET",
transformResponse: function(response) {
/*jshint -W054 */
var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
return _f($routeParams,kbn,_,moment);
}
}).then(function(result) {
if(!result) {
return false;
}
self.dash_load(dash_defaults(result.data));
return true;
},function() {
alertSrv.set('Error',
"Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard" ,
'error');
return false;
});
};
可以看到,最关键的就是那个 new Function
。知道这步传了哪些函数进去,也就知道你的 js 脚本里都可以调用哪些内容了~
最后调用的 dash_load
方法也需要提一下。这个方法的最后,有几行这样的代码:
self.availablePanels = _.difference(config.panel_names,
_.pluck(_.union(self.current.nav,self.current.pulldowns),'type'));
self.availablePanels = _.difference(self.availablePanels,config.hidden_panels);
从最外层的 config.js
里读取了 panel_names
数组,然后取出了 nav 和 pulldown 用过的 panel,剩下就是我们能在 row 里添加的 panel 类型了。
querySrv.js 里定义了跟 query 框相关的函数和属性。主要有几个值得注意的。
color
列表;queryTypes
,尤其是里么的 topN
,可以看到 topN 方式其实就是先请求了一次 termsFacet,然后把结果 map 成一组普通的 query。ids
和 idsByMode
。之后图表的绑定具体 query 的时候,就是通过这个函数来选择的。filterSrv.js 跟 querySrv 相似。特殊的是两个函数。
toEjsObjs
。根据不同的 filter 类型调用不同的 ejs 方法。timeRange
。因为在 histogram panel 上拖拽,会生成好多个 range 过滤器,都是时间。这个方法会选择最后一个类型为 time 的 filter,作为实际要用的 filter。这样保证请求 ES 的是最后一次拖拽选定的时间段。fields.js 里最重要的作用就是通过 mapping 接口获取索引的字段列表,存在 fields.list
里。这个数组后来在每个 panel 的编辑页里,都以 bs-typeahead="fields.list"
的形式作为文本输入时的自动补全提示。在 table panel 里,则是左侧栏的显示来源。
esVersion.js 里提供了对 ES 版本号的对比函数。之所以专门提供这么个 service,一来是因为不同版本的 ES 接口有变化,比如我自己开发的 percentile panel 里,就用 esVersion 判断了两次版本。因为 percentile 接口是 1.0 版之后才有,而从 1.3 版以后返回数据的结构又发生了一次变动。二来 ES 的版本号格式比较复杂,又有点又有字母。
前面在讲 app/services/dashboard.js
的时候,已经说到能添加的 panel 列表是怎么获取的。那么panel 是怎么加上的呢?
同样是之前讲过的 app/partials/dashaboard.html
里,加载了 partials/roweditor.html
页面。这里有一段:
<form class="form-inline">
<select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in dashboard.availablePanels|stringSort"></select>
<small ng-show="rowSpan(row) > 11">
Note: This row is full, new panels will wrap to a new line. You should add another row.
</small>
</form>
<div ng-show="!(_.isUndefined(panel.type))">
<div add-panel=""></div>
</div>
这个 add-panel
指令,是有 app/directives/addPanel.js
提供的。方法如下:
$scope.$watch('panel.type', function() {
var _type = $scope.panel.type;
$scope.reset_panel(_type);
if(!_.isUndefined($scope.panel.type)) {
$scope.panel.loadingEditor = true;
$scope.require(['panels/'+$scope.panel.type.replace(".","/") +'/module'], function () {
var template = '<div ng-controller="'+$scope.panel.type+'" ng-include="\'app/partials/paneladd.html\'"></div>';
elem.html($compile(angular.element(template))($scope));
$scope.panel.loadingEditor = false;
});
}
});
可以看到,其实就是 require 了对应的 panels/xxx/module.js
,然后动态生成一个 div,绑定到对应的 controller 上。
还是在 app/partials/dashaboard.html
里,用到了另一个指令 kibana-panel
:
<div
ng-repeat="(name, panel) in row.panels|filter:isPanel"
ng-cloak ng-hide="panel.hide"
kibana-panel type='panel.type' resizable
class="panel nospace" ng-class="{'dragInProgress':dashboard.panelDragging}"
style="position:relative" ng-style="{'width':!panel.span?'100%':((panel.span/1.2)*10)+'%'}"
data-drop="true" ng-model="row.panels" data-jqyoui-options
jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
</div>
当然,这里面还有 resizable
指令也是自己实现的,不过一般我们用不着关心这个的代码实现。
下面看 app/directives/kibanaPanel.js
里的实现。
这个里面大多数逻辑跟 addPanel.js 是一样的,都是为了实现一个指令嘛。对于我们来说,关注点在前面那一大段 HTML 字符串,也就是变量 panelHeader
。这个就是我们看到的实际效果中,kibana 3 每个 panel 顶部那个小图标工具栏。仔细阅读一下,可以发现除了每个 panel 都一致的那些 span 以外,还有一段是:
'<span ng-repeat="task in panelMeta.modals" class="row-button extra" ng-show="task.show">' +
'<span bs-modal="task.partial" class="pointer"><i ' +
'bs-tooltip="task.description" ng-class="task.icon" class="pointer"></i></span>'+
'</span>'
也就是说,每个 panel 可以在自己的 panelMeta.modals 数组里,定义不同的小图标,弹出不同的对话浮层。我个人给 table panel 二次开发加入的 exportAsCsv 功能,图标就是在这里加入的。
终于说到最后了。大家进入到 app/panels/
下,每个目录都是一种 panel。原因前一节已经分析过了,因为 addPanel.js 里就是直接这样拼接的。入口都是固定的:module.js。
下面以 stats panel 为例。(因为我最开始就是抄的 stats 做的 percentile,只有表格没有图形,最简单)
每个目录下都会有至少一下三个文件:
module.js 就是一个 controller。跟前面讲过的 controller 写法其实是一致的。在 $scope
对象上,有几个属性是 panel 实现时一般都会有的:
$scope.panelMeta
: 这个前面说到过,其中的 modals 用来定义 panelHeader。$scope.panel
: 用来定义 panel 的属性。一般实现上,会有一个 default 值预定义好。你会发现这个 $scope.panel
其实就是仪表板纲要里面说的每个 panel 的可设置值!然后一般 $scope.init()
都是这样的:
$scope.init = function () {
$scope.ready = false;
$scope.$on('refresh', function () {
$scope.get_data();
});
$scope.get_data();
};
也就是每次有刷新操作,就执行 get_data()
方法。这个方法就是获取 ES 数据,然后渲染效果的入口。
$scope.get_data = function () {
if(dashboard.indices.length === 0) {
return;
}
$scope.panelMeta.loading = true;
var request,
results,
boolQuery,
queries;
request = $scope.ejs.Request();
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
queries = querySrv.getQueryObjs($scope.panel.queries.ids);
boolQuery = $scope.ejs.BoolQuery();
_.each(queries,function(q) {
boolQuery = boolQuery.should(querySrv.toEjsObj(q));
});
request = request
.facet($scope.ejs.StatisticalFacet('stats')
.field($scope.panel.field)
.facetFilter($scope.ejs.QueryFilter(
$scope.ejs.FilteredQuery(
boolQuery,
filterSrv.getBoolFilter(filterSrv.ids())
)))).size(0);
_.each(queries, function (q) {
var alias = q.alias || q.query;
var query = $scope.ejs.BoolQuery();
query.should(querySrv.toEjsObj(q));
request.facet($scope.ejs.StatisticalFacet('stats_'+alias)
.field($scope.panel.field)
.facetFilter($scope.ejs.QueryFilter(
$scope.ejs.FilteredQuery(
query,
filterSrv.getBoolFilter(filterSrv.ids())
)
))
);
});
$scope.inspector = request.toJSON();
results = $scope.ejs.doSearch(dashboard.indices, request);
results.then(function(results) {
$scope.panelMeta.loading = false;
var value = results.facets.stats[$scope.panel.mode];
var rows = queries.map(function (q) {
var alias = q.alias || q.query;
var obj = _.clone(q);
obj.label = alias;
obj.Label = alias.toLowerCase(); //sort field
obj.value = results.facets['stats_'+alias];
obj.Value = results.facets['stats_'+alias]; //sort field
return obj;
});
$scope.data = {
value: value,
rows: rows
};
$scope.$emit('render');
});
};
stats panel 的这段函数几乎就跟基础示例一样了。
$scope.data
。注:stats/module.js 后面还有一个 filter,terms/module.js 后面还有一个 directive,这些都是为了实际页面效果加的功能,跟 kibana 本身的 filter,directive 本质上是一样的。就不单独讲述了。
module.html 就是 panel 的具体页面内容。没有太多可说的。大概框架是:
<div ng-controller='stats' ng-init="init()">
<table ng-style="panel.style" class="table table-striped table-condensed" ng-show="panel.chart == 'table'">
<thead>
<th>Term</th> <th>\{\{ panel.tmode == 'terms_stats' ? panel.tstat : 'Count' }}</th> <th>Action</th>
</thead>
<tr ng-repeat="term in data" ng-show="showMeta(term)">
<td class="terms-legend-term"></td>
<td></td>
</tr>
</table>
</div>
主要就是绑定要 controller 和 init 函数。对于示例的 stats,里面的 data
就是 module.js 最后生成的 $scope.data
。
editor.html 是 panel 参数的编辑页面主要内容,参数编辑还有一些共同的标签页,是在 kibana 的 app/partials/
里,就不讲了。
editor.html 里,主要就是提供对 $scope.panel
里那些参数的修改保存操作。当然实际上并不是所有参数都暴露出来了。这也是 kibana 3 用户指南里,官方说采用仪表板纲要,比通过页面修改更灵活细腻的原因。
editor.html 里需要注意的是,为了每次变更都能实时生效,所有的输入框都注册到了刷新事件。所以一般是这样子:
<select ng-change="set_refresh(true)" class="input-small" ng-model="panel.format" ng-options="f for f in ['number','float','money','bytes']"></select>
这个 set_refresh
函数是在 module.js
里定义的:
$scope.set_refresh = function (state) {
$scope.refresh = state;
};
kibana 3 源码的主体分析,就是这样了。怎么样,看完以后,大家有没有信心也做些二次开发,甚至跟 grafana 一样,替换掉 esResource,换上一个你自己的后端数据源呢?