mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13578 from grafana/react-panels-step1
WIP: React panels step1
This commit is contained in:
commit
4f4bba7f8c
@ -554,3 +554,6 @@ container_name =
|
|||||||
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||||
server_url =
|
server_url =
|
||||||
callback_url =
|
callback_url =
|
||||||
|
|
||||||
|
[panels]
|
||||||
|
enable_alpha = false
|
||||||
|
@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
||||||
}, reqOrgAdmin)
|
}, 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)
|
||||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"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)
|
orgDataSources := make([]*m.DataSource, 0)
|
||||||
|
|
||||||
if c.OrgId != 0 {
|
if c.OrgId != 0 {
|
||||||
@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
|||||||
|
|
||||||
panels := map[string]interface{}{}
|
panels := map[string]interface{}{}
|
||||||
for _, panel := range enabledPlugins.Panels {
|
for _, panel := range enabledPlugins.Panels {
|
||||||
|
if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
panels[panel.Id] = map[string]interface{}{
|
panels[panel.Id] = map[string]interface{}{
|
||||||
"module": panel.Module,
|
"module": panel.Module,
|
||||||
"baseUrl": panel.BaseUrl,
|
"baseUrl": panel.BaseUrl,
|
||||||
@ -196,8 +200,8 @@ func getPanelSort(id string) int {
|
|||||||
return sort
|
return sort
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFrontendSettings(c *m.ReqContext) {
|
func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := hs.getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(400, "Failed to get frontend settings", err)
|
c.JsonApiErr(400, "Failed to get frontend settings", err)
|
||||||
return
|
return
|
||||||
|
@ -18,7 +18,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := hs.getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -213,6 +213,8 @@ type Cfg struct {
|
|||||||
TempDataLifetime time.Duration
|
TempDataLifetime time.Duration
|
||||||
|
|
||||||
MetricsEndpointEnabled bool
|
MetricsEndpointEnabled bool
|
||||||
|
|
||||||
|
EnableAlphaPanels bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
explore := iniFile.Section("explore")
|
explore := iniFile.Section("explore")
|
||||||
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
||||||
|
|
||||||
|
panels := iniFile.Section("panels")
|
||||||
|
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
|
||||||
|
|
||||||
cfg.readSessionConfig()
|
cfg.readSessionConfig()
|
||||||
cfg.readSmtpSettings()
|
cfg.readSmtpSettings()
|
||||||
cfg.readQuotaSettings()
|
cfg.readQuotaSettings()
|
||||||
|
@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => {
|
|||||||
return array;
|
return array;
|
||||||
};
|
};
|
||||||
|
|
||||||
import { coreModule, registerAngularDirectives } from './core/core';
|
import { coreModule, angularModules } from 'app/core/core_module';
|
||||||
import { setupAngularRoutes } from './routes/routes';
|
import { registerAngularDirectives } from 'app/core/core';
|
||||||
|
import { setupAngularRoutes } from 'app/routes/routes';
|
||||||
|
|
||||||
|
import 'app/routes/GrafanaCtrl';
|
||||||
|
import 'app/features/all';
|
||||||
|
|
||||||
// import symlinked extensions
|
// import symlinked extensions
|
||||||
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
||||||
@ -109,24 +113,15 @@ export class GrafanaApp {
|
|||||||
'react',
|
'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
|
// makes it possible to add dynamic stuff
|
||||||
this.useModule(coreModule);
|
_.each(angularModules, m => {
|
||||||
|
this.useModule(m);
|
||||||
|
});
|
||||||
|
|
||||||
// register react angular wrappers
|
// register react angular wrappers
|
||||||
coreModule.config(setupAngularRoutes);
|
coreModule.config(setupAngularRoutes);
|
||||||
registerAngularDirectives();
|
registerAngularDirectives();
|
||||||
|
|
||||||
const preBootRequires = [import('app/features/all')];
|
|
||||||
|
|
||||||
Promise.all(preBootRequires)
|
|
||||||
.then(() => {
|
|
||||||
// disable tool tip animation
|
// disable tool tip animation
|
||||||
$.fn.tooltip.defaults.animation = false;
|
$.fn.tooltip.defaults.animation = false;
|
||||||
|
|
||||||
@ -138,10 +133,6 @@ export class GrafanaApp {
|
|||||||
|
|
||||||
this.preBootModules = null;
|
this.preBootModules = null;
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log('Application boot failed:', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export function geminiScrollbar() {
|
|||||||
let scrollRoot = elem.parent();
|
let scrollRoot = elem.parent();
|
||||||
const scroller = elem;
|
const scroller = elem;
|
||||||
|
|
||||||
|
console.log('scroll');
|
||||||
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
||||||
scrollRoot = scroller;
|
scrollRoot = scroller;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
|
||||||
export interface BuildInfo {
|
export interface BuildInfo {
|
||||||
version: string;
|
version: string;
|
||||||
@ -9,7 +10,7 @@ export interface BuildInfo {
|
|||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
datasources: any;
|
datasources: any;
|
||||||
panels: any;
|
panels: PanelPlugin[];
|
||||||
appSubUrl: string;
|
appSubUrl: string;
|
||||||
windowTitlePrefix: string;
|
windowTitlePrefix: string;
|
||||||
buildInfo: BuildInfo;
|
buildInfo: BuildInfo;
|
||||||
|
@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250;
|
|||||||
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
||||||
|
|
||||||
export const LS_PANEL_COPY_KEY = 'panel-copy';
|
export const LS_PANEL_COPY_KEY = 'panel-copy';
|
||||||
|
|
||||||
|
export const DASHBOARD_TOOLBAR_HEIGHT = 55;
|
||||||
|
export const DASHBOARD_TOP_PADDING = 20;
|
||||||
|
@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker';
|
|||||||
import './services/search_srv';
|
import './services/search_srv';
|
||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
|
|
||||||
import { grafanaAppDirective } from './components/grafana_app';
|
|
||||||
import { searchDirective } from './components/search/search';
|
import { searchDirective } from './components/search/search';
|
||||||
import { infoPopover } from './components/info_popover';
|
import { infoPopover } from './components/info_popover';
|
||||||
import { navbarDirective } from './components/navbar/navbar';
|
import { navbarDirective } from './components/navbar/navbar';
|
||||||
@ -60,7 +59,6 @@ export {
|
|||||||
registerAngularDirectives,
|
registerAngularDirectives,
|
||||||
arrayJoin,
|
arrayJoin,
|
||||||
coreModule,
|
coreModule,
|
||||||
grafanaAppDirective,
|
|
||||||
navbarDirective,
|
navbarDirective,
|
||||||
searchDirective,
|
searchDirective,
|
||||||
liveSrv,
|
liveSrv,
|
||||||
|
@ -1,2 +1,18 @@
|
|||||||
import angular from 'angular';
|
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;
|
||||||
|
@ -2,16 +2,21 @@ import _ from 'lodash';
|
|||||||
import coreModule from '../core_module';
|
import coreModule from '../core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function dashClass() {
|
function dashClass($timeout) {
|
||||||
return {
|
return {
|
||||||
link: ($scope, elem) => {
|
link: ($scope, elem) => {
|
||||||
$scope.onAppEvent('panel-fullscreen-enter', () => {
|
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
|
||||||
elem.toggleClass('panel-in-fullscreen', true);
|
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', $scope.ctrl.dashboard.meta.fullscreen === true);
|
||||||
elem.toggleClass('panel-in-fullscreen', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Action } from 'app/core/actions/location';
|
import { Action } from 'app/core/actions/location';
|
||||||
import { LocationState } from 'app/types';
|
import { LocationState } from 'app/types';
|
||||||
import { renderUrl } from 'app/core/utils/url';
|
import { renderUrl } from 'app/core/utils/url';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const initialState: LocationState = {
|
export const initialState: LocationState = {
|
||||||
url: '',
|
url: '',
|
||||||
@ -12,11 +13,17 @@ export const initialState: LocationState = {
|
|||||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'UPDATE_LOCATION': {
|
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 {
|
return {
|
||||||
url: renderUrl(path || state.path, query),
|
url: renderUrl(path || state.path, query),
|
||||||
path: path || state.path,
|
path: path || state.path,
|
||||||
query: query || state.query,
|
query: query,
|
||||||
routeParams: routeParams || state.routeParams,
|
routeParams: routeParams || state.routeParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import coreModule from '../core_module';
|
|||||||
|
|
||||||
class DynamicDirectiveSrv {
|
class DynamicDirectiveSrv {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $compile, private $rootScope) {}
|
constructor(private $compile) {}
|
||||||
|
|
||||||
addDirective(element, name, scope) {
|
addDirective(element, name, scope) {
|
||||||
const child = angular.element(document.createElement(name));
|
const child = angular.element(document.createElement(name));
|
||||||
@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
link(scope, elem, attrs, options) {
|
link(scope, elem, attrs, options) {
|
||||||
options
|
const directiveInfo = options.directive(scope);
|
||||||
.directive(scope)
|
|
||||||
.then(directiveInfo => {
|
|
||||||
if (!directiveInfo || !directiveInfo.fn) {
|
if (!directiveInfo || !directiveInfo.fn) {
|
||||||
elem.empty();
|
elem.empty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!directiveInfo.fn.registered) {
|
if (!directiveInfo.fn.registered) {
|
||||||
|
console.log('register panel tab');
|
||||||
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
||||||
directiveInfo.fn.registered = true;
|
directiveInfo.fn.registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addDirective(elem, directiveInfo.name, scope);
|
this.addDirective(elem, directiveInfo.name, scope);
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log('Plugin load:', err);
|
|
||||||
this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(options) {
|
create(options) {
|
||||||
|
@ -148,7 +148,7 @@ export class KeybindingSrv {
|
|||||||
this.bind('mod+o', () => {
|
this.bind('mod+o', () => {
|
||||||
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
this.$rootScope.$broadcast('refresh');
|
dashboard.startRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('mod+s', e => {
|
this.bind('mod+s', e => {
|
||||||
@ -257,7 +257,7 @@ export class KeybindingSrv {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d r', () => {
|
this.bind('d r', () => {
|
||||||
this.$rootScope.$broadcast('refresh');
|
dashboard.startRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d s', () => {
|
this.bind('d s', () => {
|
||||||
|
@ -22,7 +22,6 @@ import './export_data/export_data_modal';
|
|||||||
import './ad_hoc_filters';
|
import './ad_hoc_filters';
|
||||||
import './repeat_option/repeat_option';
|
import './repeat_option/repeat_option';
|
||||||
import './dashgrid/DashboardGridDirective';
|
import './dashgrid/DashboardGridDirective';
|
||||||
import './dashgrid/PanelLoader';
|
|
||||||
import './dashgrid/RowOptions';
|
import './dashgrid/RowOptions';
|
||||||
import './folder_picker/folder_picker';
|
import './folder_picker/folder_picker';
|
||||||
import './move_to_folder_modal/move_to_folder';
|
import './move_to_folder_modal/move_to_folder';
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { PanelContainer } from './dashgrid/PanelContainer';
|
|
||||||
import { DashboardModel } from './dashboard_model';
|
import { DashboardModel } from './dashboard_model';
|
||||||
import { PanelModel } from './panel_model';
|
import { PanelModel } from './panel_model';
|
||||||
|
|
||||||
export class DashboardCtrl implements PanelContainer {
|
export class DashboardCtrl {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
dashboardViewState: any;
|
dashboardViewState: any;
|
||||||
loadedFallbackDashboard: boolean;
|
loadedFallbackDashboard: boolean;
|
||||||
@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
private dashboardSrv,
|
private dashboardSrv,
|
||||||
private unsavedChangesSrv,
|
private unsavedChangesSrv,
|
||||||
private dashboardViewStateSrv,
|
private dashboardViewStateSrv,
|
||||||
public playlistSrv,
|
public playlistSrv
|
||||||
private panelLoader
|
|
||||||
) {
|
) {
|
||||||
// temp hack due to way dashboards are loaded
|
// temp hack due to way dashboards are loaded
|
||||||
// can't use controllerAs on route yet
|
// can't use controllerAs on route yet
|
||||||
@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
return this.dashboard;
|
return this.dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelLoader() {
|
|
||||||
return this.panelLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
timezoneChanged() {
|
|
||||||
this.$rootScope.$broadcast('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelContainer() {
|
getPanelContainer() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
this.dashboard.removePanel(panel);
|
this.dashboard.removePanel(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
if (this.dashboard) {
|
||||||
|
this.dashboard.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(dashboard) {
|
init(dashboard) {
|
||||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||||
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
||||||
|
this.$scope.$on('$destroy', this.onDestroy.bind(this));
|
||||||
this.setupDashboard(dashboard);
|
this.setupDashboard(dashboard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +200,43 @@ export class DashboardModel {
|
|||||||
this.events.emit('view-mode-changed', panel);
|
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) {
|
private ensureListExist(data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = {};
|
data = {};
|
||||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words';
|
|||||||
|
|
||||||
export interface AddPanelPanelProps {
|
export interface AddPanelPanelProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddPanelPanelState {
|
export interface AddPanelPanelState {
|
||||||
@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAddPanel = panelPluginInfo => {
|
onAddPanel = panelPluginInfo => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
const dashboard = this.props.dashboard;
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
const { gridPos } = this.props.panel;
|
const { gridPos } = this.props.panel;
|
||||||
|
|
||||||
const newPanel: any = {
|
const newPanel: any = {
|
||||||
@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
|
|
||||||
handleCloseAddPanel(evt) {
|
handleCloseAddPanel(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
dashboard.removePanel(dashboard.panels[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderText(text: string) {
|
renderText(text: string) {
|
||||||
|
@ -3,7 +3,6 @@ import ReactGridLayout from 'react-grid-layout';
|
|||||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||||
import { DashboardPanel } from './DashboardPanel';
|
import { DashboardPanel } from './DashboardPanel';
|
||||||
import { DashboardModel } from '../dashboard_model';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sizeMe from 'react-sizeme';
|
import sizeMe from 'react-sizeme';
|
||||||
@ -60,18 +59,15 @@ function GridWrapper({
|
|||||||
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
||||||
|
|
||||||
export interface DashboardGridProps {
|
export interface DashboardGridProps {
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||||
gridToPanelMap: any;
|
gridToPanelMap: any;
|
||||||
panelContainer: PanelContainer;
|
|
||||||
dashboard: DashboardModel;
|
|
||||||
panelMap: { [id: string]: PanelModel };
|
panelMap: { [id: string]: PanelModel };
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.panelContainer = this.props.getPanelContainer();
|
|
||||||
this.onLayoutChange = this.onLayoutChange.bind(this);
|
this.onLayoutChange = this.onLayoutChange.bind(this);
|
||||||
this.onResize = this.onResize.bind(this);
|
this.onResize = this.onResize.bind(this);
|
||||||
this.onResizeStop = this.onResizeStop.bind(this);
|
this.onResizeStop = this.onResizeStop.bind(this);
|
||||||
@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
this.state = { animated: false };
|
this.state = { animated: false };
|
||||||
|
|
||||||
// subscribe to dashboard events
|
// subscribe to dashboard events
|
||||||
this.dashboard = this.panelContainer.getDashboard();
|
const dashboard = this.props.dashboard;
|
||||||
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
|
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
|
||||||
this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
||||||
|
dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLayout() {
|
buildLayout() {
|
||||||
const layout = [];
|
const layout = [];
|
||||||
this.panelMap = {};
|
this.panelMap = {};
|
||||||
|
|
||||||
for (const panel of this.dashboard.panels) {
|
for (const panel of this.props.dashboard.panels) {
|
||||||
const stringId = panel.id.toString();
|
const stringId = panel.id.toString();
|
||||||
this.panelMap[stringId] = panel;
|
this.panelMap[stringId] = panel;
|
||||||
|
|
||||||
@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
this.panelMap[newPos.i].updateGridPos(newPos);
|
this.panelMap[newPos.i].updateGridPos(newPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dashboard.sortPanelsByGridPos();
|
this.props.dashboard.sortPanelsByGridPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerForceUpdate() {
|
triggerForceUpdate() {
|
||||||
@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onWidthChange() {
|
onWidthChange() {
|
||||||
for (const panel of this.dashboard.panels) {
|
for (const panel of this.props.dashboard.panels) {
|
||||||
panel.resizeDone();
|
panel.resizeDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onViewModeChanged(payload) {
|
||||||
|
this.setState({ animated: !payload.fullscreen });
|
||||||
|
}
|
||||||
|
|
||||||
updateGridPos(item, layout) {
|
updateGridPos(item, layout) {
|
||||||
this.panelMap[item.i].updateGridPos(item);
|
this.panelMap[item.i].updateGridPos(item);
|
||||||
|
|
||||||
@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState(() => {
|
this.setState({ animated: true });
|
||||||
return { animated: true };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPanels() {
|
renderPanels() {
|
||||||
const panelElements = [];
|
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 });
|
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
|
||||||
panelElements.push(
|
panelElements.push(
|
||||||
/** panel-id is set for html bookmarks */
|
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
|
||||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id.toString()}`}>
|
<DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
|
||||||
<DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
<SizedReactLayoutGrid
|
<SizedReactLayoutGrid
|
||||||
className={classNames({ layout: true, animated: this.state.animated })}
|
className={classNames({ layout: true, animated: this.state.animated })}
|
||||||
layout={this.buildLayout()}
|
layout={this.buildLayout()}
|
||||||
isResizable={this.dashboard.meta.canEdit}
|
isResizable={this.props.dashboard.meta.canEdit}
|
||||||
isDraggable={this.dashboard.meta.canEdit}
|
isDraggable={this.props.dashboard.meta.canEdit}
|
||||||
onLayoutChange={this.onLayoutChange}
|
onLayoutChange={this.onLayoutChange}
|
||||||
onWidthChange={this.onWidthChange}
|
onWidthChange={this.onWidthChange}
|
||||||
onDragStop={this.onDragStop}
|
onDragStop={this.onDragStop}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
import { DashboardGrid } from './DashboardGrid';
|
import { DashboardGrid } from './DashboardGrid';
|
||||||
|
|
||||||
react2AngularDirective('dashboardGrid', DashboardGrid, [
|
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||||
['getPanelContainer', { watchDepth: 'reference', wrapApply: false }],
|
|
||||||
]);
|
|
||||||
|
@ -1,54 +1,161 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {PanelModel} from '../panel_model';
|
import config from 'app/core/config';
|
||||||
import {PanelContainer} from './PanelContainer';
|
import { PanelModel } from '../panel_model';
|
||||||
import {AttachedPanel} from './PanelLoader';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import {DashboardRow} from './DashboardRow';
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
import {AddPanelPanel} from './AddPanelPanel';
|
import { DashboardRow } from './DashboardRow';
|
||||||
|
import { AddPanelPanel } from './AddPanelPanel';
|
||||||
|
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||||
|
import { PluginExports, PanelPlugin } from 'app/types/plugins';
|
||||||
|
import { PanelChrome } from './PanelChrome';
|
||||||
|
import { PanelEditor } from './PanelEditor';
|
||||||
|
|
||||||
export interface DashboardPanelProps {
|
export interface Props {
|
||||||
|
panelType: string;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
export interface State {
|
||||||
|
pluginExports: PluginExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardPanel extends React.Component<Props, State> {
|
||||||
element: any;
|
element: any;
|
||||||
attachedPanel: AttachedPanel;
|
angularPanel: AngularComponent;
|
||||||
|
pluginInfo: any;
|
||||||
|
specialPanels = {};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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() {
|
isSpecial() {
|
||||||
if (!this.element) {
|
return this.specialPanels[this.props.panel.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRow() {
|
||||||
|
return <DashboardRow panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAddPanel() {
|
||||||
|
return <AddPanelPanel panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPluginTypeChanged = (plugin: PanelPlugin) => {
|
||||||
|
this.props.panel.changeType(plugin.id);
|
||||||
|
this.loadPlugin();
|
||||||
|
};
|
||||||
|
|
||||||
|
onAngularPluginTypeChanged = () => {
|
||||||
|
this.loadPlugin();
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPlugin() {
|
||||||
|
if (this.isSpecial()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelContainer = this.props.getPanelContainer();
|
// handle plugin loading & changing of plugin type
|
||||||
const dashboard = panelContainer.getDashboard();
|
if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
|
||||||
const loader = panelContainer.getPanelLoader();
|
this.pluginInfo = config.panels[this.props.panel.type];
|
||||||
this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
|
|
||||||
|
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 = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
|
||||||
|
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() {
|
componentWillUnmount() {
|
||||||
if (this.attachedPanel) {
|
this.cleanUpAngularPanel();
|
||||||
this.attachedPanel.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={containerClass}>
|
||||||
|
<div className={panelWrapperClass}>
|
||||||
|
<PanelChrome
|
||||||
|
component={pluginExports.PanelComponent}
|
||||||
|
panel={this.props.panel}
|
||||||
|
dashboard={this.props.dashboard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{this.props.panel.isEditing && (
|
||||||
|
<div className="panel-editor-container__editor">
|
||||||
|
<PanelEditor
|
||||||
|
panel={this.props.panel}
|
||||||
|
panelType={this.props.panel.type}
|
||||||
|
dashboard={this.props.dashboard}
|
||||||
|
onTypeChanged={this.onPluginTypeChanged}
|
||||||
|
pluginExports={pluginExports}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// special handling for rows
|
if (this.isSpecial()) {
|
||||||
if (this.props.panel.type === 'row') {
|
return this.specialPanels[this.props.panel.type]();
|
||||||
return <DashboardRow panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.panel.type === 'add-panel') {
|
if (!this.state.pluginExports) {
|
||||||
return <AddPanelPanel panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (this.state.pluginExports.PanelComponent) {
|
||||||
<div ref={element => this.element = element} className="panel-height-helper" />
|
return this.renderReactPanel();
|
||||||
);
|
}
|
||||||
|
|
||||||
|
// legacy angular rendering
|
||||||
|
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export interface DashboardRowProps {
|
export interface DashboardRowProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||||
dashboard: any;
|
|
||||||
panelContainer: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
collapsed: this.props.panel.collapsed,
|
collapsed: this.props.panel.collapsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.panelContainer = this.props.getPanelContainer();
|
|
||||||
this.dashboard = this.panelContainer.getDashboard();
|
|
||||||
|
|
||||||
this.toggle = this.toggle.bind(this);
|
this.toggle = this.toggle.bind(this);
|
||||||
this.openSettings = this.openSettings.bind(this);
|
this.openSettings = this.openSettings.bind(this);
|
||||||
this.delete = this.delete.bind(this);
|
this.delete = this.delete.bind(this);
|
||||||
@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.dashboard.toggleRow(this.props.panel);
|
this.props.dashboard.toggleRow(this.props.panel);
|
||||||
|
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
return { collapsed: !prevState.collapsed };
|
return { collapsed: !prevState.collapsed };
|
||||||
@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.dashboard.processRepeats();
|
this.props.dashboard.processRepeats();
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
altActionText: 'Delete row only',
|
altActionText: 'Delete row only',
|
||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removeRow(this.props.panel, true);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
dashboard.removeRow(this.props.panel, true);
|
|
||||||
},
|
},
|
||||||
onAltAction: () => {
|
onAltAction: () => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removeRow(this.props.panel, false);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
dashboard.removeRow(this.props.panel, false);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,7 +77,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
|
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 count = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
||||||
const panels = count === 1 ? 'panel' : 'panels';
|
const panels = count === 1 ? 'panel' : 'panels';
|
||||||
const canEdit = this.dashboard.meta.canEdit === true;
|
const canEdit = this.props.dashboard.meta.canEdit === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
|
150
public/app/features/dashboard/dashgrid/DataPanel.tsx
Normal file
150
public/app/features/dashboard/dashgrid/DataPanel.tsx
Normal file
@ -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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div className="loading">
|
||||||
|
<p>Loading</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.loadingSpinner}
|
||||||
|
{this.props.children({
|
||||||
|
timeSeries,
|
||||||
|
loading,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get loadingSpinner(): JSX.Element {
|
||||||
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
if (loading === LoadingState.Loading) {
|
||||||
|
return (
|
||||||
|
<div className="panel__loading">
|
||||||
|
<i className="fa fa-spinner fa-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
84
public/app/features/dashboard/dashgrid/PanelChrome.tsx
Normal file
84
public/app/features/dashboard/dashgrid/PanelChrome.tsx
Normal file
@ -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<PanelProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
refreshCounter: number;
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanelChrome extends PureComponent<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div className="panel-container">
|
||||||
|
<PanelHeader panel={panel} dashboard={dashboard} />
|
||||||
|
<div className="panel-content">
|
||||||
|
<DataPanel
|
||||||
|
datasource={datasource}
|
||||||
|
queries={targets}
|
||||||
|
timeRange={timeRange}
|
||||||
|
isVisible={this.isVisible}
|
||||||
|
refreshCounter={refreshCounter}
|
||||||
|
>
|
||||||
|
{({ loading, timeSeries }) => {
|
||||||
|
return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />;
|
||||||
|
}}
|
||||||
|
</DataPanel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
import { DashboardModel } from '../dashboard_model';
|
|
||||||
import { PanelLoader } from './PanelLoader';
|
|
||||||
|
|
||||||
export interface PanelContainer {
|
|
||||||
getPanelLoader(): PanelLoader;
|
|
||||||
getDashboard(): DashboardModel;
|
|
||||||
}
|
|
121
public/app/features/dashboard/dashgrid/PanelEditor.tsx
Normal file
121
public/app/features/dashboard/dashgrid/PanelEditor.tsx
Normal file
@ -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<PanelEditorProps, any> {
|
||||||
|
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 <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPanelOptions() {
|
||||||
|
const { pluginExports } = this.props;
|
||||||
|
|
||||||
|
if (pluginExports.PanelOptions) {
|
||||||
|
const PanelOptions = pluginExports.PanelOptions;
|
||||||
|
return <PanelOptions />;
|
||||||
|
} else {
|
||||||
|
return <p>Visualization has no options</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVizTab() {
|
||||||
|
return (
|
||||||
|
<div className="viz-editor">
|
||||||
|
<div className="viz-editor-col1">
|
||||||
|
<VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.props.onTypeChanged} />
|
||||||
|
</div>
|
||||||
|
<div className="viz-editor-col2">
|
||||||
|
<h5 className="page-heading">Options</h5>
|
||||||
|
{this.renderPanelOptions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeTab = (tab: PanelEditorTab) => {
|
||||||
|
store.dispatch(
|
||||||
|
updateLocation({
|
||||||
|
query: { tab: tab.id },
|
||||||
|
partial: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { location } = store.getState();
|
||||||
|
const activeTab = location.query.tab || 'queries';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tabbed-view tabbed-view--new">
|
||||||
|
<div className="tabbed-view-header">
|
||||||
|
<ul className="gf-tabs">
|
||||||
|
{this.tabs.map(tab => {
|
||||||
|
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
|
||||||
|
<i className="fa fa-remove" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tabbed-view-body">
|
||||||
|
{activeTab === 'queries' && this.renderQueriesTab()}
|
||||||
|
{activeTab === 'visualization' && this.renderVizTab()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<li className="gf-tabs-item" key={tab.id}>
|
||||||
|
<a className={tabClasses} onClick={() => onClick(tab)}>
|
||||||
|
<i className={tab.icon} /> {tab.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
83
public/app/features/dashboard/dashgrid/PanelHeader.tsx
Normal file
83
public/app/features/dashboard/dashgrid/PanelHeader.tsx
Normal file
@ -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<PanelHeaderProps, any> {
|
||||||
|
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 (
|
||||||
|
<div className={panelHeaderClass}>
|
||||||
|
<span className="panel-info-corner">
|
||||||
|
<i className="fa" />
|
||||||
|
<span className="panel-info-corner-inner" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<span className="panel-loading">
|
||||||
|
<i className="fa fa-spinner fa-spin" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="panel-title-container">
|
||||||
|
<span className="panel-title">
|
||||||
|
<span className="icon-gf panel-alert-icon" />
|
||||||
|
<span className="panel-title-text">{this.props.panel.title}</span>
|
||||||
|
<span className="panel-menu-container dropdown">
|
||||||
|
<span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
|
||||||
|
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
||||||
|
<li>
|
||||||
|
<a onClick={this.onEditPanel}>
|
||||||
|
<i className="fa fa-fw fa-edit" /> Edit
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a onClick={this.onViewPanel}>
|
||||||
|
<i className="fa fa-fw fa-eye" /> View
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<span className="panel-time-info">
|
||||||
|
<i className="fa fa-clock-o" /> 4m
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
53
public/app/features/dashboard/dashgrid/QueriesTab.tsx
Normal file
53
public/app/features/dashboard/dashgrid/QueriesTab.tsx
Normal file
@ -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<Props> {
|
||||||
|
element: any;
|
||||||
|
component: AngularComponent;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { panel, dashboard } = this.props;
|
||||||
|
|
||||||
|
const loader = getAngularLoader();
|
||||||
|
const template = '<metrics-tab />';
|
||||||
|
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 <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||||
|
}
|
||||||
|
}
|
69
public/app/features/dashboard/dashgrid/VizTypePicker.tsx
Normal file
69
public/app/features/dashboard/dashgrid/VizTypePicker.tsx
Normal file
@ -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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
|
||||||
|
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||||
|
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="viz-picker">
|
||||||
|
<div className="viz-picker__search">
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form--has-input-icon gf-form--grow">
|
||||||
|
<input type="text" className="gf-form-input" placeholder="Search type" />
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="viz-picker__items">
|
||||||
|
<CustomScrollbar>
|
||||||
|
<div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
|
||||||
|
</CustomScrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,8 @@ export class DashNavCtrl {
|
|||||||
} else if (search.fullscreen) {
|
} else if (search.fullscreen) {
|
||||||
delete search.fullscreen;
|
delete search.fullscreen;
|
||||||
delete search.edit;
|
delete search.edit;
|
||||||
|
delete search.tab;
|
||||||
|
delete search.panelId;
|
||||||
}
|
}
|
||||||
this.$location.search(search);
|
this.$location.search(search);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
|||||||
events: true,
|
events: true,
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
|
hasRefreshed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaults: any = {
|
||||||
|
gridPos: { x: 0, y: 0, h: 3, w: 6 },
|
||||||
|
datasource: null,
|
||||||
|
targets: [{}],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PanelModel {
|
export class PanelModel {
|
||||||
@ -31,10 +38,14 @@ export class PanelModel {
|
|||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
panels?: any;
|
panels?: any;
|
||||||
soloMode?: boolean;
|
soloMode?: boolean;
|
||||||
|
targets: any[];
|
||||||
|
datasource: string;
|
||||||
|
thresholds?: any;
|
||||||
|
|
||||||
// non persisted
|
// non persisted
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
|
hasRefreshed: boolean;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
@ -45,9 +56,8 @@ export class PanelModel {
|
|||||||
this[property] = model[property];
|
this[property] = model[property];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.gridPos) {
|
// defaults
|
||||||
this.gridPos = { x: 0, y: 0, h: 3, w: 6 };
|
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSaveModel() {
|
getSaveModel() {
|
||||||
@ -57,6 +67,10 @@ export class PanelModel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_.isEqual(this[property], defaults[property])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
model[property] = _.cloneDeep(this[property]);
|
model[property] = _.cloneDeep(this[property]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +96,6 @@ export class PanelModel {
|
|||||||
this.gridPos.h = newPos.h;
|
this.gridPos.h = newPos.h;
|
||||||
|
|
||||||
if (sizeChanged) {
|
if (sizeChanged) {
|
||||||
console.log('PanelModel sizeChanged event and render events fired');
|
|
||||||
this.events.emit('panel-size-changed');
|
this.events.emit('panel-size-changed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,6 +104,34 @@ export class PanelModel {
|
|||||||
this.events.emit('panel-size-changed');
|
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() {
|
destroy() {
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export class SettingsCtrl {
|
|||||||
|
|
||||||
this.$scope.$on('$destroy', () => {
|
this.$scope.$on('$destroy', () => {
|
||||||
this.dashboard.updateSubmenuVisibility();
|
this.dashboard.updateSubmenuVisibility();
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.startRefresh();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||||
});
|
});
|
||||||
|
@ -46,8 +46,7 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.snapshot.external = external;
|
$scope.snapshot.external = external;
|
||||||
|
$scope.dashboard.startRefresh();
|
||||||
$rootScope.$broadcast('refresh');
|
|
||||||
|
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
$scope.saveSnapshot(external);
|
$scope.saveSnapshot(external);
|
||||||
|
@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AddPanelPanel', () => {
|
describe('AddPanelPanel', () => {
|
||||||
let wrapper, dashboardMock, getPanelContainer, panel;
|
let wrapper, dashboardMock, panel;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
config.panels = [
|
config.panels = [
|
||||||
@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Singlestat',
|
name: 'Singlestat',
|
||||||
sort: 2,
|
sort: 2,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
meta: {},
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: true,
|
hideFromList: true,
|
||||||
name: 'Hidden',
|
name: 'Hidden',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Graph',
|
name: 'Graph',
|
||||||
sort: 1,
|
sort: 1,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Zabbix',
|
name: 'Zabbix',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Piechart',
|
name: 'Piechart',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -77,13 +92,8 @@ describe('AddPanelPanel', () => {
|
|||||||
|
|
||||||
dashboardMock = { toggleRow: jest.fn() };
|
dashboardMock = { toggleRow: jest.fn() };
|
||||||
|
|
||||||
getPanelContainer = jest.fn().mockReturnValue({
|
|
||||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
|
||||||
getPanelLoader: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<AddPanelPanel panel={panel} dashboard={dashboardMock} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch all panels sorted with core plugins first', () => {
|
it('should fetch all panels sorted with core plugins first', () => {
|
||||||
|
@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow';
|
|||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
|
|
||||||
describe('DashboardRow', () => {
|
describe('DashboardRow', () => {
|
||||||
let wrapper, panel, getPanelContainer, dashboardMock;
|
let wrapper, panel, dashboardMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dashboardMock = {
|
dashboardMock = {
|
||||||
@ -14,13 +14,8 @@ describe('DashboardRow', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
getPanelContainer = jest.fn().mockReturnValue({
|
|
||||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
|
||||||
getPanelLoader: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not have collapsed class when collaped is false', () => {
|
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', () => {
|
it('should not show row drag handle when cannot edit', () => {
|
||||||
dashboardMock.meta.canEdit = false;
|
dashboardMock.meta.canEdit = false;
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
|
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have zero actions when cannot edit', () => {
|
it('should have zero actions when cannot edit', () => {
|
||||||
dashboardMock.meta.canEdit = false;
|
dashboardMock.meta.canEdit = false;
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -240,5 +240,5 @@ stubs['-- Grafana --'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getStub(arg) {
|
function getStub(arg) {
|
||||||
return Promise.resolve(stubs[arg]);
|
return Promise.resolve(stubs[arg || 'gfdb']);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import 'app/features/dashboard/view_state_srv';
|
import 'app/features/dashboard/view_state_srv';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { DashboardViewState } from '../view_state_srv';
|
import { DashboardViewState } from '../view_state_srv';
|
||||||
|
import { DashboardModel } from '../dashboard_model';
|
||||||
|
|
||||||
describe('when updating view state', () => {
|
describe('when updating view state', () => {
|
||||||
const location = {
|
const location = {
|
||||||
@ -10,14 +11,13 @@ describe('when updating view state', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const $scope = {
|
const $scope = {
|
||||||
|
appEvent: jest.fn(),
|
||||||
onAppEvent: jest.fn(() => {}),
|
onAppEvent: jest.fn(() => {}),
|
||||||
dashboard: {
|
dashboard: new DashboardModel({
|
||||||
meta: {},
|
panels: [{ id: 1 }],
|
||||||
panels: [],
|
}),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const $rootScope = {};
|
|
||||||
let viewState;
|
let viewState;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -33,7 +33,7 @@ describe('when updating view state', () => {
|
|||||||
location.search = jest.fn(() => {
|
location.search = jest.fn(() => {
|
||||||
return { fullscreen: true, edit: true, panelId: 1 };
|
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', () => {
|
it('should update querystring and view state', () => {
|
||||||
@ -55,7 +55,7 @@ describe('when updating view state', () => {
|
|||||||
|
|
||||||
describe('to fullscreen false', () => {
|
describe('to fullscreen false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewState = new DashboardViewState($scope, location, {}, $rootScope);
|
viewState = new DashboardViewState($scope, location, {});
|
||||||
});
|
});
|
||||||
it('should remove params from query string', () => {
|
it('should remove params from query string', () => {
|
||||||
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
||||||
|
@ -7,13 +7,13 @@ export class SubmenuCtrl {
|
|||||||
dashboard: any;
|
dashboard: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope, private variableSrv, private $location) {
|
constructor(private variableSrv, private $location) {
|
||||||
this.annotations = this.dashboard.templating.list;
|
this.annotations = this.dashboard.templating.list;
|
||||||
this.variables = this.variableSrv.variables;
|
this.variables = this.variableSrv.variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationStateChanged() {
|
annotationStateChanged() {
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.startRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
variableUpdated(variable) {
|
variableUpdated(variable) {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
// Libraries
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
|
// Utils
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
|
// Types
|
||||||
|
|
||||||
|
import { TimeRange } from 'app/types';
|
||||||
|
|
||||||
export class TimeSrv {
|
export class TimeSrv {
|
||||||
time: any;
|
time: any;
|
||||||
@ -24,7 +30,6 @@ export class TimeSrv {
|
|||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
|
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
|
||||||
this.autoRefreshBlocked = false;
|
this.autoRefreshBlocked = false;
|
||||||
|
|
||||||
this.refreshDashboard();
|
this.refreshDashboard();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -142,7 +147,7 @@ export class TimeSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshDashboard() {
|
refreshDashboard() {
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.timeRangeUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private startNextRefreshTimer(afterMs) {
|
private startNextRefreshTimer(afterMs) {
|
||||||
@ -201,7 +206,7 @@ export class TimeSrv {
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRange() {
|
timeRange(): TimeRange {
|
||||||
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
||||||
const raw = {
|
const raw = {
|
||||||
from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
|
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 timespan = range.to.valueOf() - range.from.valueOf();
|
||||||
const center = range.to.valueOf() - timespan / 2;
|
const center = range.to.valueOf() - timespan / 2;
|
||||||
|
|
||||||
let to = center + timespan * factor / 2;
|
const to = center + timespan * factor / 2;
|
||||||
let from = center - timespan * factor / 2;
|
const from = center - timespan * factor / 2;
|
||||||
|
|
||||||
if (to > Date.now() && range.to <= Date.now()) {
|
|
||||||
const offset = to - Date.now();
|
|
||||||
from = from - offset;
|
|
||||||
to = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
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);
|
coreModule.service('timeSrv', TimeSrv);
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-10">Timezone</label>
|
<label class="gf-form-label width-10">Timezone</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
|
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -31,9 +31,10 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
||||||
$rootScope.onAppEvent('shift-time-backward', () => 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);
|
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
|
||||||
|
|
||||||
|
this.dashboard.on('refresh', this.onRefresh.bind(this), $scope);
|
||||||
|
|
||||||
// init options
|
// init options
|
||||||
this.panel = this.dashboard.timepicker;
|
this.panel = this.dashboard.timepicker;
|
||||||
_.defaults(this.panel, TimePickerCtrl.defaults);
|
_.defaults(this.panel, TimePickerCtrl.defaults);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
import { DashboardModel } from './dashboard_model';
|
import { DashboardModel } from './dashboard_model';
|
||||||
|
|
||||||
// represents the transient view state
|
// represents the transient view state
|
||||||
@ -10,12 +11,11 @@ export class DashboardViewState {
|
|||||||
panelScopes: any;
|
panelScopes: any;
|
||||||
$scope: any;
|
$scope: any;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
editStateChanged: any;
|
|
||||||
fullscreenPanel: any;
|
fullscreenPanel: any;
|
||||||
oldTimeRange: any;
|
oldTimeRange: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, private $location, private $timeout, private $rootScope) {
|
constructor($scope, private $location, private $timeout) {
|
||||||
const self = this;
|
const self = this;
|
||||||
self.state = {};
|
self.state = {};
|
||||||
self.panelScopes = [];
|
self.panelScopes = [];
|
||||||
@ -33,10 +33,6 @@ export class DashboardViewState {
|
|||||||
self.update(payload);
|
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
|
// 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
|
// don't want url changes like adding orgId to add browser history
|
||||||
$location.replace();
|
$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);
|
_.extend(this.state, state);
|
||||||
this.dashboard.meta.fullscreen = this.state.fullscreen;
|
this.dashboard.meta.fullscreen = this.state.fullscreen;
|
||||||
|
|
||||||
@ -124,110 +117,59 @@ export class DashboardViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
syncState() {
|
syncState() {
|
||||||
if (this.panelScopes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dashboard.meta.fullscreen) {
|
if (this.dashboard.meta.fullscreen) {
|
||||||
const panelScope = this.getPanelScope(this.state.panelId);
|
const panel = this.dashboard.getPanelById(this.state.panelId);
|
||||||
if (!panelScope) {
|
|
||||||
|
if (!panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fullscreenPanel) {
|
if (!panel.fullscreen) {
|
||||||
// if already fullscreen
|
this.enterFullscreen(panel);
|
||||||
if (this.fullscreenPanel === panelScope && this.editStateChanged === false) {
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
this.leaveFullscreen(false);
|
// already in fullscreen view just update the view mode
|
||||||
}
|
this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
|
||||||
}
|
|
||||||
|
|
||||||
if (!panelScope.ctrl.editModeInitiated) {
|
|
||||||
panelScope.ctrl.initEditMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!panelScope.ctrl.fullscreen) {
|
|
||||||
this.enterFullscreen(panelScope);
|
|
||||||
}
|
}
|
||||||
} else if (this.fullscreenPanel) {
|
} else if (this.fullscreenPanel) {
|
||||||
this.leaveFullscreen(true);
|
this.leaveFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelScope(id) {
|
leaveFullscreen() {
|
||||||
return _.find(this.panelScopes, panelScope => {
|
const panel = this.fullscreenPanel;
|
||||||
return panelScope.ctrl.panel.id === id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveFullscreen(render) {
|
this.dashboard.setViewMode(panel, false, false);
|
||||||
const self = this;
|
|
||||||
const ctrl = self.fullscreenPanel.ctrl;
|
|
||||||
|
|
||||||
ctrl.editMode = false;
|
delete this.fullscreenPanel;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$timeout(() => {
|
this.$timeout(() => {
|
||||||
if (self.oldTimeRange !== ctrl.range) {
|
appEvents.emit('dash-scroll', { restore: true });
|
||||||
self.$rootScope.$broadcast('refresh');
|
|
||||||
|
if (this.oldTimeRange !== this.dashboard.time) {
|
||||||
|
this.dashboard.startRefresh();
|
||||||
} else {
|
} else {
|
||||||
self.$rootScope.$broadcast('render');
|
this.dashboard.render();
|
||||||
}
|
}
|
||||||
delete self.fullscreenPanel;
|
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enterFullscreen(panelScope) {
|
enterFullscreen(panel) {
|
||||||
const ctrl = panelScope.ctrl;
|
const isEditing = this.state.edit && this.dashboard.meta.canEdit;
|
||||||
|
|
||||||
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
|
this.oldTimeRange = this.dashboard.time;
|
||||||
ctrl.fullscreen = true;
|
this.fullscreenPanel = panel;
|
||||||
|
|
||||||
this.oldTimeRange = ctrl.range;
|
|
||||||
this.fullscreenPanel = panelScope;
|
|
||||||
|
|
||||||
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
|
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
|
||||||
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||||
this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
|
this.dashboard.setViewMode(panel, true, isEditing);
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function dashboardViewStateSrv($location, $timeout, $rootScope) {
|
export function dashboardViewStateSrv($location, $timeout) {
|
||||||
return {
|
return {
|
||||||
create: $scope => {
|
create: $scope => {
|
||||||
return new DashboardViewState($scope, $location, $timeout, $rootScope);
|
return new DashboardViewState($scope, $location, $timeout);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
|||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import { getExploreUrl } from 'app/core/utils/explore';
|
import { getExploreUrl } from 'app/core/utils/explore';
|
||||||
|
|
||||||
import { metricsTabDirective } from './metrics_tab';
|
import { metricsTabDirective } from './metrics_tab';
|
||||||
|
|
||||||
class MetricsPanelCtrl extends PanelCtrl {
|
class MetricsPanelCtrl extends PanelCtrl {
|
||||||
scope: any;
|
scope: any;
|
||||||
datasource: any;
|
datasource: any;
|
||||||
datasourceName: any;
|
|
||||||
$q: any;
|
$q: any;
|
||||||
$timeout: any;
|
$timeout: any;
|
||||||
contextSrv: any;
|
contextSrv: any;
|
||||||
@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
this.scope = $scope;
|
this.scope = $scope;
|
||||||
this.panel.datasource = this.panel.datasource || null;
|
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('refresh', this.onMetricsPanelRefresh.bind(this));
|
||||||
this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
|
this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
|
||||||
this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
|
this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
|
||||||
@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onInitMetricsPanelEditMode() {
|
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');
|
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() {
|
getAdditionalMenuItems() {
|
||||||
const items = [];
|
const items = [];
|
||||||
if (
|
if (
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
// Libraries
|
||||||
|
import _ from 'lodash';
|
||||||
import Remarkable from 'remarkable';
|
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 {
|
export class MetricsTabCtrl {
|
||||||
dsName: string;
|
dsName: string;
|
||||||
panel: any;
|
panel: any;
|
||||||
@ -24,6 +32,9 @@ export class MetricsTabCtrl {
|
|||||||
$scope.ctrl = this;
|
$scope.ctrl = this;
|
||||||
|
|
||||||
this.panel = this.panelCtrl.panel;
|
this.panel = this.panelCtrl.panel;
|
||||||
|
this.panel.datasource = this.panel.datasource || null;
|
||||||
|
this.panel.targets = this.panel.targets || [{}];
|
||||||
|
|
||||||
this.dashboard = this.panelCtrl.dashboard;
|
this.dashboard = this.panelCtrl.dashboard;
|
||||||
this.datasources = datasourceSrv.getMetricSources();
|
this.datasources = datasourceSrv.getMetricSources();
|
||||||
this.panelDsValue = this.panelCtrl.panel.datasource;
|
this.panelDsValue = this.panelCtrl.panel.datasource;
|
||||||
@ -66,10 +77,29 @@ export class MetricsTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.datasourceInstance = option.datasource;
|
this.datasourceInstance = option.datasource;
|
||||||
this.panelCtrl.setDatasource(option.datasource);
|
this.setDatasource(option.datasource);
|
||||||
this.updateDatasourceOptions();
|
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) {
|
addMixedQuery(option) {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return;
|
return;
|
||||||
@ -120,3 +150,5 @@ export function metricsTabDirective() {
|
|||||||
controller: MetricsTabCtrl,
|
controller: MetricsTabCtrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coreModule.directive('metricsTab', metricsTabDirective);
|
||||||
|
@ -24,10 +24,8 @@ export class PanelCtrl {
|
|||||||
$injector: any;
|
$injector: any;
|
||||||
$location: any;
|
$location: any;
|
||||||
$timeout: any;
|
$timeout: any;
|
||||||
fullscreen: boolean;
|
|
||||||
inspector: any;
|
inspector: any;
|
||||||
editModeInitiated: boolean;
|
editModeInitiated: boolean;
|
||||||
editMode: any;
|
|
||||||
height: any;
|
height: any;
|
||||||
containerHeight: any;
|
containerHeight: any;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
@ -49,7 +47,6 @@ export class PanelCtrl {
|
|||||||
this.pluginName = plugin.name;
|
this.pluginName = plugin.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.$on('refresh', () => this.refresh());
|
|
||||||
$scope.$on('component-did-mount', () => this.panelDidMount());
|
$scope.$on('component-did-mount', () => this.panelDidMount());
|
||||||
|
|
||||||
$scope.$on('$destroy', () => {
|
$scope.$on('$destroy', () => {
|
||||||
@ -58,13 +55,9 @@ export class PanelCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
|
||||||
this.events.emit('panel-initialized');
|
|
||||||
this.publishAppEvent('panel-initialized', { scope: this.$scope });
|
|
||||||
}
|
|
||||||
|
|
||||||
panelDidMount() {
|
panelDidMount() {
|
||||||
this.events.emit('component-did-mount');
|
this.events.emit('component-did-mount');
|
||||||
|
this.dashboard.panelInitialized(this.panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderingCompleted() {
|
renderingCompleted() {
|
||||||
@ -72,7 +65,7 @@ export class PanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
this.events.emit('refresh', null);
|
this.panel.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
publishAppEvent(evtName, evt) {
|
publishAppEvent(evtName, evt) {
|
||||||
@ -102,6 +95,7 @@ export class PanelCtrl {
|
|||||||
initEditMode() {
|
initEditMode() {
|
||||||
this.editorTabs = [];
|
this.editorTabs = [];
|
||||||
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
|
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
|
||||||
|
|
||||||
this.editModeInitiated = true;
|
this.editModeInitiated = true;
|
||||||
this.events.emit('init-edit-mode', null);
|
this.events.emit('init-edit-mode', null);
|
||||||
|
|
||||||
@ -122,14 +116,15 @@ export class PanelCtrl {
|
|||||||
route.updateParams();
|
route.updateParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
addEditorTab(title, directiveFn, index?) {
|
addEditorTab(title, directiveFn, index?, icon?) {
|
||||||
const editorTab = { title, directiveFn };
|
const editorTab = { title, directiveFn, icon };
|
||||||
|
|
||||||
if (_.isString(directiveFn)) {
|
if (_.isString(directiveFn)) {
|
||||||
editorTab.directiveFn = () => {
|
editorTab.directiveFn = () => {
|
||||||
return { templateUrl: directiveFn };
|
return { templateUrl: directiveFn };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index) {
|
if (index) {
|
||||||
this.editorTabs.splice(index, 0, editorTab);
|
this.editorTabs.splice(index, 0, editorTab);
|
||||||
} else {
|
} else {
|
||||||
@ -190,7 +185,7 @@ export class PanelCtrl {
|
|||||||
|
|
||||||
getExtendedMenu() {
|
getExtendedMenu() {
|
||||||
const menu = [];
|
const menu = [];
|
||||||
if (!this.fullscreen && this.dashboard.meta.canEdit) {
|
if (!this.panel.fullscreen && this.dashboard.meta.canEdit) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: 'Duplicate',
|
text: 'Duplicate',
|
||||||
click: 'ctrl.duplicate()',
|
click: 'ctrl.duplicate()',
|
||||||
@ -220,15 +215,15 @@ export class PanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
otherPanelInFullscreenMode() {
|
otherPanelInFullscreenMode() {
|
||||||
return this.dashboard.meta.fullscreen && !this.fullscreen;
|
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatePanelHeight() {
|
calculatePanelHeight() {
|
||||||
if (this.fullscreen) {
|
if (this.panel.fullscreen) {
|
||||||
const docHeight = $(window).height();
|
const docHeight = $('.react-grid-layout').height();
|
||||||
const editHeight = Math.floor(docHeight * 0.4);
|
const editHeight = Math.floor(docHeight * 0.35);
|
||||||
const fullscreenHeight = Math.floor(docHeight * 0.8);
|
const fullscreenHeight = Math.floor(docHeight * 0.8);
|
||||||
this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
|
this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
|
||||||
} else {
|
} else {
|
||||||
this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
|
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();
|
this.containerHeight = $(window).height();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hacky solution
|
||||||
|
if (this.panel.isEditing && !this.editModeInitiated) {
|
||||||
|
this.initEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
|
this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,9 +247,6 @@ export class PanelCtrl {
|
|||||||
|
|
||||||
duplicate() {
|
duplicate() {
|
||||||
this.dashboard.duplicatePanel(this.panel);
|
this.dashboard.duplicatePanel(this.panel);
|
||||||
this.$timeout(() => {
|
|
||||||
this.$scope.$root.$broadcast('render');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removePanel() {
|
removePanel() {
|
||||||
|
@ -6,8 +6,10 @@ import baron from 'baron';
|
|||||||
const module = angular.module('grafana.directives');
|
const module = angular.module('grafana.directives');
|
||||||
|
|
||||||
const panelTemplate = `
|
const panelTemplate = `
|
||||||
|
<div ng-class="{'panel-editor-container': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
|
||||||
|
<div ng-class="{'panel-editor-container__panel': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
|
||||||
<div class="panel-container">
|
<div class="panel-container">
|
||||||
<div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.fullscreen}">
|
<div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.panel.fullscreen}">
|
||||||
<span class="panel-info-corner">
|
<span class="panel-info-corner">
|
||||||
<i class="fa"></i>
|
<i class="fa"></i>
|
||||||
<span class="panel-info-corner-inner"></span>
|
<span class="panel-info-corner-inner"></span>
|
||||||
@ -24,9 +26,11 @@ const panelTemplate = `
|
|||||||
<ng-transclude class="panel-height-helper"></ng-transclude>
|
<ng-transclude class="panel-height-helper"></ng-transclude>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel-full-edit" ng-if="ctrl.editMode">
|
<div ng-if="ctrl.panel.isEditing" ng-class="{'panel-editor-container__editor': ctrl.panel.isEditing,
|
||||||
<div class="tabbed-view tabbed-view--panel-edit">
|
'panel-height-helper': !ctrl.panel.isEditing}">
|
||||||
|
<div class="tabbed-view tabbed-view--new">
|
||||||
<div class="tabbed-view-header">
|
<div class="tabbed-view-header">
|
||||||
<h3 class="tabbed-view-panel-title">
|
<h3 class="tabbed-view-panel-title">
|
||||||
{{ctrl.pluginName}}
|
{{ctrl.pluginName}}
|
||||||
@ -46,12 +50,13 @@ const panelTemplate = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabbed-view-body">
|
<div class="tabbed-view-body">
|
||||||
<div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index">
|
<div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index" class="panel-height-helper">
|
||||||
<panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
|
<panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||||
@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
ctrl.dashboard.setPanelFocus(0);
|
ctrl.dashboard.setPanelFocus(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelHeightUpdated() {
|
|
||||||
panelContent.css({ height: ctrl.height + 'px' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeScrollableContent() {
|
function resizeScrollableContent() {
|
||||||
if (panelScrollbar) {
|
if (panelScrollbar) {
|
||||||
panelScrollbar.update();
|
panelScrollbar.update();
|
||||||
@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
|
|
||||||
ctrl.events.on('panel-size-changed', () => {
|
ctrl.events.on('panel-size-changed', () => {
|
||||||
ctrl.calculatePanelHeight();
|
ctrl.calculatePanelHeight();
|
||||||
panelHeightUpdated();
|
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
resizeScrollableContent();
|
resizeScrollableContent();
|
||||||
ctrl.render();
|
ctrl.render();
|
||||||
@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
|
|
||||||
// set initial height
|
// set initial height
|
||||||
ctrl.calculatePanelHeight();
|
ctrl.calculatePanelHeight();
|
||||||
panelHeightUpdated();
|
|
||||||
|
|
||||||
ctrl.events.on('render', () => {
|
ctrl.events.on('render', () => {
|
||||||
if (transparentLastState !== ctrl.panel.transparent) {
|
if (transparentLastState !== ctrl.panel.transparent) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
const directiveModule = angular.module('grafana.directives');
|
const directiveModule = angular.module('grafana.directives');
|
||||||
|
const directiveCache = {};
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
function panelEditorTab(dynamicDirectiveSrv) {
|
function panelEditorTab(dynamicDirectiveSrv) {
|
||||||
@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) {
|
|||||||
},
|
},
|
||||||
directive: scope => {
|
directive: scope => {
|
||||||
const pluginId = scope.ctrl.pluginId;
|
const pluginId = scope.ctrl.pluginId;
|
||||||
const tabIndex = scope.index;
|
const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-');
|
||||||
// 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();
|
|
||||||
|
|
||||||
return Promise.resolve({
|
if (directiveCache[pluginId]) {
|
||||||
name: `panel-editor-tab-${pluginId}${tabIndex}`,
|
if (directiveCache[pluginId][tabName]) {
|
||||||
fn: fn,
|
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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,21 +8,6 @@ const template = `
|
|||||||
<span class="panel-menu-container dropdown">
|
<span class="panel-menu-container dropdown">
|
||||||
<span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
|
<span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
|
||||||
<ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
<ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
||||||
<li>
|
|
||||||
<a ng-click="ctrl.addDataQuery(datasource);">
|
|
||||||
<i class="fa fa-cog"></i> Edit <span class="dropdown-menu-item-shortcut">e</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="dropdown-submenu">
|
|
||||||
<a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-cube"></i> Actions</a>
|
|
||||||
<ul class="dropdown-menu panel-menu">
|
|
||||||
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-flash"></i> Add Annotation</a></li>
|
|
||||||
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-bullseye"></i> Toggle Legend</a></li>
|
|
||||||
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-download"></i> Export to CSV</a></li>
|
|
||||||
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-eye"></i> View JSON</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
|
<span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label gf-query-ds-label">
|
|
||||||
<i class="icon-gf icon-gf-datasources"></i>
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">Data Source</label>
|
<label class="gf-form-label">Data Source</label>
|
||||||
|
|
||||||
<gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
|
<gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
|
||||||
lookup-text="true"
|
lookup-text="true"
|
||||||
get-options="ctrl.getOptions(true)"
|
get-options="ctrl.getOptions(true)"
|
||||||
|
73
public/app/features/panel/viz_tab.ts
Normal file
73
public/app/features/panel/viz_tab.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||||
|
import { VizTypePicker } from '../dashboard/dashgrid/VizTypePicker';
|
||||||
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
|
||||||
|
export class VizTabCtrl {
|
||||||
|
panelCtrl: any;
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope) {
|
||||||
|
this.panelCtrl = $scope.ctrl;
|
||||||
|
this.dashboard = this.panelCtrl.dashboard;
|
||||||
|
|
||||||
|
$scope.ctrl = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTypeChanged = (plugin: PanelPlugin) => {
|
||||||
|
this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="gf-form-group ">
|
||||||
|
<div class="gf-form-query">
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<img src="public/app/plugins/panel/graph/img/icn-graph-panel.svg" style="width: 16px; height: 16px" />
|
||||||
|
Graph
|
||||||
|
<i class="fa fa-caret-down" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<label class="gf-form-label gf-form-label--grow"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="query-editor-rows gf-form-group">
|
||||||
|
<div ng-repeat="tab in ctrl.panelCtrl.optionTabs">
|
||||||
|
<div class="gf-form-query">
|
||||||
|
<div class="gf-form gf-form-query-letter-cell">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<span class="gf-form-query-letter-cell-carret">
|
||||||
|
<i class="fa fa-caret-down"></i>
|
||||||
|
</span>
|
||||||
|
<span class="gf-form-query-letter-cell-letter">{{tab.title}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<label class="gf-form-label gf-form-label--grow"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
export function vizTabDirective() {
|
||||||
|
'use strict';
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: template,
|
||||||
|
controller: VizTabCtrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]);
|
||||||
|
coreModule.directive('vizTab', vizTabDirective);
|
@ -14,6 +14,8 @@ import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
|
|||||||
import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
|
import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
|
||||||
|
|
||||||
import * as textPanel from 'app/plugins/panel/text/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 graphPanel from 'app/plugins/panel/graph/module';
|
||||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||||
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||||
@ -40,6 +42,8 @@ const builtInPlugins = {
|
|||||||
'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
|
'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
|
||||||
|
|
||||||
'app/plugins/panel/text/module': textPanel,
|
'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/graph/module': graphPanel,
|
||||||
'app/plugins/panel/dashlist/module': dashListPanel,
|
'app/plugins/panel/dashlist/module': dashListPanel,
|
||||||
'app/plugins/panel/pluginlist/module': pluginsListPanel,
|
'app/plugins/panel/pluginlist/module': pluginsListPanel,
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
// Utils
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { importPluginModule } from './plugin_loader';
|
import { importPluginModule } from './plugin_loader';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { DataSourceApi } from 'app/types/series';
|
||||||
|
|
||||||
export class DatasourceSrv {
|
export class DatasourceSrv {
|
||||||
datasources: any;
|
datasources: any;
|
||||||
|
|
||||||
@ -15,7 +21,7 @@ export class DatasourceSrv {
|
|||||||
this.datasources = {};
|
this.datasources = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name?) {
|
get(name?): Promise<DataSourceApi> {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return this.get(config.defaultDatasource);
|
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);
|
coreModule.service('datasourceSrv', DatasourceSrv);
|
||||||
export default DatasourceSrv;
|
export default DatasourceSrv;
|
||||||
|
@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader';
|
|||||||
import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
|
import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
|
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
|
||||||
function getTemplate(component) {
|
function getTemplate(component) {
|
||||||
if (component.template) {
|
if (component.template) {
|
||||||
return $q.when(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.templatePromise = getTemplate(PanelCtrl).then(template => {
|
||||||
PanelCtrl.templateUrl = null;
|
PanelCtrl.templateUrl = null;
|
||||||
PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`;
|
PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-editor-container">${template}</grafana-panel>`;
|
||||||
return componentInfo;
|
return componentInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
|
|
||||||
// let a binding digest cycle complete before adding to dom
|
// let a binding digest cycle complete before adding to dom
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
scope.$applyAsync(() => {
|
||||||
elem.append(child);
|
elem.append(child);
|
||||||
|
setTimeout(() => {
|
||||||
scope.$applyAsync(() => {
|
scope.$applyAsync(() => {
|
||||||
scope.$broadcast('component-did-mount');
|
scope.$broadcast('component-did-mount');
|
||||||
scope.$broadcast('refresh');
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -245,7 +248,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
registerPluginComponent(scope, elem, attrs, componentInfo);
|
registerPluginComponent(scope, elem, attrs, componentInfo);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
$rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
|
|
||||||
console.log('Plugin component error', err);
|
console.log('Plugin component error', err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,7 @@ import config from 'app/core/config';
|
|||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
||||||
|
import { PluginExports } from 'app/types/plugins';
|
||||||
import * as datemath from 'app/core/utils/datemath';
|
import * as datemath from 'app/core/utils/datemath';
|
||||||
import * as fileExport from 'app/core/utils/file_export';
|
import * as fileExport from 'app/core/utils/file_export';
|
||||||
import * as flatten from 'app/core/utils/flatten';
|
import * as flatten from 'app/core/utils/flatten';
|
||||||
@ -140,11 +141,12 @@ const flotDeps = [
|
|||||||
'jquery.flot.events',
|
'jquery.flot.events',
|
||||||
'jquery.flot.gauge',
|
'jquery.flot.gauge',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const flotDep of flotDeps) {
|
for (const flotDep of flotDeps) {
|
||||||
exposeToPlugin(flotDep, { fakeDep: 1 });
|
exposeToPlugin(flotDep, { fakeDep: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importPluginModule(path: string): Promise<any> {
|
export function importPluginModule(path: string): Promise<PluginExports> {
|
||||||
const builtIn = builtInPlugins[path];
|
const builtIn = builtInPlugins[path];
|
||||||
if (builtIn) {
|
if (builtIn) {
|
||||||
return Promise.resolve(builtIn);
|
return Promise.resolve(builtIn);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import '../all';
|
import '../all';
|
||||||
import { VariableSrv } from '../variable_srv';
|
import { VariableSrv } from '../variable_srv';
|
||||||
|
import { DashboardModel } from '../../dashboard/dashboard_model';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import $q from 'q';
|
import $q from 'q';
|
||||||
|
|
||||||
@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) {
|
|||||||
return getVarMockConstructor(ctr, model, ctx);
|
return getVarMockConstructor(ctr, model, ctx);
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.variableSrv.init({
|
ctx.variableSrv.init(
|
||||||
|
new DashboardModel({
|
||||||
templating: { list: [] },
|
templating: { list: [] },
|
||||||
updateSubmenuVisibility: () => {},
|
updateSubmenuVisibility: () => {},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
|
scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
|
||||||
ctx.variableSrv.addVariable(scenario.variable);
|
ctx.variableSrv.addVariable(scenario.variable);
|
||||||
|
@ -2,6 +2,7 @@ import '../all';
|
|||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { VariableSrv } from '../variable_srv';
|
import { VariableSrv } from '../variable_srv';
|
||||||
|
import { DashboardModel } from '../../dashboard/dashboard_model';
|
||||||
import $q from 'q';
|
import $q from 'q';
|
||||||
|
|
||||||
describe('VariableSrv init', function(this: any) {
|
describe('VariableSrv init', function(this: any) {
|
||||||
@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) {
|
|||||||
ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
|
ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
|
||||||
|
|
||||||
ctx.variableSrv.$location.search = () => scenario.urlParams;
|
ctx.variableSrv.$location.search = () => scenario.urlParams;
|
||||||
ctx.variableSrv.dashboard = {
|
ctx.variableSrv.dashboard = new DashboardModel({
|
||||||
templating: { list: scenario.variables },
|
templating: { list: scenario.variables },
|
||||||
};
|
});
|
||||||
|
|
||||||
await ctx.variableSrv.init(ctx.variableSrv.dashboard);
|
await ctx.variableSrv.init(ctx.variableSrv.dashboard);
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
// Libaries
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// Utils & Services
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { variableTypes } from './variable';
|
import { variableTypes } from './variable';
|
||||||
import { Graph } from 'app/core/utils/dag';
|
import { Graph } from 'app/core/utils/dag';
|
||||||
@ -10,13 +13,12 @@ export class VariableSrv {
|
|||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
|
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);
|
$rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(dashboard) {
|
init(dashboard) {
|
||||||
this.dashboard = dashboard;
|
this.dashboard = dashboard;
|
||||||
|
this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
|
||||||
|
|
||||||
// create working class models representing variables
|
// create working class models representing variables
|
||||||
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
|
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
|
||||||
@ -39,11 +41,7 @@ export class VariableSrv {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDashboardRefresh(evt, payload) {
|
onTimeRangeUpdated() {
|
||||||
if (payload && payload.fromVariableValueUpdated) {
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
|
const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
|
||||||
const previousOptions = variable.options.slice();
|
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) {
|
processVariable(variable, queryParams) {
|
||||||
@ -133,7 +133,7 @@ export class VariableSrv {
|
|||||||
return this.$q.all(promises).then(() => {
|
return this.$q.all(promises).then(() => {
|
||||||
if (emitChangeEvents) {
|
if (emitChangeEvents) {
|
||||||
this.$rootScope.$emit('template-variable-value-updated');
|
this.$rootScope.$emit('template-variable-value-updated');
|
||||||
this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true });
|
this.dashboard.startRefresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,11 @@
|
|||||||
class="dashboard-settings">
|
class="dashboard-settings">
|
||||||
</dashboard-settings>
|
</dashboard-settings>
|
||||||
|
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container" ng-class="{'dashboard-container--has-submenu': ctrl.dashboard.meta.submenuEnabled}">
|
||||||
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
||||||
</dashboard-submenu>
|
</dashboard-submenu>
|
||||||
|
|
||||||
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
|
<dashboard-grid dashboard="ctrl.dashboard"></dashboard-grid>
|
||||||
</dashboard-grid>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export class CloudWatchQueryParameter {
|
export class CloudWatchQueryParameter {
|
||||||
@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
|
coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
|
||||||
angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);
|
coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import angular from 'angular';
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as queryDef from './query_def';
|
import * as queryDef from './query_def';
|
||||||
|
|
||||||
@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const module = angular.module('grafana.directives');
|
coreModule.directive('elasticBucketAgg', elasticBucketAgg);
|
||||||
module.directive('elasticBucketAgg', elasticBucketAgg);
|
coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);
|
||||||
module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import angular from 'angular';
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as queryDef from './query_def';
|
import * as queryDef from './query_def';
|
||||||
|
|
||||||
@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const module = angular.module('grafana.directives');
|
coreModule.directive('elasticMetricAgg', elasticMetricAgg);
|
||||||
module.directive('elasticMetricAgg', elasticMetricAgg);
|
coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);
|
||||||
module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function graphiteAddFunc($compile) {
|
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) {
|
function createFunctionDropDownMenu(funcDefs) {
|
||||||
const categories = {};
|
const categories = {};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
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);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import angular from 'angular';
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as options from './constants';
|
import * as options from './constants';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation);
|
coreModule.directive('stackdriverAggregation', StackdriverAggregation);
|
||||||
angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);
|
coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import angular from 'angular';
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
|
import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
@ -281,5 +281,5 @@ export class StackdriverFilterCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter);
|
coreModule.directive('stackdriverFilter', StackdriverFilter);
|
||||||
angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
|
coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
|
||||||
|
@ -62,7 +62,6 @@ class TestDataDatasource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
return { data: data };
|
return { data: data };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import baron from 'baron';
|
import baron from 'baron';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
const module = angular.module('grafana.directives');
|
coreModule.directive('graphLegend', (popoverSrv, $timeout) => {
|
||||||
|
|
||||||
module.directive('graphLegend', (popoverSrv, $timeout) => {
|
|
||||||
return {
|
return {
|
||||||
link: (scope, elem) => {
|
link: (scope, elem) => {
|
||||||
let firstRender = true;
|
let firstRender = true;
|
||||||
|
@ -134,9 +134,9 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInitEditMode() {
|
onInitEditMode() {
|
||||||
|
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
|
||||||
this.addEditorTab('Axes', axesEditorComponent, 2);
|
this.addEditorTab('Axes', axesEditorComponent, 2);
|
||||||
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
|
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) {
|
if (config.alertingEnabled) {
|
||||||
this.addEditorTab('Alert', alertTab, 5);
|
this.addEditorTab('Alert', alertTab, 5);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import angular from 'angular';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
|
export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
|
||||||
@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
|
|||||||
$scope.updateCurrentOverrides();
|
$scope.updateCurrentOverrides();
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);
|
coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl);
|
||||||
|
5
public/app/plugins/panel/graph2/README.md
Normal file
5
public/app/plugins/panel/graph2/README.md
Normal file
@ -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.
|
26
public/app/plugins/panel/graph2/img/icn-text-panel.svg
Normal file
26
public/app/plugins/panel/graph2/img/icn-text-panel.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<rect style="opacity:0.2;fill:#414042;" width="100" height="100"/>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="50" y1="88.2189" x2="50" y2="11.7811">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0.0595" style="stop-color:#FFE029"/>
|
||||||
|
<stop offset="0.1303" style="stop-color:#FFD218"/>
|
||||||
|
<stop offset="0.2032" style="stop-color:#FEC90F"/>
|
||||||
|
<stop offset="0.2809" style="stop-color:#FDC70C"/>
|
||||||
|
<stop offset="0.6685" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8876" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_1_);" d="M15.107,30.157h-2.593l0.395-18.376h74.183l0.395,18.376h-2.424
|
||||||
|
c-0.865-5.035-2.049-8.671-3.551-10.908c-1.504-2.235-3.12-3.607-4.848-4.115c-1.729-0.507-4.679-0.761-8.85-0.761H55.524V68.32
|
||||||
|
c0,5.975,0.141,9.903,0.423,11.781c0.282,1.88,1.043,3.27,2.283,4.171c1.24,0.902,3.57,1.353,6.99,1.353h3.72v2.593H30.834v-2.593
|
||||||
|
h3.946c3.269,0,5.533-0.413,6.793-1.24c1.258-0.826,2.066-2.114,2.424-3.861c0.357-1.747,0.535-5.815,0.535-12.204V14.374h-11.33
|
||||||
|
c-4.924,0-8.23,0.235-9.921,0.704c-1.691,0.471-3.279,1.888-4.764,4.256C17.032,21.702,15.896,25.31,15.107,30.157z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#898989;" d="M99,1v98H1V1H99 M100,0H0v100h100V0L100,0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
43
public/app/plugins/panel/graph2/module.tsx
Normal file
43
public/app/plugins/panel/graph2/module.tsx
Normal file
@ -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<Props> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { timeSeries, timeRange } = this.props;
|
||||||
|
|
||||||
|
const vmSeries = getTimeSeriesVMs({
|
||||||
|
timeSeries: timeSeries,
|
||||||
|
nullValueMode: NullValueMode.Ignore,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Graph timeSeries={vmSeries} timeRange={timeRange} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextOptions extends PureComponent<any> {
|
||||||
|
render() {
|
||||||
|
return <p>Text2 Options component</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Graph2 as PanelComponent, TextOptions as PanelOptions };
|
17
public/app/plugins/panel/graph2/plugin.json
Normal file
17
public/app/plugins/panel/graph2/plugin.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { tickStep } from 'app/core/utils/ticks';
|
import { tickStep } from 'app/core/utils/ticks';
|
||||||
import { getColorScale, getOpacityScale } from './color_scale';
|
import { getColorScale, getOpacityScale } from './color_scale';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
const module = angular.module('grafana.directives');
|
|
||||||
|
|
||||||
const LEGEND_HEIGHT_PX = 6;
|
const LEGEND_HEIGHT_PX = 6;
|
||||||
const LEGEND_WIDTH_PX = 100;
|
const LEGEND_WIDTH_PX = 100;
|
||||||
@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0;
|
|||||||
/**
|
/**
|
||||||
* Color legend for heatmap editor.
|
* Color legend for heatmap editor.
|
||||||
*/
|
*/
|
||||||
module.directive('colorLegend', () => {
|
coreModule.directive('colorLegend', () => {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
|
template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
|
||||||
@ -52,7 +50,7 @@ module.directive('colorLegend', () => {
|
|||||||
/**
|
/**
|
||||||
* Heatmap legend with scale values.
|
* Heatmap legend with scale values.
|
||||||
*/
|
*/
|
||||||
module.directive('heatmapLegend', () => {
|
coreModule.directive('heatmapLegend', () => {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
|
template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
|
||||||
|
5
public/app/plugins/panel/text2/README.md
Normal file
5
public/app/plugins/panel/text2/README.md
Normal file
@ -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.
|
186
public/app/plugins/panel/text2/img/icn-graph-panel.svg
Normal file
186
public/app/plugins/panel/text2/img/icn-graph-panel.svg
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<polyline style="fill:none;stroke:#898989;stroke-width:2;stroke-miterlimit:10;" points="4.734,34.349 36.05,19.26 64.876,36.751
|
||||||
|
96.308,6.946 "/>
|
||||||
|
<circle style="fill:#898989;" cx="4.885" cy="33.929" r="4.885"/>
|
||||||
|
<circle style="fill:#898989;" cx="35.95" cy="19.545" r="4.885"/>
|
||||||
|
<circle style="fill:#898989;" cx="65.047" cy="36.046" r="4.885"/>
|
||||||
|
<circle style="fill:#898989;" cx="94.955" cy="7.135" r="4.885"/>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5" y1="103.7019" x2="5" y2="32.0424">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_1_);" d="M9.001,48.173H0.999C0.447,48.173,0,48.62,0,49.172V100h10V49.172
|
||||||
|
C10,48.62,9.553,48.173,9.001,48.173z"/>
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5" y1="98.9423" x2="5" y2="53.1961">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_2_);" d="M0,69.173v30.563h10V69.173"/>
|
||||||
|
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="5" y1="99.4343" x2="5" y2="74.4359">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_3_);" d="M0,83.166v16.701h10V83.166"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="20" y1="103.7019" x2="20" y2="32.0424">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_4_);" d="M24.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
|
||||||
|
C25,41.216,24.553,40.769,24.001,40.769z"/>
|
||||||
|
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="20" y1="98.9423" x2="20" y2="53.1961">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_5_);" d="M15,64.716v35.02h10v-35.02"/>
|
||||||
|
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="20" y1="99.4343" x2="20" y2="74.4359">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_6_);" d="M15,80.731v19.137h10V80.731"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="35" y1="103.7019" x2="35" y2="32.0424">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_7_);" d="M39.001,34.423h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V35.422
|
||||||
|
C40,34.87,39.553,34.423,39.001,34.423z"/>
|
||||||
|
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="35" y1="98.9423" x2="35" y2="53.1961">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_8_);" d="M30,60.895v38.84h10v-38.84"/>
|
||||||
|
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="35" y1="99.4343" x2="35" y2="74.4359">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_9_);" d="M30,78.643v21.225h10V78.643"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="50" y1="103.7019" x2="50" y2="32.0424">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_10_);" d="M54.001,41.827h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V42.826
|
||||||
|
C55,42.274,54.553,41.827,54.001,41.827z"/>
|
||||||
|
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="50" y1="98.9423" x2="50" y2="53.1961">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_11_);" d="M45,65.352v34.383h10V65.352"/>
|
||||||
|
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="50" y1="99.4343" x2="50" y2="74.4359">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_12_);" d="M45,81.079v18.789h10V81.079"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="65" y1="103.8575" x2="65" y2="29.1875">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_13_);" d="M69.001,50.404h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V51.403
|
||||||
|
C70,50.851,69.553,50.404,69.001,50.404z"/>
|
||||||
|
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="65" y1="98.8979" x2="65" y2="51.2298">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_14_);" d="M60,70.531v29.193h10V70.531"/>
|
||||||
|
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="65" y1="99.4105" x2="65" y2="73.3619">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_15_);" d="M60,83.909v15.953h10V83.909"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="80" y1="104.4108" x2="80" y2="19.0293">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_16_);" d="M84.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
|
||||||
|
C85,41.216,84.553,40.769,84.001,40.769z"/>
|
||||||
|
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="80" y1="98.9423" x2="80" y2="53.1961">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_17_);" d="M75,64.716v35.02h10v-35.02"/>
|
||||||
|
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="80" y1="99.4343" x2="80" y2="74.4359">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_18_);" d="M75,80.731v19.137h10V80.731"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="95" y1="103.5838" x2="95" y2="34.2115">
|
||||||
|
<stop offset="0" style="stop-color:#FFF33B"/>
|
||||||
|
<stop offset="0" style="stop-color:#FFD53F"/>
|
||||||
|
<stop offset="0" style="stop-color:#FBBC40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F7A840"/>
|
||||||
|
<stop offset="0" style="stop-color:#F59B40"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3933F"/>
|
||||||
|
<stop offset="0" style="stop-color:#F3903F"/>
|
||||||
|
<stop offset="0.8423" style="stop-color:#ED683C"/>
|
||||||
|
<stop offset="1" style="stop-color:#E93E3A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_19_);" d="M99.001,21.157h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V22.156
|
||||||
|
C100,21.604,99.553,21.157,99.001,21.157z"/>
|
||||||
|
<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="95" y1="98.9761" x2="95" y2="54.69">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#F99B1C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_20_);" d="M90,52.898v46.846h10V52.898"/>
|
||||||
|
<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="95" y1="99.4524" x2="95" y2="75.2518">
|
||||||
|
<stop offset="0" style="stop-color:#FEBC11"/>
|
||||||
|
<stop offset="1" style="stop-color:#FFDE17"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_21_);" d="M90,74.272v25.6h10v-25.6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.8 KiB |
14
public/app/plugins/panel/text2/module.tsx
Normal file
14
public/app/plugins/panel/text2/module.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { PanelProps } from 'app/types';
|
||||||
|
|
||||||
|
export class Text2 extends PureComponent<PanelProps> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <h2>Text Panel!</h2>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Text2 as PanelComponent };
|
19
public/app/plugins/panel/text2/plugin.json
Normal file
19
public/app/plugins/panel/text2/plugin.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,10 @@ import appEvents from 'app/core/app_events';
|
|||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
import colors from 'app/core/utils/colors';
|
import colors from 'app/core/utils/colors';
|
||||||
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
|
import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
export class GrafanaCtrl {
|
export class GrafanaCtrl {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -23,12 +24,15 @@ export class GrafanaCtrl {
|
|||||||
contextSrv,
|
contextSrv,
|
||||||
bridgeSrv,
|
bridgeSrv,
|
||||||
backendSrv: BackendSrv,
|
backendSrv: BackendSrv,
|
||||||
|
timeSrv: TimeSrv,
|
||||||
datasourceSrv: DatasourceSrv,
|
datasourceSrv: DatasourceSrv,
|
||||||
angularLoader: AngularLoader
|
angularLoader: AngularLoader
|
||||||
) {
|
) {
|
||||||
// sets singleston instances for angular services so react components can access them
|
// make angular loader service available to react components
|
||||||
setAngularLoader(angularLoader);
|
setAngularLoader(angularLoader);
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
|
setDatasourceSrv(datasourceSrv);
|
||||||
|
setTimeSrv(timeSrv);
|
||||||
configureStore();
|
configureStore();
|
||||||
|
|
||||||
$scope.init = () => {
|
$scope.init = () => {
|
@ -8,6 +8,19 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
|||||||
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
|
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
|
||||||
import { Invitee, OrgUser, User, UsersState } from './user';
|
import { Invitee, OrgUser, User, UsersState } from './user';
|
||||||
import { DataSource, DataSourcesState } from './datasources';
|
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';
|
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -45,6 +58,17 @@ export {
|
|||||||
OrgUser,
|
OrgUser,
|
||||||
User,
|
User,
|
||||||
UsersState,
|
UsersState,
|
||||||
|
TimeRange,
|
||||||
|
LoadingState,
|
||||||
|
PanelProps,
|
||||||
|
TimeSeries,
|
||||||
|
TimeSeriesVM,
|
||||||
|
TimeSeriesVMs,
|
||||||
|
NullValueMode,
|
||||||
|
TimeSeriesStats,
|
||||||
|
DataQuery,
|
||||||
|
DataQueryResponse,
|
||||||
|
DataQueryOptions,
|
||||||
PluginDashboard,
|
PluginDashboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ export interface LocationUpdate {
|
|||||||
path?: string;
|
path?: string;
|
||||||
query?: UrlQueryMap;
|
query?: UrlQueryMap;
|
||||||
routeParams?: UrlQueryMap;
|
routeParams?: UrlQueryMap;
|
||||||
|
partial?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocationState {
|
export interface LocationState {
|
||||||
|
7
public/app/types/panel.ts
Normal file
7
public/app/types/panel.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { LoadingState, TimeSeries, TimeRange } from './series';
|
||||||
|
|
||||||
|
export interface PanelProps {
|
||||||
|
timeSeries: TimeSeries[];
|
||||||
|
timeRange: TimeRange;
|
||||||
|
loading: LoadingState;
|
||||||
|
}
|
@ -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 {
|
export interface PluginMeta {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
91
public/app/types/series.ts
Normal file
91
public/app/types/series.ts
Normal file
@ -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<DataQueryResponse>;
|
||||||
|
}
|
124
public/app/viz/Graph.tsx
Normal file
124
public/app/viz/Graph.tsx
Normal file
@ -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<GraphProps> {
|
||||||
|
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 (
|
||||||
|
<div className="graph-panel">
|
||||||
|
<div className="graph-panel__chart" ref={e => (this.element = e)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSize()(Graph);
|
168
public/app/viz/state/timeSeries.ts
Normal file
168
public/app/viz/state/timeSeries.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -97,6 +97,7 @@
|
|||||||
@import 'components/form_select_box';
|
@import 'components/form_select_box';
|
||||||
@import 'components/user-picker';
|
@import 'components/user-picker';
|
||||||
@import 'components/description-picker';
|
@import 'components/description-picker';
|
||||||
|
@import 'components/viz_editor';
|
||||||
@import 'components/delete_button';
|
@import 'components/delete_button';
|
||||||
@import 'components/add_data_source.scss';
|
@import 'components/add_data_source.scss';
|
||||||
@import 'components/page_loader';
|
@import 'components/page_loader';
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Disable grid interaction indicators in fullscreen panels
|
// Disable grid interaction indicators in fullscreen panels
|
||||||
|
|
||||||
.panel-header:hover {
|
.panel-header:hover {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
@ -32,6 +31,11 @@
|
|||||||
.react-resizable-handle {
|
.react-resizable-handle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the react-grid has a height transition
|
||||||
|
.react-grid-layout {
|
||||||
|
transition-property: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
|
@ -85,10 +85,6 @@
|
|||||||
height: calc(100% - 15px);
|
height: calc(100% - 15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-panel__item-icon {
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-panel__searchbar {
|
.add-panel__searchbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -307,6 +307,7 @@
|
|||||||
.view {
|
.view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-vertical {
|
.track-vertical {
|
||||||
@ -337,3 +338,7 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-margin-helper {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
.tabbed-view {
|
.tabbed-view {
|
||||||
padding: $spacer*3;
|
display: flex;
|
||||||
margin-bottom: $dashboard-padding;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&.tabbed-view--panel-edit {
|
&.tabbed-view--new {
|
||||||
padding: 0;
|
padding: 25px 0 0 0;
|
||||||
|
height: 100%;
|
||||||
.tabbed-view-header {
|
|
||||||
padding: 0px 25px;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-view-header {
|
.tabbed-view-header {
|
||||||
background: $page-header-bg;
|
|
||||||
box-shadow: $page-header-shadow;
|
box-shadow: $page-header-shadow;
|
||||||
border-bottom: 1px solid $page-header-border-color;
|
border-bottom: 1px solid $page-header-border-color;
|
||||||
@include clearfix();
|
@include clearfix();
|
||||||
@ -48,7 +44,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-view-body {
|
.tabbed-view-body {
|
||||||
padding: $spacer*2 $spacer;
|
padding: $spacer*2 $spacer $spacer $spacer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
&--small {
|
&--small {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
81
public/sass/components/_viz_editor.scss
Normal file
81
public/sass/components/_viz_editor.scss
Normal file
@ -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%);
|
||||||
|
}
|
@ -1,7 +1,12 @@
|
|||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding;
|
padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&--has-submenu {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-variable {
|
.template-variable {
|
||||||
@ -29,16 +34,43 @@ div.flot-text {
|
|||||||
height: 100%;
|
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 {
|
.panel-container {
|
||||||
background-color: $panel-bg;
|
background-color: $panel-bg;
|
||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&.panel-transparent {
|
&.panel-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.panel-menu-toggle {
|
||||||
|
visibility: visible;
|
||||||
|
transition: opacity 0.1s ease-in 0.2s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--is-editing {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-content {
|
.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 {
|
.panel-time-info {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
float: right;
|
float: right;
|
||||||
@ -233,5 +257,5 @@ div.flot-text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-full-edit {
|
.panel-full-edit {
|
||||||
margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding);
|
padding-top: $dashboard-padding;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user