mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
0561c941af
commit
d066da42f8
@ -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,90 +10,10 @@ 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';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
dashboardId: number;
|
|
||||||
folder?: FolderInfo;
|
|
||||||
permissions: DashboardAcl[];
|
|
||||||
getDashboardPermissions: typeof getDashboardPermissions;
|
|
||||||
updateDashboardPermission: typeof updateDashboardPermission;
|
|
||||||
removeDashboardPermission: typeof removeDashboardPermission;
|
|
||||||
addDashboardPermission: typeof addDashboardPermission;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
isAdding: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DashboardPermissions extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAdding: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.getDashboardPermissions(this.props.dashboardId);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenAddPermissions = () => {
|
|
||||||
this.setState({ isAdding: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = (item: DashboardAcl) => {
|
|
||||||
this.props.removeDashboardPermission(this.props.dashboardId, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
|
||||||
this.props.updateDashboardPermission(this.props.dashboardId, item, level);
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddPermission = (newItem: NewDashboardAclItem) => {
|
|
||||||
return this.props.addDashboardPermission(this.props.dashboardId, newItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancelAddPermission = () => {
|
|
||||||
this.setState({ isAdding: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { permissions, folder } = this.props;
|
|
||||||
const { isAdding } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="dashboard-settings__header">
|
|
||||||
<div className="page-action-bar">
|
|
||||||
<h3 className="d-inline-block">Permissions</h3>
|
|
||||||
<Tooltip placement="auto" content={<PermissionsInfo />}>
|
|
||||||
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
|
|
||||||
</Tooltip>
|
|
||||||
<div className="page-action-bar__spacer" />
|
|
||||||
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
|
||||||
Add Permission
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SlideDown in={isAdding}>
|
|
||||||
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
|
||||||
</SlideDown>
|
|
||||||
<PermissionList
|
|
||||||
items={permissions}
|
|
||||||
onRemoveItem={this.onRemoveItem}
|
|
||||||
onPermissionChanged={this.onPermissionChanged}
|
|
||||||
isFetching={false}
|
|
||||||
folderInfo={folder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
permissions: state.dashboard.permissions,
|
permissions: state.dashboard.permissions,
|
||||||
@ -105,4 +26,99 @@ const mapDispatchToProps = {
|
|||||||
updateDashboardPermission,
|
updateDashboardPermission,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
isAdding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardPermissionsUnconnected extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAdding: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.getDashboardPermissions(this.props.dashboard.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenAddPermissions = () => {
|
||||||
|
this.setState({ isAdding: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveItem = (item: DashboardAcl) => {
|
||||||
|
this.props.removeDashboardPermission(this.props.dashboard.id, item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
||||||
|
this.props.updateDashboardPermission(this.props.dashboard.id, item, level);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddPermission = (newItem: NewDashboardAclItem) => {
|
||||||
|
return this.props.addDashboardPermission(this.props.dashboard.id, newItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelAddPermission = () => {
|
||||||
|
this.setState({ isAdding: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
getFolder() {
|
||||||
|
const { dashboard } = this.props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dashboard.meta.folderId,
|
||||||
|
title: dashboard.meta.folderTitle,
|
||||||
|
url: dashboard.meta.folderUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
permissions,
|
||||||
|
dashboard: {
|
||||||
|
meta: { hasUnsavedFolderChange },
|
||||||
|
},
|
||||||
|
} = this.props;
|
||||||
|
const { isAdding } = this.state;
|
||||||
|
|
||||||
|
return hasUnsavedFolderChange ? (
|
||||||
|
<h5>You have changed folder, please save to view permissions.</h5>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="dashboard-settings__header">
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<h3 className="d-inline-block">Permissions</h3>
|
||||||
|
<Tooltip placement="auto" content={<PermissionsInfo />}>
|
||||||
|
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
<Button className="pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||||
|
Add Permission
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SlideDown in={isAdding}>
|
||||||
|
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
||||||
|
</SlideDown>
|
||||||
|
<PermissionList
|
||||||
|
items={permissions}
|
||||||
|
onRemoveItem={this.onRemoveItem}
|
||||||
|
onPermissionChanged={this.onPermissionChanged}
|
||||||
|
isFetching={false}
|
||||||
|
folderInfo={this.getFolder()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardPermissions = connector(DashboardPermissionsUnconnected);
|
||||||
|
@ -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,39 +1,27 @@
|
|||||||
// 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;
|
|
||||||
angularCmp: AngularComponent;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const loader = getAngularLoader();
|
|
||||||
|
|
||||||
const template = '<dashboard-settings dashboard="dashboard" class="dashboard-settings__body2" />';
|
|
||||||
const scopeProps = { dashboard: this.props.dashboard };
|
|
||||||
|
|
||||||
this.angularCmp = loader.load(this.element, scopeProps, template);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.angularCmp) {
|
|
||||||
this.angularCmp.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.props.updateLocation({
|
this.props.updateLocation({
|
||||||
query: { editview: null },
|
query: { editview: null },
|
||||||
@ -41,10 +29,119 @@ export class DashboardSettings extends PureComponent<Props> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
onChangePage = (id: string) => {
|
||||||
|
this.props.updateLocation({
|
||||||
|
query: { editview: id },
|
||||||
|
partial: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getPages(): SettingsPage[] {
|
||||||
const { dashboard } = this.props;
|
const { dashboard } = this.props;
|
||||||
|
const pages: SettingsPage[] = [];
|
||||||
|
|
||||||
|
if (dashboard.meta.canEdit) {
|
||||||
|
pages.push(this.getGeneralPage());
|
||||||
|
|
||||||
|
pages.push({
|
||||||
|
title: 'Annotations',
|
||||||
|
id: 'annotations',
|
||||||
|
icon: 'comment-alt',
|
||||||
|
render: () => <AnnotationsSettings dashboard={dashboard} />,
|
||||||
|
});
|
||||||
|
|
||||||
|
pages.push({
|
||||||
|
title: 'Variables',
|
||||||
|
id: 'templating',
|
||||||
|
icon: 'calculator-alt',
|
||||||
|
render: () => <VariableEditorContainer />,
|
||||||
|
});
|
||||||
|
|
||||||
|
pages.push({
|
||||||
|
title: 'Links',
|
||||||
|
id: 'links',
|
||||||
|
icon: 'link',
|
||||||
|
render: () => <LinksSettings dashboard={dashboard} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboard.meta.canMakeEditable) {
|
||||||
|
pages.push({
|
||||||
|
title: 'General',
|
||||||
|
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() {
|
||||||
|
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'">
|
<div class="gf-form-group">
|
||||||
<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
|
|
||||||
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">
|
</div>
|
||||||
Cycle between options using Shortcut: CTRL+O or CMD+O
|
<div class="gf-form">
|
||||||
</info-popover>
|
<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>
|
</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
|
||||||
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input'
|
</bootstrap-tagsinput>
|
||||||
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 class="gf-form-button-row">
|
<folder-picker initial-title="ctrl.dashboard.meta.folderTitle" initial-folder-id="ctrl.dashboard.meta.folderId"
|
||||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete"
|
on-change="ctrl.onFolderChange" enable-create-new="true" is-valid-selection="true" label-class="width-7"
|
||||||
aria-label="Dashboard settings page delete dashboard button">
|
dashboard-id="ctrl.dashboard.id">
|
||||||
Delete Dashboard
|
</folder-picker>
|
||||||
</button>
|
<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
|
||||||
|
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">
|
||||||
|
<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 === 'annotations'"
|
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete"
|
||||||
ng-include="'public/app/features/annotations/partials/editor.html'">
|
aria-label="Dashboard settings page delete dashboard button">
|
||||||
</div>
|
Delete Dashboard
|
||||||
|
|
||||||
<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 class="gf-form">
|
|
||||||
<code-editor content="ctrl.json" data-mode="json" data-max-lines=30></code-editor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'">
|
|
||||||
<dashboard-permissions ng-if="ctrl.dashboard && !ctrl.hasUnsavedFolderChange" dashboardId="ctrl.dashboard.id"
|
|
||||||
backendSrv="ctrl.backendSrv" folder="ctrl.getFolder()" />
|
|
||||||
<div ng-if="ctrl.hasUnsavedFolderChange">
|
|
||||||
<h5>You have changed folder, please save to view permissions.</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
|
|
||||||
<h3 class="dashboard-settings__header">Settings view not found</h3>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h5>The settings page could not be found or you do not have permission to access it</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'make_editable'">
|
|
||||||
<h3 class="dashboard-settings__header">Make Editable</h3>
|
|
||||||
|
|
||||||
<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…
Reference in New Issue
Block a user