mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 15:45:43 -06:00
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:
parent
a5b9eac88e
commit
ca8d0ef041
@ -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}
|
||||
/>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
{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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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() {
|
||||
|
@ -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",
|
||||
|
@ -408,8 +408,8 @@
|
||||
"dashboard-actions": {
|
||||
"import": "Ĩmpőřŧ",
|
||||
"new": "Ńęŵ",
|
||||
"new-dashboard": "Ńęŵ Đäşĥþőäřđ",
|
||||
"new-folder": "Ńęŵ Főľđęř"
|
||||
"new-dashboard": "Ńęŵ đäşĥþőäřđ",
|
||||
"new-folder": "Ńęŵ ƒőľđęř"
|
||||
},
|
||||
"folder-view": {
|
||||
"go-to-folder": "Ğő ŧő ƒőľđęř",
|
||||
|
Loading…
Reference in New Issue
Block a user