diff --git a/conf/defaults.ini b/conf/defaults.ini index eb8debc0094..750f06f2f6a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -554,3 +554,6 @@ container_name = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer server_url = callback_url = + +[panels] +enable_alpha = false diff --git a/pkg/api/api.go b/pkg/api/api.go index c2739a66d6c..f1fe940e416 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() { pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting)) }, reqOrgAdmin) - apiRoute.Get("/frontend/settings/", GetFrontendSettings) + apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings) apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest) apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 43fa0c858fc..1de65c7b2ce 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { +func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { orgDataSources := make([]*m.DataSource, 0) if c.OrgId != 0 { @@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { panels := map[string]interface{}{} for _, panel := range enabledPlugins.Panels { + if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels { + continue + } + panels[panel.Id] = map[string]interface{}{ "module": panel.Module, "baseUrl": panel.BaseUrl, @@ -196,8 +200,8 @@ func getPanelSort(id string) int { return sort } -func GetFrontendSettings(c *m.ReqContext) { - settings, err := getFrontendSettingsMap(c) +func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) { + settings, err := hs.getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) return diff --git a/pkg/api/index.go b/pkg/api/index.go index 9f867d51cad..e61620f9586 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -18,7 +18,7 @@ const ( ) func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { - settings, err := getFrontendSettingsMap(c) + settings, err := hs.getFrontendSettingsMap(c) if err != nil { return nil, err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 58901e55c6b..16158ded002 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -213,6 +213,8 @@ type Cfg struct { TempDataLifetime time.Duration MetricsEndpointEnabled bool + + EnableAlphaPanels bool } type CommandLineArgs struct { @@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(false) + panels := iniFile.Section("panels") + cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false) + cfg.readSessionConfig() cfg.readSmtpSettings() cfg.readQuotaSettings() diff --git a/public/app/app.ts b/public/app/app.ts index 298bf5609cd..9647fbe5416 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => { return array; }; -import { coreModule, registerAngularDirectives } from './core/core'; -import { setupAngularRoutes } from './routes/routes'; +import { coreModule, angularModules } from 'app/core/core_module'; +import { registerAngularDirectives } from 'app/core/core'; +import { setupAngularRoutes } from 'app/routes/routes'; + +import 'app/routes/GrafanaCtrl'; +import 'app/features/all'; // import symlinked extensions const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/); @@ -109,39 +113,26 @@ export class GrafanaApp { 'react', ]; - const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; - - _.each(moduleTypes, type => { - const moduleName = 'grafana.' + type; - this.useModule(angular.module(moduleName, [])); - }); - // makes it possible to add dynamic stuff - this.useModule(coreModule); + _.each(angularModules, m => { + this.useModule(m); + }); // register react angular wrappers coreModule.config(setupAngularRoutes); registerAngularDirectives(); - const preBootRequires = [import('app/features/all')]; + // disable tool tip animation + $.fn.tooltip.defaults.animation = false; - Promise.all(preBootRequires) - .then(() => { - // disable tool tip animation - $.fn.tooltip.defaults.animation = false; - - // bootstrap the app - angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { - _.extend(module, this.registerFunctions); - }); - - this.preBootModules = null; - }); - }) - .catch(err => { - console.log('Application boot failed:', err); + // bootstrap the app + angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { + _.each(this.preBootModules, module => { + _.extend(module, this.registerFunctions); }); + + this.preBootModules = null; + }); } } diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index bd355817f92..49931ecaac4 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -18,6 +18,7 @@ export function geminiScrollbar() { let scrollRoot = elem.parent(); const scroller = elem; + console.log('scroll'); if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; } diff --git a/public/app/core/config.ts b/public/app/core/config.ts index bf5abe37d7f..1473f8a91f8 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { PanelPlugin } from 'app/types/plugins'; export interface BuildInfo { version: string; @@ -9,7 +10,7 @@ export interface BuildInfo { export class Settings { datasources: any; - panels: any; + panels: PanelPlugin[]; appSubUrl: string; windowTitlePrefix: string; buildInfo: BuildInfo; diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 2642c5e400a..00981156614 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; export const LS_PANEL_COPY_KEY = 'panel-copy'; + +export const DASHBOARD_TOOLBAR_HEIGHT = 55; +export const DASHBOARD_TOP_PADDING = 20; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 173d6b80b15..18a625d3307 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; -import { grafanaAppDirective } from './components/grafana_app'; import { searchDirective } from './components/search/search'; import { infoPopover } from './components/info_popover'; import { navbarDirective } from './components/navbar/navbar'; @@ -60,7 +59,6 @@ export { registerAngularDirectives, arrayJoin, coreModule, - grafanaAppDirective, navbarDirective, searchDirective, liveSrv, diff --git a/public/app/core/core_module.ts b/public/app/core/core_module.ts index f6c30e6cf15..c8401975c18 100644 --- a/public/app/core/core_module.ts +++ b/public/app/core/core_module.ts @@ -1,2 +1,18 @@ import angular from 'angular'; -export default angular.module('grafana.core', ['ngRoute']); + +const coreModule = angular.module('grafana.core', ['ngRoute']); + +// legacy modules +const angularModules = [ + coreModule, + angular.module('grafana.controllers', []), + angular.module('grafana.directives', []), + angular.module('grafana.factories', []), + angular.module('grafana.services', []), + angular.module('grafana.filters', []), + angular.module('grafana.routes', []), +]; + +export { angularModules, coreModule }; + +export default coreModule; diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 224bc2c772d..37124eb7d4b 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -2,16 +2,21 @@ import _ from 'lodash'; import coreModule from '../core_module'; /** @ngInject */ -export function dashClass() { +function dashClass($timeout) { return { link: ($scope, elem) => { - $scope.onAppEvent('panel-fullscreen-enter', () => { - elem.toggleClass('panel-in-fullscreen', true); + $scope.ctrl.dashboard.events.on('view-mode-changed', panel => { + console.log('view-mode-changed', panel.fullscreen); + if (panel.fullscreen) { + elem.addClass('panel-in-fullscreen'); + } else { + $timeout(() => { + elem.removeClass('panel-in-fullscreen'); + }); + } }); - $scope.onAppEvent('panel-fullscreen-exit', () => { - elem.toggleClass('panel-in-fullscreen', false); - }); + elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => { if (newValue) { diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 2089cfe9f59..7c7dffd04b9 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -1,6 +1,7 @@ import { Action } from 'app/core/actions/location'; import { LocationState } from 'app/types'; import { renderUrl } from 'app/core/utils/url'; +import _ from 'lodash'; export const initialState: LocationState = { url: '', @@ -12,11 +13,17 @@ export const initialState: LocationState = { export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { - const { path, query, routeParams } = action.payload; + const { path, routeParams } = action.payload; + let query = action.payload.query || state.query; + + if (action.payload.partial) { + query = _.defaults(query, state.query); + } + return { url: renderUrl(path || state.path, query), path: path || state.path, - query: query || state.query, + query: query, routeParams: routeParams || state.routeParams, }; } diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index ccd86856755..9b7ede59853 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; class DynamicDirectiveSrv { /** @ngInject */ - constructor(private $compile, private $rootScope) {} + constructor(private $compile) {} addDirective(element, name, scope) { const child = angular.element(document.createElement(name)); @@ -14,25 +14,19 @@ class DynamicDirectiveSrv { } link(scope, elem, attrs, options) { - options - .directive(scope) - .then(directiveInfo => { - if (!directiveInfo || !directiveInfo.fn) { - elem.empty(); - return; - } + const directiveInfo = options.directive(scope); + if (!directiveInfo || !directiveInfo.fn) { + elem.empty(); + return; + } - if (!directiveInfo.fn.registered) { - coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); - directiveInfo.fn.registered = true; - } + if (!directiveInfo.fn.registered) { + console.log('register panel tab'); + coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); + directiveInfo.fn.registered = true; + } - this.addDirective(elem, directiveInfo.name, scope); - }) - .catch(err => { - console.log('Plugin load:', err); - this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); - }); + this.addDirective(elem, directiveInfo.name, scope); } create(options) { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index d8dfc958dd4..f43dc96cd37 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -148,7 +148,7 @@ export class KeybindingSrv { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('mod+s', e => { @@ -257,7 +257,7 @@ export class KeybindingSrv { }); this.bind('d r', () => { - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('d s', () => { diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index f75743513f1..5ec4e5e3929 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -22,7 +22,6 @@ import './export_data/export_data_modal'; import './ad_hoc_filters'; import './repeat_option/repeat_option'; import './dashgrid/DashboardGridDirective'; -import './dashgrid/PanelLoader'; import './dashgrid/RowOptions'; import './folder_picker/folder_picker'; import './move_to_folder_modal/move_to_folder'; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index d61eb08d5b9..c34b9ddaff2 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -1,11 +1,10 @@ import config from 'app/core/config'; import coreModule from 'app/core/core_module'; -import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; -export class DashboardCtrl implements PanelContainer { +export class DashboardCtrl { dashboard: DashboardModel; dashboardViewState: any; loadedFallbackDashboard: boolean; @@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer { private dashboardSrv, private unsavedChangesSrv, private dashboardViewStateSrv, - public playlistSrv, - private panelLoader + public playlistSrv ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer { return this.dashboard; } - getPanelLoader() { - return this.panelLoader; - } - - timezoneChanged() { - this.$rootScope.$broadcast('refresh'); - } - getPanelContainer() { return this; } @@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer { this.dashboard.removePanel(panel); } + onDestroy() { + if (this.dashboard) { + this.dashboard.destroy(); + } + } + init(dashboard) { this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); + this.$scope.$on('$destroy', this.onDestroy.bind(this)); this.setupDashboard(dashboard); } } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 818765124bf..65a234a2b94 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -200,6 +200,43 @@ export class DashboardModel { this.events.emit('view-mode-changed', panel); } + timeRangeUpdated() { + this.events.emit('time-range-updated'); + } + + startRefresh() { + this.events.emit('refresh'); + + for (const panel of this.panels) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + } + + render() { + this.events.emit('render'); + + for (const panel of this.panels) { + panel.render(); + } + } + + panelInitialized(panel: PanelModel) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + + otherPanelInFullscreen(panel: PanelModel) { + return this.meta.fullscreen && !panel.fullscreen; + } + + changePanelType(panel: PanelModel, pluginId: string) { + panel.changeType(pluginId); + this.events.emit('panel-type-changed', panel); + } + private ensureListExist(data) { if (!data) { data = {}; diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index a26a0401d56..68cee112f42 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import classNames from 'classnames'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; @@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words'; export interface AddPanelPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export interface AddPanelPanelState { @@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); + const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; const newPanel: any = { @@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component PanelContainer; + dashboard: DashboardModel; } export class DashboardGrid extends React.Component { gridToPanelMap: any; - panelContainer: PanelContainer; - dashboard: DashboardModel; panelMap: { [id: string]: PanelModel }; constructor(props) { super(props); - this.panelContainer = this.props.getPanelContainer(); this.onLayoutChange = this.onLayoutChange.bind(this); this.onResize = this.onResize.bind(this); this.onResizeStop = this.onResizeStop.bind(this); @@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component { this.state = { animated: false }; // subscribe to dashboard events - this.dashboard = this.panelContainer.getDashboard(); - this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); - this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + const dashboard = this.props.dashboard; + dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); + dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); + dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); + dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); + dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this)); } buildLayout() { const layout = []; this.panelMap = {}; - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { const stringId = panel.id.toString(); this.panelMap[stringId] = panel; @@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component { this.panelMap[newPos.i].updateGridPos(newPos); } - this.dashboard.sortPanelsByGridPos(); + this.props.dashboard.sortPanelsByGridPos(); } triggerForceUpdate() { @@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component { } onWidthChange() { - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { panel.resizeDone(); } } + onViewModeChanged(payload) { + this.setState({ animated: !payload.fullscreen }); + } + updateGridPos(item, layout) { this.panelMap[item.i].updateGridPos(item); @@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component { componentDidMount() { setTimeout(() => { - this.setState(() => { - return { animated: true }; - }); + this.setState({ animated: true }); }); } renderPanels() { const panelElements = []; - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( - /** panel-id is set for html bookmarks */ -
- +
+
); } @@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component { PanelContainer; + dashboard: DashboardModel; } -export class DashboardPanel extends React.Component { +export interface State { + pluginExports: PluginExports; +} + +export class DashboardPanel extends React.Component { element: any; - attachedPanel: AttachedPanel; + angularPanel: AngularComponent; + pluginInfo: any; + specialPanels = {}; constructor(props) { super(props); - this.state = {}; + + this.state = { + pluginExports: null, + }; + + this.specialPanels['row'] = this.renderRow.bind(this); + this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); } - componentDidMount() { - if (!this.element) { + isSpecial() { + return this.specialPanels[this.props.panel.type]; + } + + renderRow() { + return ; + } + + renderAddPanel() { + return ; + } + + onPluginTypeChanged = (plugin: PanelPlugin) => { + this.props.panel.changeType(plugin.id); + this.loadPlugin(); + }; + + onAngularPluginTypeChanged = () => { + this.loadPlugin(); + }; + + loadPlugin() { + if (this.isSpecial()) { return; } - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - const loader = panelContainer.getPanelLoader(); - this.attachedPanel = loader.load(this.element, this.props.panel, dashboard); + // handle plugin loading & changing of plugin type + if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) { + this.pluginInfo = config.panels[this.props.panel.type]; + + if (this.pluginInfo.exports) { + this.cleanUpAngularPanel(); + this.setState({ pluginExports: this.pluginInfo.exports }); + } else { + importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.cleanUpAngularPanel(); + // cache plugin exports (saves a promise async cycle next time) + this.pluginInfo.exports = pluginExports; + // update panel state + this.setState({ pluginExports: pluginExports }); + }); + } + } + } + + componentDidMount() { + this.loadPlugin(); + } + + componentDidUpdate() { + this.loadPlugin(); + + // handle angular plugin loading + if (!this.element || this.angularPanel) { + return; + } + + const loader = getAngularLoader(); + const template = ''; + const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard }; + this.angularPanel = loader.load(this.element, scopeProps, template); + } + + cleanUpAngularPanel() { + if (this.angularPanel) { + this.angularPanel.destroy(); + this.angularPanel = null; + } } componentWillUnmount() { - if (this.attachedPanel) { - this.attachedPanel.destroy(); - } + this.cleanUpAngularPanel(); + } + + renderReactPanel() { + const { pluginExports } = this.state; + const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; + const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper'; + + // this might look strange with these classes that change when edit, but + // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel + return ( +
+
+ +
+ {this.props.panel.isEditing && ( +
+ +
+ )} +
+ ); } render() { - // special handling for rows - if (this.props.panel.type === 'row') { - return ; + if (this.isSpecial()) { + return this.specialPanels[this.props.panel.type](); } - if (this.props.panel.type === 'add-panel') { - return ; + if (!this.state.pluginExports) { + return null; } - return ( -
this.element = element} className="panel-height-helper" /> - ); + if (this.state.pluginExports.PanelComponent) { + return this.renderReactPanel(); + } + + // legacy angular rendering + return
(this.element = element)} className="panel-height-helper" />; } } - diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index 378cf4c2c7c..5b8ced9b2b1 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -1,19 +1,16 @@ import React from 'react'; import classNames from 'classnames'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; export interface DashboardRowProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardRow extends React.Component { - dashboard: any; - panelContainer: any; - constructor(props) { super(props); @@ -21,9 +18,6 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - this.panelContainer = this.props.getPanelContainer(); - this.dashboard = this.panelContainer.getDashboard(); - this.toggle = this.toggle.bind(this); this.openSettings = this.openSettings.bind(this); this.delete = this.delete.bind(this); @@ -31,7 +25,7 @@ export class DashboardRow extends React.Component { } toggle() { - this.dashboard.toggleRow(this.props.panel); + this.props.dashboard.toggleRow(this.props.panel); this.setState(prevState => { return { collapsed: !prevState.collapsed }; @@ -39,7 +33,7 @@ export class DashboardRow extends React.Component { } update() { - this.dashboard.processRepeats(); + this.props.dashboard.processRepeats(); this.forceUpdate(); } @@ -61,14 +55,10 @@ export class DashboardRow extends React.Component { altActionText: 'Delete row only', icon: 'fa-trash', onConfirm: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, true); + this.props.dashboard.removeRow(this.props.panel, true); }, onAltAction: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, false); + this.props.dashboard.removeRow(this.props.panel, false); }, }); } @@ -87,7 +77,7 @@ export class DashboardRow extends React.Component { const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars); const count = this.props.panel.panels ? this.props.panel.panels.length : 0; const panels = count === 1 ? 'panel' : 'panels'; - const canEdit = this.dashboard.meta.canEdit === true; + const canEdit = this.props.dashboard.meta.canEdit === true; return (
diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx new file mode 100644 index 00000000000..85821c19742 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -0,0 +1,150 @@ +// Library +import React, { Component } from 'react'; + +// Services +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + +// Types +import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types'; + +interface RenderProps { + loading: LoadingState; + timeSeries: TimeSeries[]; +} + +export interface Props { + datasource: string | null; + queries: any[]; + panelId?: number; + dashboardId?: number; + isVisible?: boolean; + timeRange?: TimeRange; + refreshCounter: number; + children: (r: RenderProps) => JSX.Element; +} + +export interface State { + isFirstLoad: boolean; + loading: LoadingState; + response: DataQueryResponse; +} + +export class DataPanel extends Component { + static defaultProps = { + isVisible: true, + panelId: 1, + dashboardId: 1, + }; + + constructor(props: Props) { + super(props); + + this.state = { + loading: LoadingState.NotStarted, + response: { + data: [], + }, + isFirstLoad: true, + }; + } + + componentDidMount() { + console.log('DataPanel mount'); + } + + async componentDidUpdate(prevProps: Props) { + if (!this.hasPropsChanged(prevProps)) { + return; + } + + this.issueQueries(); + } + + hasPropsChanged(prevProps: Props) { + return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible; + } + + issueQueries = async () => { + const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; + + if (!isVisible) { + return; + } + + if (!queries.length) { + this.setState({ loading: LoadingState.Done }); + return; + } + + this.setState({ loading: LoadingState.Loading }); + + try { + const dataSourceSrv = getDatasourceSrv(); + const ds = await dataSourceSrv.get(datasource); + + const queryOptions: DataQueryOptions = { + timezone: 'browser', + panelId: panelId, + dashboardId: dashboardId, + range: timeRange, + rangeRaw: timeRange.raw, + interval: '1s', + intervalMs: 60000, + targets: queries, + maxDataPoints: 500, + scopedVars: {}, + cacheTimeout: null, + }; + + console.log('Issuing DataPanel query', queryOptions); + const resp = await ds.query(queryOptions); + console.log('Issuing DataPanel query Resp', resp); + + this.setState({ + loading: LoadingState.Done, + response: resp, + }); + } catch (err) { + console.log('Loading error', err); + this.setState({ loading: LoadingState.Error }); + } + }; + + render() { + const { response, loading, isFirstLoad } = this.state; + console.log('data panel render'); + const timeSeries = response.data; + + if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { + return ( +
+

Loading

+
+ ); + } + + return ( + <> + {this.loadingSpinner} + {this.props.children({ + timeSeries, + loading, + })} + + ); + } + + private get loadingSpinner(): JSX.Element { + const { loading } = this.state; + + if (loading === LoadingState.Loading) { + return ( +
+ +
+ ); + } + + return null; + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx new file mode 100644 index 00000000000..82b366d8126 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -0,0 +1,84 @@ +// Libraries +import React, { ComponentClass, PureComponent } from 'react'; + +// Services +import { getTimeSrv } from '../time_srv'; + +// Components +import { PanelHeader } from './PanelHeader'; +import { DataPanel } from './DataPanel'; + +// Types +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { TimeRange, PanelProps } from 'app/types'; + +export interface Props { + panel: PanelModel; + dashboard: DashboardModel; + component: ComponentClass; +} + +export interface State { + refreshCounter: number; + timeRange?: TimeRange; +} + +export class PanelChrome extends PureComponent { + constructor(props) { + super(props); + + this.state = { + refreshCounter: 0, + }; + } + + componentDidMount() { + this.props.panel.events.on('refresh', this.onRefresh); + this.props.dashboard.panelInitialized(this.props.panel); + } + + componentWillUnmount() { + this.props.panel.events.off('refresh', this.onRefresh); + } + + onRefresh = () => { + const timeSrv = getTimeSrv(); + const timeRange = timeSrv.timeRange(); + + this.setState({ + refreshCounter: this.state.refreshCounter + 1, + timeRange: timeRange, + }); + }; + + get isVisible() { + return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); + } + + render() { + const { panel, dashboard } = this.props; + const { datasource, targets } = panel; + const { refreshCounter, timeRange } = this.state; + const PanelComponent = this.props.component; + + return ( +
+ +
+ + {({ loading, timeSeries }) => { + return ; + }} + +
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelContainer.ts b/public/app/features/dashboard/dashgrid/PanelContainer.ts deleted file mode 100644 index 87f3235a176..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelContainer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DashboardModel } from '../dashboard_model'; -import { PanelLoader } from './PanelLoader'; - -export interface PanelContainer { - getPanelLoader(): PanelLoader; - getDashboard(): DashboardModel; -} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx new file mode 100644 index 00000000000..26ac8b7d2c1 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { store } from 'app/store/configureStore'; +import { QueriesTab } from './QueriesTab'; +import { PanelPlugin, PluginExports } from 'app/types/plugins'; +import { VizTypePicker } from './VizTypePicker'; +import { updateLocation } from 'app/core/actions'; + +interface PanelEditorProps { + panel: PanelModel; + dashboard: DashboardModel; + panelType: string; + pluginExports: PluginExports; + onTypeChanged: (newType: PanelPlugin) => void; +} + +interface PanelEditorTab { + id: string; + text: string; + icon: string; +} + +export class PanelEditor extends React.Component { + tabs: PanelEditorTab[]; + + constructor(props) { + super(props); + + this.tabs = [ + { id: 'queries', text: 'Queries', icon: 'fa fa-database' }, + { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' }, + ]; + } + + renderQueriesTab() { + return ; + } + + renderPanelOptions() { + const { pluginExports } = this.props; + + if (pluginExports.PanelOptions) { + const PanelOptions = pluginExports.PanelOptions; + return ; + } else { + return

Visualization has no options

; + } + } + + renderVizTab() { + return ( +
+
+ +
+
+
Options
+ {this.renderPanelOptions()} +
+
+ ); + } + + onChangeTab = (tab: PanelEditorTab) => { + store.dispatch( + updateLocation({ + query: { tab: tab.id }, + partial: true, + }) + ); + }; + + render() { + const { location } = store.getState(); + const activeTab = location.query.tab || 'queries'; + + return ( +
+
+
    + {this.tabs.map(tab => { + return ; + })} +
+ + +
+ +
+ {activeTab === 'queries' && this.renderQueriesTab()} + {activeTab === 'visualization' && this.renderVizTab()} +
+
+ ); + } +} + +interface TabItemParams { + tab: PanelEditorTab; + activeTab: string; + onClick: (tab: PanelEditorTab) => void; +} + +function TabItem({ tab, activeTab, onClick }: TabItemParams) { + const tabClasses = classNames({ + 'gf-tabs-link': true, + active: activeTab === tab.id, + }); + + return ( +
  • + onClick(tab)}> + {tab.text} + +
  • + ); +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx new file mode 100644 index 00000000000..12d5cd37253 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; + +interface PanelHeaderProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeader extends React.Component { + onEditPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panel.id, + edit: true, + fullscreen: true, + }, + }) + ); + }; + + onViewPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panel.id, + edit: false, + fullscreen: true, + }, + }) + ); + }; + + render() { + const isFullscreen = false; + const isLoading = false; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
    + + + + + + {isLoading && ( + + + + )} + +
    + + + {this.props.panel.title} + + + + + + 4m + + +
    +
    + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx new file mode 100644 index 00000000000..f13f212826a --- /dev/null +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -0,0 +1,53 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Services & utils +import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; + +// Types +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class QueriesTab extends PureComponent { + element: any; + component: AngularComponent; + + constructor(props) { + super(props); + } + + componentDidMount() { + if (!this.element) { + return; + } + + const { panel, dashboard } = this.props; + + const loader = getAngularLoader(); + const template = ''; + const scopeProps = { + ctrl: { + panel: panel, + dashboard: dashboard, + refresh: () => panel.refresh(), + }, + }; + + this.component = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.component) { + this.component.destroy(); + } + } + + render() { + return
    (this.element = element)} className="panel-height-helper" />; + } +} diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx new file mode 100644 index 00000000000..9402133df34 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -0,0 +1,69 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import config from 'app/core/config'; +import { PanelPlugin } from 'app/types/plugins'; +import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; +import _ from 'lodash'; + +interface Props { + currentType: string; + onTypeChanged: (newType: PanelPlugin) => void; +} + +interface State { + pluginList: PanelPlugin[]; +} + +export class VizTypePicker extends PureComponent { + constructor(props) { + super(props); + + this.state = { + pluginList: this.getPanelPlugins(''), + }; + } + + getPanelPlugins(filter) { + const panels = _.chain(config.panels) + .filter({ hideFromList: false }) + .map(item => item) + .value(); + + // add sort by sort property + return _.sortBy(panels, 'sort'); + } + + renderVizPlugin = (plugin, index) => { + const cssClass = classNames({ + 'viz-picker__item': true, + 'viz-picker__item--selected': plugin.id === this.props.currentType, + }); + + return ( +
    this.props.onTypeChanged(plugin)} title={plugin.name}> + +
    {plugin.name}
    +
    + ); + }; + + render() { + return ( +
    +
    +
    + +
    +
    +
    + +
    {this.state.pluginList.map(this.renderVizPlugin)}
    +
    +
    +
    + ); + } +} diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index c4095e7948b..7312d6db784 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -42,6 +42,8 @@ export class DashNavCtrl { } else if (search.fullscreen) { delete search.fullscreen; delete search.edit; + delete search.tab; + delete search.panelId; } this.$location.search(search); } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 9a1e7fb9200..ebf8a6bb224 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = { events: true, fullscreen: true, isEditing: true, + hasRefreshed: true, +}; + +const defaults: any = { + gridPos: { x: 0, y: 0, h: 3, w: 6 }, + datasource: null, + targets: [{}], }; export class PanelModel { @@ -31,10 +38,14 @@ export class PanelModel { collapsed?: boolean; panels?: any; soloMode?: boolean; + targets: any[]; + datasource: string; + thresholds?: any; // non persisted fullscreen: boolean; isEditing: boolean; + hasRefreshed: boolean; events: Emitter; constructor(model) { @@ -45,9 +56,8 @@ export class PanelModel { this[property] = model[property]; } - if (!this.gridPos) { - this.gridPos = { x: 0, y: 0, h: 3, w: 6 }; - } + // defaults + _.defaultsDeep(this, _.cloneDeep(defaults)); } getSaveModel() { @@ -57,6 +67,10 @@ export class PanelModel { continue; } + if (_.isEqual(this[property], defaults[property])) { + continue; + } + model[property] = _.cloneDeep(this[property]); } @@ -82,7 +96,6 @@ export class PanelModel { this.gridPos.h = newPos.h; if (sizeChanged) { - console.log('PanelModel sizeChanged event and render events fired'); this.events.emit('panel-size-changed'); } } @@ -91,6 +104,34 @@ export class PanelModel { this.events.emit('panel-size-changed'); } + refresh() { + this.hasRefreshed = true; + this.events.emit('refresh'); + } + + render() { + if (!this.hasRefreshed) { + this.refresh(); + } else { + this.events.emit('render'); + } + } + + panelInitialized() { + this.events.emit('panel-initialized'); + } + + initEditMode() { + this.events.emit('panel-init-edit-mode'); + } + + changeType(pluginId: string) { + this.type = pluginId; + + delete this.thresholds; + delete this.alert; + } + destroy() { this.events.removeAllListeners(); } diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 048a51efead..b6a70ee4b98 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -32,7 +32,7 @@ export class SettingsCtrl { this.$scope.$on('$destroy', () => { this.dashboard.updateSubmenuVisibility(); - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); setTimeout(() => { this.$rootScope.appEvent('dash-scroll', { restore: true }); }); diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index ec487801948..ac09d63054d 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -46,8 +46,7 @@ export class ShareSnapshotCtrl { $scope.loading = true; $scope.snapshot.external = external; - - $rootScope.$broadcast('refresh'); + $scope.dashboard.startRefresh(); $timeout(() => { $scope.saveSnapshot(external); diff --git a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx index 872d9296d12..c5f66fed32a 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx @@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({ })); describe('AddPanelPanel', () => { - let wrapper, dashboardMock, getPanelContainer, panel; + let wrapper, dashboardMock, panel; beforeEach(() => { config.panels = [ @@ -23,6 +23,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Singlestat', sort: 2, + module: '', + baseUrl: '', + meta: {}, info: { logos: { small: '', @@ -34,6 +37,9 @@ describe('AddPanelPanel', () => { hideFromList: true, name: 'Hidden', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -45,6 +51,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Graph', sort: 1, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -56,6 +65,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Zabbix', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -67,6 +79,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Piechart', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -77,13 +92,8 @@ describe('AddPanelPanel', () => { dashboardMock = { toggleRow: jest.fn() }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn(), - }); - panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); }); it('should fetch all panels sorted with core plugins first', () => { diff --git a/public/app/features/dashboard/specs/DashboardRow.test.tsx b/public/app/features/dashboard/specs/DashboardRow.test.tsx index 3d89c22f962..77c6cb39d9d 100644 --- a/public/app/features/dashboard/specs/DashboardRow.test.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.test.tsx @@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; describe('DashboardRow', () => { - let wrapper, panel, getPanelContainer, dashboardMock; + let wrapper, panel, dashboardMock; beforeEach(() => { dashboardMock = { @@ -14,13 +14,8 @@ describe('DashboardRow', () => { }, }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn(), - }); - panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); }); it('Should not have collapsed class when collaped is false', () => { @@ -41,14 +36,14 @@ describe('DashboardRow', () => { it('should not show row drag handle when cannot edit', () => { dashboardMock.meta.canEdit = false; - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0); }); it('should have zero actions when cannot edit', () => { dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0); }); }); diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/specs/exporter.test.ts index c7a232f925b..f21e151f3dd 100644 --- a/public/app/features/dashboard/specs/exporter.test.ts +++ b/public/app/features/dashboard/specs/exporter.test.ts @@ -240,5 +240,5 @@ stubs['-- Grafana --'] = { }; function getStub(arg) { - return Promise.resolve(stubs[arg]); + return Promise.resolve(stubs[arg || 'gfdb']); } diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 905ffb8b355..f9963afbf85 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -2,6 +2,7 @@ import 'app/features/dashboard/view_state_srv'; import config from 'app/core/config'; import { DashboardViewState } from '../view_state_srv'; +import { DashboardModel } from '../dashboard_model'; describe('when updating view state', () => { const location = { @@ -10,14 +11,13 @@ describe('when updating view state', () => { }; const $scope = { + appEvent: jest.fn(), onAppEvent: jest.fn(() => {}), - dashboard: { - meta: {}, - panels: [], - }, + dashboard: new DashboardModel({ + panels: [{ id: 1 }], + }), }; - const $rootScope = {}; let viewState; beforeEach(() => { @@ -33,7 +33,7 @@ describe('when updating view state', () => { location.search = jest.fn(() => { return { fullscreen: true, edit: true, panelId: 1 }; }); - viewState = new DashboardViewState($scope, location, {}, $rootScope); + viewState = new DashboardViewState($scope, location, {}); }); it('should update querystring and view state', () => { @@ -55,7 +55,7 @@ describe('when updating view state', () => { describe('to fullscreen false', () => { beforeEach(() => { - viewState = new DashboardViewState($scope, location, {}, $rootScope); + viewState = new DashboardViewState($scope, location, {}); }); it('should remove params from query string', () => { viewState.update({ fullscreen: true, panelId: 1, edit: true }); diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index e1288b2b2ed..184d29facee 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -7,13 +7,13 @@ export class SubmenuCtrl { dashboard: any; /** @ngInject */ - constructor(private $rootScope, private variableSrv, private $location) { + constructor(private variableSrv, private $location) { this.annotations = this.dashboard.templating.list; this.variables = this.variableSrv.variables; } annotationStateChanged() { - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); } variableUpdated(variable) { diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index a96bc89daa7..03b4a408125 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -1,8 +1,14 @@ +// Libraries import moment from 'moment'; import _ from 'lodash'; -import coreModule from 'app/core/core_module'; + +// Utils import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; +// Types + +import { TimeRange } from 'app/types'; export class TimeSrv { time: any; @@ -24,7 +30,6 @@ export class TimeSrv { document.addEventListener('visibilitychange', () => { if (this.autoRefreshBlocked && document.visibilityState === 'visible') { this.autoRefreshBlocked = false; - this.refreshDashboard(); } }); @@ -142,7 +147,7 @@ export class TimeSrv { } refreshDashboard() { - this.$rootScope.$broadcast('refresh'); + this.dashboard.timeRangeUpdated(); } private startNextRefreshTimer(afterMs) { @@ -201,7 +206,7 @@ export class TimeSrv { return range; } - timeRange() { + timeRange(): TimeRange { // make copies if they are moment (do not want to return out internal moment, because they are mutable!) const raw = { from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, @@ -223,17 +228,21 @@ export class TimeSrv { const timespan = range.to.valueOf() - range.from.valueOf(); const center = range.to.valueOf() - timespan / 2; - let to = center + timespan * factor / 2; - let from = center - timespan * factor / 2; - - if (to > Date.now() && range.to <= Date.now()) { - const offset = to - Date.now(); - from = from - offset; - to = Date.now(); - } + const to = center + timespan * factor / 2; + const from = center - timespan * factor / 2; this.setTime({ from: moment.utc(from), to: moment.utc(to) }); } } +let singleton; + +export function setTimeSrv(srv: TimeSrv) { + singleton = srv; +} + +export function getTimeSrv(): TimeSrv { + return singleton; +} + coreModule.service('timeSrv', TimeSrv); diff --git a/public/app/features/dashboard/timepicker/settings.html b/public/app/features/dashboard/timepicker/settings.html index 3cb8ca061fb..fd5170013c2 100644 --- a/public/app/features/dashboard/timepicker/settings.html +++ b/public/app/features/dashboard/timepicker/settings.html @@ -5,7 +5,7 @@
    - +
    diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index c133203cefc..c89e49b54b3 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -31,9 +31,10 @@ export class TimePickerCtrl { $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope); $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope); - $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope); $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope); + this.dashboard.on('refresh', this.onRefresh.bind(this), $scope); + // init options this.panel = this.dashboard.timepicker; _.defaults(this.panel, TimePickerCtrl.defaults); diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index d9ad6827567..8805050831e 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import config from 'app/core/config'; +import appEvents from 'app/core/app_events'; import { DashboardModel } from './dashboard_model'; // represents the transient view state @@ -10,12 +11,11 @@ export class DashboardViewState { panelScopes: any; $scope: any; dashboard: DashboardModel; - editStateChanged: any; fullscreenPanel: any; oldTimeRange: any; /** @ngInject */ - constructor($scope, private $location, private $timeout, private $rootScope) { + constructor($scope, private $location, private $timeout) { const self = this; self.state = {}; self.panelScopes = []; @@ -33,10 +33,6 @@ export class DashboardViewState { self.update(payload); }); - $scope.onAppEvent('panel-initialized', (evt, payload) => { - self.registerPanel(payload.scope); - }); - // this marks changes to location during this digest cycle as not to add history item // don't want url changes like adding orgId to add browser history $location.replace(); @@ -75,9 +71,6 @@ export class DashboardViewState { } } - // remember if editStateChanged - this.editStateChanged = (state.edit || false) !== (this.state.edit || false); - _.extend(this.state, state); this.dashboard.meta.fullscreen = this.state.fullscreen; @@ -124,110 +117,59 @@ export class DashboardViewState { } syncState() { - if (this.panelScopes.length === 0) { - return; - } - if (this.dashboard.meta.fullscreen) { - const panelScope = this.getPanelScope(this.state.panelId); - if (!panelScope) { + const panel = this.dashboard.getPanelById(this.state.panelId); + + if (!panel) { return; } - if (this.fullscreenPanel) { - // if already fullscreen - if (this.fullscreenPanel === panelScope && this.editStateChanged === false) { - return; - } else { - this.leaveFullscreen(false); - } - } - - if (!panelScope.ctrl.editModeInitiated) { - panelScope.ctrl.initEditMode(); - } - - if (!panelScope.ctrl.fullscreen) { - this.enterFullscreen(panelScope); + if (!panel.fullscreen) { + this.enterFullscreen(panel); + } else { + // already in fullscreen view just update the view mode + this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { - this.leaveFullscreen(true); + this.leaveFullscreen(); } } - getPanelScope(id) { - return _.find(this.panelScopes, panelScope => { - return panelScope.ctrl.panel.id === id; - }); - } + leaveFullscreen() { + const panel = this.fullscreenPanel; - leaveFullscreen(render) { - const self = this; - const ctrl = self.fullscreenPanel.ctrl; + this.dashboard.setViewMode(panel, false, false); - ctrl.editMode = false; - ctrl.fullscreen = false; - - this.dashboard.setViewMode(ctrl.panel, false, false); - this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id }); - this.$scope.appEvent('dash-scroll', { restore: true }); - - if (!render) { - return false; - } + delete this.fullscreenPanel; this.$timeout(() => { - if (self.oldTimeRange !== ctrl.range) { - self.$rootScope.$broadcast('refresh'); + appEvents.emit('dash-scroll', { restore: true }); + + if (this.oldTimeRange !== this.dashboard.time) { + this.dashboard.startRefresh(); } else { - self.$rootScope.$broadcast('render'); + this.dashboard.render(); } - delete self.fullscreenPanel; }); - return true; } - enterFullscreen(panelScope) { - const ctrl = panelScope.ctrl; + enterFullscreen(panel) { + const isEditing = this.state.edit && this.dashboard.meta.canEdit; - ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit; - ctrl.fullscreen = true; - - this.oldTimeRange = ctrl.range; - this.fullscreenPanel = panelScope; + this.oldTimeRange = this.dashboard.time; + this.fullscreenPanel = panel; // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode); - this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id }); - } - - registerPanel(panelScope) { - const self = this; - self.panelScopes.push(panelScope); - - if (!self.dashboard.meta.soloMode) { - if (self.state.panelId === panelScope.ctrl.panel.id) { - if (self.state.edit) { - panelScope.ctrl.editPanel(); - } else { - panelScope.ctrl.viewPanel(); - } - } - } - - const unbind = panelScope.$on('$destroy', () => { - self.panelScopes = _.without(self.panelScopes, panelScope); - unbind(); - }); + this.dashboard.setViewMode(panel, true, isEditing); } } /** @ngInject */ -export function dashboardViewStateSrv($location, $timeout, $rootScope) { +export function dashboardViewStateSrv($location, $timeout) { return { create: $scope => { - return new DashboardViewState($scope, $location, $timeout, $rootScope); + return new DashboardViewState($scope, $location, $timeout); }, }; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index b42b06f1238..e517c48bb59 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -7,13 +7,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { getExploreUrl } from 'app/core/utils/explore'; - import { metricsTabDirective } from './metrics_tab'; class MetricsPanelCtrl extends PanelCtrl { scope: any; datasource: any; - datasourceName: any; $q: any; $timeout: any; contextSrv: any; @@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl { this.scope = $scope; this.panel.datasource = this.panel.datasource || null; - if (!this.panel.targets) { - this.panel.targets = [{}]; - } - this.events.on('refresh', this.onMetricsPanelRefresh.bind(this)); this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this)); this.events.on('panel-teardown', this.onPanelTearDown.bind(this)); @@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Metrics', metricsTabDirective); + this.addEditorTab('Metrics', metricsTabDirective, 1, 'fa fa-database'); this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); } @@ -291,27 +285,6 @@ class MetricsPanelCtrl extends PanelCtrl { }); } - setDatasource(datasource) { - // switching to mixed - if (datasource.meta.mixed) { - _.each(this.panel.targets, target => { - target.datasource = this.panel.datasource; - if (!target.datasource) { - 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.datasourceName = datasource.name; - this.datasource = null; - this.refresh(); - } - getAdditionalMenuItems() { const items = []; if ( diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 3a1d0abe1c2..f520b5eefc0 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,6 +1,14 @@ -import { DashboardModel } from '../dashboard/dashboard_model'; +// Libraries +import _ from 'lodash'; import Remarkable from 'remarkable'; +// Services & utils +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; + export class MetricsTabCtrl { dsName: string; panel: any; @@ -24,6 +32,9 @@ export class MetricsTabCtrl { $scope.ctrl = this; this.panel = this.panelCtrl.panel; + this.panel.datasource = this.panel.datasource || null; + this.panel.targets = this.panel.targets || [{}]; + this.dashboard = this.panelCtrl.dashboard; this.datasources = datasourceSrv.getMetricSources(); this.panelDsValue = this.panelCtrl.panel.datasource; @@ -66,10 +77,29 @@ export class MetricsTabCtrl { } this.datasourceInstance = option.datasource; - this.panelCtrl.setDatasource(option.datasource); + this.setDatasource(option.datasource); this.updateDatasourceOptions(); } + setDatasource(datasource) { + // switching to mixed + if (datasource.meta.mixed) { + _.each(this.panel.targets, target => { + target.datasource = this.panel.datasource; + if (!target.datasource) { + target.datasource = config.defaultDatasource; + } + }); + } else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) { + _.each(this.panel.targets, target => { + delete target.datasource; + }); + } + + this.panel.datasource = datasource.value; + this.panel.refresh(); + } + addMixedQuery(option) { if (!option) { return; @@ -120,3 +150,5 @@ export function metricsTabDirective() { controller: MetricsTabCtrl, }; } + +coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index e2ae5cc78a9..5e216f6b34d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -24,10 +24,8 @@ export class PanelCtrl { $injector: any; $location: any; $timeout: any; - fullscreen: boolean; inspector: any; editModeInitiated: boolean; - editMode: any; height: any; containerHeight: any; events: Emitter; @@ -49,7 +47,6 @@ export class PanelCtrl { this.pluginName = plugin.name; } - $scope.$on('refresh', () => this.refresh()); $scope.$on('component-did-mount', () => this.panelDidMount()); $scope.$on('$destroy', () => { @@ -58,13 +55,9 @@ export class PanelCtrl { }); } - init() { - this.events.emit('panel-initialized'); - this.publishAppEvent('panel-initialized', { scope: this.$scope }); - } - panelDidMount() { this.events.emit('component-did-mount'); + this.dashboard.panelInitialized(this.panel); } renderingCompleted() { @@ -72,7 +65,7 @@ export class PanelCtrl { } refresh() { - this.events.emit('refresh', null); + this.panel.refresh(); } publishAppEvent(evtName, evt) { @@ -102,6 +95,7 @@ export class PanelCtrl { initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + this.editModeInitiated = true; this.events.emit('init-edit-mode', null); @@ -122,14 +116,15 @@ export class PanelCtrl { route.updateParams(); } - addEditorTab(title, directiveFn, index?) { - const editorTab = { title, directiveFn }; + addEditorTab(title, directiveFn, index?, icon?) { + const editorTab = { title, directiveFn, icon }; if (_.isString(directiveFn)) { editorTab.directiveFn = () => { return { templateUrl: directiveFn }; }; } + if (index) { this.editorTabs.splice(index, 0, editorTab); } else { @@ -190,7 +185,7 @@ export class PanelCtrl { getExtendedMenu() { const menu = []; - if (!this.fullscreen && this.dashboard.meta.canEdit) { + if (!this.panel.fullscreen && this.dashboard.meta.canEdit) { menu.push({ text: 'Duplicate', click: 'ctrl.duplicate()', @@ -220,15 +215,15 @@ export class PanelCtrl { } otherPanelInFullscreenMode() { - return this.dashboard.meta.fullscreen && !this.fullscreen; + return this.dashboard.meta.fullscreen && !this.panel.fullscreen; } calculatePanelHeight() { - if (this.fullscreen) { - const docHeight = $(window).height(); - const editHeight = Math.floor(docHeight * 0.4); + if (this.panel.fullscreen) { + const docHeight = $('.react-grid-layout').height(); + const editHeight = Math.floor(docHeight * 0.35); const fullscreenHeight = Math.floor(docHeight * 0.8); - this.containerHeight = this.editMode ? editHeight : fullscreenHeight; + this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; } @@ -237,6 +232,11 @@ export class PanelCtrl { this.containerHeight = $(window).height(); } + // hacky solution + if (this.panel.isEditing && !this.editModeInitiated) { + this.initEditMode(); + } + this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT); } @@ -247,9 +247,6 @@ export class PanelCtrl { duplicate() { this.dashboard.duplicatePanel(this.panel); - this.$timeout(() => { - this.$scope.$root.$broadcast('render'); - }); } removePanel() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 8b742e17952..77ebf754b3a 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -6,48 +6,53 @@ import baron from 'baron'; const module = angular.module('grafana.directives'); const panelTemplate = ` -
    -
    - - - - +
    +
    +
    +
    + + + + - - - + + + - -
    + +
    -
    - -
    -
    - -
    -
    -
    -

    - {{ctrl.pluginName}} -

    - - - - +
    + +
    +
    -
    -
    - +
    +
    +
    +

    + {{ctrl.pluginName}} +

    + + + + +
    + +
    +
    + +
    @@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.dashboard.setPanelFocus(0); } - function panelHeightUpdated() { - panelContent.css({ height: ctrl.height + 'px' }); - } - function resizeScrollableContent() { if (panelScrollbar) { panelScrollbar.update(); @@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.events.on('panel-size-changed', () => { ctrl.calculatePanelHeight(); - panelHeightUpdated(); $timeout(() => { resizeScrollableContent(); ctrl.render(); @@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { // set initial height ctrl.calculatePanelHeight(); - panelHeightUpdated(); ctrl.events.on('render', () => { if (transparentLastState !== ctrl.panel.transparent) { diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index 13a9369856a..f7e1c48a323 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -1,6 +1,7 @@ import angular from 'angular'; const directiveModule = angular.module('grafana.directives'); +const directiveCache = {}; /** @ngInject */ function panelEditorTab(dynamicDirectiveSrv) { @@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) { }, directive: scope => { const pluginId = scope.ctrl.pluginId; - const tabIndex = scope.index; - // create a wrapper for directiveFn - // required for metrics tab directive - // that is the same for many panels but - // given different names in this function - const fn = () => scope.editorTab.directiveFn(); + const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-'); - return Promise.resolve({ - name: `panel-editor-tab-${pluginId}${tabIndex}`, - fn: fn, - }); + if (directiveCache[pluginId]) { + if (directiveCache[pluginId][tabName]) { + return directiveCache[pluginId][tabName]; + } + } else { + directiveCache[pluginId] = []; + } + + const result = { + fn: () => scope.editorTab.directiveFn(), + name: `panel-editor-tab-${pluginId}${tabName}`, + }; + + directiveCache[pluginId][tabName] = result; + + return result; }, }); } diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 5fa20c4714b..1d29d04ad98 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -8,21 +8,6 @@ const template = ` {{ctrl.timeInfo}} diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index 0ee1f81b0c3..815a99d6b74 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -1,11 +1,7 @@
    - - { + this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id); + }; +} + +const template = ` +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    `; + +/** @ngInject */ +export function vizTabDirective() { + 'use strict'; + return { + restrict: 'E', + template: template, + controller: VizTabCtrl, + }; +} + +react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]); +coreModule.directive('vizTab', vizTabDirective); diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index e29e1709ccf..b9779190a8b 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -14,6 +14,8 @@ import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module'; import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module'; import * as textPanel from 'app/plugins/panel/text/module'; +import * as text2Panel from 'app/plugins/panel/text2/module'; +import * as graph2Panel from 'app/plugins/panel/graph2/module'; import * as graphPanel from 'app/plugins/panel/graph/module'; import * as dashListPanel from 'app/plugins/panel/dashlist/module'; import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; @@ -40,6 +42,8 @@ const builtInPlugins = { 'app/plugins/datasource/stackdriver/module': stackdriverPlugin, 'app/plugins/panel/text/module': textPanel, + 'app/plugins/panel/text2/module': text2Panel, + 'app/plugins/panel/graph2/module': graph2Panel, 'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/dashlist/module': dashListPanel, 'app/plugins/panel/pluginlist/module': pluginsListPanel, diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 7ef82519668..71a417a882f 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -1,8 +1,14 @@ +// Libraries import _ from 'lodash'; import coreModule from 'app/core/core_module'; + +// Utils import config from 'app/core/config'; import { importPluginModule } from './plugin_loader'; +// Types +import { DataSourceApi } from 'app/types/series'; + export class DatasourceSrv { datasources: any; @@ -15,7 +21,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name?) { + get(name?): Promise { if (!name) { return this.get(config.defaultDatasource); } @@ -162,5 +168,15 @@ export class DatasourceSrv { } } +let singleton: DatasourceSrv; + +export function setDatasourceSrv(srv: DatasourceSrv) { + singleton = srv; +} + +export function getDatasourceSrv(): DatasourceSrv { + return singleton; +} + coreModule.service('datasourceSrv', DatasourceSrv); export default DatasourceSrv; diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 41d1b6f1deb..142eb942a30 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader'; import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module'; /** @ngInject */ -function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) { +function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) { function getTemplate(component) { if (component.template) { return $q.when(component.template); @@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => { PanelCtrl.templateUrl = null; - PanelCtrl.template = `${template}`; + PanelCtrl.template = `${template}`; return componentInfo; }); @@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // let a binding digest cycle complete before adding to dom setTimeout(() => { - elem.append(child); scope.$applyAsync(() => { - scope.$broadcast('component-did-mount'); - scope.$broadcast('refresh'); + elem.append(child); + setTimeout(() => { + scope.$applyAsync(() => { + scope.$broadcast('component-did-mount'); + }); + }); }); }); } @@ -245,7 +248,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ registerPluginComponent(scope, elem, attrs, componentInfo); }) .catch(err => { - $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]); console.log('Plugin component error', err); }); }, diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index bc3c719917c..8e0958f6c1b 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -18,6 +18,7 @@ import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import TableModel from 'app/core/table_model'; import { coreModule, appEvents, contextSrv } from 'app/core/core'; +import { PluginExports } from 'app/types/plugins'; import * as datemath from 'app/core/utils/datemath'; import * as fileExport from 'app/core/utils/file_export'; import * as flatten from 'app/core/utils/flatten'; @@ -140,11 +141,12 @@ const flotDeps = [ 'jquery.flot.events', 'jquery.flot.gauge', ]; + for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export function importPluginModule(path: string): Promise { +export function importPluginModule(path: string): Promise { const builtIn = builtInPlugins[path]; if (builtIn) { return Promise.resolve(builtIn); diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index 359d5b79a38..3df6ccb8b5b 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -1,5 +1,6 @@ import '../all'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import moment from 'moment'; import $q from 'q'; @@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) { return getVarMockConstructor(ctr, model, ctx); }; - ctx.variableSrv.init({ - templating: { list: [] }, - updateSubmenuVisibility: () => {}, - }); + ctx.variableSrv.init( + new DashboardModel({ + templating: { list: [] }, + updateSubmenuVisibility: () => {}, + }) + ); scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); ctx.variableSrv.addVariable(scenario.variable); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index b5d00a5289e..bda5b6aa577 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -2,6 +2,7 @@ import '../all'; import _ from 'lodash'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import $q from 'q'; describe('VariableSrv init', function(this: any) { @@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) { ctx.variableSrv.datasourceSrv = ctx.datasourceSrv; ctx.variableSrv.$location.search = () => scenario.urlParams; - ctx.variableSrv.dashboard = { + ctx.variableSrv.dashboard = new DashboardModel({ templating: { list: scenario.variables }, - }; + }); await ctx.variableSrv.init(ctx.variableSrv.dashboard); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 75e2ca35ec7..a676f9c2848 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -1,5 +1,8 @@ +// Libaries import angular from 'angular'; import _ from 'lodash'; + +// Utils & Services import coreModule from 'app/core/core_module'; import { variableTypes } from './variable'; import { Graph } from 'app/core/utils/dag'; @@ -10,13 +13,12 @@ export class VariableSrv { /** @ngInject */ constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) { - // update time variant variables - $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope); $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); } init(dashboard) { this.dashboard = dashboard; + this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); @@ -39,11 +41,7 @@ export class VariableSrv { }); } - onDashboardRefresh(evt, payload) { - if (payload && payload.fromVariableValueUpdated) { - return Promise.resolve({}); - } - + onTimeRangeUpdated() { const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => { const previousOptions = variable.options.slice(); @@ -54,7 +52,9 @@ export class VariableSrv { }); }); - return this.$q.all(promises); + return this.$q.all(promises).then(() => { + this.dashboard.startRefresh(); + }); } processVariable(variable, queryParams) { @@ -133,7 +133,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true }); + this.dashboard.startRefresh(); } }); } diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 9506587c515..32acdc435f2 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -7,12 +7,11 @@ class="dashboard-settings"> -
    +
    - - +
    diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts index 4f4b2961761..ba5a39688b3 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; export class CloudWatchQueryParameter { @@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl { } } -angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter); -angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); +coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter); +coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index 8963f2c3f4b..cacf86201fe 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticBucketAgg', elasticBucketAgg); -module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); +coreModule.directive('elasticBucketAgg', elasticBucketAgg); +coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 623eed68914..1dd0d892360 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticMetricAgg', elasticMetricAgg); -module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); +coreModule.directive('elasticMetricAgg', elasticMetricAgg); +coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts index a5c1dc49959..ea3dfe8ff5e 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.ts +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -1,8 +1,8 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; import Drop from 'tether-drop'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteAddFunc($compile) { @@ -130,7 +130,7 @@ export function graphiteAddFunc($compile) { }; } -angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc); +coreModule.directive('graphiteAddFunc', graphiteAddFunc); function createFunctionDropDownMenu(funcDefs) { const categories = {}; diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 68cc6f1452e..9e19083a9c3 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -1,7 +1,7 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { @@ -315,4 +315,4 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { }; } -angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor); +coreModule.directive('graphiteFuncEditor', graphiteFuncEditor); diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 98a1258cb15..6cd6c805463 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as options from './constants'; import kbn from 'app/core/utils/kbn'; @@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl { } } -angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); -angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); +coreModule.directive('stackdriverAggregation', StackdriverAggregation); +coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 786b2831e89..7af76720d23 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; import appEvents from 'app/core/app_events'; @@ -281,5 +281,5 @@ export class StackdriverFilterCtrl { } } -angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter); -angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl); +coreModule.directive('stackdriverFilter', StackdriverFilter); +coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl); diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index d112e656f3f..0197626cd0b 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -62,7 +62,6 @@ class TestDataDatasource { }); } - console.log(res); return { data: data }; }); } diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index cf317389941..7b01c46c4d3 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -1,11 +1,9 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import baron from 'baron'; +import coreModule from 'app/core/core_module'; -const module = angular.module('grafana.directives'); - -module.directive('graphLegend', (popoverSrv, $timeout) => { +coreModule.directive('graphLegend', (popoverSrv, $timeout) => { return { link: (scope, elem) => { let firstRender = true; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index bc0d0c0c630..07256164c56 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -134,9 +134,9 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { this.addEditorTab('Alert', alertTab, 5); diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index deb7bd8ba61..540d19fb47a 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import angular from 'angular'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { @@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.updateCurrentOverrides(); } -angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); +coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl); diff --git a/public/app/plugins/panel/graph2/README.md b/public/app/plugins/panel/graph2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/graph2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/graph2/img/icn-text-panel.svg b/public/app/plugins/panel/graph2/img/icn-text-panel.svg new file mode 100644 index 00000000000..a9d0a1d2c4a --- /dev/null +++ b/public/app/plugins/panel/graph2/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx new file mode 100644 index 00000000000..c2b8c355440 --- /dev/null +++ b/public/app/plugins/panel/graph2/module.tsx @@ -0,0 +1,43 @@ +// Libraries +import _ from 'lodash'; +import React, { PureComponent } from 'react'; + +// Components +import Graph from 'app/viz/Graph'; +import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; + +// Types +import { PanelProps, NullValueMode } from 'app/types'; + +interface Options { + showBars: boolean; +} + +interface Props extends PanelProps { + options: Options; +} + +export class Graph2 extends PureComponent { + constructor(props) { + super(props); + } + + render() { + const { timeSeries, timeRange } = this.props; + + const vmSeries = getTimeSeriesVMs({ + timeSeries: timeSeries, + nullValueMode: NullValueMode.Ignore, + }); + + return ; + } +} + +export class TextOptions extends PureComponent { + render() { + return

    Text2 Options component

    ; + } +} + +export { Graph2 as PanelComponent, TextOptions as PanelOptions }; diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json new file mode 100644 index 00000000000..b519a57fae4 --- /dev/null +++ b/public/app/plugins/panel/graph2/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "React Graph", + "id": "graph2", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" + } + } +} + diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 628186569dd..0e011e59439 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -1,12 +1,10 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import * as d3 from 'd3'; import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; - -const module = angular.module('grafana.directives'); +import coreModule from 'app/core/core_module'; const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0; /** * Color legend for heatmap editor. */ -module.directive('colorLegend', () => { +coreModule.directive('colorLegend', () => { return { restrict: 'E', template: '
    ', @@ -52,7 +50,7 @@ module.directive('colorLegend', () => { /** * Heatmap legend with scale values. */ -module.directive('heatmapLegend', () => { +coreModule.directive('heatmapLegend', () => { return { restrict: 'E', template: `
    `, diff --git a/public/app/plugins/panel/text2/README.md b/public/app/plugins/panel/text2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/text2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/text2/img/icn-graph-panel.svg b/public/app/plugins/panel/text2/img/icn-graph-panel.svg new file mode 100644 index 00000000000..463b3d5770b --- /dev/null +++ b/public/app/plugins/panel/text2/img/icn-graph-panel.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx new file mode 100644 index 00000000000..b10dc8b545e --- /dev/null +++ b/public/app/plugins/panel/text2/module.tsx @@ -0,0 +1,14 @@ +import React, { PureComponent } from 'react'; +import { PanelProps } from 'app/types'; + +export class Text2 extends PureComponent { + constructor(props) { + super(props); + } + + render() { + return

    Text Panel!

    ; + } +} + +export { Text2 as PanelComponent }; diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json new file mode 100644 index 00000000000..b1133f65e36 --- /dev/null +++ b/public/app/plugins/panel/text2/plugin.json @@ -0,0 +1,19 @@ +{ + "type": "panel", + "name": "Text v2", + "id": "text2", + + "state": "alpha", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" + } + } +} + diff --git a/public/app/core/components/grafana_app.ts b/public/app/routes/GrafanaCtrl.ts similarity index 96% rename from public/app/core/components/grafana_app.ts rename to public/app/routes/GrafanaCtrl.ts index 2774ab99426..d6291c94a6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -8,9 +8,10 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { configureStore } from 'app/store/configureStore'; +import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv'; +import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; +import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -23,12 +24,15 @@ export class GrafanaCtrl { contextSrv, bridgeSrv, backendSrv: BackendSrv, + timeSrv: TimeSrv, datasourceSrv: DatasourceSrv, angularLoader: AngularLoader ) { - // sets singleston instances for angular services so react components can access them + // make angular loader service available to react components setAngularLoader(angularLoader); setBackendSrv(backendSrv); + setDatasourceSrv(datasourceSrv); + setTimeSrv(timeSrv); configureStore(); $scope.init = () => { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 7b35f3d6787..27c1644e6ab 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -8,6 +8,19 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; +import { + TimeRange, + LoadingState, + TimeSeries, + TimeSeriesVM, + TimeSeriesVMs, + TimeSeriesStats, + NullValueMode, + DataQuery, + DataQueryResponse, + DataQueryOptions, +} from './series'; +import { PanelProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; export { @@ -45,6 +58,17 @@ export { OrgUser, User, UsersState, + TimeRange, + LoadingState, + PanelProps, + TimeSeries, + TimeSeriesVM, + TimeSeriesVMs, + NullValueMode, + TimeSeriesStats, + DataQuery, + DataQueryResponse, + DataQueryOptions, PluginDashboard, }; diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 4a7f51523a7..7dcf57f7e02 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -2,6 +2,7 @@ export interface LocationUpdate { path?: string; query?: UrlQueryMap; routeParams?: UrlQueryMap; + partial?: boolean; } export interface LocationState { diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts new file mode 100644 index 00000000000..5ece77fc5aa --- /dev/null +++ b/public/app/types/panel.ts @@ -0,0 +1,7 @@ +import { LoadingState, TimeSeries, TimeRange } from './series'; + +export interface PanelProps { + timeSeries: TimeSeries[]; + timeRange: TimeRange; + loading: LoadingState; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 184e091b5e6..826ce2d48ec 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,3 +1,25 @@ +export interface PluginExports { + PanelCtrl?; + PanelComponent?: any; + Datasource?: any; + QueryCtrl?: any; + ConfigCtrl?: any; + AnnotationsQueryCtrl?: any; + PanelOptions?: any; +} + +export interface PanelPlugin { + id: string; + name: string; + meta: any; + hideFromList: boolean; + module: string; + baseUrl: string; + info: any; + sort: number; + exports?: PluginExports; +} + export interface PluginMeta { id: string; name: string; diff --git a/public/app/types/series.ts b/public/app/types/series.ts new file mode 100644 index 00000000000..5396880611b --- /dev/null +++ b/public/app/types/series.ts @@ -0,0 +1,91 @@ +import { Moment } from 'moment'; + +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', +} + +export interface RawTimeRange { + from: Moment | string; + to: Moment | string; +} + +export interface TimeRange { + from: Moment; + to: Moment; + raw: RawTimeRange; +} + +export type TimeSeriesValue = string | number | null; + +export type TimeSeriesPoints = TimeSeriesValue[][]; + +export interface TimeSeries { + target: string; + datapoints: TimeSeriesPoints; + unit?: string; +} + +/** View model projection of a time series */ +export interface TimeSeriesVM { + label: string; + color: string; + data: TimeSeriesValue[][]; + stats: TimeSeriesStats; +} + +export interface TimeSeriesStats { + total: number; + max: number; + min: number; + logmin: number; + avg: number | null; + current: number | null; + first: number | null; + delta: number; + diff: number | null; + range: number | null; + timeStep: number; + count: number; + allIsNull: boolean; + allIsZero: boolean; +} + +export enum NullValueMode { + Null = 'null', + Ignore = 'connected', + AsZero = 'null as zero', +} + +/** View model projection of many time series */ +export interface TimeSeriesVMs { + [index: number]: TimeSeriesVM; +} + +export interface DataQueryResponse { + data: TimeSeries[]; +} + +export interface DataQuery { + refId: string; +} + +export interface DataQueryOptions { + timezone: string; + range: TimeRange; + rangeRaw: RawTimeRange; + targets: DataQuery[]; + panelId: number; + dashboardId: number; + cacheTimeout?: string; + interval: string; + intervalMs: number; + maxDataPoints: number; + scopedVars: object; +} + +export interface DataSourceApi { + query(options: DataQueryOptions): Promise; +} diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx new file mode 100644 index 00000000000..fab65225715 --- /dev/null +++ b/public/app/viz/Graph.tsx @@ -0,0 +1,124 @@ +// Libraries +import $ from 'jquery'; +import React, { PureComponent } from 'react'; +import { withSize } from 'react-sizeme'; +import 'vendor/flot/jquery.flot'; +import 'vendor/flot/jquery.flot.time'; + +// Types +import { TimeRange, TimeSeriesVMs } from 'app/types'; + +// Copied from graph.ts +function time_format(ticks, min, max) { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +const FLOT_OPTIONS = { + legend: { + show: false, + }, + series: { + lines: { + linewidth: 1, + zero: false, + }, + shadowSize: 0, + }, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + // hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, +}; + +interface GraphProps { + timeSeries: TimeSeriesVMs; + timeRange: TimeRange; + size?: { width: number; height: number }; +} + +export class Graph extends PureComponent { + element: any; + + componentDidUpdate(prevProps: GraphProps) { + if ( + prevProps.timeSeries !== this.props.timeSeries || + prevProps.timeRange !== this.props.timeRange || + prevProps.size !== this.props.size + ) { + this.draw(); + } + } + + componentDidMount() { + this.draw(); + } + + draw() { + const { size, timeSeries, timeRange } = this.props; + + if (!size) { + return; + } + + const ticks = (size.width || 0) / 100; + const min = timeRange.from.valueOf(); + const max = timeRange.to.valueOf(); + + const dynamicOptions = { + xaxis: { + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: time_format(ticks, min, max), + }, + }; + + const options = { + ...FLOT_OPTIONS, + ...dynamicOptions, + }; + + console.log('plot', timeSeries, options); + $.plot(this.element, timeSeries, options); + } + + render() { + return ( +
    +
    (this.element = e)} /> +
    + ); + } +} + +export default withSize()(Graph); diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts new file mode 100644 index 00000000000..e22cb4681b7 --- /dev/null +++ b/public/app/viz/state/timeSeries.ts @@ -0,0 +1,168 @@ +// Libraries +import _ from 'lodash'; + +// Utils +import colors from 'app/core/utils/colors'; + +// Types +import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types'; + +interface Options { + timeSeries: TimeSeries[]; + nullValueMode: NullValueMode; +} + +export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { + const vmSeries = timeSeries.map((item, index) => { + const colorIndex = index % colors.length; + const label = item.target; + const result = []; + + // stat defaults + let total = 0; + let max = -Number.MAX_VALUE; + let min = Number.MAX_VALUE; + let logmin = Number.MAX_VALUE; + let avg = null; + let current = null; + let first = null; + let delta = 0; + let diff = null; + let range = null; + let timeStep = Number.MAX_VALUE; + let allIsNull = true; + let allIsZero = true; + + const ignoreNulls = nullValueMode === NullValueMode.Ignore; + const nullAsZero = nullValueMode === NullValueMode.AsZero; + + let currentTime; + let currentValue; + let nonNulls = 0; + let previousTime; + let previousValue = 0; + let previousDeltaUp = true; + + for (let i = 0; i < item.datapoints.length; i++) { + currentValue = item.datapoints[i][0]; + currentTime = item.datapoints[i][1]; + + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== undefined) { + const currentStep = currentTime - previousTime; + if (currentStep < timeStep) { + timeStep = currentStep; + } + } + + previousTime = currentTime; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (currentValue !== null) { + if (_.isNumber(currentValue)) { + total += currentValue; + allIsNull = false; + nonNulls++; + } + + if (currentValue > max) { + max = currentValue; + } + + if (currentValue < min) { + min = currentValue; + } + + if (first === null) { + first = currentValue; + } else { + if (previousValue > currentValue) { + // counter reset + previousDeltaUp = false; + if (i === item.datapoints.length - 1) { + // reset on last + delta += currentValue; + } + } else { + if (previousDeltaUp) { + delta += currentValue - previousValue; // normal increment + } else { + delta += currentValue; // account for counter reset + } + previousDeltaUp = true; + } + } + previousValue = currentValue; + + if (currentValue < logmin && currentValue > 0) { + logmin = currentValue; + } + + if (currentValue !== 0) { + allIsZero = false; + } + } + + result.push([currentTime, currentValue]); + } + + if (max === -Number.MAX_VALUE) { + max = null; + } + + if (min === Number.MAX_VALUE) { + min = null; + } + + if (result.length && !allIsNull) { + avg = total / nonNulls; + current = result[result.length - 1][1]; + if (current === null && result.length > 1) { + current = result[result.length - 2][1]; + } + } + + if (max !== null && min !== null) { + range = max - min; + } + + if (current !== null && first !== null) { + diff = current - first; + } + + const count = result.length; + + return { + data: result, + label: label, + color: colors[colorIndex], + stats: { + total, + min, + max, + current, + logmin, + avg, + diff, + delta, + timeStep, + range, + count, + first, + allIsZero, + allIsNull, + }, + }; + }); + + return vmSeries; +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index eab681d31f8..e4c7a9c59e1 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -97,6 +97,7 @@ @import 'components/form_select_box'; @import 'components/user-picker'; @import 'components/description-picker'; +@import 'components/viz_editor'; @import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index f1908ca8786..da1f140d252 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -20,7 +20,6 @@ } // Disable grid interaction indicators in fullscreen panels - .panel-header:hover { background-color: inherit; } @@ -32,6 +31,11 @@ .react-resizable-handle { display: none; } + + // the react-grid has a height transition + .react-grid-layout { + transition-property: none; + } } @include media-breakpoint-down(sm) { diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 5bfff31a108..263b181262e 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -85,10 +85,6 @@ height: calc(100% - 15px); } -.add-panel__item-icon { - padding: 2px; -} - .add-panel__searchbar { width: 100%; margin-bottom: 10px; diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index adb9e0c54c0..00bd5f7c94c 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -307,6 +307,7 @@ .view { display: flex; flex-grow: 1; + flex-direction: column; } .track-vertical { @@ -337,3 +338,7 @@ border-radius: 6px; } } + +.scroll-margin-helper { + margin-right: 12px; +} diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index bf95d453504..87b43a31142 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,19 +1,15 @@ .tabbed-view { - padding: $spacer*3; - margin-bottom: $dashboard-padding; + display: flex; + flex-direction: column; + height: 100%; - &.tabbed-view--panel-edit { - padding: 0; - - .tabbed-view-header { - padding: 0px 25px; - background: none; - } + &.tabbed-view--new { + padding: 25px 0 0 0; + height: 100%; } } .tabbed-view-header { - background: $page-header-bg; box-shadow: $page-header-shadow; border-bottom: 1px solid $page-header-border-color; @include clearfix(); @@ -48,7 +44,10 @@ } .tabbed-view-body { - padding: $spacer*2 $spacer; + padding: $spacer*2 $spacer $spacer $spacer; + display: flex; + flex-direction: column; + flex: 1; &--small { min-height: 0px; diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss new file mode 100644 index 00000000000..048e513cfbb --- /dev/null +++ b/public/sass/components/_viz_editor.scss @@ -0,0 +1,81 @@ +.viz-editor { + display: flex; + height: 100%; +} + +.viz-editor-col1 { + width: 210px; + height: 100%; + margin-right: 40px; +} + +.viz-editor-col2 { + flex-grow: 1; +} + +.viz-picker { + display: flex; + flex-direction: column; + height: 100%; +} + +.viz-picker__search { + flex-grow: 0; +} + +.viz-picker__items { + flex-grow: 1; + height: calc(100% - 50px); +} + +.viz-picker__item { + background: $card-background; + box-shadow: $card-shadow; + + border-radius: 3px; + padding: $spacer; + width: 100%; + height: 60px; + text-align: center; + margin-bottom: 6px; + cursor: pointer; + display: flex; + flex-shrink: 0; + border: 1px solid transparent; + @include left-brand-border; + + &:hover { + background: $card-background-hover; + } + + &--selected { + // border: 1px solid $orange; + @include left-brand-border-gradient(); + + .viz-picker__item-name { + color: $text-color; + } + + .viz-picker__item-img { + filter: saturate(100%); + } + } +} + +.viz-picker__item-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: $font-size-h5; + display: flex; + flex-direction: column; + align-self: center; + padding-left: $spacer; + font-size: $font-size-md; + color: $text-muted; +} + +.viz-picker__item-img { + height: 100%; + filter: saturate(30%); +} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 6225f840973..d9ab29cc91c 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -1,7 +1,12 @@ .dashboard-container { padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding; width: 100%; - min-height: 100%; + height: 100%; + box-sizing: border-box; + + &--has-submenu { + height: calc(100% - 50px); + } } .template-variable { @@ -29,16 +34,43 @@ div.flot-text { height: 100%; } +.panel-editor-container { + display: flex; + flex-direction: column; + height: 100%; +} + +.panel-editor-container__panel { + height: 35%; +} + +.panel-editor-container__editor { + height: 65%; +} + .panel-container { background-color: $panel-bg; border: $panel-border; position: relative; border-radius: 3px; + height: 100%; &.panel-transparent { background-color: transparent; border: none; } + + &:hover { + .panel-menu-toggle { + visibility: visible; + transition: opacity 0.1s ease-in 0.2s; + opacity: 1; + } + } + + &--is-editing { + height: auto; + } } .panel-content { @@ -199,14 +231,6 @@ div.flot-text { } } -.panel-hover-highlight { - .panel-menu-toggle { - visibility: visible; - transition: opacity 0.1s ease-in 0.2s; - opacity: 1; - } -} - .panel-time-info { font-weight: bold; float: right; @@ -233,5 +257,5 @@ div.flot-text { } .panel-full-edit { - margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding); + padding-top: $dashboard-padding; }