NestedFolders: Move New folder into a drawer (#69706)

* make New folder a drawer

* use sentence case

* extract strings and update tests

* use sm drawer
This commit is contained in:
Ashley Harrison 2023-06-09 16:00:16 +01:00 committed by GitHub
parent a5b9eac88e
commit ca8d0ef041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 150 additions and 34 deletions

View File

@ -16,7 +16,7 @@ import { skipToken, useGetFolderQuery, useSaveFolderMutation } from './api/brows
import { BrowseActions } from './components/BrowseActions/BrowseActions'; import { BrowseActions } from './components/BrowseActions/BrowseActions';
import { BrowseFilters } from './components/BrowseFilters'; import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView'; import { BrowseView } from './components/BrowseView';
import { CreateNewButton } from './components/CreateNewButton'; import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton'; import { FolderActionsButton } from './components/FolderActionsButton';
import { SearchView } from './components/SearchView'; import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions'; import { getFolderPermissions } from './permissions';
@ -104,7 +104,8 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{folderDTO && <FolderActionsButton folder={folderDTO} />} {folderDTO && <FolderActionsButton folder={folderDTO} />}
{(canCreateDashboards || canCreateFolder) && ( {(canCreateDashboards || canCreateFolder) && (
<CreateNewButton <CreateNewButton
inFolder={folderUID} parentFolderTitle={folderDTO?.title}
parentFolderUid={folderUID}
canCreateDashboard={canCreateDashboards} canCreateDashboard={canCreateDashboards}
canCreateFolder={canCreateFolder} canCreateFolder={canCreateFolder}
/> />

View File

@ -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 userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { CreateNewButton } from './CreateNewButton'; import CreateNewButton from './CreateNewButton';
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
async function renderAndOpen(folderUID?: string) { async function renderAndOpen(folderUID?: string) {
render(<CreateNewButton canCreateDashboard canCreateFolder inFolder={folderUID} />); render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderUid={folderUID} />);
const newButton = screen.getByText('New'); const newButton = screen.getByText('New');
await userEvent.click(newButton); await userEvent.click(newButton);
} }
@ -14,27 +19,39 @@ describe('NewActionsButton', () => {
it('should display the correct urls with a given folderUID', async () => { it('should display the correct urls with a given folderUID', async () => {
await renderAndOpen('123'); await renderAndOpen('123');
expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=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('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123'); expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123');
}); });
it('should display urls without params when there is no folderUID', async () => { it('should display urls without params when there is no folderUID', async () => {
await renderAndOpen(); await renderAndOpen();
expect(screen.getByText('New Dashboard')).toHaveAttribute('href', '/dashboard/new'); expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new');
expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new');
expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import'); expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import');
}); });
it('clicking the "New folder" button opens the drawer', async () => {
const mockParentFolderTitle = 'mockParentFolderTitle';
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderTitle={mockParentFolderTitle} />);
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 () => { it('should only render dashboard items when folder creation is disabled', async () => {
render(<CreateNewButton canCreateDashboard canCreateFolder={false} />); render(<CreateNewButton canCreateDashboard canCreateFolder={false} />);
const newButton = screen.getByText('New'); const newButton = screen.getByText('New');
await userEvent.click(newButton); await userEvent.click(newButton);
expect(screen.getByText('New Dashboard')).toBeInTheDocument(); expect(screen.getByText('New dashboard')).toBeInTheDocument();
expect(screen.getByText('Import')).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 () => { it('should only render folder item when dashboard creation is disabled', async () => {
@ -42,8 +59,8 @@ describe('NewActionsButton', () => {
const newButton = screen.getByText('New'); const newButton = screen.getByText('New');
await userEvent.click(newButton); 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.queryByText('Import')).not.toBeInTheDocument();
expect(screen.getByText('New Folder')).toBeInTheDocument(); expect(screen.getByText('New folder')).toBeInTheDocument();
}); });
}); });

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react'; 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 { import {
getNewDashboardPhrase, getNewDashboardPhrase,
getNewFolderPhrase, getNewFolderPhrase,
@ -8,41 +10,78 @@ import {
getNewPhrase, getNewPhrase,
} from 'app/features/search/tempI18nPhrases'; } 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 * Pass a folder UID in which the dashboard or folder will be created
*/ */
inFolder?: string; parentFolderUid?: string;
canCreateFolder: boolean; canCreateFolder: boolean;
canCreateDashboard: boolean; canCreateDashboard: boolean;
} }
export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder }: Props) { type Props = OwnProps & ConnectedProps<typeof connector>;
function CreateNewButton({
parentFolderTitle,
parentFolderUid,
canCreateDashboard,
canCreateFolder,
createNewFolder,
}: Props) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);
const onCreateFolder = (folderName: string) => {
createNewFolder(folderName, parentFolderUid);
setShowNewFolderDrawer(false);
};
const newMenu = ( const newMenu = (
<Menu> <Menu>
{canCreateDashboard && ( {canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/new', inFolder)} label={getNewDashboardPhrase()} /> <MenuItem url={addFolderUidToUrl('/dashboard/new', parentFolderUid)} label={getNewDashboardPhrase()} />
)}
{canCreateFolder && (
<MenuItem url={addFolderUidToUrl('/dashboards/folder/new', inFolder)} label={getNewFolderPhrase()} />
)} )}
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
{canCreateDashboard && ( {canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/import', inFolder)} label={getImportPhrase()} /> <MenuItem url={addFolderUidToUrl('/dashboard/import', parentFolderUid)} label={getImportPhrase()} />
)} )}
</Menu> </Menu>
); );
return ( return (
<>
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}> <Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button> <Button>
{getNewPhrase()} {getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} /> <Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button> </Button>
</Dropdown> </Dropdown>
{showNewFolderDrawer && (
<Drawer
title={getNewFolderPhrase()}
subtitle={parentFolderTitle ? `Location: ${parentFolderTitle}` : undefined}
scrollableContent
onClose={() => setShowNewFolderDrawer(false)}
size="sm"
>
<NewFolderForm onConfirm={onCreateFolder} onCancel={() => setShowNewFolderDrawer(false)} />
</Drawer>
)}
</>
); );
} }
export default connector(CreateNewButton);
/** /**
* *
* @param url without any parameters * @param url without any parameters

View File

@ -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 (
<Form defaultValues={initialFormModel} onSubmit={(form: FormModel) => onConfirm(form.folderName)}>
{({ register, errors }) => (
<>
<Field
label="Folder name"
invalid={!!errors.folderName}
error={errors.folderName && errors.folderName.message}
>
<Input
id="folder-name-input"
{...register('folderName', {
required: 'Folder name is required.',
validate: async (v) => await validateFolderName(v),
})}
/>
</Field>
<HorizontalGroup>
<Button variant="secondary" fill="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Create</Button>
</HorizontalGroup>
</>
)}
</Form>
);
}

View File

@ -10,11 +10,11 @@ export function getSearchPlaceholder(includePanels = false) {
} }
export function getNewDashboardPhrase() { export function getNewDashboardPhrase() {
return t('search.dashboard-actions.new-dashboard', 'New Dashboard'); return t('search.dashboard-actions.new-dashboard', 'New dashboard');
} }
export function getNewFolderPhrase() { export function getNewFolderPhrase() {
return t('search.dashboard-actions.new-folder', 'New Folder'); return t('search.dashboard-actions.new-folder', 'New folder');
} }
export function getImportPhrase() { export function getImportPhrase() {

View File

@ -408,8 +408,8 @@
"dashboard-actions": { "dashboard-actions": {
"import": "Import", "import": "Import",
"new": "New", "new": "New",
"new-dashboard": "New Dashboard", "new-dashboard": "New dashboard",
"new-folder": "New Folder" "new-folder": "New folder"
}, },
"folder-view": { "folder-view": {
"go-to-folder": "Go to folder", "go-to-folder": "Go to folder",

View File

@ -408,8 +408,8 @@
"dashboard-actions": { "dashboard-actions": {
"import": "Ĩmpőřŧ", "import": "Ĩmpőřŧ",
"new": "Ńęŵ", "new": "Ńęŵ",
"new-dashboard": "Ńęŵ Đäşĥþőäřđ", "new-dashboard": "Ńęŵ đäşĥþőäřđ",
"new-folder": "Ńęŵ Főľđęř" "new-folder": "Ńęŵ ƒőľđęř"
}, },
"folder-view": { "folder-view": {
"go-to-folder": "Ğő ŧő ƒőľđęř", "go-to-folder": "Ğő ŧő ƒőľđęř",