diff --git a/.gitignore b/.gitignore index 3fdbcac5734..2cd3623cf83 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ awsconfig /dist /emails/dist /public_gen +/public/vendor/npm /tmp vendor/phantomjs/phantomjs diff --git a/package.json b/package.json index ef79f3367d8..03c5ab52bb4 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ }, "scripts": { "test": "grunt test", - "coveralls": "grunt karma:coveralls && rm -rf ./coverage" + "coveralls": "grunt karma:coveralls && rm -rf ./coverage", + "postinstall": "grunt copy:node_modules" }, "license": "Apache-2.0", "dependencies": { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 7bea0b6338c..0759f1e83b8 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -123,6 +123,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro panels[panel.Id] = map[string]interface{}{ "module": panel.Module, "name": panel.Name, + "id": panel.Id, + "info": panel.Info, } } diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index efcd1f2ec25..9b627e930a0 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -5,7 +5,7 @@ import store from 'app/core/store'; import _ from 'lodash'; import angular from 'angular'; import $ from 'jquery'; -import coreModule from '../core_module'; +import coreModule from 'app/core/core_module'; export class GrafanaCtrl { diff --git a/public/app/core/filters/filters.ts b/public/app/core/filters/filters.ts index 57b2b1bd9ba..6122a010182 100644 --- a/public/app/core/filters/filters.ts +++ b/public/app/core/filters/filters.ts @@ -59,11 +59,14 @@ coreModule.filter('noXml', function() { coreModule.filter('interpolateTemplateVars', function (templateSrv) { var filterFunc: any = function(text, scope) { - if (scope.panel) { - return templateSrv.replaceWithText(text, scope.panel.scopedVars); + var scopedVars; + if (scope.ctrl && scope.ctrl.panel) { + scopedVars = scope.ctrl.panel.scopedVars; } else { - return templateSrv.replaceWithText(text, scope.row.scopedVars); + scopedVars = scope.row.scopedVars; } + + return templateSrv.replaceWithText(text, scopedVars); }; filterFunc.$stateful = true; diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index 3b178c4de26..e8abb5bbb05 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/public/app/features/dashboard/rowCtrl.js @@ -116,36 +116,7 @@ function (angular, _, config) { $scope.$broadcast('render'); }; - $scope.removePanel = function(panel) { - $scope.appEvent('confirm-modal', { - title: 'Are you sure you want to remove this panel?', - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: function() { - $scope.row.panels = _.without($scope.row.panels, panel); - } - }); - }; - - $scope.updatePanelSpan = function(panel, span) { - panel.span = Math.min(Math.max(Math.floor(panel.span + span), 1), 12); - }; - - $scope.replacePanel = function(newPanel, oldPanel) { - var row = $scope.row; - var index = _.indexOf(row.panels, oldPanel); - row.panels.splice(index, 1); - - // adding it back needs to be done in next digest - $timeout(function() { - newPanel.id = oldPanel.id; - newPanel.span = oldPanel.span; - row.panels.splice(index, 0, newPanel); - }); - }; - $scope.init(); - }); module.directive('rowHeight', function() { diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 336600975dc..0ba27ec6f5d 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -26,7 +26,7 @@ function (angular, _, require, config) { $scope.modalTitle = 'Share Dashboard'; } - if (!$scope.dashboardMeta.isSnapshot) { + if (!$scope.dashboard.meta.isSnapshot) { $scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'}); } diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 58403766a31..abe1ec3279e 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -17,6 +17,7 @@ function (angular, _, $) { self.state = {}; self.panelScopes = []; self.$scope = $scope; + self.dashboard = $scope.dashboard; $scope.exitFullscreen = function() { if (self.state.fullscreen) { @@ -31,6 +32,14 @@ function (angular, _, $) { } }); + $scope.onAppEvent('panel-change-view', function(evt, payload) { + self.update(payload); + }); + + $scope.onAppEvent('panel-instantiated', function(evt, payload) { + self.registerPanel(payload.scope); + }); + this.update(this.getQueryStringState(), true); this.expandRowForPanel(); } @@ -66,7 +75,7 @@ function (angular, _, $) { DashboardViewState.prototype.update = function(state, skipUrlSync) { _.extend(this.state, state); - this.fullscreen = this.state.fullscreen; + this.dashboard.meta.fullscreen = this.state.fullscreen; if (!this.state.fullscreen) { this.state.panelId = null; @@ -84,7 +93,7 @@ function (angular, _, $) { DashboardViewState.prototype.syncState = function() { if (this.panelScopes.length === 0) { return; } - if (this.fullscreen) { + if (this.dashboard.meta.fullscreen) { if (this.fullscreenPanel) { this.leaveFullscreen(false); } @@ -105,23 +114,24 @@ function (angular, _, $) { DashboardViewState.prototype.getPanelScope = function(id) { return _.find(this.panelScopes, function(panelScope) { - return panelScope.panel.id === id; + return panelScope.ctrl.panel.id === id; }); }; DashboardViewState.prototype.leaveFullscreen = function(render) { var self = this; + var ctrl = self.fullscreenPanel.ctrl; - self.fullscreenPanel.editMode = false; - self.fullscreenPanel.fullscreen = false; - delete self.fullscreenPanel.height; + ctrl.editMode = false; + ctrl.fullscreen = false; + delete ctrl.height; - this.$scope.appEvent('panel-fullscreen-exit', {panelId: this.fullscreenPanel.panel.id}); + this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id}); if (!render) { return false;} $timeout(function() { - if (self.oldTimeRange !== self.fullscreenPanel.range) { + if (self.oldTimeRange !== ctrl.range) { self.$scope.broadcastRefresh(); } else { @@ -135,17 +145,18 @@ function (angular, _, $) { var docHeight = $(window).height(); var editHeight = Math.floor(docHeight * 0.3); var fullscreenHeight = Math.floor(docHeight * 0.7); + var ctrl = panelScope.ctrl; - panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit; - panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight; + ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit; + ctrl.height = ctrl.editMode ? editHeight : fullscreenHeight; + ctrl.fullscreen = true; - this.oldTimeRange = panelScope.range; + this.oldTimeRange = ctrl.range; this.fullscreenPanel = panelScope; $(window).scrollTop(0); - panelScope.fullscreen = true; - this.$scope.appEvent('panel-fullscreen-enter', {panelId: panelScope.panel.id}); + this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id}); $timeout(function() { panelScope.$broadcast('render'); @@ -156,8 +167,12 @@ function (angular, _, $) { var self = this; self.panelScopes.push(panelScope); - if (self.state.panelId === panelScope.panel.id) { - self.enterFullscreen(panelScope); + if (self.state.panelId === panelScope.ctrl.panel.id) { + if (self.state.edit) { + panelScope.ctrl.editPanel(); + } else { + panelScope.ctrl.viewPanel(); + } } panelScope.$on('$destroy', function() { diff --git a/public/app/features/panel/all.js b/public/app/features/panel/all.js index ef3ea9b9b0f..ad635d83f07 100644 --- a/public/app/features/panel/all.js +++ b/public/app/features/panel/all.js @@ -6,4 +6,5 @@ define([ './solo_panel_ctrl', './panel_loader', './query_editor', + './panel_editor_tab', ], function () {}); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts new file mode 100644 index 00000000000..896babade6d --- /dev/null +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -0,0 +1,234 @@ +/// + +import config from 'app/core/config'; +import $ from 'jquery'; +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import {PanelCtrl} from './panel_ctrl'; + +import * as rangeUtil from '../../core/utils/rangeutil'; +import * as dateMath from '../../core/utils/datemath'; + +class MetricsPanelCtrl extends PanelCtrl { + error: boolean; + loading: boolean; + datasource: any; + $q: any; + $timeout: any; + datasourceSrv: any; + timeSrv: any; + timing: any; + range: any; + rangeRaw: any; + interval: any; + resolution: any; + timeInfo: any; + skipDataOnInit: boolean; + datasources: any[]; + + constructor($scope, $injector) { + super($scope, $injector); + + // make metrics tab the default + this.editorTabIndex = 1; + this.$q = $injector.get('$q'); + this.datasourceSrv = $injector.get('datasourceSrv'); + this.timeSrv = $injector.get('timeSrv'); + + if (!this.panel.targets) { + this.panel.targets = [{}]; + } + + // hookup initial data fetch + this.$timeout(() => { + if (!this.skipDataOnInit) { + this.refresh(); + } + }, 30);; + } + + initEditMode() { + this.addEditorTab('Metrics', 'public/app/partials/metrics.html'); + this.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html'); + this.datasources = this.datasourceSrv.getMetricSources(); + } + + refresh() { + this.getData(); + } + + refreshData(data) { + // null op + return this.$q.when(data); + } + + loadSnapshot(data) { + // null op + return data; + } + + getData() { + // ignore fetching data if another panel is in fullscreen + if (this.otherPanelInFullscreenMode()) { return; } + + // if we have snapshot data use that + if (this.panel.snapshotData) { + if (this.loadSnapshot) { + this.loadSnapshot(this.panel.snapshotData); + } + return; + } + + // clear loading/error state + delete this.error; + this.loading = true; + + // load datasource service + this.datasourceSrv.get(this.panel.datasource).then(datasource => { + this.datasource = datasource; + return this.refreshData(this.datasource); + }).then(() => { + this.loading = false; + }).catch(err => { + console.log('Panel data error:', err); + this.loading = false; + this.error = err.message || "Timeseries data request error"; + this.inspector = {error: err}; + }); + } + + setTimeQueryStart() { + this.timing = {}; + this.timing.queryStart = new Date().getTime(); + } + + setTimeQueryEnd() { + this.timing.queryEnd = new Date().getTime(); + } + + updateTimeRange() { + this.range = this.timeSrv.timeRange(); + this.rangeRaw = this.timeSrv.timeRange(false); + + this.applyPanelTimeOverrides(); + + if (this.panel.maxDataPoints) { + this.resolution = this.panel.maxDataPoints; + } else { + this.resolution = Math.ceil($(window).width() * (this.panel.span / 12)); + } + + var panelInterval = this.panel.interval; + var datasourceInterval = (this.datasource || {}).interval; + this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval); + }; + + applyPanelTimeOverrides() { + this.timeInfo = ''; + + // check panel time overrrides + if (this.panel.timeFrom) { + var timeFromInfo = rangeUtil.describeTextRange(this.panel.timeFrom); + if (timeFromInfo.invalid) { + this.timeInfo = 'invalid time override'; + return; + } + + if (_.isString(this.rangeRaw.from)) { + var timeFromDate = dateMath.parse(timeFromInfo.from); + this.timeInfo = timeFromInfo.display; + this.rangeRaw.from = timeFromInfo.from; + this.rangeRaw.to = timeFromInfo.to; + this.range.from = timeFromDate; + } + } + + if (this.panel.timeShift) { + var timeShiftInfo = rangeUtil.describeTextRange(this.panel.timeShift); + if (timeShiftInfo.invalid) { + this.timeInfo = 'invalid timeshift'; + return; + } + + var timeShift = '-' + this.panel.timeShift; + this.timeInfo += ' timeshift ' + timeShift; + this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false); + this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true); + + this.rangeRaw = this.range; + } + + if (this.panel.hideTimeOverride) { + this.timeInfo = ''; + } + }; + + issueQueries(datasource) { + if (!this.panel.targets || this.panel.targets.length === 0) { + return this.$q.when([]); + } + + this.updateTimeRange(); + + var metricsQuery = { + range: this.range, + rangeRaw: this.rangeRaw, + interval: this.interval, + targets: this.panel.targets, + format: this.panel.renderer === 'png' ? 'png' : 'json', + maxDataPoints: this.resolution, + scopedVars: this.panel.scopedVars, + cacheTimeout: this.panel.cacheTimeout + }; + + this.setTimeQueryStart(); + return datasource.query(metricsQuery).then(results => { + this.setTimeQueryEnd(); + + if (this.dashboard.snapshot) { + this.panel.snapshotData = results; + } + + return results; + }); + } + + addDataQuery(datasource) { + this.dashboard.addDataQueryTo(this.panel, datasource); + } + + removeDataQuery(query) { + this.dashboard.removeDataQuery(this.panel, query); + this.refresh(); + }; + + duplicateDataQuery(query) { + this.dashboard.duplicateDataQuery(this.panel, query); + } + + moveDataQuery(fromIndex, toIndex) { + this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex); + } + + setDatasource(datasource) { + // switching to mixed + if (datasource.meta.mixed) { + _.each(this.panel.targets, target => { + target.datasource = this.panel.datasource; + if (target.datasource === null) { + target.datasource = config.defaultDatasource; + } + }); + } else if (this.datasource && this.datasource.meta.mixed) { + _.each(this.panel.targets, target => { + delete target.datasource; + }); + } + + this.panel.datasource = datasource.value; + this.datasource = null; + this.refresh(); + } +} + +export {MetricsPanelCtrl}; diff --git a/public/app/features/panel/panel.ts b/public/app/features/panel/panel.ts new file mode 100644 index 00000000000..7312e18ccc7 --- /dev/null +++ b/public/app/features/panel/panel.ts @@ -0,0 +1,50 @@ +/// + +import config from 'app/core/config'; + +import {PanelCtrl} from './panel_ctrl'; +import {MetricsPanelCtrl} from './metrics_panel_ctrl'; + +export class DefaultPanelCtrl extends PanelCtrl { + constructor($scope, $injector) { + super($scope, $injector); + } +} + +class PanelDirective { + template: string; + templateUrl: string; + bindToController: boolean; + scope: any; + controller: any; + controllerAs: string; + + getDirective() { + if (!this.controller) { + this.controller = DefaultPanelCtrl; + } + + return { + template: this.template, + templateUrl: this.templateUrl, + controller: this.controller, + controllerAs: 'ctrl', + bindToController: true, + scope: {dashboard: "=", panel: "=", row: "="}, + link: (scope, elem, attrs, ctrl) => { + ctrl.init(); + this.link(scope, elem, attrs, ctrl); + } + }; + } + + link(scope, elem, attrs, ctrl) { + return null; + } +} + +export { + PanelCtrl, + MetricsPanelCtrl, + PanelDirective, +} diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts new file mode 100644 index 00000000000..1b4749fd1cf --- /dev/null +++ b/public/app/features/panel/panel_ctrl.ts @@ -0,0 +1,179 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; + +export class PanelCtrl { + panel: any; + row: any; + dashboard: any; + editorTabIndex: number; + pluginName: string; + pluginId: string; + icon: string; + editorTabs: any; + $scope: any; + $injector: any; + $timeout: any; + fullscreen: boolean; + inspector: any; + editModeInitiated: boolean; + editorHelpIndex: number; + + constructor($scope, $injector) { + this.$injector = $injector; + this.$scope = $scope; + this.$timeout = $injector.get('$timeout'); + this.editorTabIndex = 0; + + var plugin = config.panels[this.panel.type]; + if (plugin) { + this.pluginId = plugin.id; + this.pluginName = plugin.name; + } + + $scope.$on("refresh", () => this.refresh()); + } + + init() { + this.publishAppEvent('panel-instantiated', {scope: this.$scope}); + this.refresh(); + } + + renderingCompleted() { + this.$scope.$root.performance.panelsRendered++; + } + + refresh() { + return; + } + + publishAppEvent(evtName, evt) { + this.$scope.$root.appEvent(evtName, evt); + } + + changeView(fullscreen, edit) { + this.publishAppEvent('panel-change-view', { + fullscreen: fullscreen, edit: edit, panelId: this.panel.id + }); + } + + viewPanel() { + this.changeView(true, false); + } + + editPanel() { + if (!this.editModeInitiated) { + this.editorTabs = []; + this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + this.initEditMode(); + } + + this.changeView(true, true); + } + + exitFullscreen() { + this.changeView(false, false); + } + + initEditMode() { + return; + } + + addEditorTab(title, directiveFn, index?) { + var editorTab = {title, directiveFn}; + + if (_.isString(directiveFn)) { + editorTab.directiveFn = function() { + return {templateUrl: directiveFn}; + }; + } + if (index) { + this.editorTabs.splice(index, 0, editorTab); + } else { + this.editorTabs.push(editorTab); + } + } + + getMenu() { + let menu = []; + menu.push({text: 'View', click: 'ctrl.viewPanel(); dismiss();'}); + menu.push({text: 'Edit', click: 'ctrl.editPanel(); dismiss();', role: 'Editor'}); + menu.push({text: 'Duplicate', click: 'ctrl.duplicate()', role: 'Editor' }); + menu.push({text: 'Share', click: 'ctrl.sharePanel(); dismiss();'}); + return menu; + } + + getExtendedMenu() { + return [{text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();'}]; + } + + otherPanelInFullscreenMode() { + return this.dashboard.meta.fullscreen && !this.fullscreen; + } + + broadcastRender(arg1?, arg2?) { + this.$scope.$broadcast('render', arg1, arg2); + } + + toggleEditorHelp(index) { + if (this.editorHelpIndex === index) { + this.editorHelpIndex = null; + return; + } + this.editorHelpIndex = index; + } + + duplicate() { + this.dashboard.duplicatePanel(this.panel, this.row); + } + + updateColumnSpan(span) { + this.panel.span = Math.min(Math.max(Math.floor(this.panel.span + span), 1), 12); + this.$timeout(() => { + this.broadcastRender(); + }); + } + + removePanel() { + this.publishAppEvent('confirm-modal', { + title: 'Are you sure you want to remove this panel?', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.row.panels = _.without(this.row.panels, this.panel); + } + }); + } + + editPanelJson() { + this.publishAppEvent('show-json-editor', { + object: this.panel, + updateHandler: this.replacePanel.bind(this) + }); + } + + replacePanel(newPanel, oldPanel) { + var row = this.row; + var index = _.indexOf(this.row.panels, oldPanel); + this.row.panels.splice(index, 1); + + // adding it back needs to be done in next digest + this.$timeout(() => { + newPanel.id = oldPanel.id; + newPanel.span = oldPanel.span; + this.row.panels.splice(index, 0, newPanel); + }); + } + + sharePanel() { + var shareScope = this.$scope.$new(); + shareScope.panel = this.panel; + shareScope.dashboard = this.dashboard; + + this.publishAppEvent('show-modal', { + src: './app/features/dashboard/partials/shareModal.html', + scope: shareScope + }); + } +} diff --git a/public/app/features/panel/panel_directive.js b/public/app/features/panel/panel_directive.js index b4c1a2d6a0f..ffe978a55ad 100644 --- a/public/app/features/panel/panel_directive.js +++ b/public/app/features/panel/panel_directive.js @@ -12,12 +12,13 @@ function (angular, $) { restrict: 'E', templateUrl: 'app/features/panel/partials/panel.html', transclude: true, + scope: { ctrl: "=" }, link: function(scope, elem) { var panelContainer = elem.find('.panel-container'); - - scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() { - panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' }); - elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false); + var ctrl = scope.ctrl; + scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() { + panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' }); + elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false); }); } }; @@ -30,6 +31,7 @@ function (angular, $) { link: function(scope, elem) { var resizing = false; var lastPanel = false; + var ctrl = scope.ctrl; var handleOffset; var originalHeight; var originalWidth; @@ -40,31 +42,31 @@ function (angular, $) { resizing = true; handleOffset = $(e.target).offset(); - originalHeight = parseInt(scope.row.height); - originalWidth = scope.panel.span; + originalHeight = parseInt(ctrl.row.height); + originalWidth = ctrl.panel.span; maxWidth = $(document).width(); - lastPanel = scope.row.panels[scope.row.panels.length - 1]; + lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1]; $('body').on('mousemove', moveHandler); $('body').on('mouseup', dragEndHandler); } function moveHandler(e) { - scope.row.height = originalHeight + (e.pageY - handleOffset.top); - scope.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12); - scope.panel.span = Math.min(Math.max(scope.panel.span, 1), 12); + ctrl.row.height = originalHeight + (e.pageY - handleOffset.top); + ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12); + ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12); - var rowSpan = scope.dashboard.rowSpan(scope.row); + var rowSpan = ctrl.dashboard.rowSpan(ctrl.row); // auto adjust other panels if (Math.floor(rowSpan) < 14) { // last panel should not push row down - if (lastPanel === scope.panel && rowSpan > 12) { + if (lastPanel === ctrl.panel && rowSpan > 12) { lastPanel.span -= rowSpan - 12; } // reduce width of last panel so total in row is 12 - else if (lastPanel !== scope.panel) { + else if (lastPanel !== ctrl.panel) { lastPanel.span = lastPanel.span - (rowSpan - 12); lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12); } @@ -77,7 +79,7 @@ function (angular, $) { function dragEndHandler() { // if close to 12 - var rowSpan = scope.dashboard.rowSpan(scope.row); + var rowSpan = ctrl.dashboard.rowSpan(ctrl.row); if (rowSpan < 12 && rowSpan > 11) { lastPanel.span += 12 - rowSpan; } diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts new file mode 100644 index 00000000000..f6c11306678 --- /dev/null +++ b/public/app/features/panel/panel_editor_tab.ts @@ -0,0 +1,28 @@ +/// + +import angular from 'angular'; +import config from 'app/core/config'; + +var directiveModule = angular.module('grafana.directives'); + +/** @ngInject */ +function panelEditorTab(dynamicDirectiveSrv) { + return dynamicDirectiveSrv.create({ + scope: { + ctrl: "=", + editorTab: "=", + index: "=", + }, + directive: scope => { + var pluginId = scope.ctrl.pluginId; + var tabIndex = scope.index; + + return Promise.resolve({ + name: `panel-editor-tab-${pluginId}${tabIndex}`, + fn: scope.editorTab.directiveFn, + }); + } + }); +} + +directiveModule.directive('panelEditorTab', panelEditorTab); diff --git a/public/app/features/panel/panel_loader.ts b/public/app/features/panel/panel_loader.ts index 8dc46f494a1..73b154e8840 100644 --- a/public/app/features/panel/panel_loader.ts +++ b/public/app/features/panel/panel_loader.ts @@ -3,28 +3,82 @@ import angular from 'angular'; import config from 'app/core/config'; -import {unknownPanelDirective} from '../../plugins/panel/unknown/module'; +import {UnknownPanel} from '../../plugins/panel/unknown/module'; + +var directiveModule = angular.module('grafana.directives'); /** @ngInject */ -function panelLoader($parse, dynamicDirectiveSrv) { - return dynamicDirectiveSrv.create({ - directive: scope => { - let panelInfo = config.panels[scope.panel.type]; - if (!panelInfo) { - return Promise.resolve({ - name: 'panel-directive-' + scope.panel.type, - fn: unknownPanelDirective +function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector) { + return { + restrict: 'E', + scope: { + dashboard: "=", + row: "=", + panel: "=" + }, + link: function(scope, elem, attrs) { + + function getTemplate(directive) { + if (directive.template) { + return $q.when(directive.template); + } + return $http.get(directive.templateUrl).then(res => { + return res.data; }); } - return System.import(panelInfo.module).then(function(panelModule) { - return { - name: 'panel-directive-' + scope.panel.type, - fn: panelModule.panel, - }; + function addPanelAndCompile(name) { + var child = angular.element(document.createElement(name)); + child.attr('dashboard', 'dashboard'); + child.attr('panel', 'panel'); + child.attr('row', 'row'); + $compile(child)(scope); + + elem.empty(); + elem.append(child); + } + + function addPanel(name, Panel) { + if (Panel.registered) { + addPanelAndCompile(name); + return; + } + + if (Panel.promise) { + Panel.promise.then(() => { + addPanelAndCompile(name); + }); + return; + } + + var panelInstance = $injector.instantiate(Panel); + var directive = panelInstance.getDirective(); + + Panel.promise = getTemplate(directive).then(template => { + directive.templateUrl = null; + directive.template = `${template}`; + directiveModule.directive(attrs.$normalize(name), function() { + return directive; + }); + Panel.registered = true; + addPanelAndCompile(name); + }); + } + + var panelElemName = 'panel-directive-' + scope.panel.type; + let panelInfo = config.panels[scope.panel.type]; + if (!panelInfo) { + addPanel(panelElemName, UnknownPanel); + return; + } + + System.import(panelInfo.module).then(function(panelModule) { + addPanel(panelElemName, panelModule.Panel); + }).catch(err => { + console.log('Panel err: ', err); }); - }, - }); + } + }; } -angular.module('grafana.directives').directive('panelLoader', panelLoader); +directiveModule.directive('panelLoader', panelLoader); diff --git a/public/app/features/panel/panel_menu.js b/public/app/features/panel/panel_menu.js index 9af81943df3..c0cf8e77f02 100644 --- a/public/app/features/panel/panel_menu.js +++ b/public/app/features/panel/panel_menu.js @@ -11,32 +11,32 @@ function (angular, $, _) { .directive('panelMenu', function($compile, linkSrv) { var linkTemplate = '' + - '{{panel.title | interpolateTemplateVars:this}}' + - '' + - ' {{panelMeta.timeInfo}}' + + '{{ctrl.panel.title | interpolateTemplateVars:this}}' + + '' + + ' {{ctrl.timeInfo}}' + ''; - function createExternalLinkMenu($scope) { + function createExternalLinkMenu(ctrl) { var template = '
'; template += '
'; - if ($scope.panel.links) { - _.each($scope.panel.links, function(link) { - var info = linkSrv.getPanelLinkAnchorInfo(link, $scope.panel.scopedVars); + if (ctrl.panel.links) { + _.each(ctrl.panel.links, function(link) { + var info = linkSrv.getPanelLinkAnchorInfo(link, ctrl.panel.scopedVars); template += '' + info.title + ''; }); } return template; } - function createMenuTemplate($scope) { + function createMenuTemplate(ctrl) { var template = '
'; - if ($scope.dashboardMeta.canEdit) { + if (ctrl.dashboard.meta.canEdit) { template += '
'; template += '
'; - template += ''; - template += ''; - template += ''; + template += ''; + template += ''; + template += ''; template += '
'; template += '
'; } @@ -44,9 +44,9 @@ function (angular, $, _) { template += '
'; template += ''; - _.each($scope.panelMeta.menu, function(item) { + _.each(ctrl.getMenu(), function(item) { // skip edit actions if not editor - if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) { + if (item.role === 'Editor' && !ctrl.dashboard.meta.canEdit) { return; } @@ -63,8 +63,8 @@ function (angular, $, _) { return template; } - function getExtendedMenu($scope) { - return angular.copy($scope.panelMeta.extendedMenu); + function getExtendedMenu(ctrl) { + return ctrl.getExtendedMenu(); } return { @@ -74,13 +74,14 @@ function (angular, $, _) { var $panelLinksBtn = $link.find(".panel-links-btn"); var $panelContainer = elem.parents(".panel-container"); var menuScope = null; + var ctrl = $scope.ctrl; var timeout = null; var $menu = null; elem.append($link); - $scope.$watchCollection('panel.links', function(newValue) { - var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== ''; + $scope.$watchCollection('ctrl.panel.links', function(newValue) { + var showIcon = (newValue ? newValue.length > 0 : false) && ctrl.panel.title !== ''; $panelLinksBtn.toggle(showIcon); }); @@ -95,7 +96,7 @@ function (angular, $, _) { // if hovering or draging pospone close if (force !== true) { - if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) { + if ($menu.is(':hover') || $scope.ctrl.dashboard.$$panelDragging) { dismiss(2200); return; } @@ -126,7 +127,7 @@ function (angular, $, _) { if ($(e.target).hasClass('fa-link')) { menuTemplate = createExternalLinkMenu($scope); } else { - menuTemplate = createMenuTemplate($scope); + menuTemplate = createMenuTemplate(ctrl); } $menu = $(menuTemplate); @@ -135,7 +136,7 @@ function (angular, $, _) { }); menuScope = $scope.$new(); - menuScope.extendedMenu = getExtendedMenu($scope); + menuScope.extendedMenu = getExtendedMenu(ctrl); menuScope.dismiss = function() { dismiss(null, true); }; diff --git a/public/app/features/panel/panel_meta.js b/public/app/features/panel/panel_meta.js deleted file mode 100644 index c110d948e08..00000000000 --- a/public/app/features/panel/panel_meta.js +++ /dev/null @@ -1,7 +0,0 @@ -define([ - './panel_meta2', -], -function (panelMeta) { - 'use strict'; - return panelMeta.default; -}); diff --git a/public/app/features/panel/panel_meta2.ts b/public/app/features/panel/panel_meta2.ts deleted file mode 100644 index ca276f9377b..00000000000 --- a/public/app/features/panel/panel_meta2.ts +++ /dev/null @@ -1,48 +0,0 @@ -export default class PanelMeta { - description: any; - fullscreen: any; - editIcon: any; - panelName: any; - menu: any; - editorTabs: any; - extendedMenu: any; - - constructor(options: any) { - this.description = options.description; - this.fullscreen = options.fullscreen; - this.editIcon = options.editIcon; - this.panelName = options.panelName; - this.menu = []; - this.editorTabs = []; - this.extendedMenu = []; - - if (options.fullscreen) { - this.addMenuItem('View', 'icon-eye-open', 'toggleFullscreen(false); dismiss();'); - } - - this.addMenuItem('Edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor'); - this.addMenuItem('Duplicate', 'icon-copy', 'duplicatePanel()', 'Editor'); - this.addMenuItem('Share', 'icon-share', 'sharePanel(); dismiss();'); - - this.addEditorTab('General', 'app/partials/panelgeneral.html'); - - if (options.metricsEditor) { - this.addEditorTab('Metrics', 'app/partials/metrics.html'); - } - - this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();'); - } - - addMenuItem (text, icon, click, role?) { - this.menu.push({text: text, icon: icon, click: click, role: role}); - } - - addExtendedMenuItem (text, icon, click, role?) { - this.extendedMenu.push({text: text, icon: icon, click: click, role: role}); - } - - addEditorTab (title, src) { - this.editorTabs.push({title: title, src: src}); - } -} - diff --git a/public/app/features/panel/partials/panel.html b/public/app/features/panel/partials/panel.html index a7fef0f5d67..13c2178e0b4 100644 --- a/public/app/features/panel/partials/panel.html +++ b/public/app/features/panel/partials/panel.html @@ -1,12 +1,12 @@ -
+
- - + + - + @@ -19,27 +19,27 @@
-
+
- - {{panelMeta.panelName}} + + {{ctrl.name}}
-
-
+
+
-
-
-
+
+
diff --git a/public/app/features/panel/partials/panelTime.html b/public/app/features/panel/partials/panelTime.html index 04954108aca..4e7c7c7259c 100644 --- a/public/app/features/panel/partials/panelTime.html +++ b/public/app/features/panel/partials/panelTime.html @@ -13,8 +13,8 @@
  • + empty-to-null ng-model="ctrl.panel.timeFrom" valid-time-span + ng-change="ctrl.refresh()" ng-model-onblur>
  • @@ -32,8 +32,8 @@
  • + empty-to-null ng-model="ctrl.panel.timeShift" valid-time-span + ng-change="ctrl.refresh()" ng-model-onblur>
  • @@ -47,9 +47,9 @@ Hide time override info
  • - - + +
  • diff --git a/public/app/features/panel/partials/soloPanel.html b/public/app/features/panel/partials/soloPanel.html index b040bf8dc72..d2a4f38844f 100644 --- a/public/app/features/panel/partials/soloPanel.html +++ b/public/app/features/panel/partials/soloPanel.html @@ -2,7 +2,8 @@
    - + +
    diff --git a/public/app/features/panel/query_editor.ts b/public/app/features/panel/query_editor.ts index 01beceaee99..7a5c6126740 100644 --- a/public/app/features/panel/query_editor.ts +++ b/public/app/features/panel/query_editor.ts @@ -5,9 +5,9 @@ import angular from 'angular'; /** @ngInject */ function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) { return dynamicDirectiveSrv.create({ - watchPath: "panel.datasource", + watchPath: "ctrl.panel.datasource", directive: scope => { - let datasource = scope.target.datasource || scope.panel.datasource; + let datasource = scope.target.datasource || scope.ctrl.panel.datasource; return datasourceSrv.get(datasource).then(ds => { scope.datasource = ds; @@ -29,9 +29,9 @@ function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) { /** @ngInject */ function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) { return dynamicDirectiveSrv.create({ - watchPath: "panel.datasource", + watchPath: "ctrl.panel.datasource", directive: scope => { - return datasourceSrv.get(scope.panel.datasource).then(ds => { + return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => { return System.import(ds.meta.module).then(dsModule => { return { name: 'metrics-query-options-' + ds.meta.id, diff --git a/public/app/features/panel/solo_panel_ctrl.js b/public/app/features/panel/solo_panel_ctrl.js index c51f9c61a9d..313c2014de2 100644 --- a/public/app/features/panel/solo_panel_ctrl.js +++ b/public/app/features/panel/solo_panel_ctrl.js @@ -39,13 +39,8 @@ function (angular, $) { } $scope.panel.span = 12; - $scope.dashboardViewState = {registerPanel: function() { }, state: {}}; }; - if (!$scope.skipAutoInit) { - $scope.init(); - } - + $scope.init(); }); - }); diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index f88ed4fcee0..b66ba845827 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -5,7 +5,7 @@ declare var System: any; // dummy modules declare module 'app/core/config' { - var config : any; + var config: any; export default config; } @@ -35,7 +35,7 @@ declare module 'app/core/utils/kbn' { } declare module 'app/core/store' { - var store : any; + var store: any; export default store; } diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index dd57c2f8e9b..3e60b2542e9 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -23,7 +23,7 @@
    -