Dashboard: Migrating dashboard settings to react (#27561)

* Dashboard: Started migrating dashboard settings

* Restore general settings from angular

* Use react permissions component

* feat(dashboard): add react LinksSettings wrapper for dash-links-editor

* feat(dashboard): add react VersionsSettings wrapper for gf-dashboard-history

* refactor(dashboard): replace DashboardPermissions connectWithStore with connect

* chore(dashboard): folderInfo as undefined

* feat(dashboard): initial commit of dashboard settings json editor

* feat(dashboard): introduce save json functionality

* chore(dashboard): delete obsolete imports

* feat(dashboard): add save and save as buttons to settings nav

* feat(dashboard): add react wrapper for annotations settings

* chore(dashboard): put back canDelete for general settings delete button

* Make editable

* Remove makeEditable from SettingsCtrl

* feat(dashboard): show json editor save button if canSave

* refactor(dashboard): move hasUnsavedFolderChange to dashboard.meta

* feat(dashboard): render hasUnsavedFolderChange view in permissions settings

* feat(dashboard): reset hasUnsavedFolderChange on settingg save success

* feat(dashboard): refresh route on sucessful settings save

* test(dashboard): update snapshots

* refactor(dashboard): automatically infer connected props for dashboard permissions

* refactor(dashboard): give dashboard versions checkboxes some padding

* Update public/app/types/folders.ts

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
pull/29911/head
Torkel Ödegaard 5 years ago committed by GitHub
parent 0561c941af
commit d066da42f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-ui/src/types/icon.ts
  2. 1
      packages/grafana-ui/src/utils/standardEditors.tsx
  3. 2
      public/app/core/angular_wrappers.ts
  4. 84
      public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
  5. 30
      public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx
  6. 179
      public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
  7. 30
      public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx
  8. 54
      public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx
  9. 30
      public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx
  10. 173
      public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
  11. 30
      public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
  12. 181
      public/app/features/dashboard/components/DashboardSettings/template.html
  13. 1
      public/app/features/dashboard/components/VersionHistory/template.html
  14. 2
      public/app/features/dashboard/containers/DashboardPage.tsx
  15. 8
      public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap
  16. 4
      public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap
  17. 2
      public/app/features/dashboard/index.ts
  18. 1
      public/app/features/dashboard/state/DashboardModel.ts
  19. 9
      public/app/features/variables/editor/VariableEditorContainer.tsx
  20. 1
      public/app/types/dashboard.ts
  21. 6
      public/app/types/folders.ts
  22. 7
      public/sass/components/_dashboard_settings.scss

@ -111,6 +111,7 @@ export type IconName =
| 'search-plus' | 'search-plus'
| 'minus-circle' | 'minus-circle'
| 'table' | 'table'
| 'arrow'
| 'plus' | 'plus'
| 'heart' | 'heart'
| 'heart-break' | 'heart-break'
@ -229,6 +230,7 @@ export const getAvailableIcons = (): IconName[] => [
'signal', 'signal',
'search-plus', 'search-plus',
'minus-circle', 'minus-circle',
'arrow',
'table', 'table',
'plus', 'plus',
'heart', 'heart',

@ -139,7 +139,6 @@ export const getStandardFieldConfigs = () => {
id: 'thresholds', id: 'thresholds',
path: 'thresholds', path: 'thresholds',
name: 'Thresholds', name: 'Thresholds',
editor: standardEditorsRegistry.get('thresholds').editor as any, editor: standardEditorsRegistry.get('thresholds').editor as any,
override: standardEditorsRegistry.get('thresholds').editor as any, override: standardEditorsRegistry.get('thresholds').editor as any,
process: thresholdsOverrideProcessor, process: thresholdsOverrideProcessor,

@ -28,7 +28,6 @@ import {
SaveDashboardAsButtonConnected, SaveDashboardAsButtonConnected,
SaveDashboardButtonConnected, SaveDashboardButtonConnected,
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton'; } from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
import { SearchField, SearchResults, SearchResultsFilter, SearchWrapper } from '../features/search'; import { SearchField, SearchResults, SearchResultsFilter, SearchWrapper } from '../features/search';
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
@ -200,7 +199,6 @@ export function registerAngularDirectives() {
['getDashboard', { watchDepth: 'reference', wrapApply: true }], ['getDashboard', { watchDepth: 'reference', wrapApply: true }],
['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }], ['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }],
]); ]);
react2AngularDirective('variableEditorContainer', VariableEditorContainer, []);
react2AngularDirective('timePickerSettings', TimePickerSettings, [ react2AngularDirective('timePickerSettings', TimePickerSettings, [
'renderCount', 'renderCount',
'refreshIntervals', 'refreshIntervals',

@ -1,7 +1,8 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Tooltip, Icon } from '@grafana/ui'; import { connect, ConnectedProps } from 'react-redux';
import { Tooltip, Icon, Button } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types'; import { StoreState } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
import { import {
getDashboardPermissions, getDashboardPermissions,
@ -9,26 +10,35 @@ import {
removeDashboardPermission, removeDashboardPermission,
updateDashboardPermission, updateDashboardPermission,
} from '../../state/actions'; } from '../../state/actions';
import { DashboardModel } from '../../state/DashboardModel';
import PermissionList from 'app/core/components/PermissionList/PermissionList'; import PermissionList from 'app/core/components/PermissionList/PermissionList';
import AddPermission from 'app/core/components/PermissionList/AddPermission'; import AddPermission from 'app/core/components/PermissionList/AddPermission';
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
const mapStateToProps = (state: StoreState) => ({
export interface Props { permissions: state.dashboard.permissions,
dashboardId: number; });
folder?: FolderInfo;
permissions: DashboardAcl[]; const mapDispatchToProps = {
getDashboardPermissions: typeof getDashboardPermissions; getDashboardPermissions,
updateDashboardPermission: typeof updateDashboardPermission; addDashboardPermission,
removeDashboardPermission: typeof removeDashboardPermission; removeDashboardPermission,
addDashboardPermission: typeof addDashboardPermission; updateDashboardPermission,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export interface OwnProps {
dashboard: DashboardModel;
} }
export type Props = OwnProps & ConnectedProps<typeof connector>;
export interface State { export interface State {
isAdding: boolean; isAdding: boolean;
} }
export class DashboardPermissions extends PureComponent<Props, State> { export class DashboardPermissionsUnconnected extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -38,7 +48,7 @@ export class DashboardPermissions extends PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
this.props.getDashboardPermissions(this.props.dashboardId); this.props.getDashboardPermissions(this.props.dashboard.id);
} }
onOpenAddPermissions = () => { onOpenAddPermissions = () => {
@ -46,26 +56,43 @@ export class DashboardPermissions extends PureComponent<Props, State> {
}; };
onRemoveItem = (item: DashboardAcl) => { onRemoveItem = (item: DashboardAcl) => {
this.props.removeDashboardPermission(this.props.dashboardId, item); this.props.removeDashboardPermission(this.props.dashboard.id, item);
}; };
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
this.props.updateDashboardPermission(this.props.dashboardId, item, level); this.props.updateDashboardPermission(this.props.dashboard.id, item, level);
}; };
onAddPermission = (newItem: NewDashboardAclItem) => { onAddPermission = (newItem: NewDashboardAclItem) => {
return this.props.addDashboardPermission(this.props.dashboardId, newItem); return this.props.addDashboardPermission(this.props.dashboard.id, newItem);
}; };
onCancelAddPermission = () => { onCancelAddPermission = () => {
this.setState({ isAdding: false }); this.setState({ isAdding: false });
}; };
getFolder() {
const { dashboard } = this.props;
return {
id: dashboard.meta.folderId,
title: dashboard.meta.folderTitle,
url: dashboard.meta.folderUrl,
};
}
render() { render() {
const { permissions, folder } = this.props; const {
permissions,
dashboard: {
meta: { hasUnsavedFolderChange },
},
} = this.props;
const { isAdding } = this.state; const { isAdding } = this.state;
return ( return hasUnsavedFolderChange ? (
<h5>You have changed folder, please save to view permissions.</h5>
) : (
<div> <div>
<div className="dashboard-settings__header"> <div className="dashboard-settings__header">
<div className="page-action-bar"> <div className="page-action-bar">
@ -74,9 +101,9 @@ export class DashboardPermissions extends PureComponent<Props, State> {
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" /> <Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
</Tooltip> </Tooltip>
<div className="page-action-bar__spacer" /> <div className="page-action-bar__spacer" />
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}> <Button className="pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
Add Permission Add Permission
</button> </Button>
</div> </div>
</div> </div>
<SlideDown in={isAdding}> <SlideDown in={isAdding}>
@ -87,22 +114,11 @@ export class DashboardPermissions extends PureComponent<Props, State> {
onRemoveItem={this.onRemoveItem} onRemoveItem={this.onRemoveItem}
onPermissionChanged={this.onPermissionChanged} onPermissionChanged={this.onPermissionChanged}
isFetching={false} isFetching={false}
folderInfo={folder} folderInfo={this.getFolder()}
/> />
</div> </div>
); );
} }
} }
const mapStateToProps = (state: StoreState) => ({ export const DashboardPermissions = connector(DashboardPermissionsUnconnected);
permissions: state.dashboard.permissions,
});
const mapDispatchToProps = {
getDashboardPermissions,
addDashboardPermission,
removeDashboardPermission,
updateDashboardPermission,
};
export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps);

@ -0,0 +1,30 @@
import React, { PureComponent } from 'react';
import { DashboardModel } from '../../state/DashboardModel';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
interface Props {
dashboard: DashboardModel;
}
export class AnnotationsSettings extends PureComponent<Props> {
element?: HTMLElement | null;
angularCmp?: AngularComponent;
componentDidMount() {
const loader = getAngularLoader();
const template = '<div ng-include="\'public/app/features/annotations/partials/editor.html\'" />';
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={ref => (this.element = ref)} />;
}
}

@ -1,50 +1,147 @@
// Libaries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { cx } from 'emotion';
// Utils & Services import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { Button, CustomScrollbar, Icon, IconName } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
// Types
import { DashboardModel } from '../../state/DashboardModel';
import { BackButton } from 'app/core/components/BackButton/BackButton'; import { BackButton } from 'app/core/components/BackButton/BackButton';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { CustomScrollbar } from '@grafana/ui'; import { DashboardModel } from '../../state/DashboardModel';
import { SaveDashboardButton, SaveDashboardAsButton } from '../SaveDashboard/SaveDashboardButton';
import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer';
import { DashboardPermissions } from '../DashboardPermissions/DashboardPermissions';
import { GeneralSettings } from './GeneralSettings';
import { AnnotationsSettings } from './AnnotationsSettings';
import { LinksSettings } from './LinksSettings';
import { VersionsSettings } from './VersionsSettings';
import { JsonEditorSettings } from './JsonEditorSettings';
export interface Props { export interface Props {
dashboard: DashboardModel; dashboard: DashboardModel;
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
editview: string;
} }
export class DashboardSettings extends PureComponent<Props> { export class DashboardSettings extends PureComponent<Props> {
element?: HTMLElement | null; onClose = () => {
angularCmp: AngularComponent; this.props.updateLocation({
query: { editview: null },
partial: true,
});
};
onChangePage = (id: string) => {
this.props.updateLocation({
query: { editview: id },
partial: true,
});
};
componentDidMount() { getPages(): SettingsPage[] {
const loader = getAngularLoader(); const { dashboard } = this.props;
const pages: SettingsPage[] = [];
const template = '<dashboard-settings dashboard="dashboard" class="dashboard-settings__body2" />'; if (dashboard.meta.canEdit) {
const scopeProps = { dashboard: this.props.dashboard }; pages.push(this.getGeneralPage());
this.angularCmp = loader.load(this.element, scopeProps, template); pages.push({
} title: 'Annotations',
id: 'annotations',
icon: 'comment-alt',
render: () => <AnnotationsSettings dashboard={dashboard} />,
});
componentWillUnmount() { pages.push({
if (this.angularCmp) { title: 'Variables',
this.angularCmp.destroy(); id: 'templating',
icon: 'calculator-alt',
render: () => <VariableEditorContainer />,
});
pages.push({
title: 'Links',
id: 'links',
icon: 'link',
render: () => <LinksSettings dashboard={dashboard} />,
});
} }
}
onClose = () => { if (dashboard.meta.canMakeEditable) {
this.props.updateLocation({ pages.push({
query: { editview: null }, title: 'General',
partial: true, icon: 'sliders-v-alt',
id: 'settings',
render: () => this.renderMakeEditable(),
});
}
if (dashboard.id && dashboard.meta.canSave) {
pages.push({
title: 'Versions',
id: 'versions',
icon: 'history',
render: () => <VersionsSettings dashboard={dashboard} />,
});
}
if (dashboard.id && dashboard.meta.canAdmin) {
pages.push({
title: 'Permissions',
id: 'permissions',
icon: 'lock',
render: () => <DashboardPermissions dashboard={dashboard} />,
});
}
pages.push({
title: 'JSON Model',
id: 'dashboard_json',
icon: 'arrow',
render: () => <JsonEditorSettings dashboard={dashboard} />,
}); });
return pages;
}
onMakeEditable = () => {
const { dashboard } = this.props;
dashboard.editable = true;
dashboard.meta.canMakeEditable = false;
dashboard.meta.canEdit = true;
dashboard.meta.canSave = true;
this.forceUpdate();
};
onPostSave = () => {
this.props.dashboard.meta.hasUnsavedFolderChange = false;
dashboardWatcher.reloadPage();
}; };
renderMakeEditable(): React.ReactNode {
return (
<div>
<div className="dashboard-settings__header">Dashboard not editable</div>
<Button onClick={this.onMakeEditable}>Make editable</Button>
</div>
);
}
getGeneralPage(): SettingsPage {
return {
title: 'General',
id: 'settings',
icon: 'sliders-v-alt',
render: () => <GeneralSettings dashboard={this.props.dashboard} />,
};
}
render() { render() {
const { dashboard } = this.props; const { dashboard, editview } = this.props;
const folderTitle = dashboard.meta.folderTitle; const folderTitle = dashboard.meta.folderTitle;
const haveFolder = (dashboard.meta.folderId ?? 0) > 0; const haveFolder = (dashboard.meta.folderId ?? 0) > 0;
const pages = this.getPages();
const currentPage = pages.find(page => page.id === editview) ?? pages[0];
const canSaveAs = contextSrv.hasEditPermissionInFolders;
const canSave = dashboard.meta.canSave;
return ( return (
<div className="dashboard-settings"> <div className="dashboard-settings">
@ -58,9 +155,37 @@ export class DashboardSettings extends PureComponent<Props> {
</div> </div>
</div> </div>
<CustomScrollbar> <CustomScrollbar>
<div className="dashboard-settings__body1" ref={element => (this.element = element)} /> <div className="dashboard-settings__body">
<aside className="dashboard-settings__aside">
{pages.map(page => (
<a
className={cx('dashboard-settings__nav-item', { active: page.id === editview })}
aria-label={selectors.pages.Dashboard.Settings.General.sectionItems(page.title)}
onClick={() => this.onChangePage(page.id)}
key={page.id}
>
<Icon name={page.icon} style={{ marginRight: '4px' }} />
{page.title}
</a>
))}
<div className="dashboard-settings__aside-actions">
{canSave && <SaveDashboardButton dashboard={dashboard} onSaveSuccess={this.onPostSave} />}
{canSaveAs && (
<SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={this.onPostSave} variant="secondary" />
)}
</div>
</aside>
<div className="dashboard-settings__content">{currentPage.render()}</div>
</div>
</CustomScrollbar> </CustomScrollbar>
</div> </div>
); );
} }
} }
export interface SettingsPage {
id: string;
title: string;
icon: IconName;
render: () => React.ReactNode;
}

@ -0,0 +1,30 @@
import React, { PureComponent } from 'react';
import { DashboardModel } from '../../state/DashboardModel';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
interface Props {
dashboard: DashboardModel;
}
export class GeneralSettings extends PureComponent<Props> {
element?: HTMLElement | null;
angularCmp?: AngularComponent;
componentDidMount() {
const loader = getAngularLoader();
const template = '<dashboard-settings 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={ref => (this.element = ref)} />;
}
}

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Button, CodeEditor } from '@grafana/ui';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { getDashboardSrv } from '../../services/DashboardSrv';
import { DashboardModel } from '../../state/DashboardModel';
interface Props {
dashboard: DashboardModel;
}
export const JsonEditorSettings: React.FC<Props> = ({ dashboard }) => {
const [dashboardJson, setDashboardJson] = useState<string>(JSON.stringify(dashboard.getSaveModelClone(), null, 2));
const onBlur = (value: string) => {
setDashboardJson(value);
};
const onClick = () => {
getDashboardSrv()
.saveJSONDashboard(dashboardJson)
.then(() => {
dashboardWatcher.reloadPage();
});
};
return (
<>
<h3 className="dashboard-settings__header">JSON Model</h3>
<div className="dashboard-settings__subheader">
The JSON Model below is data structure that defines the dashboard. Including settings, panel settings & layout,
queries etc.
</div>
<div>
<AutoSizer disableHeight>
{({ width }) => (
<CodeEditor
value={dashboardJson}
language="json"
width={width}
height="500px"
showMiniMap={false}
onBlur={onBlur}
/>
)}
</AutoSizer>
</div>
{dashboard.meta.canSave && (
<Button className="m-t-3" onClick={onClick}>
Save Changes
</Button>
)}
</>
);
};

@ -0,0 +1,30 @@
import React, { PureComponent } from 'react';
import { DashboardModel } from '../../state/DashboardModel';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
interface Props {
dashboard: DashboardModel;
}
export class LinksSettings extends PureComponent<Props> {
element?: HTMLElement | null;
angularCmp?: AngularComponent;
componentDidMount() {
const loader = getAngularLoader();
const template = '<dash-links-editor 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={ref => (this.element = ref)} />;
}
}

@ -1,183 +1,32 @@
import _ from 'lodash'; import _ from 'lodash';
import angular, { ILocationService, IScope } from 'angular'; import { ILocationService, IScope } from 'angular';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { appEvents, contextSrv, coreModule } from 'app/core/core'; import { appEvents, coreModule } from 'app/core/core';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { DashboardSrv } from '../../services/DashboardSrv';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { AppEvents, TimeZone } from '@grafana/data';
import { AppEvents, locationUtil, TimeZone, urlUtil } from '@grafana/data';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions'; import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
export class SettingsCtrl { export class SettingsCtrl {
dashboard: DashboardModel; dashboard: DashboardModel;
isOpen: boolean;
viewId: string;
json: string;
alertCount: number;
canSaveAs: boolean; canSaveAs: boolean;
canSave?: boolean; canSave?: boolean;
canDelete?: boolean; canDelete?: boolean;
sections: any[];
hasUnsavedFolderChange: boolean;
selectors: typeof selectors.pages.Dashboard.Settings.General; selectors: typeof selectors.pages.Dashboard.Settings.General;
renderCount: number; // hack to update React when Angular changes renderCount: number; // hack to update React when Angular changes
/** @ngInject */ /** @ngInject */
constructor( constructor(private $scope: IScope & Record<string, any>, private $location: ILocationService) {
private $scope: IScope & Record<string, any>,
private $route: any,
private $location: ILocationService,
private $rootScope: GrafanaRootScope,
private dashboardSrv: DashboardSrv
) {
// temp hack for annotations and variables editors // temp hack for annotations and variables editors
// that rely on inherited scope // that rely on inherited scope
$scope.dashboard = this.dashboard; $scope.dashboard = this.dashboard;
this.$scope.$on('$destroy', () => {
this.dashboard.updateSubmenuVisibility();
setTimeout(() => {
this.dashboard.startRefresh();
});
});
this.canSaveAs = contextSrv.hasEditPermissionInFolders;
this.canSave = this.dashboard.meta.canSave;
this.canDelete = this.dashboard.meta.canSave; this.canDelete = this.dashboard.meta.canSave;
this.buildSectionList();
this.onRouteUpdated();
this.$rootScope.onAppEvent(CoreEvents.routeUpdated, this.onRouteUpdated.bind(this), $scope);
appEvents.on(CoreEvents.dashboardSaved, this.onPostSave.bind(this), $scope);
this.selectors = selectors.pages.Dashboard.Settings.General; this.selectors = selectors.pages.Dashboard.Settings.General;
this.renderCount = 0; this.renderCount = 0;
} }
buildSectionList() {
this.sections = [];
if (this.dashboard.meta.canEdit) {
this.sections.push({
title: 'General',
id: 'settings',
icon: 'sliders-v-alt',
});
this.sections.push({
title: 'Annotations',
id: 'annotations',
icon: 'comment-alt',
});
this.sections.push({
title: 'Variables',
id: 'templating',
icon: 'calculator-alt',
});
this.sections.push({
title: 'Links',
id: 'links',
icon: 'link',
});
}
if (this.dashboard.id && this.dashboard.meta.canSave) {
this.sections.push({
title: 'Versions',
id: 'versions',
icon: 'history',
});
}
if (this.dashboard.id && this.dashboard.meta.canAdmin) {
this.sections.push({
title: 'Permissions',
id: 'permissions',
icon: 'lock',
});
}
if (this.dashboard.meta.canMakeEditable) {
this.sections.push({
title: 'General',
icon: 'sliders-v-alt',
id: 'make_editable',
});
}
this.sections.push({
title: 'JSON Model',
id: 'dashboard_json',
icon: 'arrow',
});
const params = this.$location.search();
const url = this.$location.path();
for (const section of this.sections) {
section.url = locationUtil.assureBaseUrl(urlUtil.renderUrl(url, { ...params, editview: section.id }));
}
}
onRouteUpdated() {
this.viewId = this.$location.search().editview;
if (this.viewId) {
this.json = angular.toJson(this.dashboard.getSaveModelClone(), true);
}
if (this.viewId === 'settings' && this.dashboard.meta.canMakeEditable) {
this.viewId = 'make_editable';
}
const currentSection: any = _.find(this.sections, { id: this.viewId } as any);
if (!currentSection) {
this.sections.unshift({
title: 'Not found',
id: '404',
icon: 'exclamation-triangle',
});
this.viewId = '404';
}
}
saveDashboardJson() {
this.dashboardSrv.saveJSONDashboard(this.json).then(() => {
this.$route.reload();
});
}
onPostSave() {
this.hasUnsavedFolderChange = false;
}
hideSettings() {
const urlParams = this.$location.search();
delete urlParams.editview;
setTimeout(() => {
this.$rootScope.$apply(() => {
this.$location.search(urlParams);
});
});
}
makeEditable() {
this.dashboard.editable = true;
this.dashboard.meta.canMakeEditable = false;
this.dashboard.meta.canEdit = true;
this.dashboard.meta.canSave = true;
this.canDelete = true;
this.viewId = 'settings';
this.buildSectionList();
const currentSection: any = _.find(this.sections, { id: this.viewId } as any);
this.$location.url(locationUtil.stripBaseFromUrl(currentSection.url));
}
deleteDashboard() { deleteDashboard() {
let confirmText = ''; let confirmText = '';
let text2 = this.dashboard.title; let text2 = this.dashboard.title;
@ -237,19 +86,7 @@ export class SettingsCtrl {
onFolderChange = (folder: { id: number; title: string }) => { onFolderChange = (folder: { id: number; title: string }) => {
this.dashboard.meta.folderId = folder.id; this.dashboard.meta.folderId = folder.id;
this.dashboard.meta.folderTitle = folder.title; this.dashboard.meta.folderTitle = folder.title;
this.hasUnsavedFolderChange = true; this.dashboard.meta.hasUnsavedFolderChange = true;
};
getFolder() {
return {
id: this.dashboard.meta.folderId,
title: this.dashboard.meta.folderTitle,
url: this.dashboard.meta.folderUrl,
};
}
getDashboard = () => {
return this.dashboard;
}; };
onRefreshIntervalChange = (intervals: string[]) => { onRefreshIntervalChange = (intervals: string[]) => {

@ -0,0 +1,30 @@
import React, { PureComponent } from 'react';
import { DashboardModel } from '../../state/DashboardModel';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
interface Props {
dashboard: DashboardModel;
}
export class VersionsSettings extends PureComponent<Props> {
element?: HTMLElement | null;
angularCmp?: AngularComponent;
componentDidMount() {
const loader = getAngularLoader();
const template = '<gf-dashboard-history 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={ref => (this.element = ref)} />;
}
}

@ -1,143 +1,64 @@
<aside class="dashboard-settings__aside">
<a href="{{::section.url}}"
class="dashboard-settings__nav-item"
ng-class="{active: ctrl.viewId === section.id}"
ng-repeat="section in ctrl.sections"
aria-label="{{ctrl.selectors.sectionItems(section.title)}}">
<icon name="'{{::section.icon}}'" style="margin-right: 4px;"></icon>
{{::section.title}}
</a>
<div class="dashboard-settings__aside-actions"> <h3 class="dashboard-settings__header" aria-label="{{::ctrl.selectors.title}}">
<div ng-show="ctrl.canSave"> General
<save-dashboard-button getDashboard="ctrl.getDashboard" /> </h3>
</div>
<div ng-show="ctrl.canSaveAs">
<save-dashboard-as-button getDashboard="ctrl.getDashboard" variant="'secondary'" />
</div>
</div>
</aside>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'settings'">
<h3 class="dashboard-settings__header" aria-label="{{::ctrl.selectors.title}}">
General
</h3>
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label width-7">Name</label>
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">Description</label>
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">
Tags
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
</label>
<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
<folder-picker initial-title="ctrl.dashboard.meta.folderTitle" initial-folder-id="ctrl.dashboard.meta.folderId"
on-change="ctrl.onFolderChange" enable-create-new="true" is-valid-selection="true" label-class="width-7"
dashboard-id="ctrl.dashboard.id">
</folder-picker>
<gf-form-switch class="gf-form" label="Editable"
tooltip="Uncheck, then save and reload to disable all dashboard editing" checked="ctrl.dashboard.editable"
label-class="width-7">
</gf-form-switch>
</div>
<time-picker-settings <div class="gf-form-group">
onTimeZoneChange="ctrl.onTimeZoneChange"
onRefreshIntervalChange="ctrl.onRefreshIntervalChange"
onNowDelayChange="ctrl.onNowDelayChange"
onHideTimePickerChange="ctrl.onHideTimePickerChange"
renderCount="ctrl.renderCount"
refreshIntervals="ctrl.dashboard.timepicker.refresh_intervals"
timePickerHidden="ctrl.dashboard.timepicker.hidden"
nowDelay="ctrl.dashboard.timepicker.nowDelay"
timezone="ctrl.dashboard.timezone"
>
</time-picker-settings>
<h5 class="section-heading">Panel Options</h5>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-11"> <label class="gf-form-label width-7">Name</label>
Graph Tooltip <input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
<info-popover mode="right-normal">
Cycle between options using Shortcut: CTRL+O or CMD+O
</info-popover>
</label>
<div class="gf-form-select-wrapper">
<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"
aria-label="Dashboard settings page delete dashboard button">
Delete Dashboard
</button>
</div>
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'"
ng-include="'public/app/features/annotations/partials/editor.html'">
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating'">
<variable-editor-container />
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'links'">
<dash-links-editor dashboard="ctrl.dashboard"></dash-links-editor>
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'versions'">
<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'dashboard_json'">
<h3 class="dashboard-settings__header">JSON Model</h3>
<div class="dashboard-settings__subheader">
The JSON Model below is data structure that defines the dashboard. Including settings, panel settings & layout,
queries etc.
</div> </div>
<div class="gf-form"> <div class="gf-form">
<code-editor content="ctrl.json" data-mode="json" data-max-lines=30></code-editor> <label class="gf-form-label width-7">Description</label>
</div> <input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
<div class="gf-form-button-row">
<button class="btn btn-primary" ng-click="ctrl.saveDashboardJson()" ng-show="ctrl.canSave">
Save Changes
</button>
</div> </div>
</div> <div class="gf-form">
<label class="gf-form-label width-7">
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'"> Tags
<dashboard-permissions ng-if="ctrl.dashboard && !ctrl.hasUnsavedFolderChange" dashboardId="ctrl.dashboard.id" <info-popover mode="right-normal">Press enter to add a tag</info-popover>
backendSrv="ctrl.backendSrv" folder="ctrl.getFolder()" /> </label>
<div ng-if="ctrl.hasUnsavedFolderChange"> <bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
<h5>You have changed folder, please save to view permissions.</h5> </bootstrap-tagsinput>
</div> </div>
<folder-picker initial-title="ctrl.dashboard.meta.folderTitle" initial-folder-id="ctrl.dashboard.meta.folderId"
on-change="ctrl.onFolderChange" enable-create-new="true" is-valid-selection="true" label-class="width-7"
dashboard-id="ctrl.dashboard.id">
</folder-picker>
<gf-form-switch class="gf-form" label="Editable"
tooltip="Uncheck, then save and reload to disable all dashboard editing" checked="ctrl.dashboard.editable"
label-class="width-7">
</gf-form-switch>
</div> </div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'"> <time-picker-settings
<h3 class="dashboard-settings__header">Settings view not found</h3> onTimeZoneChange="ctrl.onTimeZoneChange"
onRefreshIntervalChange="ctrl.onRefreshIntervalChange"
<div> onNowDelayChange="ctrl.onNowDelayChange"
<h5>The settings page could not be found or you do not have permission to access it</h5> onHideTimePickerChange="ctrl.onHideTimePickerChange"
renderCount="ctrl.renderCount"
refreshIntervals="ctrl.dashboard.timepicker.refresh_intervals"
timePickerHidden="ctrl.dashboard.timepicker.hidden"
nowDelay="ctrl.dashboard.timepicker.nowDelay"
timezone="ctrl.dashboard.timezone"
>
</time-picker-settings>
<h5 class="section-heading">Panel Options</h5>
<div class="gf-form">
<label class="gf-form-label width-11">
Graph Tooltip
<info-popover mode="right-normal">
Cycle between options using Shortcut: CTRL+O or CMD+O
</info-popover>
</label>
<div class="gf-form-select-wrapper">
<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> </div>
<div class="gf-form-button-row">
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'make_editable'"> <button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete"
<h3 class="dashboard-settings__header">Make Editable</h3> aria-label="Dashboard settings page delete dashboard button">
Delete Dashboard
<button class="btn btn-primary" ng-click="ctrl.makeEditable()">
Make Editable
</button> </button>
</div> </div>

@ -31,7 +31,6 @@
<tbody> <tbody>
<tr ng-repeat="revision in ctrl.revisions"> <tr ng-repeat="revision in ctrl.revisions">
<td <td
class="filter-table__switch-cell"
bs-tooltip="!revision.checked && ctrl.canCompare ? 'You can only compare 2 versions at a time' : ''" bs-tooltip="!revision.checked && ctrl.canCompare ? 'You can only compare 2 versions at a time' : ''"
data-placement="right" data-placement="right"
> >

@ -337,7 +337,7 @@ export class DashboardPage extends PureComponent<Props, State> {
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} defaultTab={inspectTab} />} {inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} defaultTab={inspectTab} />}
{editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} />} {editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} />}
{editview && <DashboardSettings dashboard={dashboard} updateLocation={updateLocation} />} {editview && <DashboardSettings dashboard={dashboard} updateLocation={updateLocation} editview={editview} />}
</div> </div>
); );
} }

@ -42,6 +42,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -169,6 +170,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -264,6 +266,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -414,6 +417,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -541,6 +545,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -636,6 +641,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -736,6 +742,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -792,6 +799,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"version": 0, "version": 0,
} }
} }
editview="settings"
updateLocation={[MockFunction]} updateLocation={[MockFunction]}
/> />
</div> </div>

@ -92,6 +92,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -314,6 +315,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -536,6 +538,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],
@ -758,6 +761,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true, "canSave": true,
"canShare": true, "canShare": true,
"canStar": true, "canStar": true,
"hasUnsavedFolderChange": false,
"showSettings": true, "showSettings": true,
}, },
"originalTemplating": Array [], "originalTemplating": Array [],

@ -9,7 +9,7 @@ import './components/DashNav';
import './components/VersionHistory'; import './components/VersionHistory';
import './components/DashboardSettings'; import './components/DashboardSettings';
import DashboardPermissions from './components/DashboardPermissions/DashboardPermissions'; import { DashboardPermissions } from './components/DashboardPermissions/DashboardPermissions';
// angular wrappers // angular wrappers
import { react2AngularDirective } from 'app/core/utils/react2angular'; import { react2AngularDirective } from 'app/core/utils/react2angular';

@ -170,6 +170,7 @@ export class DashboardModel {
meta.canEdit = meta.canEdit !== false; meta.canEdit = meta.canEdit !== false;
meta.showSettings = meta.canEdit; meta.showSettings = meta.canEdit;
meta.canMakeEditable = meta.canSave && !this.editable; meta.canMakeEditable = meta.canSave && !this.editable;
meta.hasUnsavedFolderChange = false;
if (!this.editable) { if (!this.editable) {
meta.canEdit = false; meta.canEdit = false;

@ -5,8 +5,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types'; import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { StoreState } from '../../../types'; import { StoreState } from '../../../types';
import { VariableEditorEditor } from './VariableEditorEditor'; import { VariableEditorEditor } from './VariableEditorEditor';
import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { getEditorVariables } from '../state/selectors'; import { getEditorVariables } from '../state/selectors';
import { VariableModel } from '../types'; import { VariableModel } from '../types';
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions'; import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
@ -140,8 +139,4 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
switchToListMode, switchToListMode,
}; };
export const VariableEditorContainer = connectWithStore( export const VariableEditorContainer = connect(mapStateToProps, mapDispatchToProps)(VariableEditorContainerUnconnected);
VariableEditorContainerUnconnected,
mapStateToProps,
mapDispatchToProps
);

@ -34,6 +34,7 @@ export interface DashboardMeta {
createdBy?: string; createdBy?: string;
updated?: string; updated?: string;
updatedBy?: string; updatedBy?: string;
hasUnsavedFolderChange?: boolean;
} }
export interface DashboardDataDTO { export interface DashboardDataDTO {

@ -23,7 +23,7 @@ export interface FolderState {
} }
export interface FolderInfo { export interface FolderInfo {
id: number; id?: number;
title: string; title?: string;
url: string; url?: string;
} }

@ -12,12 +12,7 @@
flex-direction: column; flex-direction: column;
} }
.dashboard-settings__body1 { .dashboard-settings__body {
min-height: 100%;
width: 100%;
}
.dashboard-settings__body2 {
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;

Loading…
Cancel
Save