mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Migrate general settings to react (#30914)
* feat(dashboard): initial commit of general settings migration to react * fix(dashboardsettings): force update of general settings when selects change * feat(dashboardsettings): initial commit of delete dashboard button and modal * feat(dashboardsettings): introduce useDashboardDelete hook * feat(dashboardsettings): add tags and editable inputs * refactor(dashboardsettings): fix typescript error in general settings * refactor(dashboardsettings): use grafana-ui form components for general settings * refactor(dashboardsettings): use ConfirmModal and move provisioned modal to own component * refactor(dashboardsettings): revertDashboardModal uses ConfirmModal * test(autorefreshintervals): remove renderCount prop to fix test * test(dashboardsettings): put back aria-label for e2e tests * chore(dashboardsettings): remove redundant generl settings angular code * test: change references to now deleted SettingsCtrl to GeneralSettings * refactor(dashboardsettings): swap out switch for inlineswitch component * chore(timepickersettings): remove timePickerSettings angular directive definition * feat(dashboardsettings): add tooltips, fix description field name * refactor(dashboardsettings): remove redundant await Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * refactor(usedashboarddelete): clean up Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
parent
a2cca8d488
commit
ef8a5b760f
@ -25,7 +25,6 @@ import { HelpModal } from './components/help/HelpModal';
|
|||||||
import { Footer } from './components/Footer/Footer';
|
import { Footer } from './components/Footer/Footer';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
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';
|
|
||||||
|
|
||||||
const { SecretFormField } = LegacyForms;
|
const { SecretFormField } = LegacyForms;
|
||||||
|
|
||||||
@ -185,15 +184,4 @@ export function registerAngularDirectives() {
|
|||||||
['onLoad', { watchDepth: 'reference', wrapApply: true }],
|
['onLoad', { watchDepth: 'reference', wrapApply: true }],
|
||||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('timePickerSettings', TimePickerSettings, [
|
|
||||||
'renderCount',
|
|
||||||
'refreshIntervals',
|
|
||||||
'timePickerHidden',
|
|
||||||
'nowDelay',
|
|
||||||
'timezone',
|
|
||||||
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import { TimeSrv } from '../../services/TimeSrv';
|
|||||||
|
|
||||||
const setupTestContext = (options: Partial<Props>) => {
|
const setupTestContext = (options: Partial<Props>) => {
|
||||||
const defaults: Props = {
|
const defaults: Props = {
|
||||||
renderCount: 0,
|
|
||||||
refreshIntervals: ['1s', '5s', '10s'],
|
refreshIntervals: ['1s', '5s', '10s'],
|
||||||
onRefreshIntervalChange: jest.fn(),
|
onRefreshIntervalChange: jest.fn(),
|
||||||
getIntervalsFunc: (intervals) => intervals,
|
getIntervalsFunc: (intervals) => intervals,
|
||||||
|
@ -4,7 +4,6 @@ import { Input, Tooltip, defaultIntervals } from '@grafana/ui';
|
|||||||
import { getTimeSrv } from '../../services/TimeSrv';
|
import { getTimeSrv } from '../../services/TimeSrv';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
renderCount: number; // hack to make sure Angular changes are propagated properly, please remove when DashboardSettings are migrated to React
|
|
||||||
refreshIntervals: string[];
|
refreshIntervals: string[];
|
||||||
onRefreshIntervalChange: (interval: string[]) => void;
|
onRefreshIntervalChange: (interval: string[]) => void;
|
||||||
getIntervalsFunc?: typeof getValidIntervals;
|
getIntervalsFunc?: typeof getValidIntervals;
|
||||||
@ -12,7 +11,6 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AutoRefreshIntervals: FC<Props> = ({
|
export const AutoRefreshIntervals: FC<Props> = ({
|
||||||
renderCount,
|
|
||||||
refreshIntervals,
|
refreshIntervals,
|
||||||
onRefreshIntervalChange,
|
onRefreshIntervalChange,
|
||||||
getIntervalsFunc = getValidIntervals,
|
getIntervalsFunc = getValidIntervals,
|
||||||
@ -24,7 +22,7 @@ export const AutoRefreshIntervals: FC<Props> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervals = getIntervalsFunc(refreshIntervals ?? defaultIntervals);
|
const intervals = getIntervalsFunc(refreshIntervals ?? defaultIntervals);
|
||||||
setIntervals(intervals);
|
setIntervals(intervals);
|
||||||
}, [renderCount, refreshIntervals]);
|
}, [refreshIntervals]);
|
||||||
|
|
||||||
const intervalsString = useMemo(() => {
|
const intervalsString = useMemo(() => {
|
||||||
if (!Array.isArray(intervals)) {
|
if (!Array.isArray(intervals)) {
|
||||||
|
@ -1,30 +1,122 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { SelectableValue, TimeZone } from '@grafana/data';
|
||||||
|
import { Select, InlineSwitch, TagsInput, InlineField, Input } from '@grafana/ui';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
import { DeleteDashboardButton } from '../DeleteDashboard/DeleteDashboardButton';
|
||||||
|
import { TimePickerSettings } from './TimePickerSettings';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeneralSettings extends PureComponent<Props> {
|
const GRAPH_TOOLTIP_OPTIONS = [
|
||||||
element?: HTMLElement | null;
|
{ value: 0, label: 'Default' },
|
||||||
angularCmp?: AngularComponent;
|
{ value: 1, label: 'Shared crosshair' },
|
||||||
|
{ value: 2, label: 'Shared Tooltip' },
|
||||||
|
];
|
||||||
|
|
||||||
componentDidMount() {
|
export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
|
||||||
const loader = getAngularLoader();
|
const [renderCounter, setRenderCounter] = useState(0);
|
||||||
|
|
||||||
const template = '<dashboard-settings dashboard="dashboard" />';
|
const onFolderChange = (folder: { id: number; title: string }) => {
|
||||||
const scopeProps = { dashboard: this.props.dashboard };
|
dashboard.meta.folderId = folder.id;
|
||||||
this.angularCmp = loader.load(this.element, scopeProps, template);
|
dashboard.meta.folderTitle = folder.title;
|
||||||
}
|
dashboard.meta.hasUnsavedFolderChange = true;
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
if (this.angularCmp) {
|
dashboard[event.currentTarget.name as 'title' | 'description'] = event.currentTarget.value;
|
||||||
this.angularCmp.destroy();
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const onTooltipChange = (graphTooltip: SelectableValue<number>) => {
|
||||||
return <div ref={(ref) => (this.element = ref)} />;
|
dashboard.graphTooltip = graphTooltip.value;
|
||||||
}
|
setRenderCounter(renderCounter + 1);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const onRefreshIntervalChange = (intervals: string[]) => {
|
||||||
|
dashboard.timepicker.refresh_intervals = intervals.filter((i) => i.trim() !== '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNowDelayChange = (nowDelay: string) => {
|
||||||
|
dashboard.timepicker.nowDelay = nowDelay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHideTimePickerChange = (hide: boolean) => {
|
||||||
|
dashboard.timepicker.hidden = hide;
|
||||||
|
setRenderCounter(renderCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeZoneChange = (timeZone: TimeZone) => {
|
||||||
|
dashboard.timezone = timeZone;
|
||||||
|
setRenderCounter(renderCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTagsChange = (tags: string[]) => {
|
||||||
|
dashboard.tags = tags;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditableChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
dashboard.editable = ev.currentTarget.checked;
|
||||||
|
setRenderCounter(renderCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="dashboard-settings__header" aria-label={selectors.pages.Dashboard.Settings.General.title}>
|
||||||
|
General
|
||||||
|
</h3>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<InlineField label="Name" labelWidth={14}>
|
||||||
|
<Input name="title" onBlur={onBlur} defaultValue={dashboard.title} width={60} />
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Description" labelWidth={14}>
|
||||||
|
<Input name="description" onBlur={onBlur} defaultValue={dashboard.description} width={60} />
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Tags" tooltip="Press enter to add a tag" labelWidth={14}>
|
||||||
|
<TagsInput tags={dashboard.tags} onChange={onTagsChange} />
|
||||||
|
</InlineField>
|
||||||
|
<FolderPicker
|
||||||
|
initialTitle={dashboard.meta.folderTitle}
|
||||||
|
initialFolderId={dashboard.meta.folderId}
|
||||||
|
onChange={onFolderChange}
|
||||||
|
enableCreateNew={true}
|
||||||
|
dashboardId={dashboard.id}
|
||||||
|
/>
|
||||||
|
<InlineField
|
||||||
|
label="Editable"
|
||||||
|
tooltip="Uncheck, then save and reload to disable all dashboard editing"
|
||||||
|
labelWidth={14}
|
||||||
|
>
|
||||||
|
<InlineSwitch value={dashboard.editable} onChange={onEditableChange} />
|
||||||
|
</InlineField>
|
||||||
|
</div>
|
||||||
|
<TimePickerSettings
|
||||||
|
onTimeZoneChange={onTimeZoneChange}
|
||||||
|
onRefreshIntervalChange={onRefreshIntervalChange}
|
||||||
|
onNowDelayChange={onNowDelayChange}
|
||||||
|
onHideTimePickerChange={onHideTimePickerChange}
|
||||||
|
refreshIntervals={dashboard.timepicker.refresh_intervals}
|
||||||
|
timePickerHidden={dashboard.timepicker.hidden}
|
||||||
|
nowDelay={dashboard.timepicker.nowDelay}
|
||||||
|
timezone={dashboard.timezone}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h5 className="section-heading">Panel Options</h5>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineField label="Graph Tooltip" labelWidth={14}>
|
||||||
|
<Select
|
||||||
|
onChange={onTooltipChange}
|
||||||
|
options={GRAPH_TOOLTIP_OPTIONS}
|
||||||
|
width={40}
|
||||||
|
value={dashboard.graphTooltip}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-button-row">
|
||||||
|
{dashboard.meta.canSave && <DeleteDashboardButton dashboard={dashboard} />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,125 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { ILocationService, IScope } from 'angular';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
|
|
||||||
import { appEvents, coreModule } from 'app/core/core';
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { AppEvents, TimeZone } from '@grafana/data';
|
|
||||||
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
|
||||||
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
|
|
||||||
|
|
||||||
export class SettingsCtrl {
|
|
||||||
dashboard: DashboardModel;
|
|
||||||
canSaveAs: boolean;
|
|
||||||
canSave?: boolean;
|
|
||||||
canDelete?: boolean;
|
|
||||||
selectors: typeof selectors.pages.Dashboard.Settings.General;
|
|
||||||
renderCount: number; // hack to update React when Angular changes
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private $scope: IScope & Record<string, any>, private $location: ILocationService) {
|
|
||||||
// temp hack for annotations and variables editors
|
|
||||||
// that rely on inherited scope
|
|
||||||
$scope.dashboard = this.dashboard;
|
|
||||||
this.canDelete = this.dashboard.meta.canSave;
|
|
||||||
this.selectors = selectors.pages.Dashboard.Settings.General;
|
|
||||||
this.renderCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteDashboard() {
|
|
||||||
let confirmText = '';
|
|
||||||
let text2 = this.dashboard.title;
|
|
||||||
|
|
||||||
if (this.dashboard.meta.provisioned) {
|
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
|
||||||
title: 'Cannot delete provisioned dashboard',
|
|
||||||
text: `
|
|
||||||
This dashboard is managed by Grafanas provisioning and cannot be deleted. Remove the dashboard from the
|
|
||||||
config file to delete it.
|
|
||||||
`,
|
|
||||||
text2: `
|
|
||||||
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
|
|
||||||
documentation</a> for more information about provisioning.</i>
|
|
||||||
</br>
|
|
||||||
File path: ${this.dashboard.meta.provisionedExternalId}
|
|
||||||
`,
|
|
||||||
text2htmlBind: true,
|
|
||||||
icon: 'trash-alt',
|
|
||||||
noText: 'OK',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alerts = _.sumBy(this.dashboard.panels, (panel) => {
|
|
||||||
return panel.alert ? 1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (alerts > 0) {
|
|
||||||
confirmText = 'DELETE';
|
|
||||||
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboard will also delete those alerts`;
|
|
||||||
}
|
|
||||||
|
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
|
||||||
title: 'Delete',
|
|
||||||
text: 'Do you want to delete this dashboard?',
|
|
||||||
text2: text2,
|
|
||||||
icon: 'trash-alt',
|
|
||||||
confirmText: confirmText,
|
|
||||||
yesText: 'Delete',
|
|
||||||
onConfirm: () => {
|
|
||||||
this.dashboard.meta.canSave = false;
|
|
||||||
this.deleteDashboardConfirmed();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteDashboardConfirmed() {
|
|
||||||
promiseToDigest(this.$scope)(
|
|
||||||
deleteDashboard(this.dashboard.uid, false).then(() => {
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
|
||||||
this.$location.url('/');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onFolderChange = (folder: { id: number; title: string }) => {
|
|
||||||
this.dashboard.meta.folderId = folder.id;
|
|
||||||
this.dashboard.meta.folderTitle = folder.title;
|
|
||||||
this.dashboard.meta.hasUnsavedFolderChange = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefreshIntervalChange = (intervals: string[]) => {
|
|
||||||
this.dashboard.timepicker.refresh_intervals = intervals.filter((i) => i.trim() !== '');
|
|
||||||
this.renderCount++;
|
|
||||||
};
|
|
||||||
|
|
||||||
onNowDelayChange = (nowDelay: string) => {
|
|
||||||
this.dashboard.timepicker.nowDelay = nowDelay;
|
|
||||||
this.renderCount++;
|
|
||||||
};
|
|
||||||
|
|
||||||
onHideTimePickerChange = (hide: boolean) => {
|
|
||||||
this.dashboard.timepicker.hidden = hide;
|
|
||||||
this.renderCount++;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTimeZoneChange = (timeZone: TimeZone) => {
|
|
||||||
this.dashboard.timezone = timeZone;
|
|
||||||
this.renderCount++;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dashboardSettings() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
templateUrl: 'public/app/features/dashboard/components/DashboardSettings/template.html',
|
|
||||||
controller: SettingsCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
transclude: true,
|
|
||||||
scope: { dashboard: '=' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('dashboardSettings', dashboardSettings);
|
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { InlineField, Input, Switch, TimeZonePicker, Tooltip } from '@grafana/ui';
|
import { InlineField, Input, InlineSwitch, TimeZonePicker, Tooltip } from '@grafana/ui';
|
||||||
import { rangeUtil, TimeZone } from '@grafana/data';
|
import { rangeUtil, TimeZone } from '@grafana/data';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@ -10,7 +10,6 @@ interface Props {
|
|||||||
onRefreshIntervalChange: (interval: string[]) => void;
|
onRefreshIntervalChange: (interval: string[]) => void;
|
||||||
onNowDelayChange: (nowDelay: string) => void;
|
onNowDelayChange: (nowDelay: string) => void;
|
||||||
onHideTimePickerChange: (hide: boolean) => void;
|
onHideTimePickerChange: (hide: boolean) => void;
|
||||||
renderCount: number; // hack to make sure Angular changes are propagated properly, please remove when DashboardSettings are migrated to React
|
|
||||||
refreshIntervals: string[];
|
refreshIntervals: string[];
|
||||||
timePickerHidden: boolean;
|
timePickerHidden: boolean;
|
||||||
nowDelay: string;
|
nowDelay: string;
|
||||||
@ -66,7 +65,6 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AutoRefreshIntervals
|
<AutoRefreshIntervals
|
||||||
renderCount={this.props.renderCount}
|
|
||||||
refreshIntervals={this.props.refreshIntervals}
|
refreshIntervals={this.props.refreshIntervals}
|
||||||
onRefreshIntervalChange={this.props.onRefreshIntervalChange}
|
onRefreshIntervalChange={this.props.onRefreshIntervalChange}
|
||||||
/>
|
/>
|
||||||
@ -88,7 +86,7 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<InlineField labelWidth={14} label="Hide time picker">
|
<InlineField labelWidth={14} label="Hide time picker">
|
||||||
<Switch value={!!this.props.timePickerHidden} onChange={this.onHideTimePickerChange} />
|
<InlineSwitch value={!!this.props.timePickerHidden} onChange={this.onHideTimePickerChange} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1 @@
|
|||||||
export { SettingsCtrl } from './SettingsCtrl';
|
|
||||||
export { DashboardSettings } from './DashboardSettings';
|
export { DashboardSettings } from './DashboardSettings';
|
||||||
export { TimePickerSettings } from './TimePickerSettings';
|
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
|
|
||||||
<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">
|
|
||||||
<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 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>
|
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DeleteDashboardModal } from './DeleteDashboardModal';
|
||||||
|
import { Button, ModalsController } from '@grafana/ui';
|
||||||
|
import { DashboardModel } from '../../state';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteDashboardButton = ({ dashboard }: Props) => (
|
||||||
|
<ModalsController>
|
||||||
|
{({ showModal, hideModal }) => (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
showModal(DeleteDashboardModal, {
|
||||||
|
dashboard,
|
||||||
|
hideModal,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-label="Dashboard settings page delete dashboard button"
|
||||||
|
>
|
||||||
|
Delete Dashboard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ModalsController>
|
||||||
|
);
|
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import sumBy from 'lodash/sumBy';
|
||||||
|
import { Modal, ConfirmModal, HorizontalGroup, Button } from '@grafana/ui';
|
||||||
|
import { DashboardModel, PanelModel } from '../../state';
|
||||||
|
import { useDashboardDelete } from './useDashboardDelete';
|
||||||
|
|
||||||
|
type DeleteDashboardModalProps = {
|
||||||
|
hideModal(): void;
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteDashboardModal: React.FC<DeleteDashboardModalProps> = ({ hideModal, dashboard }) => {
|
||||||
|
const isProvisioned = dashboard.meta.provisioned;
|
||||||
|
const { onRestoreDashboard } = useDashboardDelete(dashboard.uid);
|
||||||
|
const modalBody = getModalBody(dashboard.panels, dashboard.title);
|
||||||
|
|
||||||
|
if (isProvisioned) {
|
||||||
|
return <ProvisionedDeleteModal hideModal={hideModal} provisionedId={dashboard.meta.provisionedExternalId!} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={true}
|
||||||
|
body={modalBody}
|
||||||
|
onConfirm={onRestoreDashboard}
|
||||||
|
onDismiss={hideModal}
|
||||||
|
title="Delete"
|
||||||
|
icon="trash-alt"
|
||||||
|
confirmText="Delete"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModalBody = (panels: PanelModel[], title: string) => {
|
||||||
|
const totalAlerts = sumBy(panels, (panel) => (panel.alert ? 1 : 0));
|
||||||
|
return totalAlerts > 0 ? (
|
||||||
|
<>
|
||||||
|
<p>Do you want to delete this dashboard?</p>
|
||||||
|
<p>
|
||||||
|
This dashboard contains {totalAlerts} alert{totalAlerts > 1 ? 's' : ''}. Deleting this dashboard will also
|
||||||
|
delete those alerts
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>Do you want to delete this dashboard?</p>
|
||||||
|
<p>{title}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): void; provisionedId: string }) => (
|
||||||
|
<Modal
|
||||||
|
isOpen={true}
|
||||||
|
title="Cannot delete provisioned dashboard"
|
||||||
|
icon="trash-alt"
|
||||||
|
onDismiss={hideModal}
|
||||||
|
className={css`
|
||||||
|
text-align: center;
|
||||||
|
width: 500px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
This dashboard is managed by Grafanas provisioning and cannot be deleted. Remove the dashboard from the config
|
||||||
|
file to delete it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i>
|
||||||
|
See{' '}
|
||||||
|
<a
|
||||||
|
className="external-link"
|
||||||
|
href="http://docs.grafana.org/administration/provisioning/#dashboards"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
documentation
|
||||||
|
</a>{' '}
|
||||||
|
for more information about provisioning.
|
||||||
|
</i>
|
||||||
|
<br />
|
||||||
|
File path: {provisionedId}
|
||||||
|
</p>
|
||||||
|
<HorizontalGroup justify="center">
|
||||||
|
<Button variant="secondary" onClick={hideModal}>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</Modal>
|
||||||
|
);
|
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useAsyncFn } from 'react-use';
|
||||||
|
import { AppEvents } from '@grafana/data';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { updateLocation } from 'app/core/reducers/location';
|
||||||
|
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
|
||||||
|
|
||||||
|
export const useDashboardDelete = (uid: string) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [state, onRestoreDashboard] = useAsyncFn(() => deleteDashboard(uid, false), []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.value) {
|
||||||
|
dispatch(
|
||||||
|
updateLocation({
|
||||||
|
path: '/',
|
||||||
|
replace: true,
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', state.value.title + ' has been deleted']);
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
return { state, onRestoreDashboard };
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from 'emotion';
|
import { ConfirmModal } from '@grafana/ui';
|
||||||
import { HorizontalGroup, Modal, Button } from '@grafana/ui';
|
|
||||||
import { useDashboardRestore } from './useDashboardRestore';
|
import { useDashboardRestore } from './useDashboardRestore';
|
||||||
export interface RevertDashboardModalProps {
|
export interface RevertDashboardModalProps {
|
||||||
hideModal: () => void;
|
hideModal: () => void;
|
||||||
@ -12,25 +11,16 @@ export const RevertDashboardModal: React.FC<RevertDashboardModalProps> = ({ hide
|
|||||||
const { onRestoreDashboard } = useDashboardRestore(version);
|
const { onRestoreDashboard } = useDashboardRestore(version);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ConfirmModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
title="Restore Version"
|
title="Restore Version"
|
||||||
icon="history"
|
icon="history"
|
||||||
onDismiss={hideModal}
|
onDismiss={hideModal}
|
||||||
className={css`
|
onConfirm={onRestoreDashboard}
|
||||||
text-align: center;
|
body={
|
||||||
width: 500px;
|
<p>Are you sure you want to restore the dashboard to version {version}? All unsaved changes will be lost.</p>
|
||||||
`}
|
}
|
||||||
>
|
confirmText={`Yes, restore to version ${version}`}
|
||||||
<p>Are you sure you want to restore the dashboard to version {version}? All unsaved changes will be lost.</p>
|
/>
|
||||||
<HorizontalGroup justify="center">
|
|
||||||
<Button variant="destructive" type="button" onClick={onRestoreDashboard}>
|
|
||||||
Yes, restore to version {version}
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={hideModal}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
|
|||||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||||
import { cleanUpDashboardAndVariables } from '../state/actions';
|
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({}));
|
||||||
|
|
||||||
interface ScenarioContext {
|
interface ScenarioContext {
|
||||||
cleanUpDashboardAndVariablesMock: typeof cleanUpDashboardAndVariables;
|
cleanUpDashboardAndVariablesMock: typeof cleanUpDashboardAndVariables;
|
||||||
|
@ -5,7 +5,7 @@ import { Props as DashboardPanelProps } from '../dashgrid/DashboardPanel';
|
|||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
import { DashboardRouteInfo } from 'app/types';
|
import { DashboardRouteInfo } from 'app/types';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({}));
|
||||||
jest.mock('app/features/dashboard/dashgrid/DashboardPanel', () => {
|
jest.mock('app/features/dashboard/dashgrid/DashboardPanel', () => {
|
||||||
class DashboardPanel extends React.Component<DashboardPanelProps> {
|
class DashboardPanel extends React.Component<DashboardPanelProps> {
|
||||||
render() {
|
render() {
|
||||||
|
@ -8,9 +8,3 @@ import './components/DashExportModal';
|
|||||||
import './components/DashNav';
|
import './components/DashNav';
|
||||||
import './components/VersionHistory';
|
import './components/VersionHistory';
|
||||||
import './components/DashboardSettings';
|
import './components/DashboardSettings';
|
||||||
|
|
||||||
import { DashboardPermissions } from './components/DashboardPermissions/DashboardPermissions';
|
|
||||||
// angular wrappers
|
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
|
||||||
|
|
||||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
|
||||||
|
Loading…
Reference in New Issue
Block a user