mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 12556-oauth-pass-thru
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
|
||||
export default class AdminEditOrgCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
|
||||
$scope.init = () => {
|
||||
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
|
||||
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
|
||||
|
||||
if ($routeParams.id) {
|
||||
$scope.getOrg($routeParams.id);
|
||||
@@ -46,4 +45,3 @@ export default class AdminEditOrgCtrl {
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class AdminEditUserCtrl {
|
||||
$scope.user = {};
|
||||
$scope.newOrg = { name: '', role: 'Editor' };
|
||||
$scope.permissions = {};
|
||||
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
|
||||
$scope.navModel = navModelSrv.getNav('admin', 'global-users', 0);
|
||||
|
||||
$scope.init = () => {
|
||||
if ($routeParams.id) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
export default class AdminListOrgsCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, backendSrv, navModelSrv) {
|
||||
$scope.init = () => {
|
||||
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
|
||||
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
|
||||
$scope.getOrgs();
|
||||
};
|
||||
|
||||
@@ -31,4 +30,3 @@ export default class AdminListOrgsCtrl {
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class AdminListUsersCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private backendSrv, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
|
||||
this.navModel = navModelSrv.getNav('admin', 'global-users', 0);
|
||||
this.query = '';
|
||||
this.getUsers();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ServerStats extends PureComponent<Props, State> {
|
||||
|
||||
this.state = {
|
||||
stats: [],
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default class StyleGuideCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $routeParams, private backendSrv, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
|
||||
this.navModel = navModelSrv.getNav('admin', 'styleguide', 0);
|
||||
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
@@ -25,4 +25,3 @@ export default class StyleGuideCtrl {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
className="page-scrollbar-wrapper"
|
||||
>
|
||||
<div
|
||||
className="custom-scrollbars"
|
||||
className="custom-scrollbar custom-scrollbar--page"
|
||||
style={
|
||||
Object {
|
||||
"height": "auto",
|
||||
|
||||
@@ -11,7 +11,7 @@ class AdminSettingsCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, backendSrv, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1);
|
||||
this.navModel = navModelSrv.getNav('admin', 'server-settings', 0);
|
||||
|
||||
backendSrv.get('/api/admin/settings').then(settings => {
|
||||
$scope.settings = settings;
|
||||
@@ -24,7 +24,7 @@ class AdminHomeCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 1);
|
||||
this.navModel = navModelSrv.getNav('admin', 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="update()" ng-show="!createMode">Update</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="update()" ng-show="!createMode">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="update()" ng-show="!createMode">Update</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="update()" ng-show="!createMode">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="setPassword()">Update</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="setPassword()">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="updatePermissions()">Update</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="updatePermissions()">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-success gf-form-btn" ng-click="addOrgUser()">Add</button>
|
||||
<button class="btn btn-primary gf-form-btn" ng-click="addOrgUser()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="create()">Create</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="create()">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
<div class="page-container page-body">
|
||||
<div class="page-action-bar">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="page-header__cta btn btn-success" href="org/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Org
|
||||
<a class="page-header__cta btn btn-primary" href="org/new">
|
||||
New org
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
<i class="gf-form-input-icon fa fa-search"></i>
|
||||
</label>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="btn btn-success" href="admin/users/create">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add new user
|
||||
<a class="btn btn-primary" href="admin/users/create">
|
||||
New user
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent<Props> {
|
||||
'fa-pause': rule.state !== 'paused',
|
||||
});
|
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
|
||||
@@ -18,7 +18,7 @@ const setup = (propOverrides?: object) => {
|
||||
togglePauseAlertRule: jest.fn(),
|
||||
stateFilter: '',
|
||||
search: '',
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@@ -147,9 +147,8 @@ describe('Functions', () => {
|
||||
describe('Search query change', () => {
|
||||
it('should set search query', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
instance.onSearchQueryChange('dashboard');
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { NavModel, StoreState, AlertRule } from 'app/types';
|
||||
import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
|
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@@ -69,8 +70,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = evt.target;
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchQuery(value);
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
|
||||
};
|
||||
|
||||
alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
|
||||
alertStateFilterOption = ({ text, value }: { text: string; value: string }) => {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
@@ -94,16 +94,13 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
inputClassName="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">States</label>
|
||||
@@ -142,7 +139,7 @@ const mapStateToProps = (state: StoreState) => ({
|
||||
alertRules: getAlertRuleItems(state.alertRules),
|
||||
stateFilter: state.location.query.state,
|
||||
search: getSearchQuery(state.alertRules),
|
||||
isLoading: state.alertRules.isLoading
|
||||
isLoading: state.alertRules.isLoading,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
@@ -152,4 +149,9 @@ const mapDispatchToProps = {
|
||||
togglePauseAlertRule,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList));
|
||||
export default hot(module)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AlertRuleList)
|
||||
);
|
||||
|
||||
@@ -140,7 +140,7 @@ export class AlertTabCtrl {
|
||||
name: model.name,
|
||||
iconClass: this.getNotificationIcon(model.type),
|
||||
isDefault: false,
|
||||
uid: model.uid
|
||||
uid: model.uid,
|
||||
});
|
||||
|
||||
// avoid duplicates using both id and uid to be backwards compatible.
|
||||
@@ -157,8 +157,8 @@ export class AlertTabCtrl {
|
||||
removeNotification(an) {
|
||||
// remove notifiers refeered to by id and uid to support notifiers added
|
||||
// before and after we added support for uid
|
||||
_.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
|
||||
}
|
||||
|
||||
initModel() {
|
||||
|
||||
@@ -21,7 +21,7 @@ exports[`Render should render component 1`] = `
|
||||
className="alert-rule-item__name"
|
||||
>
|
||||
<a
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
@@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
|
||||
title="Edit alert rule"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -13,20 +13,13 @@ exports[`Render should render alert rules 1`] = `
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
@@ -167,20 +160,13 @@ exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
|
||||
<input type="text" class="gf-form-input max-width-6 gf-form-input--has-help-icon" ng-model="ctrl.alert.for"
|
||||
spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group gf-form-button-row">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success width-7">Save</button>
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-primary width-7">Save</button>
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary width-7">Send Test</button>
|
||||
<a href="alerting/notifications" class="btn btn-inverse">Back</a>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
<div class="page-action-bar__spacer">
|
||||
</div>
|
||||
|
||||
<a href="alerting/notification/new" class="btn btn-success">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Channel
|
||||
<a href="alerting/notification/new" class="btn btn-primary">
|
||||
New channel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,3 +12,4 @@ import './manage-dashboards';
|
||||
import './teams/CreateTeamCtrl';
|
||||
import './profile/all';
|
||||
import './datasources/settings/HttpSettingsCtrl';
|
||||
import './datasources/settings/TlsAuthSettingsCtrl';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div ng-if="ctrl.mode === 'list'">
|
||||
<div class="page-action-bar" ng-if="ctrl.annotations.length > 1">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a type="button" class="btn btn-success" ng-click="ctrl.setupNew();"><i class="fa fa-plus" ></i> New</a>
|
||||
<a type="button" class="btn btn-primary" ng-click="ctrl.setupNew();"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
|
||||
<table class="filter-table filter-table--hover">
|
||||
@@ -48,7 +48,7 @@
|
||||
<div ng-if="ctrl.annotations.length === 1" class="p-t-2">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">There are no custom annotation queries added yet</div>
|
||||
<a ng-click="ctrl.setupNew()" class="empty-list-cta__button btn btn-xlarge btn-success">
|
||||
<a ng-click="ctrl.setupNew()" class="empty-list-cta__button btn btn-xlarge btn-primary">
|
||||
<i class="gicon gicon-add-annotation"></i>
|
||||
Add Annotation Query
|
||||
</a>
|
||||
@@ -105,8 +105,8 @@
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-primary" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-primary pull-left" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.save()">Save</button>
|
||||
<button ng-if="ctrl.event.id" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button>
|
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {
|
||||
main: {
|
||||
text: 'Configuration'
|
||||
text: 'Configuration',
|
||||
},
|
||||
node: {
|
||||
text: 'Api Keys'
|
||||
}
|
||||
text: 'Api Keys',
|
||||
},
|
||||
} as NavModel,
|
||||
apiKeys: [] as ApiKey[],
|
||||
searchQuery: '',
|
||||
@@ -78,9 +78,8 @@ describe('Functions', () => {
|
||||
describe('on search query change', () => {
|
||||
it('should call setSearchQuery', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'test' } };
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
instance.onSearchQueryChange('test');
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@@ -59,8 +60,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
this.props.deleteApiKey(key.id);
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.setSearchQuery(evt.target.value);
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchQuery(value);
|
||||
};
|
||||
|
||||
onToggleAdding = () => {
|
||||
@@ -107,7 +108,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
renderEmptyList() {
|
||||
const { isAdding } = this.state;
|
||||
return (
|
||||
<div className="page-container page-body">
|
||||
<>
|
||||
{!isAdding && (
|
||||
<EmptyListCTA
|
||||
model={{
|
||||
@@ -124,7 +125,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
/>
|
||||
)}
|
||||
{this.renderAddApiKeyForm()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
</span>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<button className="btn gf-form-btn btn-success">Add</button>
|
||||
<button className="btn gf-form-btn btn-primary">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -183,24 +184,21 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
const { apiKeys, searchQuery } = this.props;
|
||||
|
||||
return (
|
||||
<div className="page-container page-body">
|
||||
<>
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search keys"
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
inputClassName="gf-form-input"
|
||||
placeholder="Search keys"
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||
<i className="fa fa-plus" /> Add API Key
|
||||
<button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||
Add API key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +229,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
</tbody>
|
||||
) : null}
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,13 +239,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
{hasFetched && (
|
||||
apiKeysCount > 0 ? (
|
||||
this.renderApiKeyList()
|
||||
) : (
|
||||
this.renderEmptyList()
|
||||
)
|
||||
)}
|
||||
{hasFetched && (apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList())}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
@@ -271,4 +263,9 @@ const mapDispatchToProps = {
|
||||
addApiKey,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage));
|
||||
export default hot(module)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ApiKeysPage)
|
||||
);
|
||||
|
||||
@@ -35,118 +35,114 @@ exports[`Render should render CTA if there are no API keys 1`] = `
|
||||
<PageContents
|
||||
isLoading={false}
|
||||
>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<EmptyListCTA
|
||||
model={
|
||||
Object {
|
||||
"buttonIcon": "fa fa-plus",
|
||||
"buttonLink": "#",
|
||||
"buttonTitle": " New API Key",
|
||||
"onClick": [Function],
|
||||
"proTip": "Remember you can provide view-only API access to other applications.",
|
||||
"proTipLink": "",
|
||||
"proTipLinkTitle": "",
|
||||
"proTipTarget": "_blank",
|
||||
"title": "You haven't added any API Keys yet.",
|
||||
}
|
||||
<EmptyListCTA
|
||||
model={
|
||||
Object {
|
||||
"buttonIcon": "fa fa-plus",
|
||||
"buttonLink": "#",
|
||||
"buttonTitle": " New API Key",
|
||||
"onClick": [Function],
|
||||
"proTip": "Remember you can provide view-only API access to other applications.",
|
||||
"proTipLink": "",
|
||||
"proTipLinkTitle": "",
|
||||
"proTipTarget": "_blank",
|
||||
"title": "You haven't added any API Keys yet.",
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
in={false}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
in={false}
|
||||
>
|
||||
<div
|
||||
className="cta-form"
|
||||
>
|
||||
<div
|
||||
className="cta-form"
|
||||
<button
|
||||
className="cta-form__close btn btn-transparent"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="cta-form__close btn btn-transparent"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-close"
|
||||
/>
|
||||
</button>
|
||||
<h5>
|
||||
Add API Key
|
||||
</h5>
|
||||
<form
|
||||
className="gf-form-group"
|
||||
onSubmit={[Function]}
|
||||
<i
|
||||
className="fa fa-close"
|
||||
/>
|
||||
</button>
|
||||
<h5>
|
||||
Add API Key
|
||||
</h5>
|
||||
<form
|
||||
className="gf-form-group"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
className="gf-form max-width-21"
|
||||
>
|
||||
<div
|
||||
className="gf-form max-width-21"
|
||||
<span
|
||||
className="gf-form-label"
|
||||
>
|
||||
<span
|
||||
className="gf-form-label"
|
||||
>
|
||||
Key name
|
||||
</span>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
Key name
|
||||
</span>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<span
|
||||
className="gf-form-label"
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<span
|
||||
className="gf-form-select-wrapper"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input gf-size-auto"
|
||||
onChange={[Function]}
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<span
|
||||
className="gf-form-label"
|
||||
value="Viewer"
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<span
|
||||
className="gf-form-select-wrapper"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input gf-size-auto"
|
||||
onChange={[Function]}
|
||||
<option
|
||||
key="Viewer"
|
||||
label="Viewer"
|
||||
value="Viewer"
|
||||
>
|
||||
<option
|
||||
key="Viewer"
|
||||
label="Viewer"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor"
|
||||
label="Editor"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin"
|
||||
label="Admin"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<button
|
||||
className="btn gf-form-btn btn-success"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor"
|
||||
label="Editor"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin"
|
||||
label="Admin"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<button
|
||||
className="btn gf-form-btn btn-primary"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Component>
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
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 */
|
||||
constructor(
|
||||
private uiSegmentSrv,
|
||||
private datasourceSrv,
|
||||
private $q,
|
||||
private variableSrv,
|
||||
$scope,
|
||||
private $rootScope
|
||||
) {
|
||||
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, $scope) {
|
||||
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 +166,7 @@ export function adHocFiltersComponent() {
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
variable: '=',
|
||||
dashboard: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AddPanelWidget, Props } from './AddPanelWidget';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
dashboard: {} as DashboardModel,
|
||||
panel: {} as PanelModel,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<AddPanelWidget {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,20 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import store from 'app/core/store';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Store
|
||||
import { store as reduxStore } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../../state';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { LocationUpdate } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -46,6 +54,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
copiedPanels.push(pluginCopy);
|
||||
}
|
||||
}
|
||||
|
||||
return _.sortBy(copiedPanels, 'sort');
|
||||
}
|
||||
|
||||
@@ -54,28 +63,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
|
||||
}
|
||||
|
||||
copyButton(panel) {
|
||||
return (
|
||||
<button className="btn-inverse btn" onClick={() => this.onPasteCopiedPanel(panel)} title={panel.name}>
|
||||
Paste copied Panel
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
moveToEdit(panel) {
|
||||
reduxStore.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: panel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onCreateNewPanel = () => {
|
||||
onCreateNewPanel = (tab = 'queries') => {
|
||||
const dashboard = this.props.dashboard;
|
||||
const { gridPos } = this.props.panel;
|
||||
|
||||
@@ -88,7 +76,21 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
dashboard.addPanel(newPanel);
|
||||
dashboard.removePanel(this.props.panel);
|
||||
|
||||
this.moveToEdit(newPanel);
|
||||
const location: LocationUpdate = {
|
||||
query: {
|
||||
panelId: newPanel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
};
|
||||
|
||||
if (tab === 'visualization') {
|
||||
location.query.tab = 'visualization';
|
||||
location.query.openVizPicker = true;
|
||||
}
|
||||
|
||||
reduxStore.dispatch(updateLocation(location));
|
||||
};
|
||||
|
||||
onPasteCopiedPanel = panelPluginInfo => {
|
||||
@@ -98,7 +100,12 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
const newPanel: any = {
|
||||
type: panelPluginInfo.id,
|
||||
title: 'Panel Title',
|
||||
gridPos: { x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h },
|
||||
gridPos: {
|
||||
x: gridPos.x,
|
||||
y: gridPos.y,
|
||||
w: panelPluginInfo.defaults.gridPos.w,
|
||||
h: panelPluginInfo.defaults.gridPos.h,
|
||||
},
|
||||
};
|
||||
|
||||
// apply panel template / defaults
|
||||
@@ -125,30 +132,52 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
dashboard.removePanel(this.props.panel);
|
||||
};
|
||||
|
||||
render() {
|
||||
let addCopyButton;
|
||||
renderOptionLink = (icon, text, onClick) => {
|
||||
return (
|
||||
<div>
|
||||
<a href="#" onClick={onClick} className="add-panel-widget__link btn btn-inverse">
|
||||
<div className="add-panel-widget__icon">
|
||||
<i className={`gicon gicon-${icon}`} />
|
||||
</div>
|
||||
<span>{text}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.copiedPanelPlugins.length === 1) {
|
||||
addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]);
|
||||
}
|
||||
render() {
|
||||
const { copiedPanelPlugins } = this.state;
|
||||
|
||||
return (
|
||||
<div className="panel-container add-panel-widget-container">
|
||||
<div className="add-panel-widget">
|
||||
<div className="add-panel-widget__header grid-drag-handle">
|
||||
<i className="gicon gicon-add-panel" />
|
||||
<span className="add-panel-widget__title">New Panel</span>
|
||||
<button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="add-panel-widget__btn-container">
|
||||
<button className="btn-success btn btn-large" onClick={this.onCreateNewPanel}>
|
||||
Edit Panel
|
||||
</button>
|
||||
{addCopyButton}
|
||||
<button className="btn-inverse btn" onClick={this.onCreateNewRow}>
|
||||
Add Row
|
||||
</button>
|
||||
<div className="add-panel-widget__create">
|
||||
{this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)}
|
||||
{this.renderOptionLink('visualization', 'Choose Visualization', () =>
|
||||
this.onCreateNewPanel('visualization')
|
||||
)}
|
||||
</div>
|
||||
<div className="add-panel-widget__actions">
|
||||
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>
|
||||
Convert to row
|
||||
</button>
|
||||
{copiedPanelPlugins.length === 1 && (
|
||||
<button
|
||||
className="btn btn-inverse add-panel-widget__action"
|
||||
onClick={() => this.onPasteCopiedPanel(copiedPanelPlugins[0])}
|
||||
>
|
||||
Paste copied panel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
cursor: move;
|
||||
background: $page-header-bg;
|
||||
box-shadow: $page-header-shadow;
|
||||
border-bottom: 1px solid $page-header-border-color;
|
||||
|
||||
.gicon {
|
||||
font-size: 30px;
|
||||
margin-right: $spacer;
|
||||
margin-right: $space-md;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -26,6 +29,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.add-panel-widget__title {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
margin-right: $space-xl;
|
||||
}
|
||||
|
||||
.add-panel-widget__link {
|
||||
margin: 0 $space-sm;
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
.add-panel-widget__icon {
|
||||
margin-bottom: $space-sm;
|
||||
|
||||
.gicon {
|
||||
color: white;
|
||||
height: 44px;
|
||||
width: 53px;
|
||||
position: relative;
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-panel-widget__close {
|
||||
margin-left: auto;
|
||||
background-color: transparent;
|
||||
@@ -34,14 +60,25 @@
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.add-panel-widget__create {
|
||||
display: inherit;
|
||||
margin-bottom: $space-lg;
|
||||
// this is to have the big button appear centered
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
.add-panel-widget__actions {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.add-panel-widget__action {
|
||||
margin: 0 $space-xs;
|
||||
}
|
||||
|
||||
.add-panel-widget__btn-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="panel-container add-panel-widget-container"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__header grid-drag-handle"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-add-panel"
|
||||
/>
|
||||
<span
|
||||
className="add-panel-widget__title"
|
||||
>
|
||||
New Panel
|
||||
</span>
|
||||
<button
|
||||
className="add-panel-widget__close"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-close"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="add-panel-widget__btn-container"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__create"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-queries"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
Add Query
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-visualization"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
Choose Visualization
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="add-panel-widget__actions"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse add-panel-widget__action"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Convert to row
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -12,7 +12,7 @@
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.saveDashboardAsFile()">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-primary" ng-click="ctrl.saveDashboardAsFile()">
|
||||
<i class="fa fa-save"></i> Save to file
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.viewJson()">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="empty-list-cta__title">
|
||||
There are no dashboard links added yet
|
||||
</div>
|
||||
<a ng-click="ctrl.setupNew()" class="empty-list-cta__button btn btn-xlarge btn-success">
|
||||
<a ng-click="ctrl.setupNew()" class="empty-list-cta__button btn btn-xlarge btn-primary">
|
||||
<i class="gicon gicon-add-link"></i>
|
||||
Add Dashboard Link
|
||||
</a>
|
||||
@@ -26,7 +26,7 @@
|
||||
<div ng-if="ctrl.dashboard.links.length > 0">
|
||||
<div class="page-action-bar">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a type="button" class="btn btn-success" ng-click="ctrl.setupNew()">
|
||||
<a type="button" class="btn btn-primary" ng-click="ctrl.setupNew()">
|
||||
<i class="fa fa-plus"></i> New</a>
|
||||
</div>
|
||||
<table class="filter-table filter-table--hover">
|
||||
@@ -126,10 +126,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success" ng-if="ctrl.mode == 'new'" ng-click="ctrl.addLink()">
|
||||
<button class="btn btn-primary" ng-if="ctrl.mode == 'new'" ng-click="ctrl.addLink()">
|
||||
Add
|
||||
</button>
|
||||
<button class="btn btn-success" ng-if="ctrl.mode == 'edit'" ng-click="ctrl.saveLink()">
|
||||
<button class="btn btn-primary" ng-if="ctrl.mode == 'edit'" ng-click="ctrl.saveLink()">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
|
||||
273
public/app/features/dashboard/components/DashNav/DashNav.tsx
Normal file
273
public/app/features/dashboard/components/DashNav/DashNav.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
// 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';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
|
||||
// State
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../../state';
|
||||
|
||||
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, tab: 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,
|
||||
});
|
||||
};
|
||||
|
||||
renderDashboardTitleSearchButton() {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
const folderTitle = dashboard.meta.folderTitle;
|
||||
const haveFolder = dashboard.meta.folderId > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
||||
{!this.isInFullscreenOrSettings && <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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
get isInFullscreenOrSettings() {
|
||||
return this.props.editview || this.props.isFullscreen;
|
||||
}
|
||||
|
||||
renderBackButton() {
|
||||
return (
|
||||
<div className="navbar-edit">
|
||||
<Tooltip content="Go back (Esc)">
|
||||
<button className="navbar-edit__back-btn" onClick={this.onClose}>
|
||||
<i className="fa fa-arrow-left" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, onAddPanel } = this.props;
|
||||
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
|
||||
const { snapshot } = dashboard;
|
||||
|
||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
{this.isInFullscreenOrSettings && this.renderBackButton()}
|
||||
{this.renderDashboardTitleSearchButton()}
|
||||
|
||||
{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="Cycle view mode"
|
||||
classSuffix="tv"
|
||||
icon="fa fa-desktop"
|
||||
onClick={this.onToggleTVMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
||||
</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>
|
||||
@@ -76,8 +76,8 @@ export class DashboardPermissions extends PureComponent<Props, State> {
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||
<i className="fa fa-plus" /> Add Permission
|
||||
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||
Add Permission
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ describe('DashboardRow', () => {
|
||||
beforeEach(() => {
|
||||
dashboardMock = {
|
||||
toggleRow: jest.fn(),
|
||||
on: jest.fn(),
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
|
||||
@@ -18,16 +18,16 @@ 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 = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onToggle = () => {
|
||||
this.props.dashboard.toggleRow(this.props.panel);
|
||||
@@ -35,12 +35,12 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
this.setState(prevState => {
|
||||
return { collapsed: !prevState.collapsed };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onUpdate = () => {
|
||||
this.props.dashboard.processRepeats();
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onOpenSettings = () => {
|
||||
appEvents.emit('show-modal', {
|
||||
@@ -51,7 +51,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
onUpdated: this.onUpdate,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
appEvents.emit('confirm-modal', {
|
||||
@@ -66,7 +66,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
this.props.dashboard.removeRow(this.props.panel, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = classNames({
|
||||
|
||||
@@ -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)} />;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class SettingsCtrl {
|
||||
});
|
||||
});
|
||||
|
||||
this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders;
|
||||
this.canSaveAs = contextSrv.hasEditPermissionInFolders;
|
||||
this.canSave = this.dashboard.meta.canSave;
|
||||
this.canDelete = this.dashboard.meta.canSave;
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { SettingsCtrl } from './SettingsCtrl';
|
||||
export { DashboardSettings } from './DashboardSettings';
|
||||
|
||||
@@ -10,17 +10,12 @@
|
||||
</a>
|
||||
|
||||
<div class="dashboard-settings__aside-actions">
|
||||
<button class="btn btn-success" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
|
||||
<i class="fa fa-save"></i> Save
|
||||
<button class="btn btn-primary" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
|
||||
<i class="fa fa-copy"></i>
|
||||
Save As...
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
<i class="fa fa-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -72,6 +67,11 @@
|
||||
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
Delete Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'" ng-include="'public/app/features/annotations/partials/editor.html'">
|
||||
@@ -100,8 +100,8 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-success" ng-click="ctrl.saveDashboardJson()" ng-show="ctrl.canSave">
|
||||
<i class="fa fa-save"></i> Save Changes
|
||||
<button class="btn btn-primary" ng-click="ctrl.saveDashboardJson()" ng-show="ctrl.canSave">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'make_editable'">
|
||||
<h3 class="dashboard-settings__header">Make Editable</h3>
|
||||
|
||||
<button class="btn btn-success" ng-click="ctrl.makeEditable()">
|
||||
<button class="btn btn-primary" ng-click="ctrl.makeEditable()">
|
||||
Make Editable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<a class="btn btn-success" ng-click="ctrl.export();">Export</a>
|
||||
<a class="btn btn-primary" ng-click="ctrl.export();">Export</a>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.update()">Update</button>
|
||||
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,23 +16,25 @@ const template = `
|
||||
<form name="ctrl.saveForm" class="modal-content" novalidate>
|
||||
<div class="p-t-2">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">New name</label>
|
||||
<label class="gf-form-label width-8">New name</label>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<folder-picker initial-folder-id="ctrl.folderId"
|
||||
<folder-picker initial-folder-id="ctrl.folderId"
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
enter-folder-creation="ctrl.onEnterFolderCreation()"
|
||||
exit-folder-creation="ctrl.onExitFolderCreation()"
|
||||
enable-create-new="true"
|
||||
label-class="width-7"
|
||||
label-class="width-8"
|
||||
dashboard-id="ctrl.clone.id">
|
||||
</folder-picker>
|
||||
</folder-picker>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Copy tags" label-class="width-8" checked="ctrl.copyTags">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()" ng-disabled="!ctrl.isValidFolderSelection">Save</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.save()" ng-disabled="!ctrl.isValidFolderSelection">Save</button>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -44,6 +46,7 @@ export class SaveDashboardAsModalCtrl {
|
||||
folderId: any;
|
||||
dismiss: () => void;
|
||||
isValidFolderSelection = true;
|
||||
copyTags: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv) {
|
||||
@@ -55,6 +58,7 @@ export class SaveDashboardAsModalCtrl {
|
||||
this.clone.editable = true;
|
||||
this.clone.hideControls = false;
|
||||
this.folderId = dashboard.meta.folderId;
|
||||
this.copyTags = false;
|
||||
|
||||
// remove alerts if source dashboard is already persisted
|
||||
// do not want to create alert dupes
|
||||
@@ -71,6 +75,10 @@ export class SaveDashboardAsModalCtrl {
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.copyTags) {
|
||||
this.clone.tags = [];
|
||||
}
|
||||
|
||||
return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ const template = `
|
||||
<button
|
||||
id="saveBtn"
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
ng-class="{'btn-success--processing': ctrl.isSaving}"
|
||||
class="btn btn-primary"
|
||||
ng-class="{'btn-primary--processing': ctrl.isSaving}"
|
||||
ng-disabled="ctrl.saveForm.$invalid || ctrl.isSaving"
|
||||
>
|
||||
<span ng-if="!ctrl.isSaving">Save</span>
|
||||
|
||||
@@ -26,7 +26,7 @@ const template = `
|
||||
<code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-success" clipboard-button="ctrl.getJsonForClipboard()">
|
||||
<button class="btn btn-primary" clipboard-button="ctrl.getJsonForClipboard()">
|
||||
<i class="fa fa-clipboard"></i> Copy JSON to Clipboard
|
||||
</button>
|
||||
<button class="btn btn-secondary" clipboard-button="ctrl.save()">
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
||||
export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
||||
export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="step === 1" class="gf-form-button-row">
|
||||
<button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
|
||||
<button class="btn gf-form-btn width-10 btn-primary" ng-click="createSnapshot()" ng-disabled="loading">
|
||||
<i class="fa fa-save"></i>
|
||||
Local Snapshot
|
||||
</button>
|
||||
|
||||
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';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
||||
{{variable.label || variable.name}}
|
||||
</label>
|
||||
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
||||
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" dashboard="ctrl.dashboard" 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">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<button class="btn gf-form-btn btn-secondary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<button class="btn gf-form-btn btn-secondary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button type="submit" class="btn gf-form-btn btn-secondary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
<button type="submit" class="btn gf-form-btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -20,7 +20,7 @@ const template = `
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal-buttons">
|
||||
<button type="button" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="ctrl.save()">Save</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="ctrl.discard()">Discard</button>
|
||||
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
Show more versions
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-success"
|
||||
class="btn btn-primary"
|
||||
ng-if="ctrl.revisions.length > 1"
|
||||
ng-disabled="!ctrl.canCompare"
|
||||
ng-click="ctrl.getDiff(ctrl.diff)"
|
||||
|
||||
@@ -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);
|
||||
280
public/app/features/dashboard/containers/DashboardPage.test.tsx
Normal file
280
public/app/features/dashboard/containers/DashboardPage.test.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardPage, Props, State, mapStateToProps } 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('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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps with bool fullscreen', () => {
|
||||
const props = mapStateToProps({
|
||||
location: {
|
||||
routeParams: {},
|
||||
query: {
|
||||
fullscreen: true,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
} as any);
|
||||
|
||||
expect(props.urlFullscreen).toBe(true);
|
||||
expect(props.urlEdit).toBe(false);
|
||||
});
|
||||
|
||||
describe('mapStateToProps with string edit true', () => {
|
||||
const props = mapStateToProps({
|
||||
location: {
|
||||
routeParams: {},
|
||||
query: {
|
||||
fullscreen: false,
|
||||
edit: 'true',
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
} as any);
|
||||
|
||||
expect(props.urlFullscreen).toBe(false);
|
||||
expect(props.urlEdit).toBe(true);
|
||||
});
|
||||
});
|
||||
333
public/app/features/dashboard/containers/DashboardPage.tsx
Normal file
333
public/app/features/dashboard/containers/DashboardPage.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
// 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();
|
||||
this.setPanelFullscreenClass(false);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId, urlUid } = this.props;
|
||||
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if we just got dashboard update title
|
||||
if (!prevProps.dashboard) {
|
||||
document.title = dashboard.title + ' - Grafana';
|
||||
}
|
||||
|
||||
// Due to the angular -> react url bridge we can ge an update here with new uid before the container unmounts
|
||||
// Can remove this condition after we switch to react router
|
||||
if (prevProps.urlUid !== urlUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
},
|
||||
this.triggerPanelsRendering.bind(this)
|
||||
);
|
||||
|
||||
this.setPanelFullscreenClass(false);
|
||||
}
|
||||
|
||||
triggerPanelsRendering() {
|
||||
try {
|
||||
this.props.dashboard.render();
|
||||
} catch (err) {
|
||||
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
updateAfterMountMs={500}
|
||||
className="custom-scrollbar--page"
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export 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,
|
||||
urlEdit: !!state.location.query.edit,
|
||||
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,17 @@ 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));
|
||||
export default hot(module)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SoloPanelPage)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
// 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": 18,
|
||||
"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}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
<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": 18,
|
||||
"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": 18,
|
||||
"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}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
<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": 18,
|
||||
"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": 18,
|
||||
"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,17 +1,20 @@
|
||||
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;
|
||||
|
||||
interface GridWrapperProps {
|
||||
size: { width: number; };
|
||||
size: { width: number };
|
||||
layout: ReactGridLayout.Layout[];
|
||||
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
@@ -38,7 +41,7 @@ function GridWrapper({
|
||||
isResizable,
|
||||
isDraggable,
|
||||
isFullscreen,
|
||||
}: GridWrapperProps) {
|
||||
}: GridWrapperProps) {
|
||||
const width = size.width > 0 ? size.width : lastGridWidth;
|
||||
|
||||
// logic to ignore width changes (optimization)
|
||||
@@ -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 = {};
|
||||
@@ -137,22 +149,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
}
|
||||
|
||||
this.props.dashboard.sortPanelsByGridPos();
|
||||
}
|
||||
};
|
||||
|
||||
triggerForceUpdate = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onWidthChange = () => {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
panel.resizeDone();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onViewModeChanged = () => {
|
||||
ignoreNextWidthChange = true;
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
|
||||
this.panelMap[item.i].updateGridPos(item);
|
||||
@@ -160,21 +171,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
|
||||
// react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
|
||||
// So it's required to call it explicitly when panel resized or moved to save layout changes.
|
||||
this.onLayoutChange(layout);
|
||||
}
|
||||
};
|
||||
|
||||
onResize: ItemCallback = (layout, oldItem, newItem) => {
|
||||
console.log();
|
||||
this.panelMap[newItem.i].updateGridPos(newItem);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
|
||||
this.updateGridPos(newItem, layout);
|
||||
this.panelMap[newItem.i].resizeDone();
|
||||
}
|
||||
};
|
||||
|
||||
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
|
||||
this.updateGridPos(newItem, layout);
|
||||
}
|
||||
};
|
||||
|
||||
renderPanels() {
|
||||
const panelElements = [];
|
||||
@@ -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>
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
|
||||
// handle plugin loading & changing of plugin type
|
||||
if (!this.state.plugin || this.state.plugin.id !== pluginId) {
|
||||
const plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId);
|
||||
let plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId);
|
||||
|
||||
// remember if this is from an angular panel
|
||||
const fromAngularPanel = this.state.angularPanel != null;
|
||||
@@ -76,16 +76,26 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
// unmount angular panel
|
||||
this.cleanUpAngularPanel();
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
this.props.panel.changeType(pluginId, fromAngularPanel);
|
||||
if (!plugin.exports) {
|
||||
try {
|
||||
plugin.exports = await importPluginModule(plugin.module);
|
||||
} catch (e) {
|
||||
plugin = getPanelPluginNotFound(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.exports) {
|
||||
this.setState({ plugin: plugin, angularPanel: null });
|
||||
} else {
|
||||
plugin.exports = await importPluginModule(plugin.module);
|
||||
this.setState({ plugin: plugin, angularPanel: null });
|
||||
if (panel.type !== pluginId) {
|
||||
if (fromAngularPanel) {
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
panel.destroy();
|
||||
|
||||
this.props.panel.changeType(pluginId);
|
||||
} else {
|
||||
panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,10 +136,10 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderReactPanel() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
|
||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} />;
|
||||
}
|
||||
|
||||
renderAngularPanel() {
|
||||
@@ -168,7 +178,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
style={styles}
|
||||
>
|
||||
{plugin.exports.Panel && this.renderReactPanel()}
|
||||
{plugin.exports.reactPanel && this.renderReactPanel()}
|
||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Library
|
||||
import React, { Component } from 'react';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
|
||||
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
|
||||
// Services
|
||||
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
// Utils
|
||||
@@ -11,15 +9,15 @@ import kbn from 'app/core/utils/kbn';
|
||||
import {
|
||||
DataQueryOptions,
|
||||
DataQueryResponse,
|
||||
DataQueryError,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
TableData,
|
||||
TimeRange,
|
||||
TimeSeries,
|
||||
ScopedVars,
|
||||
} from '@grafana/ui';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
|
||||
interface RenderProps {
|
||||
loading: LoadingState;
|
||||
panelData: PanelData;
|
||||
@@ -28,7 +26,7 @@ interface RenderProps {
|
||||
export interface Props {
|
||||
datasource: string | null;
|
||||
queries: any[];
|
||||
panelId?: number;
|
||||
panelId: number;
|
||||
dashboardId?: number;
|
||||
isVisible?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
@@ -36,21 +34,22 @@ export interface Props {
|
||||
refreshCounter: number;
|
||||
minInterval?: string;
|
||||
maxDataPoints?: number;
|
||||
scopedVars?: ScopedVars;
|
||||
children: (r: RenderProps) => JSX.Element;
|
||||
onDataResponse?: (data: DataQueryResponse) => void;
|
||||
onError: (message: string, error: DataQueryError) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isFirstLoad: boolean;
|
||||
loading: LoadingState;
|
||||
errorMessage: string;
|
||||
response: DataQueryResponse;
|
||||
panelData: PanelData;
|
||||
}
|
||||
|
||||
export class DataPanel extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
isVisible: true,
|
||||
panelId: 1,
|
||||
dashboardId: 1,
|
||||
};
|
||||
|
||||
@@ -62,10 +61,10 @@ export class DataPanel extends Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
loading: LoadingState.NotStarted,
|
||||
errorMessage: '',
|
||||
response: {
|
||||
data: [],
|
||||
},
|
||||
panelData: {},
|
||||
isFirstLoad: true,
|
||||
};
|
||||
}
|
||||
@@ -100,7 +99,9 @@ export class DataPanel extends Component<Props, State> {
|
||||
timeRange,
|
||||
widthPixels,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
onDataResponse,
|
||||
onError,
|
||||
} = this.props;
|
||||
|
||||
if (!isVisible) {
|
||||
@@ -112,10 +113,10 @@ export class DataPanel extends Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: LoadingState.Loading, errorMessage: '' });
|
||||
this.setState({ loading: LoadingState.Loading });
|
||||
|
||||
try {
|
||||
const ds = await this.dataSourceSrv.get(datasource);
|
||||
const ds = await this.dataSourceSrv.get(datasource, scopedVars);
|
||||
|
||||
// TODO interpolate variables
|
||||
const minInterval = this.props.minInterval || ds.interval;
|
||||
@@ -131,7 +132,7 @@ export class DataPanel extends Component<Props, State> {
|
||||
intervalMs: intervalRes.intervalMs,
|
||||
targets: queries,
|
||||
maxDataPoints: maxDataPoints || widthPixels,
|
||||
scopedVars: {},
|
||||
scopedVars: scopedVars || {},
|
||||
cacheTimeout: null,
|
||||
};
|
||||
|
||||
@@ -148,27 +149,30 @@ export class DataPanel extends Component<Props, State> {
|
||||
this.setState({
|
||||
loading: LoadingState.Done,
|
||||
response: resp,
|
||||
panelData: this.getPanelData(resp),
|
||||
isFirstLoad: false,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Loading error', err);
|
||||
this.onError('Request Error');
|
||||
console.log('DataPanel error', err);
|
||||
|
||||
let message = 'Query error';
|
||||
|
||||
if (err.message) {
|
||||
message = err.message;
|
||||
} else if (err.data && err.data.message) {
|
||||
message = err.data.message;
|
||||
} else if (err.data && err.data.error) {
|
||||
message = err.data.error;
|
||||
} else if (err.status) {
|
||||
message = `Query error: ${err.status} ${err.statusText}`;
|
||||
}
|
||||
|
||||
onError(message, err);
|
||||
this.setState({ isFirstLoad: false, loading: LoadingState.Error });
|
||||
}
|
||||
};
|
||||
|
||||
onError = (errorMessage: string) => {
|
||||
if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
|
||||
this.setState({
|
||||
loading: LoadingState.Error,
|
||||
isFirstLoad: false,
|
||||
errorMessage: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getPanelData = () => {
|
||||
const { response } = this.state;
|
||||
|
||||
getPanelData(response: DataQueryResponse) {
|
||||
if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
|
||||
return {
|
||||
tableData: response.data[0] as TableData,
|
||||
@@ -180,16 +184,15 @@ export class DataPanel extends Component<Props, State> {
|
||||
timeSeries: response.data as TimeSeries[],
|
||||
tableData: null,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { queries } = this.props;
|
||||
const { loading, isFirstLoad } = this.state;
|
||||
const { loading, isFirstLoad, panelData } = this.state;
|
||||
|
||||
const panelData = this.getPanelData();
|
||||
|
||||
if (isFirstLoad && loading === LoadingState.Loading) {
|
||||
return this.renderLoadingStates();
|
||||
// do not render component until we have first data
|
||||
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||
return this.renderLoadingState();
|
||||
}
|
||||
|
||||
if (!queries.length) {
|
||||
@@ -202,46 +205,17 @@ export class DataPanel extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderLoadingStates()}
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
if (errorInfo) {
|
||||
this.onError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.props.children({
|
||||
loading,
|
||||
panelData,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
{this.props.children({ loading, panelData })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingStates(): JSX.Element {
|
||||
const { loading, errorMessage } = this.state;
|
||||
if (loading === LoadingState.Loading) {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
} else if (loading === LoadingState.Error) {
|
||||
return (
|
||||
<Tooltip content={errorMessage} placement="bottom-start" theme="error">
|
||||
<div className="panel-info-corner panel-info-corner--error">
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
private renderLoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
30
public/app/features/dashboard/dashgrid/PanelChrome.test.tsx
Normal file
30
public/app/features/dashboard/dashgrid/PanelChrome.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
|
||||
describe('PanelChrome', () => {
|
||||
let chrome: PanelChrome;
|
||||
|
||||
beforeEach(() => {
|
||||
chrome = new PanelChrome({
|
||||
panel: {
|
||||
scopedVars: {
|
||||
aaa: { value: 'AAA', text: 'upperA' },
|
||||
bbb: { value: 'BBB', text: 'upperB' },
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
plugin: {},
|
||||
isFullscreen: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should replace a panel variable', () => {
|
||||
const out = chrome.replaceVariables('hello $aaa');
|
||||
expect(out).toBe('hello AAA');
|
||||
});
|
||||
|
||||
it('But it should prefer the local variable value', () => {
|
||||
const extra = { aaa: { text: '???', value: 'XXX' } };
|
||||
const out = chrome.replaceVariables('hello $aaa and $bbb', extra);
|
||||
expect(out).toBe('hello XXX and BBB');
|
||||
});
|
||||
});
|
||||
@@ -8,25 +8,29 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
// Components
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { DataPanel } from './DataPanel';
|
||||
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
|
||||
|
||||
// Utils
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
|
||||
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import config from 'app/core/config';
|
||||
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange, LoadingState } from '@grafana/ui';
|
||||
import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
|
||||
import { ScopedVars } from '@grafana/ui';
|
||||
|
||||
import variables from 'sass/_variables.scss';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import { DataQueryResponse } from '@grafana/ui/src';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -34,6 +38,7 @@ export interface State {
|
||||
renderCounter: number;
|
||||
timeInfo?: string;
|
||||
timeRange?: TimeRange;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
@@ -45,6 +50,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
this.state = {
|
||||
refreshCounter: 0,
|
||||
renderCounter: 0,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,24 +86,66 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
onInterpolate = (value: string, format?: string) => {
|
||||
return templateSrv.replace(value, this.props.panel.scopedVars, format);
|
||||
replaceVariables = (value: string, extraVars?: ScopedVars, format?: string) => {
|
||||
let vars = this.props.panel.scopedVars;
|
||||
if (extraVars) {
|
||||
vars = vars ? { ...vars, ...extraVars } : extraVars;
|
||||
}
|
||||
return templateSrv.replace(value, vars, format);
|
||||
};
|
||||
|
||||
onDataResponse = (dataQueryResponse: DataQueryResponse) => {
|
||||
if (this.props.dashboard.isSnapshot()) {
|
||||
this.props.panel.snapshotData = dataQueryResponse.data;
|
||||
}
|
||||
// clear error state (if any)
|
||||
this.clearErrorState();
|
||||
|
||||
// This event is used by old query editors and panel editor options
|
||||
this.props.panel.events.emit('data-received', dataQueryResponse.data);
|
||||
};
|
||||
|
||||
onDataError = (message: string, error: DataQueryError) => {
|
||||
if (this.state.errorMessage !== message) {
|
||||
this.setState({ errorMessage: message });
|
||||
}
|
||||
// this event is used by old query editors
|
||||
this.props.panel.events.emit('data-error', error);
|
||||
};
|
||||
|
||||
onPanelError = (message: string) => {
|
||||
if (this.state.errorMessage !== message) {
|
||||
this.setState({ errorMessage: message });
|
||||
}
|
||||
};
|
||||
|
||||
clearErrorState() {
|
||||
if (this.state.errorMessage) {
|
||||
this.setState({ errorMessage: null });
|
||||
}
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
|
||||
renderPanel(loading, panelData, width, height): JSX.Element {
|
||||
get hasPanelSnapshot() {
|
||||
const { panel } = this.props;
|
||||
return panel.snapshotData && panel.snapshotData.length;
|
||||
}
|
||||
|
||||
get needsQueryExecution() {
|
||||
return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
|
||||
}
|
||||
|
||||
get getDataForPanel() {
|
||||
return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
|
||||
}
|
||||
|
||||
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
|
||||
const { panel, plugin } = this.props;
|
||||
const { timeRange, renderCounter } = this.state;
|
||||
const PanelComponent = plugin.exports.Panel;
|
||||
const PanelComponent = plugin.exports.reactPanel.panel;
|
||||
|
||||
// This is only done to increase a counter that is used by backend
|
||||
// image rendering (phantomjs/headless chrome) to know when to capture image
|
||||
@@ -111,21 +159,51 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
loading={loading}
|
||||
panelData={panelData}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||
width={width - 2 * variables.panelHorizontalPadding}
|
||||
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
|
||||
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
|
||||
width={width - 2 * config.theme.panelPadding.horizontal}
|
||||
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
|
||||
renderCounter={renderCounter}
|
||||
onInterpolate={this.onInterpolate}
|
||||
replaceVariables={this.replaceVariables}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { refreshCounter, timeRange, timeInfo } = this.state;
|
||||
renderPanelBody = (width: number, height: number): JSX.Element => {
|
||||
const { panel } = this.props;
|
||||
const { refreshCounter, timeRange } = this.state;
|
||||
const { datasource, targets } = panel;
|
||||
return (
|
||||
<>
|
||||
{this.needsQueryExecution ? (
|
||||
<DataPanel
|
||||
panelId={panel.id}
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
widthPixels={width}
|
||||
refreshCounter={refreshCounter}
|
||||
scopedVars={panel.scopedVars}
|
||||
onDataResponse={this.onDataResponse}
|
||||
onError={this.onDataError}
|
||||
>
|
||||
{({ loading, panelData }) => {
|
||||
return this.renderPanelPlugin(loading, panelData, width, height);
|
||||
}}
|
||||
</DataPanel>
|
||||
) : (
|
||||
this.renderPanelPlugin(LoadingState.Done, this.getDataForPanel, width, height)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { errorMessage, timeInfo } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
const { datasource, targets, transparent } = panel;
|
||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||
return (
|
||||
<AutoSizer>
|
||||
@@ -144,24 +222,18 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
{panel.snapshotData ? (
|
||||
this.renderPanel(false, panel.snapshotData, width, height)
|
||||
) : (
|
||||
<DataPanel
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
widthPixels={width}
|
||||
refreshCounter={refreshCounter}
|
||||
onDataResponse={this.onDataResponse}
|
||||
>
|
||||
{({ loading, panelData }) => {
|
||||
return this.renderPanel(loading, panelData, width, height);
|
||||
}}
|
||||
</DataPanel>
|
||||
)}
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
if (errorInfo) {
|
||||
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||
return null;
|
||||
}
|
||||
return this.renderPanelBody(width, height);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ScopedVars } from '@grafana/ui';
|
||||
|
||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||
@@ -16,8 +17,10 @@ export interface Props {
|
||||
timeInfo: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
scopedVars?: string;
|
||||
scopedVars?: ScopedVars;
|
||||
links?: [];
|
||||
error?: string;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
interface ClickCoordinates {
|
||||
@@ -30,18 +33,18 @@ interface State {
|
||||
}
|
||||
|
||||
export class PanelHeader extends Component<Props, State> {
|
||||
clickCoordinates: ClickCoordinates = {x: 0, y: 0};
|
||||
clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
|
||||
state = {
|
||||
panelMenuOpen: false,
|
||||
clickCoordinates: {x: 0, y: 0}
|
||||
clickCoordinates: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.clickCoordinates = this.eventToClickCoordinates(event);
|
||||
@@ -49,7 +52,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
|
||||
isClick = (clickCoordinates: ClickCoordinates) => {
|
||||
return isEqual(clickCoordinates, this.clickCoordinates);
|
||||
}
|
||||
};
|
||||
|
||||
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (this.isClick(this.eventToClickCoordinates(event))) {
|
||||
@@ -68,10 +71,9 @@ export class PanelHeader extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props;
|
||||
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
const { panel, dashboard, timeInfo, scopedVars } = this.props;
|
||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||
|
||||
return (
|
||||
@@ -82,13 +84,9 @@ export class PanelHeader extends Component<Props, State> {
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={error}
|
||||
/>
|
||||
<div className={panelHeaderClass}>
|
||||
{isLoading && (
|
||||
<span className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
)}
|
||||
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import Remarkable from 'remarkable';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { Tooltip, ScopedVars } from '@grafana/ui';
|
||||
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
enum InfoModes {
|
||||
enum InfoMode {
|
||||
Error = 'Error',
|
||||
Info = 'Info',
|
||||
Links = 'Links',
|
||||
@@ -16,20 +17,24 @@ interface Props {
|
||||
panel: PanelModel;
|
||||
title?: string;
|
||||
description?: string;
|
||||
scopedVars?: string;
|
||||
scopedVars?: ScopedVars;
|
||||
links?: [];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class PanelHeaderCorner extends Component<Props> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
|
||||
getInfoMode = () => {
|
||||
const { panel } = this.props;
|
||||
const { panel, error } = this.props;
|
||||
if (error) {
|
||||
return InfoMode.Error;
|
||||
}
|
||||
if (!!panel.description) {
|
||||
return InfoModes.Info;
|
||||
return InfoMode.Info;
|
||||
}
|
||||
if (panel.links && panel.links.length) {
|
||||
return InfoModes.Links;
|
||||
return InfoMode.Links;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -42,54 +47,55 @@ export class PanelHeaderCorner extends Component<Props> {
|
||||
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
||||
const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
|
||||
|
||||
const html = (
|
||||
return (
|
||||
<div className="markdown-html">
|
||||
<div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
|
||||
{panel.links &&
|
||||
panel.links.length > 0 && (
|
||||
<ul className="text-left">
|
||||
{panel.links.map((link, idx) => {
|
||||
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
|
||||
return (
|
||||
<li key={idx}>
|
||||
<a className="panel-menu-link" href={info.href} target={info.target}>
|
||||
{info.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{panel.links && panel.links.length > 0 && (
|
||||
<ul className="text-left">
|
||||
{panel.links.map((link, idx) => {
|
||||
const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
|
||||
return (
|
||||
<li key={idx}>
|
||||
<a className="panel-menu-link" href={info.href} target={info.target}>
|
||||
{info.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
renderCornerType(infoMode: InfoMode, content: string | JSX.Element) {
|
||||
const theme = infoMode === InfoMode.Error ? 'error' : 'info';
|
||||
return (
|
||||
<Tooltip content={content} placement="bottom-start" theme={theme}>
|
||||
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const infoMode: InfoModes | undefined = this.getInfoMode();
|
||||
const infoMode: InfoMode | undefined = this.getInfoMode();
|
||||
|
||||
if (!infoMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
|
||||
<Tooltip
|
||||
content={this.getInfoContent()}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<div
|
||||
className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
|
||||
>
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (infoMode === InfoMode.Error) {
|
||||
return this.renderCornerType(infoMode, this.props.error);
|
||||
}
|
||||
|
||||
if (infoMode === InfoMode.Info) {
|
||||
return this.renderCornerType(infoMode, this.getInfoContent());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
|
||||
|
||||
// Types
|
||||
import { PanelProps } from '@grafana/ui';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { PanelPlugin, AppNotificationSeverity } from 'app/types';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
@@ -19,15 +22,13 @@ class PanelPluginNotFound extends PureComponent<Props> {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center' as 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div className="alert alert-error" style={{ margin: '0 auto' }}>
|
||||
Panel plugin with id {this.props.pluginId} could not be found
|
||||
</div>
|
||||
<AlertBox severity={AppNotificationSeverity.Error} title={`Panel plugin not found: ${this.props.pluginId}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +47,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
|
||||
sort: 100,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
dataFormats: [],
|
||||
info: {
|
||||
author: {
|
||||
name: '',
|
||||
@@ -62,7 +64,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
|
||||
},
|
||||
|
||||
exports: {
|
||||
Panel: NotFound,
|
||||
reactPanel: new ReactPanelPlugin(NotFound),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class PanelResizer extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{render(isEditing ? {height: editorHeight} : this.noStyles)}
|
||||
{render(isEditing ? { height: editorHeight } : this.noStyles)}
|
||||
{isEditing && (
|
||||
<div className="panel-editor-container__resizer">
|
||||
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import './containers/DashboardCtrl';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
|
||||
// Services
|
||||
import './services/DashboardViewStateSrv';
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
import './services/DashboardSrv';
|
||||
@@ -29,4 +27,3 @@ import DashboardPermissions from './components/DashboardPermissions/DashboardPer
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, ChangeEvent } from 'react';
|
||||
import { FormLabel } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
onChange?: (evt: any) => void;
|
||||
name: string;
|
||||
value: string;
|
||||
onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
tooltipInfo?: any;
|
||||
}
|
||||
|
||||
export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
|
||||
export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
|
||||
return (
|
||||
<div className="gf-form gf-form--flex-end">
|
||||
<FormLabel tooltip={tooltipInfo}>{label}</FormLabel>
|
||||
@@ -20,10 +21,10 @@ export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value,
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
spellCheck={false}
|
||||
onBlur={evt => onChange(evt.target.value)}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourceOptions;
|
||||
|
||||
@@ -118,7 +118,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
||||
{toolbarItems.map(item => this.renderButton(item))}
|
||||
</div>
|
||||
<div className="panel-editor__scroll">
|
||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
|
||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop} updateAfterMountMs={300}>
|
||||
<div className="panel-editor__content">
|
||||
<FadeIn in={isOpen} duration={200} unmountOnExit={true}>
|
||||
{openView && this.renderOpenView(openView)}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditorTabBody heading="Panel Options" toolbarItems={[]}>
|
||||
<EditorTabBody heading="General" toolbarItems={[]}>
|
||||
<div ref={element => (this.element = element)} />
|
||||
</EditorTabBody>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { QueriesTab } from './QueriesTab';
|
||||
import { VisualizationTab } from './VisualizationTab';
|
||||
import VisualizationTab from './VisualizationTab';
|
||||
import { GeneralTab } from './GeneralTab';
|
||||
import { AlertTab } from '../../alerting/AlertTab';
|
||||
|
||||
@@ -30,6 +30,32 @@ interface PanelEditorTab {
|
||||
text: string;
|
||||
}
|
||||
|
||||
enum PanelEditorTabIds {
|
||||
Queries = 'queries',
|
||||
Visualization = 'visualization',
|
||||
Advanced = 'advanced',
|
||||
Alert = 'alert',
|
||||
}
|
||||
|
||||
interface PanelEditorTab {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const panelEditorTabTexts = {
|
||||
[PanelEditorTabIds.Queries]: 'Queries',
|
||||
[PanelEditorTabIds.Visualization]: 'Visualization',
|
||||
[PanelEditorTabIds.Advanced]: 'General',
|
||||
[PanelEditorTabIds.Alert]: 'Alert',
|
||||
};
|
||||
|
||||
const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
|
||||
return {
|
||||
id: tabId,
|
||||
text: panelEditorTabTexts[tabId],
|
||||
};
|
||||
};
|
||||
|
||||
export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -38,7 +64,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
onChangeTab = (tab: PanelEditorTab) => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: { tab: tab.id },
|
||||
query: { tab: tab.id, openVizPicker: null },
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
@@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
|
||||
render() {
|
||||
const { plugin } = this.props;
|
||||
let activeTab = store.getState().location.query.tab || 'queries';
|
||||
let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries;
|
||||
|
||||
const tabs: PanelEditorTab[] = [
|
||||
{ id: 'queries', text: 'Queries' },
|
||||
{ id: 'visualization', text: 'Visualization' },
|
||||
{ id: 'advanced', text: 'Panel Options' },
|
||||
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||
];
|
||||
|
||||
// handle panels that do not have queries tab
|
||||
if (plugin.exports.PanelCtrl) {
|
||||
if (!plugin.exports.PanelCtrl.prototype.onDataReceived) {
|
||||
// remove queries tab
|
||||
tabs.shift();
|
||||
// switch tab
|
||||
if (activeTab === 'queries') {
|
||||
activeTab = 'visualization';
|
||||
}
|
||||
if (plugin.dataFormats.length === 0) {
|
||||
// remove queries tab
|
||||
tabs.shift();
|
||||
// switch tab
|
||||
if (activeTab === PanelEditorTabIds.Queries) {
|
||||
activeTab = PanelEditorTabIds.Visualization;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.alertingEnabled && plugin.id === 'graph') {
|
||||
tabs.push({
|
||||
id: 'alert',
|
||||
text: 'Alert',
|
||||
});
|
||||
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -133,9 +133,9 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||
<div className="flex-grow" />
|
||||
<div className="flex-grow-1" />
|
||||
{!isAddingMixed && (
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
|
||||
Add Query
|
||||
</button>
|
||||
)}
|
||||
@@ -176,7 +176,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panel } = this.props;
|
||||
const { panel, dashboard } = this.props;
|
||||
const { currentDS, scrollTop } = this.state;
|
||||
|
||||
const queryInspector: EditorToolbarView = {
|
||||
@@ -205,6 +205,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
dataSourceValue={query.datasource || panel.datasource}
|
||||
key={query.refId}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
query={query}
|
||||
onChange={query => this.onQueryChange(query, index)}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
|
||||
@@ -7,14 +7,17 @@ import _ from 'lodash';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DataQuery, DataSourceApi } from '@grafana/ui';
|
||||
import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
query: DataQuery;
|
||||
dashboard: DashboardModel;
|
||||
onAddQuery: (query?: DataQuery) => void;
|
||||
onRemoveQuery: (query: DataQuery) => void;
|
||||
onMoveQuery: (query: DataQuery, direction: number) => void;
|
||||
@@ -27,35 +30,73 @@ interface State {
|
||||
loadedDataSourceValue: string | null | undefined;
|
||||
datasource: DataSourceApi | null;
|
||||
isCollapsed: boolean;
|
||||
angularScope: AngularQueryComponentScope | null;
|
||||
hasTextEditMode: boolean;
|
||||
}
|
||||
|
||||
export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
element: HTMLElement | null = null;
|
||||
angularScope: AngularQueryComponentScope | null;
|
||||
angularQueryEditor: AngularComponent | null = null;
|
||||
|
||||
state: State = {
|
||||
datasource: null,
|
||||
isCollapsed: false,
|
||||
angularScope: null,
|
||||
loadedDataSourceValue: undefined,
|
||||
hasTextEditMode: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadDatasource();
|
||||
this.props.panel.events.on('refresh', this.onPanelRefresh);
|
||||
this.props.panel.events.on('data-error', this.onPanelDataError);
|
||||
this.props.panel.events.on('data-received', this.onPanelDataReceived);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onPanelRefresh);
|
||||
this.props.panel.events.off('data-error', this.onPanelDataError);
|
||||
this.props.panel.events.off('data-received', this.onPanelDataReceived);
|
||||
|
||||
if (this.angularQueryEditor) {
|
||||
this.angularQueryEditor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onPanelDataError = () => {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
if (this.angularQueryEditor) {
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
}
|
||||
};
|
||||
|
||||
onPanelDataReceived = () => {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
if (this.angularQueryEditor) {
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
}
|
||||
};
|
||||
|
||||
onPanelRefresh = () => {
|
||||
if (this.angularScope) {
|
||||
this.angularScope.range = getTimeSrv().timeRange();
|
||||
}
|
||||
};
|
||||
|
||||
getAngularQueryComponentScope(): AngularQueryComponentScope {
|
||||
const { panel, query } = this.props;
|
||||
const { panel, query, dashboard } = this.props;
|
||||
const { datasource } = this.state;
|
||||
|
||||
return {
|
||||
datasource: datasource,
|
||||
target: query,
|
||||
panel: panel,
|
||||
dashboard: dashboard,
|
||||
refresh: () => panel.refresh(),
|
||||
render: () => panel.render(),
|
||||
events: panel.events,
|
||||
range: getTimeSrv().timeRange(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +105,11 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
const dataSourceSrv = getDatasourceSrv();
|
||||
const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
|
||||
|
||||
this.setState({ datasource, loadedDataSourceValue: this.props.dataSourceValue });
|
||||
this.setState({
|
||||
datasource,
|
||||
loadedDataSourceValue: this.props.dataSourceValue,
|
||||
hasTextEditMode: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@@ -89,19 +134,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
|
||||
|
||||
this.angularQueryEditor = loader.load(this.element, scopeProps, template);
|
||||
this.angularScope = scopeProps.ctrl;
|
||||
|
||||
// give angular time to compile
|
||||
setTimeout(() => {
|
||||
this.setState({ angularScope: scopeProps.ctrl });
|
||||
this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.angularQueryEditor) {
|
||||
this.angularQueryEditor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onToggleCollapse = () => {
|
||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
||||
};
|
||||
@@ -120,24 +160,15 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
if (datasource.pluginExports.QueryEditor) {
|
||||
const QueryEditor = datasource.pluginExports.QueryEditor;
|
||||
return (
|
||||
<QueryEditor
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
/>
|
||||
);
|
||||
return <QueryEditor query={query} datasource={datasource} onChange={onChange} onRunQuery={this.onRunQuery} />;
|
||||
}
|
||||
|
||||
return <div>Data source plugin does not export any Query Editor component</div>;
|
||||
}
|
||||
|
||||
onToggleEditMode = () => {
|
||||
const { angularScope } = this.state;
|
||||
|
||||
if (angularScope && angularScope.toggleEditorMode) {
|
||||
angularScope.toggleEditorMode();
|
||||
if (this.angularScope && this.angularScope.toggleEditorMode) {
|
||||
this.angularScope.toggleEditorMode();
|
||||
this.angularQueryEditor.digest();
|
||||
}
|
||||
|
||||
@@ -146,11 +177,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
get hasTextEditMode() {
|
||||
const { angularScope } = this.state;
|
||||
return angularScope && angularScope.toggleEditorMode;
|
||||
}
|
||||
|
||||
onRemoveQuery = () => {
|
||||
this.props.onRemoveQuery(this.props.query);
|
||||
};
|
||||
@@ -167,10 +193,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderCollapsedText(): string | null {
|
||||
const { angularScope } = this.state;
|
||||
|
||||
if (angularScope && angularScope.getCollapsedText) {
|
||||
return angularScope.getCollapsedText();
|
||||
if (this.angularScope && this.angularScope.getCollapsedText) {
|
||||
return this.angularScope.getCollapsedText();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -178,7 +202,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { query, inMixedMode } = this.props;
|
||||
const { datasource, isCollapsed } = this.state;
|
||||
const { datasource, isCollapsed, hasTextEditMode } = this.state;
|
||||
const isDisabled = query.hide;
|
||||
|
||||
const bodyClasses = classNames('query-editor-row__body gf-form-query', {
|
||||
@@ -208,7 +232,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
{isCollapsed && <div>{this.renderCollapsedText()}</div>}
|
||||
</div>
|
||||
<div className="query-editor-row__actions">
|
||||
{this.hasTextEditMode && (
|
||||
{hasTextEditMode && (
|
||||
<button
|
||||
className="query-editor-row__action"
|
||||
onClick={this.onToggleEditMode}
|
||||
@@ -244,10 +268,12 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
export interface AngularQueryComponentScope {
|
||||
target: DataQuery;
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
events: Emitter;
|
||||
refresh: () => void;
|
||||
render: () => void;
|
||||
datasource: DataSourceApi;
|
||||
toggleEditorMode?: () => void;
|
||||
getCollapsedText?: () => string;
|
||||
range: TimeRange;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
|
||||
|
||||
// Utils
|
||||
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
|
||||
@@ -9,7 +9,7 @@ import { Switch } from '@grafana/ui';
|
||||
import { Input } from 'app/core/components/Form';
|
||||
import { EventsWithValidation } from 'app/core/components/Form/Input';
|
||||
import { InputStatus } from 'app/core/components/Form/Input';
|
||||
import DataSourceOption from './DataSourceOption';
|
||||
import { DataSourceOption } from './DataSourceOption';
|
||||
import { FormLabel } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
@@ -43,32 +43,79 @@ interface Props {
|
||||
interface State {
|
||||
relativeTime: string;
|
||||
timeShift: string;
|
||||
cacheTimeout: string;
|
||||
maxDataPoints: string;
|
||||
interval: string;
|
||||
hideTimeOverride: boolean;
|
||||
}
|
||||
|
||||
export class QueryOptions extends PureComponent<Props, State> {
|
||||
allOptions = {
|
||||
cacheTimeout: {
|
||||
label: 'Cache timeout',
|
||||
placeholder: '60',
|
||||
name: 'cacheTimeout',
|
||||
tooltipInfo: (
|
||||
<>
|
||||
If your time series store has a query cache this option can override the default cache timeout. Specify a
|
||||
numeric value in seconds.
|
||||
</>
|
||||
),
|
||||
},
|
||||
maxDataPoints: {
|
||||
label: 'Max data points',
|
||||
placeholder: 'auto',
|
||||
name: 'maxDataPoints',
|
||||
tooltipInfo: (
|
||||
<>
|
||||
The maximum data points the query should return. For graphs this is automatically set to one data point per
|
||||
pixel.
|
||||
</>
|
||||
),
|
||||
},
|
||||
minInterval: {
|
||||
label: 'Min time interval',
|
||||
placeholder: '0',
|
||||
name: 'minInterval',
|
||||
panelKey: 'interval',
|
||||
tooltipInfo: (
|
||||
<>
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
|
||||
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
|
||||
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
|
||||
be used in math expressions.
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
relativeTime: props.panel.timeFrom || '',
|
||||
timeShift: props.panel.timeShift || '',
|
||||
cacheTimeout: props.panel.cacheTimeout || '',
|
||||
maxDataPoints: props.panel.maxDataPoints || '',
|
||||
interval: props.panel.interval || '',
|
||||
hideTimeOverride: props.panel.hideTimeOverride || false,
|
||||
};
|
||||
}
|
||||
|
||||
onRelativeTimeChange = event => {
|
||||
onRelativeTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
relativeTime: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onTimeShiftChange = event => {
|
||||
onTimeShiftChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
timeShift: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOverrideTime = (evt, status: InputStatus) => {
|
||||
const { value } = evt.target;
|
||||
onOverrideTime = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
|
||||
const { value } = event.target;
|
||||
const { panel } = this.props;
|
||||
const emptyToNullValue = emptyToNull(value);
|
||||
if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
|
||||
@@ -77,8 +124,8 @@ export class QueryOptions extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
onTimeShift = (evt, status: InputStatus) => {
|
||||
const { value } = evt.target;
|
||||
onTimeShift = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
|
||||
const { value } = event.target;
|
||||
const { panel } = this.props;
|
||||
const emptyToNullValue = emptyToNull(value);
|
||||
if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
|
||||
@@ -89,77 +136,49 @@ export class QueryOptions extends PureComponent<Props, State> {
|
||||
|
||||
onToggleTimeOverride = () => {
|
||||
const { panel } = this.props;
|
||||
panel.hideTimeOverride = !panel.hideTimeOverride;
|
||||
this.setState({ hideTimeOverride: !this.state.hideTimeOverride }, () => {
|
||||
panel.hideTimeOverride = this.state.hideTimeOverride;
|
||||
panel.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
onDataSourceOptionBlur = (panelKey: string) => () => {
|
||||
const { panel } = this.props;
|
||||
|
||||
panel[panelKey] = this.state[panelKey];
|
||||
panel.refresh();
|
||||
};
|
||||
|
||||
renderOptions() {
|
||||
const { datasource, panel } = this.props;
|
||||
onDataSourceOptionChange = (panelKey: string) => (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ ...this.state, [panelKey]: event.target.value });
|
||||
};
|
||||
|
||||
renderOptions = () => {
|
||||
const { datasource } = this.props;
|
||||
const { queryOptions } = datasource.meta;
|
||||
|
||||
if (!queryOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onChangeFn = (panelKey: string) => {
|
||||
return (value: string | number) => {
|
||||
panel[panelKey] = value;
|
||||
panel.refresh();
|
||||
};
|
||||
};
|
||||
|
||||
const allOptions = {
|
||||
cacheTimeout: {
|
||||
label: 'Cache timeout',
|
||||
placeholder: '60',
|
||||
name: 'cacheTimeout',
|
||||
value: panel.cacheTimeout,
|
||||
tooltipInfo: (
|
||||
<>
|
||||
If your time series store has a query cache this option can override the default cache timeout. Specify a
|
||||
numeric value in seconds.
|
||||
</>
|
||||
),
|
||||
},
|
||||
maxDataPoints: {
|
||||
label: 'Max data points',
|
||||
placeholder: 'auto',
|
||||
name: 'maxDataPoints',
|
||||
value: panel.maxDataPoints,
|
||||
tooltipInfo: (
|
||||
<>
|
||||
The maximum data points the query should return. For graphs this is automatically set to one data point per
|
||||
pixel.
|
||||
</>
|
||||
),
|
||||
},
|
||||
minInterval: {
|
||||
label: 'Min time interval',
|
||||
placeholder: '0',
|
||||
name: 'minInterval',
|
||||
value: panel.interval,
|
||||
panelKey: 'interval',
|
||||
tooltipInfo: (
|
||||
<>
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
|
||||
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
|
||||
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
|
||||
be used in math expressions.
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return Object.keys(queryOptions).map(key => {
|
||||
const options = allOptions[key];
|
||||
return <DataSourceOption key={key} {...options} onChange={onChangeFn(allOptions[key].panelKey || key)} />;
|
||||
const options = this.allOptions[key];
|
||||
const panelKey = options.panelKey || key;
|
||||
return (
|
||||
<DataSourceOption
|
||||
key={key}
|
||||
{...options}
|
||||
onChange={this.onDataSourceOptionChange(panelKey)}
|
||||
onBlur={this.onDataSourceOptionBlur(panelKey)}
|
||||
value={this.state[panelKey]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const hideTimeOverride = this.props.panel.hideTimeOverride;
|
||||
const { hideTimeOverride } = this.state;
|
||||
const { relativeTime, timeShift } = this.state;
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
{this.renderOptions()}
|
||||
@@ -191,10 +210,11 @@ export class QueryOptions extends PureComponent<Props, State> {
|
||||
value={timeShift}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<Switch label="Hide time info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
|
||||
</div>
|
||||
{(timeShift || relativeTime) && (
|
||||
<div className="gf-form-inline">
|
||||
<Switch label="Hide time info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||
import { StoreState } from 'app/types';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Components
|
||||
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
||||
@@ -11,9 +14,10 @@ import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { PanelModel } from '../state';
|
||||
import { DashboardModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
import { VizPickerSearch } from './VizPickerSearch';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -21,56 +25,53 @@ interface Props {
|
||||
plugin: PanelPlugin;
|
||||
angularPanel?: AngularComponent;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
updateLocation: typeof updateLocation;
|
||||
urlOpenVizPicker: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isVizPickerOpen: boolean;
|
||||
searchQuery: string;
|
||||
scrollTop: number;
|
||||
hasBeenFocused: boolean;
|
||||
}
|
||||
|
||||
export class VisualizationTab extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
searchInput: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isVizPickerOpen: false,
|
||||
isVizPickerOpen: this.props.urlOpenVizPicker,
|
||||
hasBeenFocused: false,
|
||||
searchQuery: '',
|
||||
scrollTop: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getPanelDefaultOptions = () => {
|
||||
getReactPanelOptions = () => {
|
||||
const { panel, plugin } = this.props;
|
||||
|
||||
if (plugin.exports.PanelDefaults) {
|
||||
return panel.getOptions(plugin.exports.PanelDefaults.options);
|
||||
}
|
||||
|
||||
return panel.getOptions(plugin.exports.PanelDefaults);
|
||||
return panel.getOptions(plugin.exports.reactPanel.defaults);
|
||||
};
|
||||
|
||||
renderPanelOptions() {
|
||||
const { plugin, angularPanel } = this.props;
|
||||
const { PanelOptions } = plugin.exports;
|
||||
|
||||
if (angularPanel) {
|
||||
return <div ref={element => (this.element = element)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{PanelOptions ? (
|
||||
<PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
|
||||
) : (
|
||||
<p>Visualization has no options</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (plugin.exports.reactPanel) {
|
||||
const PanelEditor = plugin.exports.reactPanel.editor;
|
||||
|
||||
if (PanelEditor) {
|
||||
return <PanelEditor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -114,7 +115,12 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
template +=
|
||||
`
|
||||
<div class="panel-options-group" ng-cloak>` +
|
||||
(i > 0 ? `<div class="panel-options-group__header">{{ctrl.editorTabs[${i}].title}}</div>` : '') +
|
||||
(i > 0
|
||||
? `<div class="panel-options-group__header">
|
||||
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
|
||||
</span>
|
||||
</div>`
|
||||
: '') +
|
||||
`<div class="panel-options-group__body">
|
||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||
</div>
|
||||
@@ -139,6 +145,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
clearQuery = () => {
|
||||
this.setState({ searchQuery: '' });
|
||||
};
|
||||
|
||||
onPanelOptionsChanged = (options: any) => {
|
||||
this.props.panel.updateOptions(options);
|
||||
this.forceUpdate();
|
||||
@@ -149,11 +159,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
onCloseVizPicker = () => {
|
||||
this.setState({ isVizPickerOpen: false });
|
||||
if (this.props.urlOpenVizPicker) {
|
||||
this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
|
||||
}
|
||||
|
||||
this.setState({ isVizPickerOpen: false, hasBeenFocused: false });
|
||||
};
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
const value = evt.target.value;
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.setState({
|
||||
searchQuery: value,
|
||||
});
|
||||
@@ -161,26 +174,16 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
|
||||
renderToolbar = (): JSX.Element => {
|
||||
const { plugin } = this.props;
|
||||
const { searchQuery } = this.state;
|
||||
const { isVizPickerOpen, searchQuery } = this.state;
|
||||
|
||||
if (this.state.isVizPickerOpen) {
|
||||
if (isVizPickerOpen) {
|
||||
return (
|
||||
<>
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-13"
|
||||
placeholder=""
|
||||
onChange={this.onSearchQueryChange}
|
||||
value={searchQuery}
|
||||
ref={elem => elem && elem.focus()}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<button className="btn btn-link toolbar__close" onClick={this.onCloseVizPicker}>
|
||||
<i className="fa fa-chevron-up" />
|
||||
</button>
|
||||
</>
|
||||
<VizPickerSearch
|
||||
plugin={plugin}
|
||||
searchQuery={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
onClose={this.onCloseVizPicker}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
@@ -219,10 +222,15 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}
|
||||
scrollTop={scrollTop} setScrollTop={this.setScrollTop}>
|
||||
<EditorTabBody
|
||||
heading="Visualization"
|
||||
renderToolbar={this.renderToolbar}
|
||||
toolbarItems={[pluginHelp]}
|
||||
scrollTop={scrollTop}
|
||||
setScrollTop={this.setScrollTop}
|
||||
>
|
||||
<>
|
||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
|
||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
|
||||
<VizTypePicker
|
||||
current={plugin}
|
||||
onTypeChanged={this.onTypeChanged}
|
||||
@@ -236,3 +244,13 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
urlOpenVizPicker: !!state.location.query.openVizPicker,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
};
|
||||
|
||||
export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
plugin: PanelPlugin;
|
||||
searchQuery: string;
|
||||
onChange: (query: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export class VizPickerSearch extends PureComponent<Props> {
|
||||
render() {
|
||||
const { searchQuery, onChange, onClose } = this.props;
|
||||
return (
|
||||
<>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon"
|
||||
inputClassName="gf-form-input width-13"
|
||||
placeholder=""
|
||||
onChange={onChange}
|
||||
value={searchQuery}
|
||||
ref={element => element && element.focus()}
|
||||
/>
|
||||
<button className="btn btn-link toolbar__close" onClick={onClose}>
|
||||
<i className="fa fa-chevron-up" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
import VizTypePickerPlugin from './VizTypePickerPlugin';
|
||||
import { EmptySearchResult } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
current: PanelPlugin;
|
||||
@@ -14,9 +14,9 @@ export interface Props {
|
||||
|
||||
export class VizTypePicker extends PureComponent<Props> {
|
||||
searchInput: HTMLElement;
|
||||
pluginList = this.getPanelPlugins('');
|
||||
pluginList = this.getPanelPlugins;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@@ -25,14 +25,13 @@ export class VizTypePicker extends PureComponent<Props> {
|
||||
return filteredPluginList.length - 1;
|
||||
}
|
||||
|
||||
getPanelPlugins(filter): PanelPlugin[] {
|
||||
const panels = _.chain(config.panels)
|
||||
.filter({ hideFromList: false })
|
||||
.map(item => item)
|
||||
.value();
|
||||
get getPanelPlugins(): PanelPlugin[] {
|
||||
const allPanels = config.panels;
|
||||
|
||||
// add sort by sort property
|
||||
return _.sortBy(panels, 'sort');
|
||||
return Object.keys(allPanels)
|
||||
.filter(key => allPanels[key]['hideFromList'] === false)
|
||||
.map(key => allPanels[key])
|
||||
.sort((a: PanelPlugin, b: PanelPlugin) => a.sort - b.sort);
|
||||
}
|
||||
|
||||
renderVizPlugin = (plugin: PanelPlugin, index: number) => {
|
||||
@@ -63,11 +62,15 @@ export class VizTypePicker extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const filteredPluginList = this.getFilteredPluginList();
|
||||
|
||||
const hasResults = filteredPluginList.length > 0;
|
||||
return (
|
||||
<div className="viz-picker">
|
||||
<div className="viz-picker-list">
|
||||
{filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))}
|
||||
{hasResults ? (
|
||||
filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))
|
||||
) : (
|
||||
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,76 @@
|
||||
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;
|
||||
delete urlParams.tab;
|
||||
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;
|
||||
delete urlParams.tab;
|
||||
}
|
||||
|
||||
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 +126,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 +139,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 +154,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 +214,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);
|
||||
@@ -227,8 +227,8 @@ export class TimeSrv {
|
||||
const timespan = range.to.valueOf() - range.from.valueOf();
|
||||
const center = range.to.valueOf() - timespan / 2;
|
||||
|
||||
const to = center + timespan * factor / 2;
|
||||
const from = center - timespan * factor / 2;
|
||||
const to = center + (timespan * factor) / 2;
|
||||
const from = center - (timespan * factor) / 2;
|
||||
|
||||
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', () => {
|
||||
expect(model.schemaVersion).toBe(17);
|
||||
expect(model.schemaVersion).toBe(18);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export class DashboardMigrator {
|
||||
let i, j, k, n;
|
||||
const oldVersion = this.dashboard.schemaVersion;
|
||||
const panelUpgrades = [];
|
||||
this.dashboard.schemaVersion = 17;
|
||||
this.dashboard.schemaVersion = 18;
|
||||
|
||||
if (oldVersion === this.dashboard.schemaVersion) {
|
||||
return;
|
||||
@@ -387,6 +387,36 @@ export class DashboardMigrator {
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 18) {
|
||||
// migrate change to gauge options
|
||||
panelUpgrades.push(panel => {
|
||||
if (panel['options-gauge']) {
|
||||
panel.options = panel['options-gauge'];
|
||||
panel.options.valueOptions = {
|
||||
unit: panel.options.unit,
|
||||
stat: panel.options.stat,
|
||||
decimals: panel.options.decimals,
|
||||
prefix: panel.options.prefix,
|
||||
suffix: panel.options.suffix,
|
||||
};
|
||||
|
||||
// correct order
|
||||
if (panel.options.thresholds) {
|
||||
panel.options.thresholds.reverse();
|
||||
}
|
||||
|
||||
// this options prop was due to a bug
|
||||
delete panel.options.options;
|
||||
delete panel.options.unit;
|
||||
delete panel.options.stat;
|
||||
delete panel.options.decimals;
|
||||
delete panel.options.prefix;
|
||||
delete panel.options.suffix;
|
||||
delete panel['options-gauge'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -457,7 +487,7 @@ export class DashboardMigrator {
|
||||
for (const panel of row.panels) {
|
||||
panel.span = panel.span || DEFAULT_PANEL_SPAN;
|
||||
if (panel.minSpan) {
|
||||
panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan);
|
||||
panel.minSpan = Math.min(GRID_COLUMN_COUNT, (GRID_COLUMN_COUNT / 12) * panel.minSpan);
|
||||
}
|
||||
const panelWidth = Math.floor(panel.span) * widthFactor;
|
||||
const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight;
|
||||
|
||||
@@ -635,4 +635,32 @@ describe('DashboardModel', () => {
|
||||
expect(saveModel.templating.list[0].filters[0].value).toBe('server 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given a dashboard with one panel legend on and two off', () => {
|
||||
let model;
|
||||
|
||||
beforeEach(() => {
|
||||
const data = {
|
||||
panels: [
|
||||
{ id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 }, legend: { show: true } },
|
||||
{ id: 3, type: 'graph', gridPos: { x: 0, y: 4, w: 12, h: 2 }, legend: { show: false } },
|
||||
{ id: 4, type: 'graph', gridPos: { x: 12, y: 4, w: 12, h: 2 }, legend: { show: false } },
|
||||
],
|
||||
};
|
||||
model = new DashboardModel(data);
|
||||
});
|
||||
|
||||
it('toggleLegendsForAll should toggle all legends on on first execution', () => {
|
||||
model.toggleLegendsForAll();
|
||||
const legendsOn = model.panels.filter(panel => panel.legend.show === true);
|
||||
expect(legendsOn.length).toBe(3);
|
||||
});
|
||||
|
||||
it('toggleLegendsForAll should toggle all legends off on second execution', () => {
|
||||
model.toggleLegendsForAll();
|
||||
model.toggleLegendsForAll();
|
||||
const legendsOn = model.panels.filter(panel => panel.legend.show === true);
|
||||
expect(legendsOn.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,13 +882,13 @@ 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') {
|
||||
visibleHeight += 55;
|
||||
if (kioskMode && kioskMode !== KIOSK_MODE_TV) {
|
||||
visibleHeight += navbarHeight;
|
||||
}
|
||||
|
||||
const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
|
||||
@@ -895,4 +899,38 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleLegendsForAll() {
|
||||
const panelsWithLegends = this.panels.filter(panel => {
|
||||
return panel.legend !== undefined && panel.legend !== null;
|
||||
});
|
||||
|
||||
// determine if more panels are displaying legends or not
|
||||
const onCount = panelsWithLegends.filter(panel => panel.legend.show).length;
|
||||
const offCount = panelsWithLegends.length - onCount;
|
||||
const panelLegendsOn = onCount >= offCount;
|
||||
|
||||
for (const panel of panelsWithLegends) {
|
||||
panel.legend.show = !panelLegendsOn;
|
||||
panel.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { PanelModel } from './PanelModel';
|
||||
|
||||
describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
@@ -9,10 +8,21 @@ describe('PanelModel', () => {
|
||||
model = new PanelModel({
|
||||
type: 'table',
|
||||
showColumns: true,
|
||||
targets: [
|
||||
{refId: 'A'},
|
||||
{noRefId: true}
|
||||
]
|
||||
targets: [{ refId: 'A' }, { noRefId: true }],
|
||||
options: {
|
||||
thresholds: [
|
||||
{
|
||||
color: '#F2495C',
|
||||
index: 1,
|
||||
value: 50,
|
||||
},
|
||||
{
|
||||
color: '#73BF69',
|
||||
index: 0,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,9 +48,24 @@ describe('PanelModel', () => {
|
||||
expect(saveModel.events).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should restore -Infinity value for base threshold', () => {
|
||||
expect(model.options.thresholds).toEqual([
|
||||
{
|
||||
color: '#F2495C',
|
||||
index: 1,
|
||||
value: 50,
|
||||
},
|
||||
{
|
||||
color: '#73BF69',
|
||||
index: 0,
|
||||
value: -Infinity,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('when changing panel type', () => {
|
||||
beforeEach(() => {
|
||||
model.changeType('graph', true);
|
||||
model.changeType('graph');
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
@@ -49,14 +74,28 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
it('should restore table properties when changing back', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.showColumns).toBe(true);
|
||||
});
|
||||
|
||||
it('should remove alert rule when changing type that does not support it', () => {
|
||||
model.changeType('table', true);
|
||||
model.changeType('table');
|
||||
expect(model.alert).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get panel options', () => {
|
||||
it('should apply defaults', () => {
|
||||
model.options = { existingProp: 10 };
|
||||
const options = model.getOptions({
|
||||
defaultProp: true,
|
||||
existingProp: 0,
|
||||
});
|
||||
|
||||
expect(options.defaultProp).toBe(true);
|
||||
expect(options.existingProp).toBe(10);
|
||||
expect(model.options).toBe(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ import _ from 'lodash';
|
||||
|
||||
// Types
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
||||
import { DataQuery, TimeSeries } from '@grafana/ui';
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars } from '@grafana/ui';
|
||||
import { TableData } from '@grafana/ui/src';
|
||||
|
||||
export interface GridPos {
|
||||
@@ -47,8 +46,6 @@ const mustKeepProps: { [str: string]: boolean } = {
|
||||
timeFrom: true,
|
||||
timeShift: true,
|
||||
hideTimeOverride: true,
|
||||
maxDataPoints: true,
|
||||
interval: true,
|
||||
description: true,
|
||||
links: true,
|
||||
fullscreen: true,
|
||||
@@ -74,7 +71,7 @@ export class PanelModel {
|
||||
type: string;
|
||||
title: string;
|
||||
alert?: any;
|
||||
scopedVars?: any;
|
||||
scopedVars?: ScopedVars;
|
||||
repeat?: string;
|
||||
repeatIteration?: number;
|
||||
repeatPanelId?: number;
|
||||
@@ -92,6 +89,9 @@ export class PanelModel {
|
||||
timeFrom?: any;
|
||||
timeShift?: any;
|
||||
hideTimeOverride?: any;
|
||||
options: {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
maxDataPoints?: number;
|
||||
interval?: string;
|
||||
@@ -105,9 +105,8 @@ export class PanelModel {
|
||||
hasRefreshed: boolean;
|
||||
events: Emitter;
|
||||
cacheTimeout?: any;
|
||||
|
||||
// cache props between plugins
|
||||
cachedPluginOptions?: any;
|
||||
legend?: { show: boolean };
|
||||
|
||||
constructor(model) {
|
||||
this.events = new Emitter();
|
||||
@@ -121,6 +120,8 @@ export class PanelModel {
|
||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||
// queries must have refId
|
||||
this.ensureQueryIds();
|
||||
|
||||
this.restoreInfintyForThresholds();
|
||||
}
|
||||
|
||||
ensureQueryIds() {
|
||||
@@ -133,21 +134,28 @@ export class PanelModel {
|
||||
}
|
||||
}
|
||||
|
||||
restoreInfintyForThresholds() {
|
||||
if (this.options && this.options.thresholds) {
|
||||
this.options.thresholds = this.options.thresholds.map((threshold: Threshold) => {
|
||||
// JSON serialization of -Infinity is 'null' so lets convert it back to -Infinity
|
||||
if (threshold.index === 0 && threshold.value === null) {
|
||||
return { ...threshold, value: -Infinity };
|
||||
}
|
||||
|
||||
return threshold;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getOptions(panelDefaults) {
|
||||
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
|
||||
return _.defaultsDeep(this.options || {}, panelDefaults);
|
||||
}
|
||||
|
||||
updateOptions(options: object) {
|
||||
const update: any = {};
|
||||
update[this.getOptionsKey()] = options;
|
||||
Object.assign(this, update);
|
||||
this.options = options;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private getOptionsKey() {
|
||||
return PANEL_OPTIONS_KEY_PREFIX + this.type;
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
const model: any = {};
|
||||
for (const property in this) {
|
||||
@@ -221,10 +229,6 @@ export class PanelModel {
|
||||
}, {});
|
||||
}
|
||||
|
||||
private saveCurrentPanelOptions() {
|
||||
this.cachedPluginOptions[this.type] = this.getOptionsToRemember();
|
||||
}
|
||||
|
||||
private restorePanelOptions(pluginId: string) {
|
||||
const prevOptions = this.cachedPluginOptions[pluginId] || {};
|
||||
|
||||
@@ -233,24 +237,28 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
changeType(pluginId: string, fromAngularPanel: boolean) {
|
||||
this.saveCurrentPanelOptions();
|
||||
changeType(pluginId: string, preserveOptions?: any) {
|
||||
const oldOptions: any = this.getOptionsToRemember();
|
||||
const oldPluginId = this.type;
|
||||
|
||||
this.type = pluginId;
|
||||
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
if (fromAngularPanel) {
|
||||
this.destroy();
|
||||
|
||||
for (const key of _.keys(this)) {
|
||||
if (mustKeepProps[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete this[key];
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
if (mustKeepProps[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete this[key];
|
||||
}
|
||||
|
||||
this.cachedPluginOptions[oldPluginId] = oldOptions;
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
||||
if (preserveOptions && oldOptions) {
|
||||
this.options = this.options || {};
|
||||
Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
|
||||
}
|
||||
}
|
||||
|
||||
addQuery(query?: Partial<DataQuery>) {
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
148
public/app/features/dashboard/state/initDashboard.test.ts
Normal file
148
public/app/features/dashboard/state/initDashboard.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user