mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 { 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}
|
||||||
/>
|
/>
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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}>
|
<>
|
||||||
<Button>
|
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
|
||||||
{getNewPhrase()}
|
<Button>
|
||||||
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
|
{getNewPhrase()}
|
||||||
</Button>
|
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
|
||||||
</Dropdown>
|
</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
|
* @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() {
|
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() {
|
||||||
|
@ -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",
|
||||||
|
@ -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": "Ğő ŧő ƒőľđęř",
|
||||||
|
Loading…
Reference in New Issue
Block a user