diff --git a/packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx index 7040c3b32df..4928397b76b 100644 --- a/packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx @@ -194,6 +194,9 @@ export function SelectBase({ return v === o.value || o.value === v.value; })[0]; }); + } else if (loadOptions) { + const hasValue = defaultValue || value; + selectedValue = hasValue ? [hasValue] : []; } else { selectedValue = options.filter(o => o.value === value || o === value); } diff --git a/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts b/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts index ff8d09a5508..30ef70f2db9 100644 --- a/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts +++ b/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts @@ -16,6 +16,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => { box-shadow: 0px 4px 4px ${menuShadowColor}; position: relative; min-width: 100%; + z-index: 1; `, option: css` padding: 8px; diff --git a/packages/grafana-ui/src/components/Forms/index.ts b/packages/grafana-ui/src/components/Forms/index.ts index f5001f3c31a..0cf719b7cfe 100644 --- a/packages/grafana-ui/src/components/Forms/index.ts +++ b/packages/grafana-ui/src/components/Forms/index.ts @@ -1,8 +1,8 @@ import { getFormStyles } from './getFormStyles'; import { Label } from './Label'; import { Input } from './Input/Input'; -import { Select } from './Select/Select'; import { ButtonSelect } from './Select/ButtonSelect'; +import { AsyncSelect, Select } from './Select/Select'; import { Form } from './Form'; import { Field } from './Field'; import { Button, LinkButton } from './Button'; @@ -19,6 +19,7 @@ const Forms = { Select, ButtonSelect, InputControl, + AsyncSelect, }; export default Forms; diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index ef9514c9550..6584c79162c 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -23,6 +23,7 @@ import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper'; import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor'; import { HelpModal } from './components/help/HelpModal'; import { Footer } from './components/Footer/Footer'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; export function registerAngularDirectives() { react2AngularDirective('footer', Footer, []); @@ -136,4 +137,18 @@ export function registerAngularDirectives() { 'dataSourceConfig', ['onChange', { watchDepth: 'reference', wrapApply: true }], ]); + react2AngularDirective('folderPicker', FolderPicker, [ + 'labelClass', + 'rootName', + 'enableCreateNew', + 'enableReset', + 'initialTitle', + 'initialFolderId', + 'dashboardId', + 'onCreateFolder', + ['enterFolderCreation', { watchDepth: 'reference', wrapApply: true }], + ['exitFolderCreation', { watchDepth: 'reference', wrapApply: true }], + ['onLoad', { watchDepth: 'reference', wrapApply: true }], + ['onChange', { watchDepth: 'reference', wrapApply: true }], + ]); } diff --git a/public/app/core/components/Select/FolderPicker.test.tsx b/public/app/core/components/Select/FolderPicker.test.tsx new file mode 100644 index 00000000000..ec8eed3bd63 --- /dev/null +++ b/public/app/core/components/Select/FolderPicker.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import * as Backend from 'app/core/services/backend_srv'; +import { FolderPicker } from './FolderPicker'; + +jest.spyOn(Backend, 'getBackendSrv').mockReturnValue({ + search: jest.fn(() => [ + { title: 'Dash 1', id: 'A' }, + { title: 'Dash 2', id: 'B' }, + ]), +} as any); + +jest.mock('app/core/core', () => ({ + contextSrv: { + isEditor: true, + }, +})); + +describe('FolderPicker', () => { + it('should render', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Select/FolderPicker.tsx b/public/app/core/components/Select/FolderPicker.tsx new file mode 100644 index 00000000000..334c68a3652 --- /dev/null +++ b/public/app/core/components/Select/FolderPicker.tsx @@ -0,0 +1,178 @@ +import React, { PureComponent } from 'react'; +import { Forms } from '@grafana/ui'; +import { AppEvents, SelectableValue } from '@grafana/data'; +import { debounce } from 'lodash'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { contextSrv } from 'app/core/core'; +import appEvents from '../../app_events'; + +export interface Props { + onChange: ($folder: { title: string; id: number }) => void; + enableCreateNew: boolean; + rootName?: string; + enableReset?: boolean; + dashboardId?: any; + initialTitle?: string; + initialFolderId?: number; +} + +interface State { + folder: SelectableValue; + validationError: string; + hasValidationError: boolean; +} + +export class FolderPicker extends PureComponent { + debouncedSearch: any; + + constructor(props: Props) { + super(props); + + this.state = { + folder: {}, + validationError: '', + hasValidationError: false, + }; + + this.debouncedSearch = debounce(this.getOptions, 300, { + leading: true, + trailing: true, + }); + } + + static defaultProps = { + rootName: 'General', + enableReset: false, + initialTitle: '', + enableCreateNew: false, + }; + + componentDidMount = async () => { + await this.loadInitialValue(); + }; + + getOptions = async (query: string) => { + const { rootName, enableReset, initialTitle } = this.props; + const params = { + query, + type: 'dash-folder', + permission: 'Edit', + }; + + const searchHits = await getBackendSrv().search(params); + const options: Array> = searchHits.map(hit => ({ label: hit.title, value: hit.id })); + if (contextSrv.isEditor && rootName?.toLowerCase().startsWith(query.toLowerCase())) { + options.unshift({ label: rootName, value: 0 }); + } + + if (enableReset && query === '' && initialTitle !== '') { + options.unshift({ label: initialTitle, value: undefined }); + } + + return options; + }; + + onFolderChange = async (newFolder: SelectableValue) => { + if (!newFolder) { + newFolder = { value: 0, label: this.props.rootName }; + } + + this.setState( + { + folder: newFolder, + }, + () => this.props.onChange({ id: newFolder.value!, title: newFolder.label! }) + ); + }; + + createNewFolder = async (folderName: string) => { + const newFolder = await getBackendSrv().createFolder({ title: folderName }); + let folder = { value: -1, label: 'Not created' }; + if (newFolder.id > -1) { + appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); + folder = { value: newFolder.id, label: newFolder.title }; + await this.onFolderChange(folder); + } else { + appEvents.emit(AppEvents.alertError, ['Folder could not be created']); + } + + return folder; + }; + + private loadInitialValue = async () => { + const { initialTitle, rootName, initialFolderId, enableReset, dashboardId } = this.props; + const resetFolder: SelectableValue = { label: initialTitle, value: undefined }; + const rootFolder: SelectableValue = { label: rootName, value: 0 }; + + const options = await this.getOptions(''); + + let folder: SelectableValue = { value: -1 }; + if (initialFolderId || (initialFolderId && initialFolderId > -1)) { + folder = options.find(option => option.value === initialFolderId) || { value: -1 }; + } else if (enableReset && initialTitle && initialFolderId === undefined) { + folder = resetFolder; + } + + if (!folder) { + if (contextSrv.isEditor) { + folder = rootFolder; + } else { + // We shouldn't assign a random folder without the user actively choosing it on a persisted dashboard + const isPersistedDashBoard = !!dashboardId; + if (isPersistedDashBoard) { + folder = resetFolder; + } else { + folder = options.length > 0 ? options[0] : resetFolder; + } + } + } + + this.setState( + { + folder, + }, + () => { + // if this is not the same as our initial value notify parent + if (folder.value !== initialFolderId) { + this.props.onChange({ id: folder.value!, title: folder.text }); + } + } + ); + }; + + render() { + const { folder, validationError, hasValidationError } = this.state; + const { enableCreateNew } = this.props; + + return ( + <> +
+
+ + +
+
+ {hasValidationError && ( +
+
+ +
+
+ )} + + ); + } +} diff --git a/public/app/core/components/Select/__snapshots__/FolderPicker.test.tsx.snap b/public/app/core/components/Select/__snapshots__/FolderPicker.test.tsx.snap new file mode 100644 index 00000000000..ceb4b2402ac --- /dev/null +++ b/public/app/core/components/Select/__snapshots__/FolderPicker.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FolderPicker should render 1`] = ` + +
+
+ + +
+
+
+`; diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index 03ca5aba711..5abd3b4590e 100644 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -244,11 +244,11 @@ export class SettingsCtrl { ); } - onFolderChange(folder: { id: number; title: string }) { + onFolderChange = (folder: { id: number; title: string }) => { this.dashboard.meta.folderId = folder.id; this.dashboard.meta.folderTitle = folder.title; this.hasUnsavedFolderChange = true; - } + }; getFolder() { return { diff --git a/public/app/features/dashboard/components/DashboardSettings/template.html b/public/app/features/dashboard/components/DashboardSettings/template.html index 97186a6e4d2..cd028ab3564 100644 --- a/public/app/features/dashboard/components/DashboardSettings/template.html +++ b/public/app/features/dashboard/components/DashboardSettings/template.html @@ -1,108 +1,103 @@
-

- General -

+

+ General +

-
-
- - -
-
- - -
-
- - - -
- - - - -
+
+
+ + +
+
+ + +
+
+ + + +
+ + + + +
- + -
Panel Options
-
- -
- -
-
-
- +
Panel Options
+
+ +
+ +
+
+
+
-
+
-
+
-
- +
+
-
- +
+
-
-

JSON Model

+
+

JSON Model

The JSON Model below is data structure that defines the dashboard. Including settings, panel settings & layout, queries etc.
-
- -
+
+ +
-
- +
+
You have changed folder, please save to view permissions.
@@ -137,4 +129,3 @@ Make Editable
- diff --git a/public/app/features/dashboard/components/FolderPicker/index.ts b/public/app/features/dashboard/components/FolderPicker/index.ts deleted file mode 100644 index 7550f7fd573..00000000000 --- a/public/app/features/dashboard/components/FolderPicker/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FolderPickerCtrl } from './FolderPickerCtrl'; diff --git a/public/app/features/dashboard/components/FolderPicker/template.html b/public/app/features/dashboard/components/FolderPicker/template.html deleted file mode 100644 index d50bdc47c7c..00000000000 --- a/public/app/features/dashboard/components/FolderPicker/template.html +++ /dev/null @@ -1,38 +0,0 @@ -
-
- - - -
-
- -
-
- -
-
-
-
- -
-
diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts index 2241287ad6f..8814d565366 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts @@ -22,9 +22,9 @@ const template = `
@@ -97,17 +97,17 @@ export class SaveDashboardAsModalCtrl { } } - onFolderChange(folder: { id: any }) { + onFolderChange = (folder: { id: any }) => { this.folderId = folder.id; - } + }; - onEnterFolderCreation() { + onEnterFolderCreation = () => { this.isValidFolderSelection = false; - } + }; - onExitFolderCreation() { + onExitFolderCreation = () => { this.isValidFolderSelection = true; - } + }; } export function saveDashboardAsDirective() { diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index e2c042ef138..f09c829739d 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -8,7 +8,6 @@ import './components/DashLinks'; import './components/DashExportModal'; import './components/DashNav'; import './components/ExportDataModal'; -import './components/FolderPicker'; import './components/VersionHistory'; import './components/DashboardSettings'; import './components/SubMenu'; diff --git a/public/app/features/manage-dashboards/DashboardImportCtrl.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.ts index 07add9b3e22..9d4b5cbfe58 100644 --- a/public/app/features/manage-dashboards/DashboardImportCtrl.ts +++ b/public/app/features/manage-dashboards/DashboardImportCtrl.ts @@ -163,18 +163,18 @@ export class DashboardImportCtrl { ); } - onFolderChange(folder: any) { + onFolderChange = (folder: any) => { this.folderId = folder.id; this.titleChanged(); - } + }; - onEnterFolderCreation() { + onEnterFolderCreation = () => { this.inputsValid = false; - } + }; - onExitFolderCreation() { + onExitFolderCreation = () => { this.inputValueChanged(); - } + }; isValid() { return this.inputsValid && this.folderId !== null; diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts index a9388b5da67..52d5b8b2949 100644 --- a/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts +++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts @@ -1,8 +1,8 @@ +import { IScope } from 'angular'; +import { AppEvents } from '@grafana/data'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { backendSrv } from 'app/core/services/backend_srv'; -import { AppEvents } from '@grafana/data'; -import { IScope } from 'angular'; import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export class MoveToFolderCtrl { @@ -14,11 +14,11 @@ export class MoveToFolderCtrl { constructor(private $scope: IScope) {} - onFolderChange(folder: any) { + onFolderChange = (folder: any) => { this.folder = folder; - } + }; - save() { + save = () => { return promiseToDigest(this.$scope)( backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => { if (result.successCount > 0) { @@ -37,15 +37,15 @@ export class MoveToFolderCtrl { return this.afterSave(); }) ); - } + }; - onEnterFolderCreation() { + onEnterFolderCreation = () => { this.isValidFolderSelection = false; - } + }; - onExitFolderCreation() { + onExitFolderCreation = () => { this.isValidFolderSelection = true; - } + }; } export function moveToFolderModal() { diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html index fd805465a55..fb5730b0477 100644 --- a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html +++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html @@ -1,7 +1,7 @@