diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index c67fe825014..e2c1700633c 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -16,7 +16,7 @@ import { skipToken, useGetFolderQuery, useSaveFolderMutation } from './api/brows
import { BrowseActions } from './components/BrowseActions/BrowseActions';
import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView';
-import { CreateNewButton } from './components/CreateNewButton';
+import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
@@ -104,7 +104,8 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{folderDTO && }
{(canCreateDashboards || canCreateFolder) && (
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
index d81b3818161..d5811f9903d 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
@@ -1,11 +1,16 @@
-import { render, screen } from '@testing-library/react';
+import { render as rtlRender, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
+import { TestProvider } from 'test/helpers/TestProvider';
-import { CreateNewButton } from './CreateNewButton';
+import CreateNewButton from './CreateNewButton';
+
+function render(...[ui, options]: Parameters) {
+ rtlRender({ui}, options);
+}
async function renderAndOpen(folderUID?: string) {
- render();
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
}
@@ -14,27 +19,39 @@ describe('NewActionsButton', () => {
it('should display the correct urls with a given folderUID', async () => {
await renderAndOpen('123');
- expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123');
- expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new?folderUid=123');
+ expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123');
});
it('should display urls without params when there is no folderUID', async () => {
await renderAndOpen();
- expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new');
- expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new');
+ expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import');
});
+ it('clicking the "New folder" button opens the drawer', async () => {
+ const mockParentFolderTitle = 'mockParentFolderTitle';
+ render();
+
+ const newButton = screen.getByText('New');
+ await userEvent.click(newButton);
+ await userEvent.click(screen.getByText('New folder'));
+
+ const drawer = screen.getByRole('dialog', { name: 'Drawer title New folder' });
+ expect(drawer).toBeInTheDocument();
+ expect(within(drawer).getByRole('heading', { name: 'New folder' })).toBeInTheDocument();
+ expect(within(drawer).getByText(`Location: ${mockParentFolderTitle}`)).toBeInTheDocument();
+ });
+
it('should only render dashboard items when folder creation is disabled', async () => {
render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
- expect(screen.getByText('New Dashboard')).toBeInTheDocument();
+ expect(screen.getByText('New dashboard')).toBeInTheDocument();
expect(screen.getByText('Import')).toBeInTheDocument();
- expect(screen.queryByText('New Folder')).not.toBeInTheDocument();
+ expect(screen.queryByText('New folder')).not.toBeInTheDocument();
});
it('should only render folder item when dashboard creation is disabled', async () => {
@@ -42,8 +59,8 @@ describe('NewActionsButton', () => {
const newButton = screen.getByText('New');
await userEvent.click(newButton);
- expect(screen.queryByText('New Dashboard')).not.toBeInTheDocument();
+ expect(screen.queryByText('New dashboard')).not.toBeInTheDocument();
expect(screen.queryByText('Import')).not.toBeInTheDocument();
- expect(screen.getByText('New Folder')).toBeInTheDocument();
+ expect(screen.getByText('New folder')).toBeInTheDocument();
});
});
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
index c1757a4a744..6d9b1367313 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
@@ -1,6 +1,8 @@
import React, { useState } from 'react';
+import { connect, ConnectedProps } from 'react-redux';
-import { Button, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
+import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
+import { createNewFolder } from 'app/features/folders/state/actions';
import {
getNewDashboardPhrase,
getNewFolderPhrase,
@@ -8,41 +10,78 @@ import {
getNewPhrase,
} from 'app/features/search/tempI18nPhrases';
-interface Props {
+import { NewFolderForm } from './NewFolderForm';
+
+const mapDispatchToProps = {
+ createNewFolder,
+};
+
+const connector = connect(null, mapDispatchToProps);
+
+interface OwnProps {
+ parentFolderTitle?: string;
/**
* Pass a folder UID in which the dashboard or folder will be created
*/
- inFolder?: string;
+ parentFolderUid?: string;
canCreateFolder: boolean;
canCreateDashboard: boolean;
}
-export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder }: Props) {
+type Props = OwnProps & ConnectedProps;
+
+function CreateNewButton({
+ parentFolderTitle,
+ parentFolderUid,
+ canCreateDashboard,
+ canCreateFolder,
+ createNewFolder,
+}: Props) {
const [isOpen, setIsOpen] = useState(false);
+ const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);
+
+ const onCreateFolder = (folderName: string) => {
+ createNewFolder(folderName, parentFolderUid);
+ setShowNewFolderDrawer(false);
+ };
+
const newMenu = (
);
return (
-
-
-
+ <>
+
+
+
+ {showNewFolderDrawer && (
+ setShowNewFolderDrawer(false)}
+ size="sm"
+ >
+ setShowNewFolderDrawer(false)} />
+
+ )}
+ >
);
}
+export default connector(CreateNewButton);
+
/**
*
* @param url without any parameters
diff --git a/public/app/features/browse-dashboards/components/NewFolderForm.tsx b/public/app/features/browse-dashboards/components/NewFolderForm.tsx
new file mode 100644
index 00000000000..9a426df9d1a
--- /dev/null
+++ b/public/app/features/browse-dashboards/components/NewFolderForm.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+
+import { Button, Input, Form, Field, HorizontalGroup } from '@grafana/ui';
+
+import { validationSrv } from '../../manage-dashboards/services/ValidationSrv';
+
+interface Props {
+ onConfirm: (folderName: string) => void;
+ onCancel: () => void;
+}
+
+interface FormModel {
+ folderName: string;
+}
+
+const initialFormModel: FormModel = { folderName: '' };
+
+export function NewFolderForm({ onCancel, onConfirm }: Props) {
+ const validateFolderName = async (folderName: string) => {
+ try {
+ await validationSrv.validateNewFolderName(folderName);
+ return true;
+ } catch (e) {
+ if (e instanceof Error) {
+ return e.message;
+ } else {
+ throw e;
+ }
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/public/app/features/search/tempI18nPhrases.ts b/public/app/features/search/tempI18nPhrases.ts
index 83b99bf2570..40c276270ae 100644
--- a/public/app/features/search/tempI18nPhrases.ts
+++ b/public/app/features/search/tempI18nPhrases.ts
@@ -10,11 +10,11 @@ export function getSearchPlaceholder(includePanels = false) {
}
export function getNewDashboardPhrase() {
- return t('search.dashboard-actions.new-dashboard', 'New Dashboard');
+ return t('search.dashboard-actions.new-dashboard', 'New dashboard');
}
export function getNewFolderPhrase() {
- return t('search.dashboard-actions.new-folder', 'New Folder');
+ return t('search.dashboard-actions.new-folder', 'New folder');
}
export function getImportPhrase() {
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 920c860799c..f789b63014b 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -408,8 +408,8 @@
"dashboard-actions": {
"import": "Import",
"new": "New",
- "new-dashboard": "New Dashboard",
- "new-folder": "New Folder"
+ "new-dashboard": "New dashboard",
+ "new-folder": "New folder"
},
"folder-view": {
"go-to-folder": "Go to folder",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index bbfc8c0ab83..f0f3b927701 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -408,8 +408,8 @@
"dashboard-actions": {
"import": "Ĩmpőřŧ",
"new": "Ńęŵ",
- "new-dashboard": "Ńęŵ Đäşĥþőäřđ",
- "new-folder": "Ńęŵ Főľđęř"
+ "new-dashboard": "Ńęŵ đäşĥþőäřđ",
+ "new-folder": "Ńęŵ ƒőľđęř"
},
"folder-view": {
"go-to-folder": "Ğő ŧő ƒőľđęř",