diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index e7f708f52a1..dca4f5b8acc 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -25,7 +25,6 @@ import { HelpModal } from './components/help/HelpModal'; import { Footer } from './components/Footer/Footer'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { SearchField, SearchResults, SearchResultsFilter, SearchWrapper } from '../features/search'; -import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; const { SecretFormField } = LegacyForms; @@ -185,15 +184,4 @@ export function registerAngularDirectives() { ['onLoad', { 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 }], - ]); } diff --git a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.test.tsx b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.test.tsx index df73a722635..d400a4238a9 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.test.tsx @@ -8,7 +8,6 @@ import { TimeSrv } from '../../services/TimeSrv'; const setupTestContext = (options: Partial) => { const defaults: Props = { - renderCount: 0, refreshIntervals: ['1s', '5s', '10s'], onRefreshIntervalChange: jest.fn(), getIntervalsFunc: (intervals) => intervals, diff --git a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx index 7b1033269cf..5e9f6e54561 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx @@ -4,7 +4,6 @@ import { Input, Tooltip, defaultIntervals } from '@grafana/ui'; import { getTimeSrv } from '../../services/TimeSrv'; export interface Props { - renderCount: number; // hack to make sure Angular changes are propagated properly, please remove when DashboardSettings are migrated to React refreshIntervals: string[]; onRefreshIntervalChange: (interval: string[]) => void; getIntervalsFunc?: typeof getValidIntervals; @@ -12,7 +11,6 @@ export interface Props { } export const AutoRefreshIntervals: FC = ({ - renderCount, refreshIntervals, onRefreshIntervalChange, getIntervalsFunc = getValidIntervals, @@ -24,7 +22,7 @@ export const AutoRefreshIntervals: FC = ({ useEffect(() => { const intervals = getIntervalsFunc(refreshIntervals ?? defaultIntervals); setIntervals(intervals); - }, [renderCount, refreshIntervals]); + }, [refreshIntervals]); const intervalsString = useMemo(() => { if (!Array.isArray(intervals)) { diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index 995adc75b94..90692636244 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -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 { AngularComponent, getAngularLoader } from '@grafana/runtime'; +import { DeleteDashboardButton } from '../DeleteDashboard/DeleteDashboardButton'; +import { TimePickerSettings } from './TimePickerSettings'; interface Props { dashboard: DashboardModel; } -export class GeneralSettings extends PureComponent { - element?: HTMLElement | null; - angularCmp?: AngularComponent; +const GRAPH_TOOLTIP_OPTIONS = [ + { value: 0, label: 'Default' }, + { value: 1, label: 'Shared crosshair' }, + { value: 2, label: 'Shared Tooltip' }, +]; - componentDidMount() { - const loader = getAngularLoader(); +export const GeneralSettings: React.FC = ({ dashboard }) => { + const [renderCounter, setRenderCounter] = useState(0); - const template = ''; - const scopeProps = { dashboard: this.props.dashboard }; - this.angularCmp = loader.load(this.element, scopeProps, template); - } + const onFolderChange = (folder: { id: number; title: string }) => { + dashboard.meta.folderId = folder.id; + dashboard.meta.folderTitle = folder.title; + dashboard.meta.hasUnsavedFolderChange = true; + }; - componentWillUnmount() { - if (this.angularCmp) { - this.angularCmp.destroy(); - } - } + const onBlur = (event: React.FocusEvent) => { + dashboard[event.currentTarget.name as 'title' | 'description'] = event.currentTarget.value; + }; - render() { - return
(this.element = ref)} />; - } -} + const onTooltipChange = (graphTooltip: SelectableValue) => { + 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) => { + dashboard.editable = ev.currentTarget.checked; + setRenderCounter(renderCounter + 1); + }; + + return ( + <> +

+ General +

+
+ + + + + + + + + + + + + +
+ + +
Panel Options
+
+ + -
-
- - -
-
- - - -
- - - - -
- - - - -
Panel Options
-
- -
- -
-
-
- -
diff --git a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton.tsx b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton.tsx new file mode 100644 index 00000000000..a32207fc831 --- /dev/null +++ b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton.tsx @@ -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) => ( + + {({ showModal, hideModal }) => ( + + )} + +); diff --git a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx new file mode 100644 index 00000000000..8123b2e8abf --- /dev/null +++ b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx @@ -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 = ({ hideModal, dashboard }) => { + const isProvisioned = dashboard.meta.provisioned; + const { onRestoreDashboard } = useDashboardDelete(dashboard.uid); + const modalBody = getModalBody(dashboard.panels, dashboard.title); + + if (isProvisioned) { + return ; + } + + return ( + + ); +}; + +const getModalBody = (panels: PanelModel[], title: string) => { + const totalAlerts = sumBy(panels, (panel) => (panel.alert ? 1 : 0)); + return totalAlerts > 0 ? ( + <> +

Do you want to delete this dashboard?

+

+ This dashboard contains {totalAlerts} alert{totalAlerts > 1 ? 's' : ''}. Deleting this dashboard will also + delete those alerts +

+ + ) : ( + <> +

Do you want to delete this dashboard?

+

{title}

+ + ); +}; + +const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): void; provisionedId: string }) => ( + +

+ This dashboard is managed by Grafanas provisioning and cannot be deleted. Remove the dashboard from the config + file to delete it. +

+

+ + See{' '} + + documentation + {' '} + for more information about provisioning. + +
+ File path: {provisionedId} +

+ + + +
+); diff --git a/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx b/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx new file mode 100644 index 00000000000..91a0429f140 --- /dev/null +++ b/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx @@ -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 }; +}; diff --git a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx index 6fd9fd677d1..831e1e83d7c 100644 --- a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx +++ b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { css } from 'emotion'; -import { HorizontalGroup, Modal, Button } from '@grafana/ui'; +import { ConfirmModal } from '@grafana/ui'; import { useDashboardRestore } from './useDashboardRestore'; export interface RevertDashboardModalProps { hideModal: () => void; @@ -12,25 +11,16 @@ export const RevertDashboardModal: React.FC = ({ hide const { onRestoreDashboard } = useDashboardRestore(version); return ( - -

Are you sure you want to restore the dashboard to version {version}? All unsaved changes will be lost.

- - - - -
+ onConfirm={onRestoreDashboard} + body={ +

Are you sure you want to restore the dashboard to version {version}? All unsaved changes will be lost.

+ } + confirmText={`Yes, restore to version ${version}`} + /> ); }; diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index b1af1163eee..fb379119eb6 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -7,7 +7,7 @@ import { DashboardInitPhase, DashboardRouteInfo } from 'app/types'; import { notifyApp, updateLocation } from 'app/core/actions'; import { cleanUpDashboardAndVariables } from '../state/actions'; -jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({})); +jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({})); interface ScenarioContext { cleanUpDashboardAndVariablesMock: typeof cleanUpDashboardAndVariables; diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 835544feeea..9294ee0052c 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -5,7 +5,7 @@ import { Props as DashboardPanelProps } from '../dashgrid/DashboardPanel'; import { DashboardModel } from '../state'; 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', () => { class DashboardPanel extends React.Component { render() { diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index ebc3370ca87..74070a4d051 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -8,9 +8,3 @@ import './components/DashExportModal'; import './components/DashNav'; import './components/VersionHistory'; import './components/DashboardSettings'; - -import { DashboardPermissions } from './components/DashboardPermissions/DashboardPermissions'; -// angular wrappers -import { react2AngularDirective } from 'app/core/utils/react2angular'; - -react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);