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 { 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 && <FolderActionsButton folder={folderDTO} />}
{(canCreateDashboards || canCreateFolder) && (
<CreateNewButton
inFolder={folderUID}
parentFolderTitle={folderDTO?.title}
parentFolderUid={folderUID}
canCreateDashboard={canCreateDashboards}
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 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) {
render(<CreateNewButton canCreateDashboard canCreateFolder inFolder={folderUID} />);
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderUid={folderUID} />);
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(<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 () => {
render(<CreateNewButton canCreateDashboard canCreateFolder={false} />);
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();
});
});

View File

@ -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<typeof connector>;
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 = (
<Menu>
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/new', inFolder)} label={getNewDashboardPhrase()} />
)}
{canCreateFolder && (
<MenuItem url={addFolderUidToUrl('/dashboards/folder/new', inFolder)} label={getNewFolderPhrase()} />
<MenuItem url={addFolderUidToUrl('/dashboard/new', parentFolderUid)} label={getNewDashboardPhrase()} />
)}
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/import', inFolder)} label={getImportPhrase()} />
<MenuItem url={addFolderUidToUrl('/dashboard/import', parentFolderUid)} label={getImportPhrase()} />
)}
</Menu>
);
return (
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
{getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</Dropdown>
<>
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
{getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</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

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() {
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() {

View File

@ -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",

View File

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