mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15212 from grafana/dashboard-react-page
Dashboard react page
This commit is contained in:
commit
78ea7ae783
@ -85,6 +85,7 @@
|
||||
"prettier": "1.9.2",
|
||||
"react-hot-loader": "^4.3.6",
|
||||
"react-test-renderer": "^16.5.0",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"regexp-replace-loader": "^1.0.1",
|
||||
"sass-lint": "^1.10.2",
|
||||
"sass-loader": "^7.0.1",
|
||||
|
@ -5,6 +5,7 @@ type PlaylistDashboard struct {
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
Url string `json:"url"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]i
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
Url: m.GetDashboardUrl(item.Uid, item.Slug),
|
||||
Order: dashboardIDOrder[item.Id],
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Emitter } from './utils/emitter';
|
||||
|
||||
const appEvents = new Emitter();
|
||||
export const appEvents = new Emitter();
|
||||
|
||||
export default appEvents;
|
||||
|
42
public/app/core/components/AlertBox/AlertBox.tsx
Normal file
42
public/app/core/components/AlertBox/AlertBox.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
icon?: string;
|
||||
text?: string;
|
||||
severity: AppNotificationSeverity;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
function getIconFromSeverity(severity: AppNotificationSeverity): string {
|
||||
switch (severity) {
|
||||
case AppNotificationSeverity.Error: {
|
||||
return 'fa fa-exclamation-triangle';
|
||||
}
|
||||
case AppNotificationSeverity.Success: {
|
||||
return 'fa fa-check';
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const AlertBox: FunctionComponent<Props> = ({ title, icon, text, severity, onClose }) => {
|
||||
return (
|
||||
<div className={`alert alert-${severity}`}>
|
||||
<div className="alert-icon">
|
||||
<i className={icon || getIconFromSeverity(severity)} />
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title">{title}</div>
|
||||
{text && <div className="alert-text">{text}</div>}
|
||||
</div>
|
||||
{onClose && (
|
||||
<button type="button" className="alert-close" onClick={onClose}>
|
||||
<i className="fa fa fa-remove" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { AppNotification } from 'app/types';
|
||||
import { AlertBox } from '../AlertBox/AlertBox';
|
||||
|
||||
interface Props {
|
||||
appNotification: AppNotification;
|
||||
@ -22,18 +23,13 @@ export default class AppNotificationItem extends Component<Props> {
|
||||
const { appNotification, onClearNotification } = this.props;
|
||||
|
||||
return (
|
||||
<div className={`alert-${appNotification.severity} alert`}>
|
||||
<div className="alert-icon">
|
||||
<i className={appNotification.icon} />
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title">{appNotification.title}</div>
|
||||
<div className="alert-text">{appNotification.text}</div>
|
||||
</div>
|
||||
<button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
|
||||
<i className="fa fa fa-remove" />
|
||||
</button>
|
||||
</div>
|
||||
<AlertBox
|
||||
severity={appNotification.severity}
|
||||
title={appNotification.title}
|
||||
text={appNotification.text}
|
||||
icon={appNotification.icon}
|
||||
onClose={() => onClearNotification(appNotification.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,10 @@ interface Props {
|
||||
}
|
||||
|
||||
class Page extends Component<Props> {
|
||||
private bodyClass = 'is-react';
|
||||
private body = document.body;
|
||||
static Header = PageHeader;
|
||||
static Contents = PageContents;
|
||||
|
||||
componentDidMount() {
|
||||
this.body.classList.add(this.bodyClass);
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
@ -33,10 +30,6 @@ class Page extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.body.classList.remove(this.bodyClass);
|
||||
}
|
||||
|
||||
updateTitle = () => {
|
||||
const title = this.getPageTitle;
|
||||
document.title = title ? title + ' - Grafana' : 'Grafana';
|
||||
|
@ -1,40 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
<div class="scroll-canvas">
|
||||
<navbar model="model"></navbar>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<i class="{{::model.node.icon}}" ng-if="::model.node.icon"></i>
|
||||
<img ng-src="{{::model.node.img}}" ng-if="::model.node.img"></i>
|
||||
{{::model.node.text}}
|
||||
</h1>
|
||||
|
||||
<div class="page-header__actions" ng-transclude="header"></div>
|
||||
</div>
|
||||
|
||||
<div class="page-body" ng-transclude="body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export function gfPageDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
model: '=',
|
||||
},
|
||||
transclude: {
|
||||
header: '?gfPageHeader',
|
||||
body: 'gfPageBody',
|
||||
},
|
||||
link: (scope, elem, attrs) => {
|
||||
console.log(scope);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfPage', gfPageDirective);
|
@ -1,43 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export function pageScrollbar() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: (scope, elem, attrs) => {
|
||||
let lastPos = 0;
|
||||
|
||||
appEvents.on(
|
||||
'dash-scroll',
|
||||
evt => {
|
||||
if (evt.restore) {
|
||||
elem[0].scrollTop = lastPos;
|
||||
return;
|
||||
}
|
||||
|
||||
lastPos = elem[0].scrollTop;
|
||||
|
||||
if (evt.animate) {
|
||||
elem.animate({ scrollTop: evt.pos }, 500);
|
||||
} else {
|
||||
elem[0].scrollTop = evt.pos;
|
||||
}
|
||||
},
|
||||
scope
|
||||
);
|
||||
|
||||
scope.$on('$routeChangeSuccess', () => {
|
||||
lastPos = 0;
|
||||
elem[0].scrollTop = 0;
|
||||
// Focus page to enable scrolling by keyboard
|
||||
elem[0].focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
elem[0].tabIndex = -1;
|
||||
// Focus page to enable scrolling by keyboard
|
||||
elem[0].focus({ preventScroll: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('pageScrollbar', pageScrollbar);
|
@ -68,5 +68,5 @@ const bootData = (window as any).grafanaBootData || {
|
||||
const options = bootData.settings;
|
||||
options.bootData = bootData;
|
||||
|
||||
const config = new Settings(options);
|
||||
export const config = new Settings(options);
|
||||
export default config;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
|
||||
const defaultSuccessNotification: AppNotification = {
|
||||
title: '',
|
||||
@ -31,12 +32,14 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti
|
||||
id: Date.now(),
|
||||
});
|
||||
|
||||
export const createErrorNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultErrorNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
export const createErrorNotification = (title: string, text?: any): AppNotification => {
|
||||
return {
|
||||
...defaultErrorNotification,
|
||||
title: title,
|
||||
text: getMessageFromError(text),
|
||||
id: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createWarningNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultWarningNotification,
|
||||
|
@ -43,8 +43,6 @@ import { helpModal } from './components/help/help';
|
||||
import { JsonExplorer } from './components/json_explorer/json_explorer';
|
||||
import { NavModelSrv, NavModel } from './nav_model_srv';
|
||||
import { geminiScrollbar } from './components/scroll/scroll';
|
||||
import { pageScrollbar } from './components/scroll/page_scroll';
|
||||
import { gfPageDirective } from './components/gf_page';
|
||||
import { orgSwitcher } from './components/org_switcher';
|
||||
import { profiler } from './profiler';
|
||||
import { registerAngularDirectives } from './angular_wrappers';
|
||||
@ -79,8 +77,6 @@ export {
|
||||
NavModelSrv,
|
||||
NavModel,
|
||||
geminiScrollbar,
|
||||
pageScrollbar,
|
||||
gfPageDirective,
|
||||
orgSwitcher,
|
||||
manageDashboardsDirective,
|
||||
TimeSeries,
|
||||
|
@ -8,12 +8,13 @@ export const initialState: LocationState = {
|
||||
path: '',
|
||||
query: {},
|
||||
routeParams: {},
|
||||
replace: false,
|
||||
};
|
||||
|
||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||
switch (action.type) {
|
||||
case CoreActionTypes.UpdateLocation: {
|
||||
const { path, routeParams } = action.payload;
|
||||
const { path, routeParams, replace } = action.payload;
|
||||
let query = action.payload.query || state.query;
|
||||
|
||||
if (action.payload.partial) {
|
||||
@ -26,6 +27,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
|
||||
path: path || state.path,
|
||||
query: { ...query },
|
||||
routeParams: routeParams || state.routeParams,
|
||||
replace: replace === true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -53,5 +53,20 @@ export const noPayloadActionCreatorFactory = (type: string): NoPayloadActionCrea
|
||||
return { create };
|
||||
};
|
||||
|
||||
export interface NoPayloadActionCreatorMock extends NoPayloadActionCreator {
|
||||
calls: number;
|
||||
}
|
||||
|
||||
export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator): NoPayloadActionCreatorMock => {
|
||||
const mock: NoPayloadActionCreatorMock = Object.assign(
|
||||
(): ActionOf<undefined> => {
|
||||
mock.calls++;
|
||||
return { type: creator.type, payload: undefined };
|
||||
},
|
||||
{ type: creator.type, calls: 0 }
|
||||
);
|
||||
return mock;
|
||||
};
|
||||
|
||||
// Should only be used by tests
|
||||
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
|
||||
|
@ -1,4 +1,2 @@
|
||||
import { actionCreatorFactory } from './actionCreatorFactory';
|
||||
import { reducerFactory } from './reducerFactory';
|
||||
|
||||
export { actionCreatorFactory, reducerFactory };
|
||||
export * from './actionCreatorFactory';
|
||||
export * from './reducerFactory';
|
||||
|
14
public/app/core/services/__mocks__/backend_srv.ts
Normal file
14
public/app/core/services/__mocks__/backend_srv.ts
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
getDashboardByUid: jest.fn(),
|
||||
getFolderByUid: jest.fn(),
|
||||
post: jest.fn(),
|
||||
};
|
||||
|
||||
export function getBackendSrv() {
|
||||
return backendSrv;
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,10 @@ export class BridgeSrv {
|
||||
if (angularUrl !== url) {
|
||||
this.$timeout(() => {
|
||||
this.$location.url(url);
|
||||
// some state changes should not trigger new browser history
|
||||
if (state.location.replace) {
|
||||
this.$location.replace();
|
||||
}
|
||||
});
|
||||
console.log('store updating angular $location.url', url);
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export class KeybindingSrv {
|
||||
}
|
||||
|
||||
if (search.fullscreen) {
|
||||
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
|
||||
appEvents.emit('panel-change-view', { fullscreen: false, edit: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ export class KeybindingSrv {
|
||||
// edit panel
|
||||
this.bind('e', () => {
|
||||
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
|
||||
this.$rootScope.appEvent('panel-change-view', {
|
||||
appEvents.emit('panel-change-view', {
|
||||
fullscreen: true,
|
||||
edit: true,
|
||||
panelId: dashboard.meta.focusPanelId,
|
||||
@ -186,7 +186,7 @@ export class KeybindingSrv {
|
||||
// view panel
|
||||
this.bind('v', () => {
|
||||
if (dashboard.meta.focusPanelId) {
|
||||
this.$rootScope.appEvent('panel-change-view', {
|
||||
appEvents.emit('panel-change-view', {
|
||||
fullscreen: true,
|
||||
edit: null,
|
||||
panelId: dashboard.meta.focusPanelId,
|
||||
@ -212,9 +212,7 @@ export class KeybindingSrv {
|
||||
// delete panel
|
||||
this.bind('p r', () => {
|
||||
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
|
||||
this.$rootScope.appEvent('panel-remove', {
|
||||
panelId: dashboard.meta.focusPanelId,
|
||||
});
|
||||
appEvents.emit('remove-panel', dashboard.meta.focusPanelId);
|
||||
dashboard.meta.focusPanelId = 0;
|
||||
}
|
||||
});
|
||||
|
17
public/app/core/utils/errors.ts
Normal file
17
public/app/core/utils/errors.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export function getMessageFromError(err: any): string | null {
|
||||
if (err && !_.isString(err)) {
|
||||
if (err.message) {
|
||||
return err.message;
|
||||
} else if (err.data && err.data.message) {
|
||||
return err.data.message;
|
||||
} else if (err.statusText) {
|
||||
return err.statusText;
|
||||
} else {
|
||||
return JSON.stringify(err);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const stripBaseFromUrl = url => {
|
||||
export const stripBaseFromUrl = (url: string): string => {
|
||||
const appSubUrl = config.appSubUrl;
|
||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||
const urlWithoutBase =
|
||||
|
@ -2,6 +2,7 @@ import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
export class AnnotationsEditorCtrl {
|
||||
mode: any;
|
||||
@ -10,6 +11,7 @@ export class AnnotationsEditorCtrl {
|
||||
currentAnnotation: any;
|
||||
currentDatasource: any;
|
||||
currentIsNew: any;
|
||||
dashboard: DashboardModel;
|
||||
|
||||
annotationDefaults: any = {
|
||||
name: '',
|
||||
@ -26,9 +28,10 @@ export class AnnotationsEditorCtrl {
|
||||
constructor($scope, private datasourceSrv) {
|
||||
$scope.ctrl = this;
|
||||
|
||||
this.dashboard = $scope.dashboard;
|
||||
this.mode = 'list';
|
||||
this.datasources = datasourceSrv.getAnnotationSources();
|
||||
this.annotations = $scope.dashboard.annotations.list;
|
||||
this.annotations = this.dashboard.annotations.list;
|
||||
this.reset();
|
||||
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
@ -78,11 +81,13 @@ export class AnnotationsEditorCtrl {
|
||||
this.annotations.push(this.currentAnnotation);
|
||||
this.reset();
|
||||
this.mode = 'list';
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
removeAnnotation(annotation) {
|
||||
const index = _.indexOf(this.annotations, annotation);
|
||||
this.annotations.splice(index, 1);
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
onColorChange(newColor) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
export class AdHocFiltersCtrl {
|
||||
segments: any;
|
||||
variable: any;
|
||||
dashboard: DashboardModel;
|
||||
removeTagFilterSegment: any;
|
||||
|
||||
/** @ngInject */
|
||||
@ -14,14 +16,13 @@ export class AdHocFiltersCtrl {
|
||||
private $q,
|
||||
private variableSrv,
|
||||
$scope,
|
||||
private $rootScope
|
||||
) {
|
||||
this.removeTagFilterSegment = uiSegmentSrv.newSegment({
|
||||
fake: true,
|
||||
value: '-- remove filter --',
|
||||
});
|
||||
this.buildSegmentModel();
|
||||
this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope);
|
||||
this.dashboard.events.on('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope);
|
||||
}
|
||||
|
||||
buildSegmentModel() {
|
||||
@ -171,6 +172,7 @@ export function adHocFiltersComponent() {
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
variable: '=',
|
||||
dashboard: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
export let iconMap = {
|
||||
'external link': 'fa-external-link',
|
||||
@ -12,7 +13,7 @@ export let iconMap = {
|
||||
};
|
||||
|
||||
export class DashLinksEditorCtrl {
|
||||
dashboard: any;
|
||||
dashboard: DashboardModel;
|
||||
iconMap: any;
|
||||
mode: any;
|
||||
link: any;
|
||||
@ -40,6 +41,7 @@ export class DashLinksEditorCtrl {
|
||||
addLink() {
|
||||
this.dashboard.links.push(this.link);
|
||||
this.mode = 'list';
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
editLink(link) {
|
||||
|
253
public/app/features/dashboard/components/DashNav/DashNav.tsx
Normal file
253
public/app/features/dashboard/components/DashNav/DashNav.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
// Libaries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
|
||||
|
||||
// Components
|
||||
import { DashNavButton } from './DashNavButton';
|
||||
|
||||
// State
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
editview: string;
|
||||
isEditing: boolean;
|
||||
isFullscreen: boolean;
|
||||
$injector: any;
|
||||
updateLocation: typeof updateLocation;
|
||||
onAddPanel: () => void;
|
||||
}
|
||||
|
||||
export class DashNav extends PureComponent<Props> {
|
||||
timePickerEl: HTMLElement;
|
||||
timepickerCmp: AngularComponent;
|
||||
playlistSrv: PlaylistSrv;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.playlistSrv = this.props.$injector.get('playlistSrv');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const loader = getAngularLoader();
|
||||
|
||||
const template =
|
||||
'<gf-time-picker class="gf-timepicker-nav" dashboard="dashboard" ng-if="!dashboard.timepicker.hidden" />';
|
||||
const scopeProps = { dashboard: this.props.dashboard };
|
||||
|
||||
this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timepickerCmp) {
|
||||
this.timepickerCmp.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onOpenSearch = () => {
|
||||
appEvents.emit('show-dash-search');
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
if (this.props.editview) {
|
||||
this.props.updateLocation({
|
||||
query: { editview: null },
|
||||
partial: true,
|
||||
});
|
||||
} else {
|
||||
this.props.updateLocation({
|
||||
query: { panelId: null, edit: null, fullscreen: null },
|
||||
partial: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onToggleTVMode = () => {
|
||||
appEvents.emit('toggle-kiosk-mode');
|
||||
};
|
||||
|
||||
onSave = () => {
|
||||
const { $injector } = this.props;
|
||||
const dashboardSrv = $injector.get('dashboardSrv');
|
||||
dashboardSrv.saveDashboard();
|
||||
};
|
||||
|
||||
onOpenSettings = () => {
|
||||
this.props.updateLocation({
|
||||
query: { editview: 'settings' },
|
||||
partial: true,
|
||||
});
|
||||
};
|
||||
|
||||
onStarDashboard = () => {
|
||||
const { dashboard, $injector } = this.props;
|
||||
const dashboardSrv = $injector.get('dashboardSrv');
|
||||
|
||||
dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then(newState => {
|
||||
dashboard.meta.isStarred = newState;
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
onPlaylistPrev = () => {
|
||||
this.playlistSrv.prev();
|
||||
};
|
||||
|
||||
onPlaylistNext = () => {
|
||||
this.playlistSrv.next();
|
||||
};
|
||||
|
||||
onPlaylistStop = () => {
|
||||
this.playlistSrv.stop();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onOpenShare = () => {
|
||||
const $rootScope = this.props.$injector.get('$rootScope');
|
||||
const modalScope = $rootScope.$new();
|
||||
modalScope.tabIndex = 0;
|
||||
modalScope.dashboard = this.props.dashboard;
|
||||
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/components/ShareModal/template.html',
|
||||
scope: modalScope,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, isFullscreen, editview, onAddPanel } = this.props;
|
||||
const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta;
|
||||
const { snapshot } = dashboard;
|
||||
|
||||
const haveFolder = dashboard.meta.folderId > 0;
|
||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
<div>
|
||||
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
||||
<i className="gicon gicon-dashboard" />
|
||||
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
|
||||
{dashboard.title}
|
||||
<i className="fa fa-caret-down" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="navbar__spacer" />
|
||||
|
||||
{this.playlistSrv.isPlaying && (
|
||||
<div className="navbar-buttons navbar-buttons--playlist">
|
||||
<DashNavButton
|
||||
tooltip="Go to previous dashboard"
|
||||
classSuffix="tight"
|
||||
icon="fa fa-step-backward"
|
||||
onClick={this.onPlaylistPrev}
|
||||
/>
|
||||
<DashNavButton
|
||||
tooltip="Stop playlist"
|
||||
classSuffix="tight"
|
||||
icon="fa fa-stop"
|
||||
onClick={this.onPlaylistStop}
|
||||
/>
|
||||
<DashNavButton
|
||||
tooltip="Go to next dashboard"
|
||||
classSuffix="tight"
|
||||
icon="fa fa-forward"
|
||||
onClick={this.onPlaylistNext}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="navbar-buttons navbar-buttons--actions">
|
||||
{canSave && (
|
||||
<DashNavButton
|
||||
tooltip="Add panel"
|
||||
classSuffix="add-panel"
|
||||
icon="gicon gicon-add-panel"
|
||||
onClick={onAddPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canStar && (
|
||||
<DashNavButton
|
||||
tooltip="Mark as favorite"
|
||||
classSuffix="star"
|
||||
icon={`${isStarred ? 'fa fa-star' : 'fa fa-star-o'}`}
|
||||
onClick={this.onStarDashboard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canShare && (
|
||||
<DashNavButton
|
||||
tooltip="Share dashboard"
|
||||
classSuffix="share"
|
||||
icon="fa fa-share-square-o"
|
||||
onClick={this.onOpenShare}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canSave && (
|
||||
<DashNavButton tooltip="Save dashboard" classSuffix="save" icon="fa fa-save" onClick={this.onSave} />
|
||||
)}
|
||||
|
||||
{snapshotUrl && (
|
||||
<DashNavButton
|
||||
tooltip="Open original dashboard"
|
||||
classSuffix="snapshot-origin"
|
||||
icon="fa fa-link"
|
||||
href={snapshotUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSettings && (
|
||||
<DashNavButton
|
||||
tooltip="Dashboard settings"
|
||||
classSuffix="settings"
|
||||
icon="fa fa-cog"
|
||||
onClick={this.onOpenSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="navbar-buttons navbar-buttons--tv">
|
||||
<DashNavButton
|
||||
tooltip="Cycke view mode"
|
||||
classSuffix="tv"
|
||||
icon="fa fa-desktop"
|
||||
onClick={this.onToggleTVMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
||||
|
||||
{(isFullscreen || editview) && (
|
||||
<div className="navbar-buttons navbar-buttons--close">
|
||||
<DashNavButton
|
||||
tooltip="Back to dashboard"
|
||||
classSuffix="primary"
|
||||
icon="fa fa-reply"
|
||||
onClick={this.onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DashNav);
|
@ -0,0 +1,33 @@
|
||||
// Libraries
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
classSuffix: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export const DashNavButton: FunctionComponent<Props> = ({ icon, tooltip, classSuffix, onClick, href }) => {
|
||||
if (onClick) {
|
||||
return (
|
||||
<Tooltip content={tooltip}>
|
||||
<button className={`btn navbar-button navbar-button--${classSuffix}`} onClick={onClick}>
|
||||
<i className={icon} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip}>
|
||||
<a className={`btn navbar-button navbar-button--${classSuffix}`} href={href}>
|
||||
<i className={icon} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -1,119 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import angular from 'angular';
|
||||
import { appEvents, NavModel } from 'app/core/core';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
|
||||
export class DashNavCtrl {
|
||||
dashboard: DashboardModel;
|
||||
navModel: NavModel;
|
||||
titleTooltip: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private dashboardSrv, private $location, public playlistSrv) {
|
||||
appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope);
|
||||
|
||||
if (this.dashboard.meta.isSnapshot) {
|
||||
const meta = this.dashboard.meta;
|
||||
this.titleTooltip = 'Created: ' + moment(meta.created).calendar();
|
||||
if (meta.expires) {
|
||||
this.titleTooltip += '<br>Expires: ' + moment(meta.expires).fromNow() + '<br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleSettings() {
|
||||
const search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
} else {
|
||||
search.editview = 'settings';
|
||||
}
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
||||
toggleViewMode() {
|
||||
appEvents.emit('toggle-kiosk-mode');
|
||||
}
|
||||
|
||||
close() {
|
||||
const search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
} else if (search.fullscreen) {
|
||||
delete search.fullscreen;
|
||||
delete search.edit;
|
||||
delete search.tab;
|
||||
delete search.panelId;
|
||||
}
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
||||
starDashboard() {
|
||||
this.dashboardSrv.starDashboard(this.dashboard.id, this.dashboard.meta.isStarred).then(newState => {
|
||||
this.dashboard.meta.isStarred = newState;
|
||||
});
|
||||
}
|
||||
|
||||
shareDashboard(tabIndex) {
|
||||
const modalScope = this.$scope.$new();
|
||||
modalScope.tabIndex = tabIndex;
|
||||
modalScope.dashboard = this.dashboard;
|
||||
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/components/ShareModal/template.html',
|
||||
scope: modalScope,
|
||||
});
|
||||
}
|
||||
|
||||
hideTooltip(evt) {
|
||||
angular.element(evt.currentTarget).tooltip('hide');
|
||||
}
|
||||
|
||||
saveDashboard() {
|
||||
return this.dashboardSrv.saveDashboard();
|
||||
}
|
||||
|
||||
showSearch() {
|
||||
if (this.dashboard.meta.fullscreen) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
appEvents.emit('show-dash-search');
|
||||
}
|
||||
|
||||
addPanel() {
|
||||
appEvents.emit('dash-scroll', { animate: true, evt: 0 });
|
||||
|
||||
if (this.dashboard.panels.length > 0 && this.dashboard.panels[0].type === 'add-panel') {
|
||||
return; // Return if the "Add panel" exists already
|
||||
}
|
||||
|
||||
this.dashboard.addPanel({
|
||||
type: 'add-panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 8 },
|
||||
title: 'Panel Title',
|
||||
});
|
||||
}
|
||||
|
||||
navItemClicked(navItem, evt) {
|
||||
if (navItem.clickHandler) {
|
||||
navItem.clickHandler();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dashNavDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/components/DashNav/template.html',
|
||||
controller: DashNavCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
transclude: true,
|
||||
scope: { dashboard: '=' },
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('grafana.directives').directive('dashnav', dashNavDirective);
|
@ -1 +1,2 @@
|
||||
export { DashNavCtrl } from './DashNavCtrl';
|
||||
import DashNav from './DashNav';
|
||||
export { DashNav };
|
||||
|
@ -1,61 +0,0 @@
|
||||
<div class="navbar">
|
||||
|
||||
<div>
|
||||
<a class="navbar-page-btn" ng-click="ctrl.showSearch()">
|
||||
<i class="gicon gicon-dashboard"></i>
|
||||
<span ng-if="ctrl.dashboard.meta.folderId > 0" class="navbar-page-btn--folder">{{ctrl.dashboard.meta.folderTitle}} / </span>{{ctrl.dashboard.title}}
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar__spacer"></div>
|
||||
|
||||
<div class="navbar-buttons navbar-buttons--playlist" ng-if="ctrl.playlistSrv.isPlaying">
|
||||
<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
|
||||
<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
|
||||
<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-buttons navbar-buttons--actions">
|
||||
<button class="btn navbar-button navbar-button--add-panel" ng-show="::ctrl.dashboard.meta.canSave" bs-tooltip="'Add panel'" data-placement="bottom" ng-click="ctrl.addPanel()">
|
||||
<i class="gicon gicon-add-panel"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button navbar-button--star" ng-show="::ctrl.dashboard.meta.canStar" ng-click="ctrl.starDashboard()" bs-tooltip="'Mark as favorite'" data-placement="bottom">
|
||||
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button navbar-button--share" ng-show="::ctrl.dashboard.meta.canShare" ng-click="ctrl.shareDashboard(0)" bs-tooltip="'Share dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-share-square-o"></i></a>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button navbar-button--save" ng-show="ctrl.dashboard.meta.canSave" ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
|
||||
<a class="btn navbar-button navbar-button--snapshot-origin" ng-if="::ctrl.dashboard.snapshot.originalUrl" href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-link"></i>
|
||||
</a>
|
||||
|
||||
<button class="btn navbar-button navbar-button--settings" ng-click="ctrl.toggleSettings()" bs-tooltip="'Dashboard Settings'" data-placement="bottom" ng-show="ctrl.dashboard.meta.showSettings">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="navbar-buttons navbar-buttons--tv">
|
||||
<button class="btn navbar-button navbar-button--tv" ng-click="ctrl.toggleViewMode()" bs-tooltip="'Cycle view mode'" data-placement="bottom">
|
||||
<i class="fa fa-desktop"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<gf-time-picker class="gf-timepicker-nav" dashboard="ctrl.dashboard" ng-if="!ctrl.dashboard.timepicker.hidden"></gf-time-picker>
|
||||
|
||||
<div class="navbar-buttons navbar-buttons--close">
|
||||
<button class="btn navbar-button navbar-button--primary" ng-click="ctrl.close()" bs-tooltip="'Back to dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-reply"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<dashboard-search></dashboard-search>
|
@ -9,6 +9,7 @@ describe('DashboardRow', () => {
|
||||
beforeEach(() => {
|
||||
dashboardMock = {
|
||||
toggleRow: jest.fn(),
|
||||
on: jest.fn(),
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
|
@ -18,11 +18,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
collapsed: this.props.panel.collapsed,
|
||||
};
|
||||
|
||||
appEvents.on('template-variable-value-updated', this.onVariableUpdated);
|
||||
this.props.dashboard.on('template-variable-value-updated', this.onVariableUpdated);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
appEvents.off('template-variable-value-updated', this.onVariableUpdated);
|
||||
this.props.dashboard.off('template-variable-value-updated', this.onVariableUpdated);
|
||||
}
|
||||
|
||||
onVariableUpdated = () => {
|
||||
|
@ -0,0 +1,36 @@
|
||||
// Libaries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardModel | null;
|
||||
}
|
||||
|
||||
export class DashboardSettings extends PureComponent<Props> {
|
||||
element: HTMLElement;
|
||||
angularCmp: AngularComponent;
|
||||
|
||||
componentDidMount() {
|
||||
const loader = getAngularLoader();
|
||||
|
||||
const template = '<dashboard-settings dashboard="dashboard" class="dashboard-settings" />';
|
||||
const scopeProps = { dashboard: this.props.dashboard };
|
||||
|
||||
this.angularCmp = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.angularCmp) {
|
||||
this.angularCmp.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="panel-height-helper" ref={element => this.element = element} />;
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export { SettingsCtrl } from './SettingsCtrl';
|
||||
export { DashboardSettings } from './DashboardSettings';
|
||||
|
36
public/app/features/dashboard/components/SubMenu/SubMenu.tsx
Normal file
36
public/app/features/dashboard/components/SubMenu/SubMenu.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
// Libaries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardModel | null;
|
||||
}
|
||||
|
||||
export class SubMenu extends PureComponent<Props> {
|
||||
element: HTMLElement;
|
||||
angularCmp: AngularComponent;
|
||||
|
||||
componentDidMount() {
|
||||
const loader = getAngularLoader();
|
||||
|
||||
const template = '<dashboard-submenu dashboard="dashboard" />';
|
||||
const scopeProps = { dashboard: this.props.dashboard };
|
||||
|
||||
this.angularCmp = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.angularCmp) {
|
||||
this.angularCmp.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={element => this.element = element} />;
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export { SubMenuCtrl } from './SubMenuCtrl';
|
||||
export { SubMenu } from './SubMenu';
|
||||
|
@ -7,7 +7,7 @@
|
||||
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
||||
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
|
||||
</div>
|
||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
|
||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
||||
|
@ -1,156 +0,0 @@
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { removePanel } from 'app/features/dashboard/utils/panel';
|
||||
|
||||
// Services
|
||||
import { AnnotationsSrv } from '../../annotations/annotations_srv';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
export class DashboardCtrl {
|
||||
dashboard: DashboardModel;
|
||||
dashboardViewState: any;
|
||||
loadedFallbackDashboard: boolean;
|
||||
editTab: number;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $scope,
|
||||
private keybindingSrv,
|
||||
private timeSrv,
|
||||
private variableSrv,
|
||||
private dashboardSrv,
|
||||
private unsavedChangesSrv,
|
||||
private dashboardViewStateSrv,
|
||||
private annotationsSrv: AnnotationsSrv,
|
||||
public playlistSrv
|
||||
) {
|
||||
// temp hack due to way dashboards are loaded
|
||||
// can't use controllerAs on route yet
|
||||
$scope.ctrl = this;
|
||||
|
||||
// TODO: break out settings view to separate view & controller
|
||||
this.editTab = 0;
|
||||
|
||||
// funcs called from React component bindings and needs this binding
|
||||
this.getPanelContainer = this.getPanelContainer.bind(this);
|
||||
}
|
||||
|
||||
setupDashboard(data) {
|
||||
try {
|
||||
this.setupDashboardInternal(data);
|
||||
} catch (err) {
|
||||
this.onInitFailed(err, 'Dashboard init failed', true);
|
||||
}
|
||||
}
|
||||
|
||||
setupDashboardInternal(data) {
|
||||
const dashboard = this.dashboardSrv.create(data.dashboard, data.meta);
|
||||
this.dashboardSrv.setCurrent(dashboard);
|
||||
|
||||
// init services
|
||||
this.timeSrv.init(dashboard);
|
||||
this.annotationsSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
this.variableSrv
|
||||
.init(dashboard)
|
||||
// template values failes are non fatal
|
||||
.catch(this.onInitFailed.bind(this, 'Templating init failed', false))
|
||||
// continue
|
||||
.finally(() => {
|
||||
this.dashboard = dashboard;
|
||||
this.dashboard.processRepeats();
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
this.dashboard.autoFitPanels(window.innerHeight);
|
||||
|
||||
this.unsavedChangesSrv.init(dashboard, this.$scope);
|
||||
|
||||
// TODO refactor ViewStateSrv
|
||||
this.$scope.dashboard = dashboard;
|
||||
this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope);
|
||||
|
||||
this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
|
||||
this.setWindowTitleAndTheme();
|
||||
|
||||
appEvents.emit('dashboard-initialized', dashboard);
|
||||
})
|
||||
.catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
|
||||
}
|
||||
|
||||
onInitFailed(msg, fatal, err) {
|
||||
console.log(msg, err);
|
||||
|
||||
if (err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
} else if (!err.message) {
|
||||
err = { message: err.toString() };
|
||||
}
|
||||
|
||||
this.$scope.appEvent('alert-error', [msg, err.message]);
|
||||
|
||||
// protect against recursive fallbacks
|
||||
if (fatal && !this.loadedFallbackDashboard) {
|
||||
this.loadedFallbackDashboard = true;
|
||||
this.setupDashboard({ dashboard: { title: 'Dashboard Init failed' } });
|
||||
}
|
||||
}
|
||||
|
||||
templateVariableUpdated() {
|
||||
this.dashboard.processRepeats();
|
||||
}
|
||||
|
||||
setWindowTitleAndTheme() {
|
||||
window.document.title = config.windowTitlePrefix + this.dashboard.title;
|
||||
}
|
||||
|
||||
showJsonEditor(evt, options) {
|
||||
const model = {
|
||||
object: options.object,
|
||||
updateHandler: options.updateHandler,
|
||||
};
|
||||
|
||||
this.$scope.appEvent('show-dash-editor', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
model: model,
|
||||
});
|
||||
}
|
||||
|
||||
getDashboard() {
|
||||
return this.dashboard;
|
||||
}
|
||||
|
||||
getPanelContainer() {
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemovingPanel(evt, options) {
|
||||
options = options || {};
|
||||
if (!options.panelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panelInfo = this.dashboard.getPanelInfoById(options.panelId);
|
||||
removePanel(this.dashboard, panelInfo.panel, true);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (this.dashboard) {
|
||||
this.dashboard.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
init(dashboard) {
|
||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
||||
this.$scope.$on('$destroy', this.onDestroy.bind(this));
|
||||
this.setupDashboard(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DashboardCtrl', DashboardCtrl);
|
251
public/app/features/dashboard/containers/DashboardPage.test.tsx
Normal file
251
public/app/features/dashboard/containers/DashboardPage.test.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardPage, Props, State } from './DashboardPage';
|
||||
import { DashboardModel } from '../state';
|
||||
import { cleanUpDashboard } from '../state/actions';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||
|
||||
jest.mock('sass/_variables.scss', () => ({
|
||||
panelhorizontalpadding: 10,
|
||||
panelVerticalPadding: 10,
|
||||
}));
|
||||
|
||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||
|
||||
interface ScenarioContext {
|
||||
cleanUpDashboardMock: NoPayloadActionCreatorMock;
|
||||
dashboard?: DashboardModel;
|
||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => void;
|
||||
wrapper?: ShallowWrapper<Props, State, DashboardPage>;
|
||||
mount: (propOverrides?: Partial<Props>) => void;
|
||||
setup?: (fn: () => void) => void;
|
||||
}
|
||||
|
||||
function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
|
||||
const data = Object.assign({
|
||||
title: 'My dashboard',
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'My graph',
|
||||
gridPos: { x: 0, y: 0, w: 1, h: 1 },
|
||||
},
|
||||
],
|
||||
}, overrides);
|
||||
|
||||
const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
|
||||
return new DashboardModel(data, meta);
|
||||
}
|
||||
|
||||
function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) => void) {
|
||||
describe(description, () => {
|
||||
let setupFn: () => void;
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
cleanUpDashboardMock: getNoPayloadActionCreatorMock(cleanUpDashboard),
|
||||
setup: fn => {
|
||||
setupFn = fn;
|
||||
},
|
||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => {
|
||||
ctx.dashboard = getTestDashboard(overrides, metaOverrides);
|
||||
ctx.wrapper.setProps({ dashboard: ctx.dashboard });
|
||||
},
|
||||
mount: (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
urlSlug: 'my-dash',
|
||||
$scope: {},
|
||||
urlUid: '11',
|
||||
$injector: {},
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
urlEdit: false,
|
||||
urlFullscreen: false,
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
isInitSlow: false,
|
||||
initDashboard: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
notifyApp: jest.fn(),
|
||||
cleanUpDashboard: ctx.cleanUpDashboardMock,
|
||||
dashboard: null,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
ctx.dashboard = props.dashboard;
|
||||
ctx.wrapper = shallow(<DashboardPage {...props} />);
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setupFn();
|
||||
});
|
||||
|
||||
scenarioFn(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describe('DashboardPage', () => {
|
||||
|
||||
dashboardPageScenario("Given initial state", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
});
|
||||
|
||||
it('Should render nothing', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Dashboard is fetching slowly", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.wrapper.setProps({
|
||||
isInitSlow: true,
|
||||
initPhase: DashboardInitPhase.Fetching,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render slow init state', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Dashboard init completed ", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
});
|
||||
|
||||
it('Should update title', () => {
|
||||
expect(document.title).toBe('My dashboard - Grafana');
|
||||
});
|
||||
|
||||
it('Should render dashboard grid', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When user goes into panel edit", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
ctx.wrapper.setProps({
|
||||
urlFullscreen: true,
|
||||
urlEdit: true,
|
||||
urlPanelId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update model state to fullscreen & edit', () => {
|
||||
expect(ctx.dashboard.meta.fullscreen).toBe(true);
|
||||
expect(ctx.dashboard.meta.isEditing).toBe(true);
|
||||
});
|
||||
|
||||
it('Should update component state to fullscreen and edit', () => {
|
||||
const state = ctx.wrapper.state();
|
||||
expect(state.isEditing).toBe(true);
|
||||
expect(state.isFullscreen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When user goes back to dashboard from panel edit", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
ctx.wrapper.setState({ scrollTop: 100 });
|
||||
ctx.wrapper.setProps({
|
||||
urlFullscreen: true,
|
||||
urlEdit: true,
|
||||
urlPanelId: '1',
|
||||
});
|
||||
ctx.wrapper.setProps({
|
||||
urlFullscreen: false,
|
||||
urlEdit: false,
|
||||
urlPanelId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update model state normal state', () => {
|
||||
expect(ctx.dashboard.meta.fullscreen).toBe(false);
|
||||
expect(ctx.dashboard.meta.isEditing).toBe(false);
|
||||
});
|
||||
|
||||
it('Should update component state to normal and restore scrollTop', () => {
|
||||
const state = ctx.wrapper.state();
|
||||
expect(state.isEditing).toBe(false);
|
||||
expect(state.isFullscreen).toBe(false);
|
||||
expect(state.scrollTop).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When dashboard has editview url state", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
ctx.wrapper.setProps({
|
||||
editview: 'settings',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render settings view', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set animation state', () => {
|
||||
expect(ctx.wrapper.state().isSettingsOpening).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When adding panel", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
ctx.wrapper.setState({ scrollTop: 100 });
|
||||
ctx.wrapper.instance().onAddPanel();
|
||||
});
|
||||
|
||||
it('should set scrollTop to 0', () => {
|
||||
expect(ctx.wrapper.state().scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it('should add panel widget to dashboard panels', () => {
|
||||
expect(ctx.dashboard.panels[0].type).toBe('add-panel');
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Given panel with id 0", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp({
|
||||
panels: [{ id: 0, type: 'graph'}],
|
||||
schemaVersion: 17,
|
||||
});
|
||||
ctx.wrapper.setProps({
|
||||
urlEdit: true,
|
||||
urlFullscreen: true,
|
||||
urlPanelId: '0'
|
||||
});
|
||||
});
|
||||
|
||||
it('Should go into edit mode' , () => {
|
||||
expect(ctx.wrapper.state().isEditing).toBe(true);
|
||||
expect(ctx.wrapper.state().fullscreenPanel.id).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When dashboard unmounts", (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp({
|
||||
panels: [{ id: 0, type: 'graph'}],
|
||||
schemaVersion: 17,
|
||||
});
|
||||
ctx.wrapper.unmount();
|
||||
});
|
||||
|
||||
it('Should call clean up action' , () => {
|
||||
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
309
public/app/features/dashboard/containers/DashboardPage.tsx
Normal file
309
public/app/features/dashboard/containers/DashboardPage.tsx
Normal file
@ -0,0 +1,309 @@
|
||||
// Libraries
|
||||
import $ from 'jquery';
|
||||
import React, { PureComponent, MouseEvent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Services & Utils
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
|
||||
// Components
|
||||
import { DashboardGrid } from '../dashgrid/DashboardGrid';
|
||||
import { DashNav } from '../components/DashNav';
|
||||
import { SubMenu } from '../components/SubMenu';
|
||||
import { DashboardSettings } from '../components/DashboardSettings';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
|
||||
|
||||
// Redux
|
||||
import { initDashboard } from '../state/initDashboard';
|
||||
import { cleanUpDashboard } from '../state/actions';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import {
|
||||
StoreState,
|
||||
DashboardInitPhase,
|
||||
DashboardRouteInfo,
|
||||
DashboardInitError,
|
||||
AppNotificationSeverity,
|
||||
} from 'app/types';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
|
||||
export interface Props {
|
||||
urlUid?: string;
|
||||
urlSlug?: string;
|
||||
urlType?: string;
|
||||
editview?: string;
|
||||
urlPanelId?: string;
|
||||
urlFolderId?: string;
|
||||
$scope: any;
|
||||
$injector: any;
|
||||
routeInfo: DashboardRouteInfo;
|
||||
urlEdit: boolean;
|
||||
urlFullscreen: boolean;
|
||||
initPhase: DashboardInitPhase;
|
||||
isInitSlow: boolean;
|
||||
dashboard: DashboardModel | null;
|
||||
initError?: DashboardInitError;
|
||||
initDashboard: typeof initDashboard;
|
||||
cleanUpDashboard: typeof cleanUpDashboard;
|
||||
notifyApp: typeof notifyApp;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isSettingsOpening: boolean;
|
||||
isEditing: boolean;
|
||||
isFullscreen: boolean;
|
||||
fullscreenPanel: PanelModel | null;
|
||||
scrollTop: number;
|
||||
rememberScrollTop: number;
|
||||
showLoadingState: boolean;
|
||||
}
|
||||
|
||||
export class DashboardPage extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isSettingsOpening: false,
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
showLoadingState: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: 0,
|
||||
rememberScrollTop: 0,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
this.props.initDashboard({
|
||||
$injector: this.props.$injector,
|
||||
$scope: this.props.$scope,
|
||||
urlSlug: this.props.urlSlug,
|
||||
urlUid: this.props.urlUid,
|
||||
urlType: this.props.urlType,
|
||||
urlFolderId: this.props.urlFolderId,
|
||||
routeInfo: this.props.routeInfo,
|
||||
fixUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.dashboard) {
|
||||
this.props.cleanUpDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props;
|
||||
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if we just got dashboard update title
|
||||
if (!prevProps.dashboard) {
|
||||
document.title = dashboard.title + ' - Grafana';
|
||||
}
|
||||
|
||||
// handle animation states when opening dashboard settings
|
||||
if (!prevProps.editview && editview) {
|
||||
this.setState({ isSettingsOpening: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ isSettingsOpening: false });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Sync url state with model
|
||||
if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) {
|
||||
if (!isNaN(parseInt(urlPanelId, 10))) {
|
||||
this.onEnterFullscreen();
|
||||
} else {
|
||||
this.onLeaveFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEnterFullscreen() {
|
||||
const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props;
|
||||
|
||||
const panelId = parseInt(urlPanelId, 10);
|
||||
|
||||
// need to expand parent row if this panel is inside a row
|
||||
dashboard.expandParentRowFor(panelId);
|
||||
|
||||
const panel = dashboard.getPanelById(panelId);
|
||||
|
||||
if (panel) {
|
||||
dashboard.setViewMode(panel, urlFullscreen, urlEdit);
|
||||
this.setState({
|
||||
isEditing: urlEdit && dashboard.meta.canEdit,
|
||||
isFullscreen: urlFullscreen,
|
||||
fullscreenPanel: panel,
|
||||
rememberScrollTop: this.state.scrollTop,
|
||||
});
|
||||
this.setPanelFullscreenClass(urlFullscreen);
|
||||
} else {
|
||||
this.handleFullscreenPanelNotFound(urlPanelId);
|
||||
}
|
||||
}
|
||||
|
||||
onLeaveFullscreen() {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
if (this.state.fullscreenPanel) {
|
||||
dashboard.setViewMode(this.state.fullscreenPanel, false, false);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: this.state.rememberScrollTop,
|
||||
},
|
||||
() => {
|
||||
dashboard.render();
|
||||
}
|
||||
);
|
||||
|
||||
this.setPanelFullscreenClass(false);
|
||||
}
|
||||
|
||||
handleFullscreenPanelNotFound(urlPanelId: string) {
|
||||
// Panel not found
|
||||
this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
|
||||
// Clear url state
|
||||
this.props.updateLocation({
|
||||
query: {
|
||||
edit: null,
|
||||
fullscreen: null,
|
||||
panelId: null,
|
||||
},
|
||||
partial: true,
|
||||
});
|
||||
}
|
||||
|
||||
setPanelFullscreenClass(isFullscreen: boolean) {
|
||||
$('body').toggleClass('panel-in-fullscreen', isFullscreen);
|
||||
}
|
||||
|
||||
setScrollTop = (e: MouseEvent<HTMLElement>): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
this.setState({ scrollTop: target.scrollTop });
|
||||
};
|
||||
|
||||
onAddPanel = () => {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
// Return if the "Add panel" exists already
|
||||
if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboard.addPanel({
|
||||
type: 'add-panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 8 },
|
||||
title: 'Panel Title',
|
||||
});
|
||||
|
||||
// scroll to top after adding panel
|
||||
this.setState({ scrollTop: 0 });
|
||||
};
|
||||
|
||||
renderSlowInitState() {
|
||||
return (
|
||||
<div className="dashboard-loading">
|
||||
<div className="dashboard-loading__text">
|
||||
<i className="fa fa-spinner fa-spin" /> {this.props.initPhase}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderInitFailedState() {
|
||||
const { initError } = this.props;
|
||||
|
||||
return (
|
||||
<div className="dashboard-loading">
|
||||
<AlertBox
|
||||
severity={AppNotificationSeverity.Error}
|
||||
title={initError.message}
|
||||
text={getMessageFromError(initError.error)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
|
||||
const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
|
||||
|
||||
if (!dashboard) {
|
||||
if (isInitSlow) {
|
||||
return this.renderSlowInitState();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'dashboard-page--settings-opening': isSettingsOpening,
|
||||
'dashboard-page--settings-open': !isSettingsOpening && editview,
|
||||
});
|
||||
|
||||
const gridWrapperClasses = classNames({
|
||||
'dashboard-container': true,
|
||||
'dashboard-container--has-submenu': dashboard.meta.submenuEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<DashNav
|
||||
dashboard={dashboard}
|
||||
isEditing={isEditing}
|
||||
isFullscreen={isFullscreen}
|
||||
editview={editview}
|
||||
$injector={$injector}
|
||||
onAddPanel={this.onAddPanel}
|
||||
/>
|
||||
<div className="scroll-canvas scroll-canvas--dashboard">
|
||||
<CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
|
||||
{editview && <DashboardSettings dashboard={dashboard} />}
|
||||
|
||||
{initError && this.renderInitFailedState()}
|
||||
|
||||
<div className={gridWrapperClasses}>
|
||||
{dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
|
||||
<DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
urlUid: state.location.routeParams.uid,
|
||||
urlSlug: state.location.routeParams.slug,
|
||||
urlType: state.location.routeParams.type,
|
||||
editview: state.location.query.editview,
|
||||
urlPanelId: state.location.query.panelId,
|
||||
urlFolderId: state.location.query.folderId,
|
||||
urlFullscreen: state.location.query.fullscreen === true,
|
||||
urlEdit: state.location.query.edit === true,
|
||||
initPhase: state.dashboard.initPhase,
|
||||
isInitSlow: state.dashboard.isInitSlow,
|
||||
initError: state.dashboard.initError,
|
||||
dashboard: state.dashboard.model as DashboardModel,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initDashboard,
|
||||
cleanUpDashboard,
|
||||
notifyApp,
|
||||
updateLocation,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
|
@ -3,98 +3,84 @@ import React, { Component } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Utils & Services
|
||||
import appEvents from 'app/core/app_events';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
// Components
|
||||
import { DashboardPanel } from '../dashgrid/DashboardPanel';
|
||||
|
||||
// Redux
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { initDashboard } from '../state/initDashboard';
|
||||
|
||||
// Types
|
||||
import { StoreState } from 'app/types';
|
||||
import { StoreState, DashboardRouteInfo } from 'app/types';
|
||||
import { PanelModel, DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
interface Props {
|
||||
panelId: string;
|
||||
urlPanelId: string;
|
||||
urlUid?: string;
|
||||
urlSlug?: string;
|
||||
urlType?: string;
|
||||
$scope: any;
|
||||
$injector: any;
|
||||
updateLocation: typeof updateLocation;
|
||||
routeInfo: DashboardRouteInfo;
|
||||
initDashboard: typeof initDashboard;
|
||||
dashboard: DashboardModel | null;
|
||||
}
|
||||
|
||||
interface State {
|
||||
panel: PanelModel | null;
|
||||
dashboard: DashboardModel | null;
|
||||
notFound: boolean;
|
||||
}
|
||||
|
||||
export class SoloPanelPage extends Component<Props, State> {
|
||||
|
||||
state: State = {
|
||||
panel: null,
|
||||
dashboard: null,
|
||||
notFound: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { $injector, $scope, urlUid, urlType, urlSlug } = this.props;
|
||||
const { $injector, $scope, urlUid, urlType, urlSlug, routeInfo } = this.props;
|
||||
|
||||
// handle old urls with no uid
|
||||
if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) {
|
||||
this.redirectToNewUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv');
|
||||
|
||||
// subscribe to event to know when dashboard controller is done with inititalization
|
||||
appEvents.on('dashboard-initialized', this.onDashoardInitialized);
|
||||
|
||||
dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => {
|
||||
result.meta.soloMode = true;
|
||||
$scope.initDashboard(result, $scope);
|
||||
this.props.initDashboard({
|
||||
$injector: $injector,
|
||||
$scope: $scope,
|
||||
urlSlug: urlSlug,
|
||||
urlUid: urlUid,
|
||||
urlType: urlType,
|
||||
routeInfo: routeInfo,
|
||||
fixUrl: false,
|
||||
});
|
||||
}
|
||||
|
||||
redirectToNewUrl() {
|
||||
getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||
this.props.updateLocation(url);
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { urlPanelId, dashboard } = this.props;
|
||||
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we just got the dashboard!
|
||||
if (!prevProps.dashboard) {
|
||||
const panelId = parseInt(urlPanelId, 10);
|
||||
|
||||
// need to expand parent row if this panel is inside a row
|
||||
dashboard.expandParentRowFor(panelId);
|
||||
|
||||
const panel = dashboard.getPanelById(panelId);
|
||||
|
||||
if (!panel) {
|
||||
this.setState({ notFound: true });
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDashoardInitialized = () => {
|
||||
const { $scope, panelId } = this.props;
|
||||
|
||||
const dashboard: DashboardModel = $scope.dashboard;
|
||||
const panel = dashboard.getPanelById(parseInt(panelId, 10));
|
||||
|
||||
if (!panel) {
|
||||
this.setState({ notFound: true });
|
||||
return;
|
||||
this.setState({ panel });
|
||||
}
|
||||
|
||||
this.setState({ dashboard, panel });
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panelId } = this.props;
|
||||
const { notFound, panel, dashboard } = this.state;
|
||||
const { urlPanelId, dashboard } = this.props;
|
||||
const { notFound, panel } = this.state;
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
Panel with id { panelId } not found
|
||||
</div>
|
||||
);
|
||||
return <div className="alert alert-error">Panel with id {urlPanelId} not found</div>;
|
||||
}
|
||||
|
||||
if (!panel) {
|
||||
@ -113,11 +99,12 @@ const mapStateToProps = (state: StoreState) => ({
|
||||
urlUid: state.location.routeParams.uid,
|
||||
urlSlug: state.location.routeParams.slug,
|
||||
urlType: state.location.routeParams.type,
|
||||
panelId: state.location.query.panelId
|
||||
urlPanelId: state.location.query.panelId,
|
||||
dashboard: state.dashboard.model as DashboardModel,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation
|
||||
initDashboard,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage));
|
||||
|
@ -0,0 +1,546 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`] = `
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<Connect(DashNav)
|
||||
$injector={Object {}}
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
onAddPanel={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="scroll-canvas scroll-canvas--dashboard"
|
||||
>
|
||||
<CustomScrollbar
|
||||
autoHeightMax="100%"
|
||||
autoHeightMin="100%"
|
||||
autoHide={false}
|
||||
autoHideDuration={200}
|
||||
autoHideTimeout={200}
|
||||
customClassName="custom-scrollbars"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
>
|
||||
<div
|
||||
className="dashboard-container"
|
||||
>
|
||||
<DashboardGrid
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
/>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DashboardPage Dashboard is fetching slowly Should render slow init state 1`] = `
|
||||
<div
|
||||
className="dashboard-loading"
|
||||
>
|
||||
<div
|
||||
className="dashboard-loading__text"
|
||||
>
|
||||
<i
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>
|
||||
|
||||
Fetching
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DashboardPage Given initial state Should render nothing 1`] = `""`;
|
||||
|
||||
exports[`DashboardPage When dashboard has editview url state should render settings view 1`] = `
|
||||
<div
|
||||
className="dashboard-page--settings-opening"
|
||||
>
|
||||
<Connect(DashNav)
|
||||
$injector={Object {}}
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
editview="settings"
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
onAddPanel={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="scroll-canvas scroll-canvas--dashboard"
|
||||
>
|
||||
<CustomScrollbar
|
||||
autoHeightMax="100%"
|
||||
autoHeightMin="100%"
|
||||
autoHide={false}
|
||||
autoHideDuration={200}
|
||||
autoHideTimeout={200}
|
||||
customClassName="custom-scrollbars"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
>
|
||||
<DashboardSettings
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="dashboard-container"
|
||||
>
|
||||
<DashboardGrid
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
/>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
// Libaries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
|
||||
import classNames from 'classnames';
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
// Types
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import classNames from 'classnames';
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
let lastGridWidth = 1200;
|
||||
let ignoreNextWidthChange = false;
|
||||
@ -76,19 +79,18 @@ function GridWrapper({
|
||||
|
||||
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
||||
|
||||
export interface DashboardGridProps {
|
||||
export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
isEditing: boolean;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
export class DashboardGrid extends PureComponent<Props> {
|
||||
gridToPanelMap: any;
|
||||
panelMap: { [id: string]: PanelModel };
|
||||
|
||||
constructor(props: DashboardGridProps) {
|
||||
super(props);
|
||||
|
||||
// subscribe to dashboard events
|
||||
const dashboard = this.props.dashboard;
|
||||
componentDidMount() {
|
||||
const { dashboard } = this.props;
|
||||
dashboard.on('panel-added', this.triggerForceUpdate);
|
||||
dashboard.on('panel-removed', this.triggerForceUpdate);
|
||||
dashboard.on('repeats-processed', this.triggerForceUpdate);
|
||||
@ -97,6 +99,16 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
dashboard.on('row-expanded', this.triggerForceUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { dashboard } = this.props;
|
||||
dashboard.off('panel-added', this.triggerForceUpdate);
|
||||
dashboard.off('panel-removed', this.triggerForceUpdate);
|
||||
dashboard.off('repeats-processed', this.triggerForceUpdate);
|
||||
dashboard.off('view-mode-changed', this.onViewModeChanged);
|
||||
dashboard.off('row-collapsed', this.triggerForceUpdate);
|
||||
dashboard.off('row-expanded', this.triggerForceUpdate);
|
||||
}
|
||||
|
||||
buildLayout() {
|
||||
const layout = [];
|
||||
this.panelMap = {};
|
||||
@ -151,7 +163,6 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
|
||||
onViewModeChanged = () => {
|
||||
ignoreNextWidthChange = true;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
|
||||
@ -197,18 +208,20 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, isFullscreen } = this.props;
|
||||
|
||||
return (
|
||||
<SizedReactLayoutGrid
|
||||
className={classNames({ layout: true })}
|
||||
layout={this.buildLayout()}
|
||||
isResizable={this.props.dashboard.meta.canEdit}
|
||||
isDraggable={this.props.dashboard.meta.canEdit}
|
||||
isResizable={dashboard.meta.canEdit}
|
||||
isDraggable={dashboard.meta.canEdit}
|
||||
onLayoutChange={this.onLayoutChange}
|
||||
onWidthChange={this.onWidthChange}
|
||||
onDragStop={this.onDragStop}
|
||||
onResize={this.onResize}
|
||||
onResizeStop={this.onResizeStop}
|
||||
isFullscreen={this.props.dashboard.meta.fullscreen}
|
||||
isFullscreen={isFullscreen}
|
||||
>
|
||||
{this.renderPanels()}
|
||||
</SizedReactLayoutGrid>
|
||||
|
@ -1,8 +1,6 @@
|
||||
import './containers/DashboardCtrl';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
|
||||
// Services
|
||||
import './services/DashboardViewStateSrv';
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
import './services/DashboardSrv';
|
||||
|
@ -1,25 +1,74 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { removePanel } from '../utils/panel';
|
||||
|
||||
export class DashboardSrv {
|
||||
dash: any;
|
||||
dashboard: DashboardModel;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $rootScope, private $location) {}
|
||||
constructor(private backendSrv, private $rootScope, private $location) {
|
||||
appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope);
|
||||
appEvents.on('panel-change-view', this.onPanelChangeView);
|
||||
appEvents.on('remove-panel', this.onRemovePanel);
|
||||
}
|
||||
|
||||
create(dashboard, meta) {
|
||||
return new DashboardModel(dashboard, meta);
|
||||
}
|
||||
|
||||
setCurrent(dashboard) {
|
||||
this.dash = dashboard;
|
||||
setCurrent(dashboard: DashboardModel) {
|
||||
this.dashboard = dashboard;
|
||||
}
|
||||
|
||||
getCurrent() {
|
||||
return this.dash;
|
||||
getCurrent(): DashboardModel {
|
||||
return this.dashboard;
|
||||
}
|
||||
|
||||
onRemovePanel = (panelId: number) => {
|
||||
const dashboard = this.getCurrent();
|
||||
removePanel(dashboard, dashboard.getPanelById(panelId), true);
|
||||
};
|
||||
|
||||
onPanelChangeView = (options) => {
|
||||
const urlParams = this.$location.search();
|
||||
|
||||
// handle toggle logic
|
||||
if (options.fullscreen === urlParams.fullscreen) {
|
||||
// I hate using these truthy converters (!!) but in this case
|
||||
// I think it's appropriate. edit can be null/false/undefined and
|
||||
// here i want all of those to compare the same
|
||||
if (!!options.edit === !!urlParams.edit) {
|
||||
delete urlParams.fullscreen;
|
||||
delete urlParams.edit;
|
||||
delete urlParams.panelId;
|
||||
this.$location.search(urlParams);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.fullscreen) {
|
||||
urlParams.fullscreen = true;
|
||||
} else {
|
||||
delete urlParams.fullscreen;
|
||||
}
|
||||
|
||||
if (options.edit) {
|
||||
urlParams.edit = true;
|
||||
} else {
|
||||
delete urlParams.edit;
|
||||
}
|
||||
|
||||
if (options.panelId || options.panelId === 0) {
|
||||
urlParams.panelId = options.panelId;
|
||||
} else {
|
||||
delete urlParams.panelId;
|
||||
}
|
||||
|
||||
this.$location.search(urlParams);
|
||||
};
|
||||
|
||||
handleSaveDashboardError(clone, options, err) {
|
||||
options = options || {};
|
||||
options.overwrite = true;
|
||||
@ -75,10 +124,10 @@ export class DashboardSrv {
|
||||
}
|
||||
|
||||
postSave(clone, data) {
|
||||
this.dash.version = data.version;
|
||||
this.dashboard.version = data.version;
|
||||
|
||||
// important that these happens before location redirect below
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dashboard);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||
|
||||
const newUrl = locationUtil.stripBaseFromUrl(data.url);
|
||||
@ -88,12 +137,12 @@ export class DashboardSrv {
|
||||
this.$location.url(newUrl).replace();
|
||||
}
|
||||
|
||||
return this.dash;
|
||||
return this.dashboard;
|
||||
}
|
||||
|
||||
save(clone, options) {
|
||||
options = options || {};
|
||||
options.folderId = options.folderId >= 0 ? options.folderId : this.dash.meta.folderId || clone.folderId;
|
||||
options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId;
|
||||
|
||||
return this.backendSrv
|
||||
.saveDashboard(clone, options)
|
||||
@ -103,26 +152,26 @@ export class DashboardSrv {
|
||||
|
||||
saveDashboard(options?, clone?) {
|
||||
if (clone) {
|
||||
this.setCurrent(this.create(clone, this.dash.meta));
|
||||
this.setCurrent(this.create(clone, this.dashboard.meta));
|
||||
}
|
||||
|
||||
if (this.dash.meta.provisioned) {
|
||||
if (this.dashboard.meta.provisioned) {
|
||||
return this.showDashboardProvisionedModal();
|
||||
}
|
||||
|
||||
if (!this.dash.meta.canSave && options.makeEditable !== true) {
|
||||
if (!this.dashboard.meta.canSave && options.makeEditable !== true) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.dash.title === 'New dashboard') {
|
||||
if (this.dashboard.title === 'New dashboard') {
|
||||
return this.showSaveAsModal();
|
||||
}
|
||||
|
||||
if (this.dash.version > 0) {
|
||||
if (this.dashboard.version > 0) {
|
||||
return this.showSaveModal();
|
||||
}
|
||||
|
||||
return this.save(this.dash.getSaveModelClone(), options);
|
||||
return this.save(this.dashboard.getSaveModelClone(), options);
|
||||
}
|
||||
|
||||
saveJSONDashboard(json: string) {
|
||||
@ -163,8 +212,8 @@ export class DashboardSrv {
|
||||
}
|
||||
|
||||
return promise.then(res => {
|
||||
if (this.dash && this.dash.id === dashboardId) {
|
||||
this.dash.meta.isStarred = res;
|
||||
if (this.dashboard && this.dashboard.id === dashboardId) {
|
||||
this.dashboard.meta.isStarred = res;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
@ -1,64 +0,0 @@
|
||||
import config from 'app/core/config';
|
||||
import { DashboardViewStateSrv } from './DashboardViewStateSrv';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
describe('when updating view state', () => {
|
||||
const location = {
|
||||
replace: jest.fn(),
|
||||
search: jest.fn(),
|
||||
};
|
||||
|
||||
const $scope = {
|
||||
appEvent: jest.fn(),
|
||||
onAppEvent: jest.fn(() => {}),
|
||||
dashboard: new DashboardModel({
|
||||
panels: [{ id: 1 }],
|
||||
}),
|
||||
};
|
||||
|
||||
let viewState;
|
||||
|
||||
beforeEach(() => {
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('to fullscreen true and edit true', () => {
|
||||
beforeEach(() => {
|
||||
location.search = jest.fn(() => {
|
||||
return { fullscreen: true, edit: true, panelId: 1 };
|
||||
});
|
||||
viewState = new DashboardViewStateSrv($scope, location, {});
|
||||
});
|
||||
|
||||
it('should update querystring and view state', () => {
|
||||
const updateState = { fullscreen: true, edit: true, panelId: 1 };
|
||||
|
||||
viewState.update(updateState);
|
||||
|
||||
expect(location.search).toHaveBeenCalledWith({
|
||||
edit: true,
|
||||
editview: null,
|
||||
fullscreen: true,
|
||||
orgId: 1,
|
||||
panelId: 1,
|
||||
});
|
||||
expect(viewState.dashboard.meta.fullscreen).toBe(true);
|
||||
expect(viewState.state.fullscreen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to fullscreen false', () => {
|
||||
beforeEach(() => {
|
||||
viewState = new DashboardViewStateSrv($scope, location, {});
|
||||
});
|
||||
it('should remove params from query string', () => {
|
||||
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
||||
viewState.update({ fullscreen: false });
|
||||
expect(viewState.state.fullscreen).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,185 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
// represents the transient view state
|
||||
// like fullscreen panel & edit
|
||||
export class DashboardViewStateSrv {
|
||||
state: any;
|
||||
panelScopes: any;
|
||||
$scope: any;
|
||||
dashboard: DashboardModel;
|
||||
fullscreenPanel: any;
|
||||
oldTimeRange: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, private $location, private $timeout) {
|
||||
const self = this;
|
||||
self.state = {};
|
||||
self.panelScopes = [];
|
||||
self.$scope = $scope;
|
||||
self.dashboard = $scope.dashboard;
|
||||
|
||||
$scope.onAppEvent('$routeUpdate', () => {
|
||||
const urlState = self.getQueryStringState();
|
||||
if (self.needsSync(urlState)) {
|
||||
self.update(urlState, true);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.onAppEvent('panel-change-view', (evt, payload) => {
|
||||
self.update(payload);
|
||||
});
|
||||
|
||||
// this marks changes to location during this digest cycle as not to add history item
|
||||
// don't want url changes like adding orgId to add browser history
|
||||
$location.replace();
|
||||
this.update(this.getQueryStringState());
|
||||
}
|
||||
|
||||
needsSync(urlState) {
|
||||
return _.isEqual(this.state, urlState) === false;
|
||||
}
|
||||
|
||||
getQueryStringState() {
|
||||
const state = this.$location.search();
|
||||
state.panelId = parseInt(state.panelId, 10) || null;
|
||||
state.fullscreen = state.fullscreen ? true : null;
|
||||
state.edit = state.edit === 'true' || state.edit === true || null;
|
||||
state.editview = state.editview || null;
|
||||
state.orgId = config.bootData.user.orgId;
|
||||
return state;
|
||||
}
|
||||
|
||||
serializeToUrl() {
|
||||
const urlState = _.clone(this.state);
|
||||
urlState.fullscreen = this.state.fullscreen ? true : null;
|
||||
urlState.edit = this.state.edit ? true : null;
|
||||
return urlState;
|
||||
}
|
||||
|
||||
update(state, fromRouteUpdated?) {
|
||||
// implement toggle logic
|
||||
if (state.toggle) {
|
||||
delete state.toggle;
|
||||
if (this.state.fullscreen && state.fullscreen) {
|
||||
if (this.state.edit === state.edit) {
|
||||
state.fullscreen = !state.fullscreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.extend(this.state, state);
|
||||
|
||||
if (!this.state.fullscreen) {
|
||||
this.state.fullscreen = null;
|
||||
this.state.edit = null;
|
||||
// clear panel id unless in solo mode
|
||||
if (!this.dashboard.meta.soloMode) {
|
||||
this.state.panelId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.state.fullscreen || this.dashboard.meta.soloMode) && this.state.panelId) {
|
||||
// Trying to render panel in fullscreen when it's in the collapsed row causes an issue.
|
||||
// So in this case expand collapsed row first.
|
||||
this.toggleCollapsedPanelRow(this.state.panelId);
|
||||
}
|
||||
|
||||
// if no edit state cleanup tab parm
|
||||
if (!this.state.edit) {
|
||||
delete this.state.tab;
|
||||
}
|
||||
|
||||
// do not update url params if we are here
|
||||
// from routeUpdated event
|
||||
if (fromRouteUpdated !== true) {
|
||||
this.$location.search(this.serializeToUrl());
|
||||
}
|
||||
|
||||
this.syncState();
|
||||
}
|
||||
|
||||
toggleCollapsedPanelRow(panelId) {
|
||||
for (const panel of this.dashboard.panels) {
|
||||
if (panel.collapsed) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
if (rowPanel.id === panelId) {
|
||||
this.dashboard.toggleRow(panel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncState() {
|
||||
if (this.state.fullscreen) {
|
||||
const panel = this.dashboard.getPanelById(this.state.panelId);
|
||||
|
||||
if (!panel) {
|
||||
this.state.fullscreen = null;
|
||||
this.state.panelId = null;
|
||||
this.state.edit = null;
|
||||
|
||||
this.update(this.state);
|
||||
|
||||
setTimeout(() => {
|
||||
appEvents.emit('alert-error', ['Error', 'Panel not found']);
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panel.fullscreen) {
|
||||
this.enterFullscreen(panel);
|
||||
} else if (this.dashboard.meta.isEditing !== this.state.edit) {
|
||||
this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
|
||||
}
|
||||
} else if (this.fullscreenPanel) {
|
||||
this.leaveFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
leaveFullscreen() {
|
||||
const panel = this.fullscreenPanel;
|
||||
|
||||
this.dashboard.setViewMode(panel, false, false);
|
||||
|
||||
delete this.fullscreenPanel;
|
||||
|
||||
this.$timeout(() => {
|
||||
appEvents.emit('dash-scroll', { restore: true });
|
||||
|
||||
if (this.oldTimeRange !== this.dashboard.time) {
|
||||
this.dashboard.startRefresh();
|
||||
} else {
|
||||
this.dashboard.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enterFullscreen(panel) {
|
||||
const isEditing = this.state.edit && this.dashboard.meta.canEdit;
|
||||
|
||||
this.oldTimeRange = this.dashboard.time;
|
||||
this.fullscreenPanel = panel;
|
||||
|
||||
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
|
||||
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||
this.dashboard.setViewMode(panel, true, isEditing);
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function dashboardViewStateSrv($location, $timeout) {
|
||||
return {
|
||||
create: $scope => {
|
||||
return new DashboardViewStateSrv($scope, $location, $timeout);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('grafana.services').factory('dashboardViewStateSrv', dashboardViewStateSrv);
|
@ -1,20 +1,26 @@
|
||||
// Libaries
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
|
||||
// Constants
|
||||
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
|
||||
// Utils & Services
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from './PanelModel';
|
||||
import { DashboardMigrator } from './DashboardMigrator';
|
||||
import { TimeRange } from '@grafana/ui/src';
|
||||
import { UrlQueryValue, KIOSK_MODE_TV, DashboardMeta } from 'app/types';
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
uid: any;
|
||||
title: any;
|
||||
uid: string;
|
||||
title: string;
|
||||
autoUpdate: any;
|
||||
description: any;
|
||||
tags: any;
|
||||
@ -43,7 +49,7 @@ export class DashboardModel {
|
||||
|
||||
// repeat process cycles
|
||||
iteration: number;
|
||||
meta: any;
|
||||
meta: DashboardMeta;
|
||||
events: Emitter;
|
||||
|
||||
static nonPersistedProperties: { [str: string]: boolean } = {
|
||||
@ -127,6 +133,8 @@ export class DashboardModel {
|
||||
meta.canEdit = meta.canEdit !== false;
|
||||
meta.showSettings = meta.canEdit;
|
||||
meta.canMakeEditable = meta.canSave && !this.editable;
|
||||
meta.fullscreen = false;
|
||||
meta.isEditing = false;
|
||||
|
||||
if (!this.editable) {
|
||||
meta.canEdit = false;
|
||||
@ -860,11 +868,7 @@ export class DashboardModel {
|
||||
return !_.isEqual(updated, this.originalTemplating);
|
||||
}
|
||||
|
||||
autoFitPanels(viewHeight: number) {
|
||||
if (!this.meta.autofitpanels) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
|
||||
const currentGridHeight = Math.max(
|
||||
...this.panels.map(panel => {
|
||||
return panel.gridPos.h + panel.gridPos.y;
|
||||
@ -878,12 +882,12 @@ export class DashboardModel {
|
||||
let visibleHeight = viewHeight - navbarHeight - margin;
|
||||
|
||||
// Remove submenu height if visible
|
||||
if (this.meta.submenuEnabled && !this.meta.kiosk) {
|
||||
if (this.meta.submenuEnabled && !kioskMode) {
|
||||
visibleHeight -= submenuHeight;
|
||||
}
|
||||
|
||||
// add back navbar height
|
||||
if (this.meta.kiosk === 'b') {
|
||||
if (kioskMode === KIOSK_MODE_TV) {
|
||||
visibleHeight += 55;
|
||||
}
|
||||
|
||||
@ -895,4 +899,23 @@ export class DashboardModel {
|
||||
panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
|
||||
});
|
||||
}
|
||||
|
||||
templateVariableValueUpdated() {
|
||||
this.processRepeats();
|
||||
this.events.emit('template-variable-value-updated');
|
||||
}
|
||||
|
||||
expandParentRowFor(panelId: number) {
|
||||
for (const panel of this.panels) {
|
||||
if (panel.collapsed) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
if (rowPanel.id === panelId) {
|
||||
this.toggleRow(panel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,39 +1,43 @@
|
||||
import { StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
// Services & Utils
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { actionCreatorFactory, noPayloadActionCreatorFactory } from 'app/core/redux';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import {
|
||||
ThunkResult,
|
||||
DashboardAcl,
|
||||
DashboardAclDTO,
|
||||
PermissionLevel,
|
||||
DashboardAclUpdateDTO,
|
||||
NewDashboardAclItem,
|
||||
} from 'app/types/acl';
|
||||
MutableDashboard,
|
||||
DashboardInitError,
|
||||
} from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS',
|
||||
LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
|
||||
}
|
||||
export const loadDashboardPermissions = actionCreatorFactory<DashboardAclDTO[]>('LOAD_DASHBOARD_PERMISSIONS').create();
|
||||
|
||||
export interface LoadDashboardPermissionsAction {
|
||||
type: ActionTypes.LoadDashboardPermissions;
|
||||
payload: DashboardAcl[];
|
||||
}
|
||||
export const dashboardInitFetching = noPayloadActionCreatorFactory('DASHBOARD_INIT_FETCHING').create();
|
||||
|
||||
export interface LoadStarredDashboardsAction {
|
||||
type: ActionTypes.LoadStarredDashboards;
|
||||
payload: DashboardAcl[];
|
||||
}
|
||||
export const dashboardInitServices = noPayloadActionCreatorFactory('DASHBOARD_INIT_SERVICES').create();
|
||||
|
||||
export type Action = LoadDashboardPermissionsAction | LoadStarredDashboardsAction;
|
||||
export const dashboardInitSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_INIT_SLOW').create();
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||
export const dashboardInitCompleted = actionCreatorFactory<MutableDashboard>('DASHBOARD_INIT_COMLETED').create();
|
||||
|
||||
export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({
|
||||
type: ActionTypes.LoadDashboardPermissions,
|
||||
payload: items,
|
||||
});
|
||||
/*
|
||||
* Unrecoverable init failure (fetch or model creation failed)
|
||||
*/
|
||||
export const dashboardInitFailed = actionCreatorFactory<DashboardInitError>('DASHBOARD_INIT_FAILED').create();
|
||||
|
||||
/*
|
||||
* When leaving dashboard, resets state
|
||||
* */
|
||||
export const cleanUpDashboard = noPayloadActionCreatorFactory('DASHBOARD_CLEAN_UP').create();
|
||||
|
||||
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@ -124,7 +128,7 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar
|
||||
export function importDashboard(data, dashboardTitle: string): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().post('/api/dashboards/import', data);
|
||||
appEvents.emit('alert-success', ['Dashboard Imported', dashboardTitle]);
|
||||
dispatch(notifyApp(createSuccessNotification('Dashboard Imported', dashboardTitle)));
|
||||
dispatch(loadPluginDashboards());
|
||||
};
|
||||
}
|
||||
@ -135,3 +139,4 @@ export function removeDashboard(uri: string): ThunkResult<void> {
|
||||
dispatch(loadPluginDashboards());
|
||||
};
|
||||
}
|
||||
|
||||
|
152
public/app/features/dashboard/state/initDashboard.test.ts
Normal file
152
public/app/features/dashboard/state/initDashboard.test.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { initDashboard, InitDashboardArgs } from './initDashboard';
|
||||
import { DashboardRouteInfo } from 'app/types';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import {
|
||||
dashboardInitFetching,
|
||||
dashboardInitCompleted,
|
||||
dashboardInitServices,
|
||||
} from './actions';
|
||||
|
||||
jest.mock('app/core/services/backend_srv');
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
interface ScenarioContext {
|
||||
args: InitDashboardArgs;
|
||||
timeSrv: any;
|
||||
annotationsSrv: any;
|
||||
unsavedChangesSrv: any;
|
||||
variableSrv: any;
|
||||
dashboardSrv: any;
|
||||
keybindingSrv: any;
|
||||
backendSrv: any;
|
||||
setup: (fn: () => void) => void;
|
||||
actions: any[];
|
||||
storeState: any;
|
||||
}
|
||||
|
||||
type ScenarioFn = (ctx: ScenarioContext) => void;
|
||||
|
||||
function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
describe(description, () => {
|
||||
const timeSrv = { init: jest.fn() };
|
||||
const annotationsSrv = { init: jest.fn() };
|
||||
const unsavedChangesSrv = { init: jest.fn() };
|
||||
const variableSrv = { init: jest.fn() };
|
||||
const dashboardSrv = { setCurrent: jest.fn() };
|
||||
const keybindingSrv = { setupDashboardBindings: jest.fn() };
|
||||
|
||||
const injectorMock = {
|
||||
get: (name: string) => {
|
||||
switch (name) {
|
||||
case 'timeSrv':
|
||||
return timeSrv;
|
||||
case 'annotationsSrv':
|
||||
return annotationsSrv;
|
||||
case 'unsavedChangesSrv':
|
||||
return unsavedChangesSrv;
|
||||
case 'dashboardSrv':
|
||||
return dashboardSrv;
|
||||
case 'variableSrv':
|
||||
return variableSrv;
|
||||
case 'keybindingSrv':
|
||||
return keybindingSrv;
|
||||
default:
|
||||
throw { message: 'Unknown service ' + name };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let setupFn = () => {};
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
args: {
|
||||
$injector: injectorMock,
|
||||
$scope: {},
|
||||
fixUrl: false,
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
},
|
||||
backendSrv: getBackendSrv(),
|
||||
timeSrv,
|
||||
annotationsSrv,
|
||||
unsavedChangesSrv,
|
||||
variableSrv,
|
||||
dashboardSrv,
|
||||
keybindingSrv,
|
||||
actions: [],
|
||||
storeState: {
|
||||
location: {
|
||||
query: {},
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
setup: (fn: () => void) => {
|
||||
setupFn = fn;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
setupFn();
|
||||
|
||||
const store = mockStore(ctx.storeState);
|
||||
|
||||
await store.dispatch(initDashboard(ctx.args));
|
||||
|
||||
ctx.actions = store.getActions();
|
||||
});
|
||||
|
||||
scenarioFn(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describeInitScenario('Initializing new dashboard', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.storeState.user.orgId = 12;
|
||||
ctx.args.routeInfo = DashboardRouteInfo.New;
|
||||
});
|
||||
|
||||
it('Should send action dashboardInitFetching', () => {
|
||||
expect(ctx.actions[0].type).toBe(dashboardInitFetching.type);
|
||||
});
|
||||
|
||||
it('Should send action dashboardInitServices ', () => {
|
||||
expect(ctx.actions[1].type).toBe(dashboardInitServices.type);
|
||||
});
|
||||
|
||||
it('Should update location with orgId query param', () => {
|
||||
expect(ctx.actions[2].type).toBe('UPDATE_LOCATION');
|
||||
expect(ctx.actions[2].payload.query.orgId).toBe(12);
|
||||
});
|
||||
|
||||
it('Should send action dashboardInitCompleted', () => {
|
||||
expect(ctx.actions[3].type).toBe(dashboardInitCompleted.type);
|
||||
expect(ctx.actions[3].payload.title).toBe('New dashboard');
|
||||
});
|
||||
|
||||
it('Should Initializing services', () => {
|
||||
expect(ctx.timeSrv.init).toBeCalled();
|
||||
expect(ctx.annotationsSrv.init).toBeCalled();
|
||||
expect(ctx.variableSrv.init).toBeCalled();
|
||||
expect(ctx.unsavedChangesSrv.init).toBeCalled();
|
||||
expect(ctx.keybindingSrv.setupDashboardBindings).toBeCalled();
|
||||
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('Initializing home dashboard', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.args.routeInfo = DashboardRouteInfo.Home;
|
||||
ctx.backendSrv.get.mockReturnValue(Promise.resolve({
|
||||
redirectUri: '/u/123/my-home'
|
||||
}));
|
||||
});
|
||||
|
||||
it('Should redirect to custom home dashboard', () => {
|
||||
expect(ctx.actions[1].type).toBe('UPDATE_LOCATION');
|
||||
expect(ctx.actions[1].payload.path).toBe('/u/123/my-home');
|
||||
});
|
||||
});
|
||||
|
||||
|
233
public/app/features/dashboard/state/initDashboard.ts
Normal file
233
public/app/features/dashboard/state/initDashboard.ts
Normal file
@ -0,0 +1,233 @@
|
||||
// Services & Utils
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
|
||||
import { VariableSrv } from 'app/features/templating/variable_srv';
|
||||
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
|
||||
|
||||
// Actions
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import {
|
||||
dashboardInitFetching,
|
||||
dashboardInitCompleted,
|
||||
dashboardInitFailed,
|
||||
dashboardInitSlow,
|
||||
dashboardInitServices,
|
||||
} from './actions';
|
||||
|
||||
// Types
|
||||
import { DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult, DashboardDTO } from 'app/types';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
|
||||
export interface InitDashboardArgs {
|
||||
$injector: any;
|
||||
$scope: any;
|
||||
urlUid?: string;
|
||||
urlSlug?: string;
|
||||
urlType?: string;
|
||||
urlFolderId?: string;
|
||||
routeInfo: DashboardRouteInfo;
|
||||
fixUrl: boolean;
|
||||
}
|
||||
|
||||
async function redirectToNewUrl(slug: string, dispatch: ThunkDispatch, currentPath: string) {
|
||||
const res = await getBackendSrv().getDashboardBySlug(slug);
|
||||
|
||||
if (res) {
|
||||
let newUrl = res.meta.url;
|
||||
|
||||
// fix solo route urls
|
||||
if (currentPath.indexOf('dashboard-solo') !== -1) {
|
||||
newUrl = newUrl.replace('/d/', '/d-solo/');
|
||||
}
|
||||
|
||||
const url = locationUtil.stripBaseFromUrl(newUrl);
|
||||
dispatch(updateLocation({ path: url, partial: true, replace: true }));
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDashboard(
|
||||
args: InitDashboardArgs,
|
||||
dispatch: ThunkDispatch,
|
||||
getState: () => StoreState
|
||||
): Promise<DashboardDTO | null> {
|
||||
try {
|
||||
switch (args.routeInfo) {
|
||||
case DashboardRouteInfo.Home: {
|
||||
// load home dash
|
||||
const dashDTO: DashboardDTO = await getBackendSrv().get('/api/dashboards/home');
|
||||
|
||||
// if user specified a custom home dashboard redirect to that
|
||||
if (dashDTO.redirectUri) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri);
|
||||
dispatch(updateLocation({ path: newUrl, replace: true }));
|
||||
return null;
|
||||
}
|
||||
|
||||
// disable some actions on the default home dashboard
|
||||
dashDTO.meta.canSave = false;
|
||||
dashDTO.meta.canShare = false;
|
||||
dashDTO.meta.canStar = false;
|
||||
return dashDTO;
|
||||
}
|
||||
case DashboardRouteInfo.Normal: {
|
||||
// for old db routes we redirect
|
||||
if (args.urlType === 'db') {
|
||||
redirectToNewUrl(args.urlSlug, dispatch, getState().location.path);
|
||||
return null;
|
||||
}
|
||||
|
||||
const loaderSrv: DashboardLoaderSrv = args.$injector.get('dashboardLoaderSrv');
|
||||
const dashDTO: DashboardDTO = await loaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);
|
||||
|
||||
if (args.fixUrl && dashDTO.meta.url) {
|
||||
// check if the current url is correct (might be old slug)
|
||||
const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url);
|
||||
const currentPath = getState().location.path;
|
||||
|
||||
if (dashboardUrl !== currentPath) {
|
||||
// replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times.
|
||||
dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true }));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return dashDTO;
|
||||
}
|
||||
case DashboardRouteInfo.New: {
|
||||
return getNewDashboardModelData(args.urlFolderId);
|
||||
}
|
||||
default:
|
||||
throw { message: 'Unknown route ' + args.routeInfo };
|
||||
}
|
||||
} catch (err) {
|
||||
dispatch(dashboardInitFailed({ message: 'Failed to fetch dashboard', error: err }));
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action (or saga) does everything needed to bootstrap a dashboard & dashboard model.
|
||||
* First it handles the process of fetching the dashboard, correcting the url if required (causing redirects/url updates)
|
||||
*
|
||||
* This is used both for single dashboard & solo panel routes, home & new dashboard routes.
|
||||
*
|
||||
* Then it handles the initializing of the old angular services that the dashboard components & panels still depend on
|
||||
*
|
||||
*/
|
||||
export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
// set fetching state
|
||||
dispatch(dashboardInitFetching());
|
||||
|
||||
// Detect slow loading / initializing and set state flag
|
||||
// This is in order to not show loading indication for fast loading dashboards as it creates blinking/flashing
|
||||
setTimeout(() => {
|
||||
if (getState().dashboard.model === null) {
|
||||
dispatch(dashboardInitSlow());
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// fetch dashboard data
|
||||
const dashDTO = await fetchDashboard(args, dispatch, getState);
|
||||
|
||||
// returns null if there was a redirect or error
|
||||
if (!dashDTO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set initializing state
|
||||
dispatch(dashboardInitServices());
|
||||
|
||||
// create model
|
||||
let dashboard: DashboardModel;
|
||||
try {
|
||||
dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta);
|
||||
} catch (err) {
|
||||
dispatch(dashboardInitFailed({ message: 'Failed create dashboard model', error: err }));
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// add missing orgId query param
|
||||
const storeState = getState();
|
||||
if (!storeState.location.query.orgId) {
|
||||
dispatch(updateLocation({ query: { orgId: storeState.user.orgId }, partial: true, replace: true }));
|
||||
}
|
||||
|
||||
// init services
|
||||
const timeSrv: TimeSrv = args.$injector.get('timeSrv');
|
||||
const annotationsSrv: AnnotationsSrv = args.$injector.get('annotationsSrv');
|
||||
const variableSrv: VariableSrv = args.$injector.get('variableSrv');
|
||||
const keybindingSrv: KeybindingSrv = args.$injector.get('keybindingSrv');
|
||||
const unsavedChangesSrv = args.$injector.get('unsavedChangesSrv');
|
||||
const dashboardSrv: DashboardSrv = args.$injector.get('dashboardSrv');
|
||||
|
||||
timeSrv.init(dashboard);
|
||||
annotationsSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
try {
|
||||
await variableSrv.init(dashboard);
|
||||
} catch (err) {
|
||||
dispatch(notifyApp(createErrorNotification('Templating init failed', err)));
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
try {
|
||||
dashboard.processRepeats();
|
||||
dashboard.updateSubmenuVisibility();
|
||||
|
||||
// handle auto fix experimental feature
|
||||
const queryParams = getState().location.query;
|
||||
if (queryParams.autofitpanels) {
|
||||
dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk);
|
||||
}
|
||||
|
||||
// init unsaved changes tracking
|
||||
unsavedChangesSrv.init(dashboard, args.$scope);
|
||||
keybindingSrv.setupDashboardBindings(args.$scope, dashboard);
|
||||
} catch (err) {
|
||||
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err)));
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// legacy srv state
|
||||
dashboardSrv.setCurrent(dashboard);
|
||||
// yay we are done
|
||||
dispatch(dashboardInitCompleted(dashboard));
|
||||
};
|
||||
}
|
||||
|
||||
function getNewDashboardModelData(urlFolderId?: string): any {
|
||||
const data = {
|
||||
meta: {
|
||||
canStar: false,
|
||||
canShare: false,
|
||||
isNew: true,
|
||||
folderId: 0,
|
||||
},
|
||||
dashboard: {
|
||||
title: 'New dashboard',
|
||||
panels: [
|
||||
{
|
||||
type: 'add-panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 9 },
|
||||
title: 'Panel Title',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (urlFolderId) {
|
||||
data.meta.folderId = parseInt(urlFolderId, 10);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
@ -1,19 +1,23 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { OrgRole, PermissionLevel, DashboardState } from 'app/types';
|
||||
import {
|
||||
loadDashboardPermissions,
|
||||
dashboardInitFetching,
|
||||
dashboardInitCompleted,
|
||||
dashboardInitFailed,
|
||||
dashboardInitSlow,
|
||||
} from './actions';
|
||||
import { OrgRole, PermissionLevel, DashboardState, DashboardInitPhase } from 'app/types';
|
||||
import { initialState, dashboardReducer } from './reducers';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
|
||||
describe('dashboard reducer', () => {
|
||||
describe('loadDashboardPermissions', () => {
|
||||
let state: DashboardState;
|
||||
|
||||
beforeEach(() => {
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadDashboardPermissions,
|
||||
payload: [
|
||||
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||
],
|
||||
};
|
||||
const action = loadDashboardPermissions([
|
||||
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||
]);
|
||||
state = dashboardReducer(initialState, action);
|
||||
});
|
||||
|
||||
@ -21,4 +25,47 @@ describe('dashboard reducer', () => {
|
||||
expect(state.permissions.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboardInitCompleted', () => {
|
||||
let state: DashboardState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = dashboardReducer(initialState, dashboardInitFetching());
|
||||
state = dashboardReducer(state, dashboardInitSlow());
|
||||
state = dashboardReducer(state, dashboardInitCompleted(new DashboardModel({ title: 'My dashboard' })));
|
||||
});
|
||||
|
||||
it('should set model', async () => {
|
||||
expect(state.model.title).toBe('My dashboard');
|
||||
});
|
||||
|
||||
it('should set reset isInitSlow', async () => {
|
||||
expect(state.isInitSlow).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboardInitFailed', () => {
|
||||
let state: DashboardState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = dashboardReducer(initialState, dashboardInitFetching());
|
||||
state = dashboardReducer(state, dashboardInitFailed({message: 'Oh no', error: 'sad'}));
|
||||
});
|
||||
|
||||
it('should set model', async () => {
|
||||
expect(state.model.title).toBe('Dashboard init failed');
|
||||
});
|
||||
|
||||
it('should set reset isInitSlow', async () => {
|
||||
expect(state.isInitSlow).toBe(false);
|
||||
});
|
||||
|
||||
it('should set initError', async () => {
|
||||
expect(state.initError.message).toBe('Oh no');
|
||||
});
|
||||
|
||||
it('should set phase failed', async () => {
|
||||
expect(state.initPhase).toBe(DashboardInitPhase.Failed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,21 +1,90 @@
|
||||
import { DashboardState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { DashboardState, DashboardInitPhase } from 'app/types';
|
||||
import {
|
||||
loadDashboardPermissions,
|
||||
dashboardInitFetching,
|
||||
dashboardInitSlow,
|
||||
dashboardInitServices,
|
||||
dashboardInitFailed,
|
||||
dashboardInitCompleted,
|
||||
cleanUpDashboard,
|
||||
} from './actions';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
|
||||
export const initialState: DashboardState = {
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
isInitSlow: false,
|
||||
model: null,
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
export const dashboardReducer = (state = initialState, action: Action): DashboardState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadDashboardPermissions:
|
||||
export const dashboardReducer = reducerFactory(initialState)
|
||||
.addMapper({
|
||||
filter: loadDashboardPermissions,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
permissions: processAclItems(action.payload),
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitFetching,
|
||||
mapper: state => ({
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Fetching,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitServices,
|
||||
mapper: state => ({
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Services,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitSlow,
|
||||
mapper: state => ({
|
||||
...state,
|
||||
isInitSlow: true,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitFailed,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Failed,
|
||||
isInitSlow: false,
|
||||
initError: action.payload,
|
||||
model: new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }),
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitCompleted,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Completed,
|
||||
model: action.payload,
|
||||
isInitSlow: false,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: cleanUpDashboard,
|
||||
mapper: (state, action) => {
|
||||
|
||||
// Destroy current DashboardModel
|
||||
// Very important as this removes all dashboard event listeners
|
||||
state.model.destroy();
|
||||
|
||||
return {
|
||||
...state,
|
||||
permissions: processAclItems(action.payload),
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
model: null,
|
||||
isInitSlow: false,
|
||||
initError: null,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
},
|
||||
})
|
||||
.create();
|
||||
|
||||
export default {
|
||||
dashboard: dashboardReducer,
|
||||
|
@ -102,10 +102,10 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||
<div className="explore-toolbar-header">
|
||||
<div className="explore-toolbar-header-title">
|
||||
{exploreId === 'left' && (
|
||||
<a className="navbar-page-btn">
|
||||
<span className="navbar-page-btn">
|
||||
<i className="fa fa-rocket fa-fw" />
|
||||
Explore
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="explore-toolbar-header-close">
|
||||
|
@ -7,6 +7,7 @@ import { Emitter } from 'app/core/core';
|
||||
import getFactors from 'app/core/utils/factors';
|
||||
import {
|
||||
duplicatePanel,
|
||||
removePanel,
|
||||
copyPanel as copyPanelUtil,
|
||||
editPanelJson as editPanelJsonUtil,
|
||||
sharePanel as sharePanelUtil,
|
||||
@ -213,9 +214,7 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
removePanel() {
|
||||
this.publishAppEvent('panel-remove', {
|
||||
panelId: this.panel.id,
|
||||
});
|
||||
removePanel(this.dashboard, this.panel, true);
|
||||
}
|
||||
|
||||
editPanelJson() {
|
||||
|
@ -1,12 +1,16 @@
|
||||
import coreModule from '../../core/core_module';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import appEvents from 'app/core/app_events';
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import { toUrlParams } from 'app/core/utils/url';
|
||||
import coreModule from '../../core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class PlaylistSrv {
|
||||
private cancelPromise: any;
|
||||
private dashboards: Array<{ uri: string }>;
|
||||
private dashboards: Array<{ url: string }>;
|
||||
private index: number;
|
||||
private interval: number;
|
||||
private startUrl: string;
|
||||
@ -36,7 +40,12 @@ export class PlaylistSrv {
|
||||
const queryParams = this.$location.search();
|
||||
const filteredParams = _.pickBy(queryParams, value => value !== null);
|
||||
|
||||
this.$location.url('dashboard/' + dash.uri + '?' + toUrlParams(filteredParams));
|
||||
// this is done inside timeout to make sure digest happens after
|
||||
// as this can be called from react
|
||||
this.$timeout(() => {
|
||||
const stripedUrl = locationUtil.stripBaseFromUrl(dash.url);
|
||||
this.$location.url(stripedUrl + '?' + toUrlParams(filteredParams));
|
||||
});
|
||||
|
||||
this.index++;
|
||||
this.cancelPromise = this.$timeout(() => this.next(), this.interval);
|
||||
@ -54,6 +63,8 @@ export class PlaylistSrv {
|
||||
this.index = 0;
|
||||
this.isPlaying = true;
|
||||
|
||||
appEvents.emit('playlist-started');
|
||||
|
||||
return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
|
||||
return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
|
||||
this.dashboards = dashboards;
|
||||
@ -77,6 +88,8 @@ export class PlaylistSrv {
|
||||
if (this.cancelPromise) {
|
||||
this.$timeout.cancel(this.cancelPromise);
|
||||
}
|
||||
|
||||
appEvents.emit('playlist-stopped');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PlaylistSrv } from '../playlist_srv';
|
||||
|
||||
const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }];
|
||||
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
|
||||
|
||||
const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any> }] => {
|
||||
const mockBackendSrv = {
|
||||
@ -50,13 +50,12 @@ const mockWindowLocation = (): [jest.MockInstance<any>, () => void] => {
|
||||
|
||||
describe('PlaylistSrv', () => {
|
||||
let srv: PlaylistSrv;
|
||||
let mockLocationService: { url: jest.MockInstance<any> };
|
||||
let hrefMock: jest.MockInstance<any>;
|
||||
let unmockLocation: () => void;
|
||||
const initialUrl = 'http://localhost/playlist';
|
||||
|
||||
beforeEach(() => {
|
||||
[srv, mockLocationService] = createPlaylistSrv();
|
||||
[srv] = createPlaylistSrv();
|
||||
[hrefMock, unmockLocation] = mockWindowLocation();
|
||||
|
||||
// This will be cached in the srv when start() is called
|
||||
@ -71,7 +70,6 @@ describe('PlaylistSrv', () => {
|
||||
await srv.start(1);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
@ -84,7 +82,6 @@ describe('PlaylistSrv', () => {
|
||||
|
||||
// 1 complete loop
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
@ -93,7 +90,6 @@ describe('PlaylistSrv', () => {
|
||||
|
||||
// Another 2 loops
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
|
14
public/app/features/profile/state/reducers.ts
Normal file
14
public/app/features/profile/state/reducers.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { UserState } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const initialState: UserState = {
|
||||
orgId: config.bootData.user.orgId,
|
||||
};
|
||||
|
||||
export const userReducer = (state = initialState, action: any): UserState => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
user: userReducer,
|
||||
};
|
@ -48,7 +48,6 @@ describe('VariableSrv', function(this: any) {
|
||||
ds.metricFindQuery = () => Promise.resolve(scenario.queryResult);
|
||||
|
||||
ctx.variableSrv = new VariableSrv(
|
||||
ctx.$rootScope,
|
||||
$q,
|
||||
ctx.$location,
|
||||
ctx.$injector,
|
||||
|
@ -25,10 +25,6 @@ describe('VariableSrv init', function(this: any) {
|
||||
};
|
||||
|
||||
const $injector = {} as any;
|
||||
const $rootscope = {
|
||||
$on: () => {},
|
||||
};
|
||||
|
||||
let ctx = {} as any;
|
||||
|
||||
function describeInitScenario(desc, fn) {
|
||||
@ -54,7 +50,7 @@ describe('VariableSrv init', function(this: any) {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv, timeSrv);
|
||||
ctx.variableSrv = new VariableSrv($q, {}, $injector, templateSrv, timeSrv);
|
||||
|
||||
$injector.instantiate = (variable, model) => {
|
||||
return getVarMockConstructor(variable, model, ctx);
|
||||
|
@ -18,18 +18,18 @@ export class VariableSrv {
|
||||
variables: any[];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope,
|
||||
private $q,
|
||||
constructor(private $q,
|
||||
private $location,
|
||||
private $injector,
|
||||
private templateSrv: TemplateSrv,
|
||||
private timeSrv: TimeSrv) {
|
||||
$rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
|
||||
|
||||
}
|
||||
|
||||
init(dashboard: DashboardModel) {
|
||||
this.dashboard = dashboard;
|
||||
this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
|
||||
this.dashboard.events.on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this));
|
||||
|
||||
// create working class models representing variables
|
||||
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
|
||||
@ -59,7 +59,7 @@ export class VariableSrv {
|
||||
|
||||
return variable.updateOptions().then(() => {
|
||||
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
|
||||
this.$rootScope.$emit('template-variable-value-updated');
|
||||
this.dashboard.templateVariableValueUpdated();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -144,7 +144,7 @@ export class VariableSrv {
|
||||
|
||||
return this.$q.all(promises).then(() => {
|
||||
if (emitChangeEvents) {
|
||||
this.$rootScope.appEvent('template-variable-value-updated');
|
||||
this.dashboard.templateVariableValueUpdated();
|
||||
this.dashboard.startRefresh();
|
||||
}
|
||||
});
|
||||
|
@ -1,17 +0,0 @@
|
||||
<div dash-class ng-if="ctrl.dashboard">
|
||||
<dashnav dashboard="ctrl.dashboard"></dashnav>
|
||||
|
||||
<div class="scroll-canvas scroll-canvas--dashboard" page-scrollbar>
|
||||
<dashboard-settings dashboard="ctrl.dashboard"
|
||||
ng-if="ctrl.dashboardViewState.state.editview"
|
||||
class="dashboard-settings">
|
||||
</dashboard-settings>
|
||||
|
||||
<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>
|
||||
|
||||
<dashboard-grid dashboard="ctrl.dashboard"></dashboard-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,9 +1,11 @@
|
||||
import config from 'app/core/config';
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
// Utils and servies
|
||||
import { colors } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@ -13,6 +15,9 @@ import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource
|
||||
import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
// Types
|
||||
import { KioskUrlValue } from 'app/types';
|
||||
|
||||
export class GrafanaCtrl {
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
@ -46,11 +51,6 @@ export class GrafanaCtrl {
|
||||
|
||||
$rootScope.colors = colors;
|
||||
|
||||
$scope.initDashboard = (dashboardData, viewScope) => {
|
||||
$scope.appEvent('dashboard-fetch-end', dashboardData);
|
||||
$controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData);
|
||||
};
|
||||
|
||||
$rootScope.onAppEvent = function(name, callback, localScope) {
|
||||
const unbind = $rootScope.$on(name, callback);
|
||||
let callerScope = this;
|
||||
@ -72,7 +72,7 @@ export class GrafanaCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) {
|
||||
function setViewModeBodyClass(body, mode: KioskUrlValue, sidemenuOpen: boolean) {
|
||||
body.removeClass('view-mode--tv');
|
||||
body.removeClass('view-mode--kiosk');
|
||||
body.removeClass('view-mode--inactive');
|
||||
@ -126,12 +126,13 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
body.toggleClass('sidemenu-hidden');
|
||||
});
|
||||
|
||||
scope.$watch(
|
||||
() => playlistSrv.isPlaying,
|
||||
newValue => {
|
||||
elem.toggleClass('view-mode--playlist', newValue === true);
|
||||
}
|
||||
);
|
||||
appEvents.on('playlist-started', () => {
|
||||
elem.toggleClass('view-mode--playlist', true);
|
||||
});
|
||||
|
||||
appEvents.on('playlist-stopped', () => {
|
||||
elem.toggleClass('view-mode--playlist', false);
|
||||
});
|
||||
|
||||
// check if we are in server side render
|
||||
if (document.cookie.indexOf('renderKey') !== -1) {
|
||||
@ -165,6 +166,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
for (const drop of Drop.drops) {
|
||||
drop.destroy();
|
||||
}
|
||||
|
||||
appEvents.emit('hide-dash-search');
|
||||
});
|
||||
|
||||
// handle kiosk mode
|
||||
@ -262,10 +265,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (target.parents('.navbar-buttons--playlist').length === 0) {
|
||||
playlistSrv.stop();
|
||||
}
|
||||
|
||||
// hide search
|
||||
if (body.find('.search-container').length > 0) {
|
||||
if (target.parents('.search-results-container, .search-field-wrapper').length === 0) {
|
||||
|
@ -44,11 +44,15 @@ export function reactContainer(
|
||||
$injector: $injector,
|
||||
$rootScope: $rootScope,
|
||||
$scope: scope,
|
||||
routeInfo: $route.current.$$route.routeInfo,
|
||||
};
|
||||
|
||||
document.body.classList.add('is-react');
|
||||
|
||||
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
document.body.classList.remove('is-react');
|
||||
ReactDOM.unmountComponentAtNode(elem[0]);
|
||||
});
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import './dashboard_loaders';
|
||||
import './ReactContainer';
|
||||
import { applyRouteRegistrationHandlers } from './registry';
|
||||
|
||||
// Pages
|
||||
import ServerStats from 'app/features/admin/ServerStats';
|
||||
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||
import TeamPages from 'app/features/teams/TeamPages';
|
||||
@ -20,40 +21,66 @@ import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards'
|
||||
import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage';
|
||||
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
||||
import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
|
||||
import DashboardPage from '../features/dashboard/containers/DashboardPage';
|
||||
import config from 'app/core/config';
|
||||
|
||||
// Types
|
||||
import { DashboardRouteInfo } from 'app/types';
|
||||
|
||||
/** @ngInject */
|
||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
template: '<react-container />',
|
||||
pageClass: 'page-dashboard',
|
||||
routeInfo: DashboardRouteInfo.Home,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => DashboardPage,
|
||||
},
|
||||
})
|
||||
.when('/d/:uid/:slug', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
template: '<react-container />',
|
||||
pageClass: 'page-dashboard',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => DashboardPage,
|
||||
},
|
||||
})
|
||||
.when('/d/:uid', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
template: '<react-container />',
|
||||
pageClass: 'page-dashboard',
|
||||
reloadOnSearch: false,
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
resolve: {
|
||||
component: () => DashboardPage,
|
||||
},
|
||||
})
|
||||
.when('/dashboard/:type/:slug', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
template: '<react-container />',
|
||||
pageClass: 'page-dashboard',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => DashboardPage,
|
||||
},
|
||||
})
|
||||
.when('/dashboard/new', {
|
||||
template: '<react-container />',
|
||||
pageClass: 'page-dashboard',
|
||||
routeInfo: DashboardRouteInfo.New,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => DashboardPage,
|
||||
},
|
||||
})
|
||||
.when('/d-solo/:uid/:slug', {
|
||||
template: '<react-container />',
|
||||
pageClass: 'dashboard-solo',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
resolve: {
|
||||
component: () => SoloPanelPage,
|
||||
},
|
||||
@ -61,16 +88,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
.when('/dashboard-solo/:type/:slug', {
|
||||
template: '<react-container />',
|
||||
pageClass: 'dashboard-solo',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
resolve: {
|
||||
component: () => SoloPanelPage,
|
||||
},
|
||||
})
|
||||
.when('/dashboard/new', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'NewDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/dashboard/import', {
|
||||
templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_import.html',
|
||||
controller: DashboardImportCtrl,
|
||||
|
@ -11,6 +11,7 @@ import exploreReducers from 'app/features/explore/state/reducers';
|
||||
import pluginReducers from 'app/features/plugins/state/reducers';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
import usersReducers from 'app/features/users/state/reducers';
|
||||
import userReducers from 'app/features/profile/state/reducers';
|
||||
import organizationReducers from 'app/features/org/state/reducers';
|
||||
import { setStore } from './store';
|
||||
|
||||
@ -25,6 +26,7 @@ const rootReducers = {
|
||||
...pluginReducers,
|
||||
...dataSourcesReducers,
|
||||
...usersReducers,
|
||||
...userReducers,
|
||||
...organizationReducers,
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,71 @@
|
||||
import { DashboardAcl } from './acl';
|
||||
|
||||
export interface DashboardState {
|
||||
permissions: DashboardAcl[];
|
||||
export interface MutableDashboard {
|
||||
title: string;
|
||||
meta: DashboardMeta;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export interface DashboardDTO {
|
||||
redirectUri?: string;
|
||||
dashboard: DashboardDataDTO;
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export interface DashboardMeta {
|
||||
canSave?: boolean;
|
||||
canEdit?: boolean;
|
||||
canShare?: boolean;
|
||||
canStar?: boolean;
|
||||
canAdmin?: boolean;
|
||||
url?: string;
|
||||
folderId?: number;
|
||||
fullscreen?: boolean;
|
||||
isEditing?: boolean;
|
||||
canMakeEditable?: boolean;
|
||||
submenuEnabled?: boolean;
|
||||
provisioned?: boolean;
|
||||
focusPanelId?: boolean;
|
||||
isStarred?: boolean;
|
||||
showSettings?: boolean;
|
||||
expires?: string;
|
||||
isSnapshot?: boolean;
|
||||
folderTitle?: string;
|
||||
folderUrl?: string;
|
||||
created?: string;
|
||||
}
|
||||
|
||||
export interface DashboardDataDTO {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export enum DashboardRouteInfo {
|
||||
Home = 'home-dashboard',
|
||||
New = 'new-dashboard',
|
||||
Normal = 'normal-dashboard',
|
||||
Scripted = 'scripted-dashboard',
|
||||
}
|
||||
|
||||
export enum DashboardInitPhase {
|
||||
NotStarted = 'Not started',
|
||||
Fetching = 'Fetching',
|
||||
Services = 'Services',
|
||||
Failed = 'Failed',
|
||||
Completed = 'Completed',
|
||||
}
|
||||
|
||||
export interface DashboardInitError {
|
||||
message: string;
|
||||
error: any;
|
||||
}
|
||||
|
||||
export const KIOSK_MODE_TV = 'tv';
|
||||
export type KioskUrlValue = 'tv' | '1' | true;
|
||||
|
||||
export interface DashboardState {
|
||||
model: MutableDashboard | null;
|
||||
initPhase: DashboardInitPhase;
|
||||
isInitSlow: boolean;
|
||||
initError?: DashboardInitError;
|
||||
permissions: DashboardAcl[] | null;
|
||||
}
|
||||
|
@ -3,6 +3,10 @@ export interface LocationUpdate {
|
||||
query?: UrlQueryMap;
|
||||
routeParams?: UrlQueryMap;
|
||||
partial?: boolean;
|
||||
/*
|
||||
* If true this will replace url state (ie cause no new browser history)
|
||||
*/
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface LocationState {
|
||||
@ -10,6 +14,7 @@ export interface LocationState {
|
||||
path: string;
|
||||
query: UrlQueryMap;
|
||||
routeParams: UrlQueryMap;
|
||||
replace: boolean;
|
||||
}
|
||||
|
||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { ThunkAction, ThunkDispatch as GenericThunkDispatch } from 'redux-thunk';
|
||||
import { ActionOf } from 'app/core/redux';
|
||||
|
||||
import { NavIndex } from './navModel';
|
||||
import { LocationState } from './location';
|
||||
import { AlertRulesState } from './alerting';
|
||||
@ -27,3 +30,10 @@ export interface StoreState {
|
||||
user: UserState;
|
||||
plugins: PluginsState;
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility type to get strongly types thunks
|
||||
*/
|
||||
export type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ActionOf<any>>;
|
||||
|
||||
export type ThunkDispatch = GenericThunkDispatch<StoreState, undefined, any>;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { DashboardSearchHit } from './search';
|
||||
|
||||
export interface OrgUser {
|
||||
avatarUrl: string;
|
||||
email: string;
|
||||
@ -47,5 +45,5 @@ export interface UsersState {
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
starredDashboards: DashboardSearchHit[];
|
||||
orgId: number;
|
||||
}
|
||||
|
@ -16,6 +16,9 @@
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease-in-out;
|
||||
}
|
||||
.dashboard-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-settings__content {
|
||||
|
@ -83,8 +83,7 @@
|
||||
font-size: 19px;
|
||||
line-height: 8px;
|
||||
opacity: 0.75;
|
||||
margin-right: 8px;
|
||||
// icon hidden on smaller screens
|
||||
margin-right: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -102,7 +101,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: $spacer;
|
||||
margin-left: 10px;
|
||||
|
||||
&--close {
|
||||
display: none;
|
||||
|
@ -276,3 +276,19 @@ div.flot-text {
|
||||
.panel-full-edit {
|
||||
padding-top: $dashboard-padding;
|
||||
}
|
||||
|
||||
.dashboard-loading {
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.alert {
|
||||
max-width: 600px;
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-loading__text {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
@ -189,10 +189,10 @@
|
||||
<grafana-app class="grafana-app" ng-cloak>
|
||||
<sidemenu class="sidemenu"></sidemenu>
|
||||
<app-notifications-list class="page-alert-list"></app-notifications-list>
|
||||
|
||||
<dashboard-search></dashboard-search>
|
||||
|
||||
<div class="main-view">
|
||||
<div class="scroll-canvas" page-scrollbar>
|
||||
<div class="scroll-canvas">
|
||||
<div ng-view></div>
|
||||
|
||||
<footer class="footer">
|
||||
|
@ -14582,6 +14582,13 @@ redux-logger@^3.0.6:
|
||||
dependencies:
|
||||
deep-diff "^0.3.5"
|
||||
|
||||
redux-mock-store@^1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
|
||||
integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==
|
||||
dependencies:
|
||||
lodash.isplainobject "^4.0.6"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
Loading…
Reference in New Issue
Block a user