From ca8d0ef041d65e77d929e340d93fe48f6434fc1c Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 9 Jun 2023 16:00:16 +0100 Subject: [PATCH] NestedFolders: Move `New folder` into a drawer (#69706) * make New folder a drawer * use sentence case * extract strings and update tests * use sm drawer --- .../BrowseDashboardsPage.tsx | 5 +- .../components/CreateNewButton.test.tsx | 39 ++++++++--- .../components/CreateNewButton.tsx | 69 +++++++++++++++---- .../components/NewFolderForm.tsx | 59 ++++++++++++++++ public/app/features/search/tempI18nPhrases.ts | 4 +- public/locales/en-US/grafana.json | 4 +- public/locales/pseudo-LOCALE/grafana.json | 4 +- 7 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/NewFolderForm.tsx 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 = ( {canCreateDashboard && ( - - )} - {canCreateFolder && ( - + )} + {canCreateFolder && setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />} {canCreateDashboard && ( - + )} ); 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 ( +
onConfirm(form.folderName)}> + {({ register, errors }) => ( + <> + + await validateFolderName(v), + })} + /> + + + + + + + )} +
+ ); +} 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": "Ğő ŧő ƒőľđęř",