mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Nested folders: Create basic Move/Delete modals (#67140)
* add modal scaffolding * add some unit tests * remove dummy api, add some TODO comments * small test refactor * another small test refactor * fix unit tests due to aria-label/data-testid change
This commit is contained in:
parent
bb66f14c1d
commit
e6e741546f
@ -962,9 +962,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||||
],
|
],
|
||||||
"packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx:5381": [
|
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx:5381": [
|
"packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
@ -38,7 +38,7 @@ export const Pages = {
|
|||||||
dataSourcePluginsV2: (pluginName: string) => `Add new data source ${pluginName}`,
|
dataSourcePluginsV2: (pluginName: string) => `Add new data source ${pluginName}`,
|
||||||
},
|
},
|
||||||
ConfirmModal: {
|
ConfirmModal: {
|
||||||
delete: 'Confirm Modal Danger Button',
|
delete: 'data-testid Confirm Modal Danger Button',
|
||||||
},
|
},
|
||||||
AddDashboard: {
|
AddDashboard: {
|
||||||
url: '/dashboard/new',
|
url: '/dashboard/new',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, screen, within } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ConfirmModal } from './ConfirmModal';
|
import { ConfirmModal } from './ConfirmModal';
|
||||||
@ -23,8 +23,7 @@ describe('ConfirmModal', () => {
|
|||||||
expect(screen.getByText('Some Body')).toBeInTheDocument();
|
expect(screen.getByText('Some Body')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Dismiss Text' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Dismiss Text' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Alternative Text' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Alternative Text' })).toBeInTheDocument();
|
||||||
const button = screen.getByRole('button', { name: 'Confirm Modal Danger Button' });
|
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeInTheDocument();
|
||||||
expect(within(button).getByText('Please Confirm')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render nothing when isOpen is false', () => {
|
it('should render nothing when isOpen is false', () => {
|
||||||
@ -43,6 +42,6 @@ describe('ConfirmModal', () => {
|
|||||||
expect(screen.queryByText('Some Body')).not.toBeInTheDocument();
|
expect(screen.queryByText('Some Body')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Dismiss Text' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Dismiss Text' })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Alternative Text' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Alternative Text' })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Confirm Modal Danger Button' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Confirm' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -105,7 +105,7 @@ export const ConfirmModal = ({
|
|||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
aria-label={selectors.pages.ConfirmModal.delete}
|
data-testid={selectors.pages.ConfirmModal.delete}
|
||||||
>
|
>
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -84,7 +84,7 @@ export function Modal(props: PropsWithChildren<Props>) {
|
|||||||
typeof title !== 'string' && title
|
typeof title !== 'string' && title
|
||||||
}
|
}
|
||||||
<div className={styles.modalHeaderClose}>
|
<div className={styles.modalHeaderClose}>
|
||||||
<IconButton aria-label="Close dialogue" name="times" size="xl" onClick={onDismiss} />
|
<IconButton aria-label="Close dialog" name="times" size="xl" onClick={onDismiss} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx(styles.modalContent, contentClassName)}>{children}</div>
|
<div className={cx(styles.modalContent, contentClassName)}>{children}</div>
|
||||||
|
@ -75,7 +75,7 @@ const dataSources = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
confirmButton: byRole('button', { name: /Confirm Modal Danger Button/ }),
|
confirmButton: byRole('button', { name: /Yes, reset configuration/ }),
|
||||||
resetButton: byRole('button', { name: /Reset configuration/ }),
|
resetButton: byRole('button', { name: /Reset configuration/ }),
|
||||||
saveButton: byRole('button', { name: /Save/ }),
|
saveButton: byRole('button', { name: /Save/ }),
|
||||||
configInput: byLabelText<HTMLTextAreaElement>(/Configuration/),
|
configInput: byLabelText<HTMLTextAreaElement>(/Configuration/),
|
||||||
|
@ -12,7 +12,7 @@ import { buildNavModel } from '../folders/state/navModel';
|
|||||||
import { parseRouteParams } from '../search/utils';
|
import { parseRouteParams } from '../search/utils';
|
||||||
|
|
||||||
import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI';
|
import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI';
|
||||||
import { BrowseActions } from './components/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 { SearchView } from './components/SearchView';
|
import { SearchView } from './components/SearchView';
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
export interface Props {}
|
|
||||||
|
|
||||||
export function BrowseActions() {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
const onMove = () => {
|
|
||||||
// TODO real implemenation, stub for now
|
|
||||||
console.log('onMoveClicked');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = () => {
|
|
||||||
// TODO real implementation, stub for now
|
|
||||||
console.log('onDeleteClicked');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.row} data-testid="manage-actions">
|
|
||||||
<Button onClick={onMove} variant="secondary">
|
|
||||||
Move
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onDelete} variant="destructive">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
row: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
}),
|
|
||||||
});
|
|
@ -1,8 +1,13 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render as rtlRender, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
import { BrowseActions } from './BrowseActions';
|
import { BrowseActions } from './BrowseActions';
|
||||||
|
|
||||||
|
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
||||||
|
rtlRender(<TestProvider>{ui}</TestProvider>, options);
|
||||||
|
}
|
||||||
|
|
||||||
describe('browse-dashboards BrowseActions', () => {
|
describe('browse-dashboards BrowseActions', () => {
|
||||||
it('displays Move and Delete buttons', () => {
|
it('displays Move and Delete buttons', () => {
|
||||||
render(<BrowseActions />);
|
render(<BrowseActions />);
|
@ -0,0 +1,67 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { ShowModalReactEvent } from 'app/types/events';
|
||||||
|
|
||||||
|
import { useSelectedItemsState } from '../../state';
|
||||||
|
|
||||||
|
import { DeleteModal } from './DeleteModal';
|
||||||
|
import { MoveModal } from './MoveModal';
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
export function BrowseActions() {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const selectedItems = useSelectedItemsState();
|
||||||
|
|
||||||
|
const onMove = () => {
|
||||||
|
appEvents.publish(
|
||||||
|
new ShowModalReactEvent({
|
||||||
|
component: MoveModal,
|
||||||
|
props: {
|
||||||
|
selectedItems,
|
||||||
|
onConfirm: (moveTarget: string) => {
|
||||||
|
console.log(`MoveModal onConfirm clicked with target ${moveTarget}!`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
appEvents.publish(
|
||||||
|
new ShowModalReactEvent({
|
||||||
|
component: DeleteModal,
|
||||||
|
props: {
|
||||||
|
selectedItems,
|
||||||
|
onConfirm: () => {
|
||||||
|
console.log('DeleteModal onConfirm clicked!');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.row} data-testid="manage-actions">
|
||||||
|
<Button onClick={onMove} variant="secondary">
|
||||||
|
Move
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onDelete} variant="destructive">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
row: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DeleteModal, Props } from './DeleteModal';
|
||||||
|
|
||||||
|
describe('browse-dashboards DeleteModal', () => {
|
||||||
|
const mockOnDismiss = jest.fn();
|
||||||
|
const mockOnConfirm = jest.fn();
|
||||||
|
|
||||||
|
const defaultProps: Props = {
|
||||||
|
isOpen: true,
|
||||||
|
onConfirm: mockOnConfirm,
|
||||||
|
onDismiss: mockOnDismiss,
|
||||||
|
selectedItems: {
|
||||||
|
folder: {},
|
||||||
|
dashboard: {},
|
||||||
|
panel: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders a dialog with the correct title', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('dialog', { name: 'Delete Compute Resources' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a `Delete` button', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Delete' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a `Cancel` button', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only enables the `Delete` button if the confirmation text is typed', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
const confirmationInput = await screen.findByPlaceholderText('Type Delete to confirm');
|
||||||
|
await userEvent.type(confirmationInput, 'Delete');
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Delete' })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onConfirm when clicking the `Delete` button', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
const confirmationInput = await screen.findByPlaceholderText('Type Delete to confirm');
|
||||||
|
await userEvent.type(confirmationInput, 'Delete');
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Delete' }));
|
||||||
|
expect(mockOnConfirm).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onDismiss when clicking the `Cancel` button', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
|
||||||
|
expect(mockOnDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onDismiss when clicking the X', async () => {
|
||||||
|
render(<DeleteModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Close dialog' }));
|
||||||
|
expect(mockOnDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||||
|
import { ConfirmModal, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { DashboardTreeSelection } from '../../types';
|
||||||
|
|
||||||
|
import { buildBreakdownString } from './utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onDismiss: () => void;
|
||||||
|
selectedItems: DashboardTreeSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// TODO abstract all this counting logic out
|
||||||
|
const folderCount = Object.values(selectedItems.folder).filter(isTruthy).length;
|
||||||
|
const dashboardCount = Object.values(selectedItems.dashboard).filter(isTruthy).length;
|
||||||
|
// hardcoded values for now
|
||||||
|
// TODO replace with dummy API
|
||||||
|
const libraryPanelCount = 1;
|
||||||
|
const alertRuleCount = 1;
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
onConfirm();
|
||||||
|
onDismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
body={
|
||||||
|
<div className={styles.modalBody}>
|
||||||
|
This action will delete the following content:
|
||||||
|
<p className={styles.breakdown}>
|
||||||
|
{buildBreakdownString(folderCount, dashboardCount, libraryPanelCount, alertRuleCount)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmationText="Delete"
|
||||||
|
confirmText="Delete"
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
title="Delete Compute Resources"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
breakdown: css({
|
||||||
|
...theme.typography.bodySmall,
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
}),
|
||||||
|
modalBody: css({
|
||||||
|
...theme.typography.body,
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,101 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
|
||||||
|
|
||||||
|
import * as api from 'app/features/manage-dashboards/state/actions';
|
||||||
|
import { DashboardSearchHit } from 'app/features/search/types';
|
||||||
|
|
||||||
|
import { MoveModal, Props } from './MoveModal';
|
||||||
|
|
||||||
|
describe('browse-dashboards MoveModal', () => {
|
||||||
|
const mockOnDismiss = jest.fn();
|
||||||
|
const mockOnConfirm = jest.fn();
|
||||||
|
const mockFolders = [
|
||||||
|
{ title: 'General', uid: '' } as DashboardSearchHit,
|
||||||
|
{ title: 'Folder 1', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||||
|
];
|
||||||
|
let props: Props;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
isOpen: true,
|
||||||
|
onConfirm: mockOnConfirm,
|
||||||
|
onDismiss: mockOnDismiss,
|
||||||
|
selectedItems: {
|
||||||
|
folder: {},
|
||||||
|
dashboard: {},
|
||||||
|
panel: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// mock the searchFolders api call so the folder picker has some folders in it
|
||||||
|
jest.spyOn(api, 'searchFolders').mockResolvedValue(mockFolders);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a dialog with the correct title', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('dialog', { name: 'Move' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a `Move` button', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Move' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a `Cancel` button', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a folder picker', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('combobox', { name: 'Select a folder' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a warning about permissions if a folder is selected', async () => {
|
||||||
|
props.selectedItems.folder = {
|
||||||
|
myFolderUid: true,
|
||||||
|
};
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('Moving this item may change its permissions.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only enables the `Move` button if a folder is selected', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Move' })).toBeDisabled();
|
||||||
|
const folderPicker = await screen.findByRole('combobox', { name: 'Select a folder' });
|
||||||
|
|
||||||
|
await selectOptionInTest(folderPicker, mockFolders[1].title);
|
||||||
|
expect(await screen.findByRole('button', { name: 'Move' })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onConfirm when clicking the `Move` button', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
const folderPicker = await screen.findByRole('combobox', { name: 'Select a folder' });
|
||||||
|
|
||||||
|
await selectOptionInTest(folderPicker, mockFolders[1].title);
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Move' }));
|
||||||
|
expect(mockOnConfirm).toHaveBeenCalledWith(mockFolders[1].uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onDismiss when clicking the `Cancel` button', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
|
||||||
|
expect(mockOnDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onDismiss when clicking the X', async () => {
|
||||||
|
render(<MoveModal {...props} />);
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: 'Close dialog' }));
|
||||||
|
expect(mockOnDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,65 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||||
|
import { Alert, Button, Field, Modal, useStyles2 } from '@grafana/ui';
|
||||||
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
|
|
||||||
|
import { DashboardTreeSelection } from '../../types';
|
||||||
|
|
||||||
|
import { buildBreakdownString } from './utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onConfirm: (targetFolderUid: string) => void;
|
||||||
|
onDismiss: () => void;
|
||||||
|
selectedItems: DashboardTreeSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||||
|
const [moveTarget, setMoveTarget] = useState<string>();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// TODO abstract all this counting logic out
|
||||||
|
const folderCount = Object.values(selectedItems.folder).filter(isTruthy).length;
|
||||||
|
const dashboardCount = Object.values(selectedItems.dashboard).filter(isTruthy).length;
|
||||||
|
// hardcoded values for now
|
||||||
|
// TODO replace with dummy API
|
||||||
|
const libraryPanelCount = 1;
|
||||||
|
const alertRuleCount = 1;
|
||||||
|
|
||||||
|
const onMove = () => {
|
||||||
|
if (moveTarget !== undefined) {
|
||||||
|
onConfirm(moveTarget);
|
||||||
|
}
|
||||||
|
onDismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Move" onDismiss={onDismiss} {...props}>
|
||||||
|
{folderCount > 0 && <Alert severity="warning" title="Moving this item may change its permissions." />}
|
||||||
|
This action will move the following content:
|
||||||
|
<p className={styles.breakdown}>
|
||||||
|
{buildBreakdownString(folderCount, dashboardCount, libraryPanelCount, alertRuleCount)}
|
||||||
|
</p>
|
||||||
|
<Field label="Folder name">
|
||||||
|
<FolderPicker allowEmpty onChange={({ uid }) => setMoveTarget(uid)} />
|
||||||
|
</Field>
|
||||||
|
<Modal.ButtonRow>
|
||||||
|
<Button onClick={onDismiss} variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button disabled={moveTarget === undefined} onClick={onMove} variant="primary">
|
||||||
|
Move
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonRow>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
breakdown: css({
|
||||||
|
...theme.typography.bodySmall,
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,23 @@
|
|||||||
|
import { buildBreakdownString } from './utils';
|
||||||
|
|
||||||
|
describe('browse-dashboards utils', () => {
|
||||||
|
describe('buildBreakdownString', () => {
|
||||||
|
it.each`
|
||||||
|
folderCount | dashboardCount | libraryPanelCount | alertRuleCount | expected
|
||||||
|
${0} | ${0} | ${0} | ${0} | ${'0 items'}
|
||||||
|
${1} | ${0} | ${0} | ${0} | ${'1 item: 1 folder'}
|
||||||
|
${2} | ${0} | ${0} | ${0} | ${'2 items: 2 folders'}
|
||||||
|
${0} | ${1} | ${0} | ${0} | ${'1 item: 1 dashboard'}
|
||||||
|
${0} | ${2} | ${0} | ${0} | ${'2 items: 2 dashboards'}
|
||||||
|
${1} | ${0} | ${1} | ${1} | ${'3 items: 1 folder, 1 library panel, 1 alert rule'}
|
||||||
|
${2} | ${0} | ${3} | ${4} | ${'9 items: 2 folders, 3 library panels, 4 alert rules'}
|
||||||
|
${1} | ${1} | ${1} | ${1} | ${'4 items: 1 folder, 1 dashboard, 1 library panel, 1 alert rule'}
|
||||||
|
${1} | ${2} | ${3} | ${4} | ${'10 items: 1 folder, 2 dashboards, 3 library panels, 4 alert rules'}
|
||||||
|
`(
|
||||||
|
'returns the correct message for the various inputs',
|
||||||
|
({ folderCount, dashboardCount, libraryPanelCount, alertRuleCount, expected }) => {
|
||||||
|
expect(buildBreakdownString(folderCount, dashboardCount, libraryPanelCount, alertRuleCount)).toEqual(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
export function buildBreakdownString(
|
||||||
|
folderCount: number,
|
||||||
|
dashboardCount: number,
|
||||||
|
libraryPanelCount: number,
|
||||||
|
alertRuleCount: number
|
||||||
|
) {
|
||||||
|
const total = folderCount + dashboardCount + libraryPanelCount + alertRuleCount;
|
||||||
|
const parts = [];
|
||||||
|
if (folderCount) {
|
||||||
|
parts.push(`${folderCount} ${folderCount === 1 ? 'folder' : 'folders'}`);
|
||||||
|
}
|
||||||
|
if (dashboardCount) {
|
||||||
|
parts.push(`${dashboardCount} ${dashboardCount === 1 ? 'dashboard' : 'dashboards'}`);
|
||||||
|
}
|
||||||
|
if (libraryPanelCount) {
|
||||||
|
parts.push(`${libraryPanelCount} ${libraryPanelCount === 1 ? 'library panel' : 'library panels'}`);
|
||||||
|
}
|
||||||
|
if (alertRuleCount) {
|
||||||
|
parts.push(`${alertRuleCount} ${alertRuleCount === 1 ? 'alert rule' : 'alert rules'}`);
|
||||||
|
}
|
||||||
|
let breakdownString = `${total} ${total === 1 ? 'item' : 'items'}`;
|
||||||
|
if (parts.length > 0) {
|
||||||
|
breakdownString += `: ${parts.join(', ')}`;
|
||||||
|
}
|
||||||
|
return breakdownString;
|
||||||
|
}
|
@ -184,7 +184,7 @@ describe('FolderSettingsPage', () => {
|
|||||||
await userEvent.click(deleteButton);
|
await userEvent.click(deleteButton);
|
||||||
const deleteModal = screen.getByRole('dialog', { name: 'Delete' });
|
const deleteModal = screen.getByRole('dialog', { name: 'Delete' });
|
||||||
expect(deleteModal).toBeInTheDocument();
|
expect(deleteModal).toBeInTheDocument();
|
||||||
const deleteButtonModal = within(deleteModal).getByRole('button', { name: 'Confirm Modal Danger Button' });
|
const deleteButtonModal = within(deleteModal).getByRole('button', { name: 'Delete' });
|
||||||
await userEvent.click(deleteButtonModal);
|
await userEvent.click(deleteButtonModal);
|
||||||
expect(mockDeleteFolder).toHaveBeenCalledWith(mockFolder.uid);
|
expect(mockDeleteFolder).toHaveBeenCalledWith(mockFolder.uid);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, screen, within } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
@ -14,8 +14,7 @@ describe('ConfirmModal', () => {
|
|||||||
expect(screen.getByRole('heading', { name: 'Delete' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: 'Delete' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('Do you want to delete the 2 selected dashboards?')).toBeInTheDocument();
|
expect(screen.getByText('Do you want to delete the 2 selected dashboards?')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
const button = screen.getByRole('button', { name: 'Confirm Modal Danger Button' });
|
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
|
||||||
expect(within(button).getByText('Delete')).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(screen.queryByPlaceholderText('Type delete to confirm')).not.toBeInTheDocument();
|
expect(screen.queryByPlaceholderText('Type delete to confirm')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -135,7 +135,7 @@ describe('ServiceAccountsListPage tests', () => {
|
|||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
await user.click(screen.getByRole('button', { name: /Disable/ }));
|
await user.click(screen.getByRole('button', { name: /Disable/ }));
|
||||||
await user.click(screen.getByLabelText(/Confirm Modal Danger Button/));
|
await user.click(screen.getByRole('button', { name: 'Disable service account' }));
|
||||||
|
|
||||||
expect(updateServiceAccountMock).toHaveBeenCalledWith({
|
expect(updateServiceAccountMock).toHaveBeenCalledWith({
|
||||||
...getDefaultServiceAccount(),
|
...getDefaultServiceAccount(),
|
||||||
@ -152,7 +152,7 @@ describe('ServiceAccountsListPage tests', () => {
|
|||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
await user.click(screen.getByLabelText(/Delete service account/));
|
await user.click(screen.getByLabelText(/Delete service account/));
|
||||||
await user.click(screen.getByLabelText(/Confirm Modal Danger Button/));
|
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||||
|
|
||||||
expect(deleteServiceAccountMock).toHaveBeenCalledWith(42);
|
expect(deleteServiceAccountMock).toHaveBeenCalledWith(42);
|
||||||
});
|
});
|
||||||
|
@ -68,7 +68,7 @@ describe('SelectedLogsGroups', () => {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByText('Are you sure you want to clear all log groups?')).toBeInTheDocument()
|
expect(screen.getByText('Are you sure you want to clear all log groups?')).toBeInTheDocument()
|
||||||
);
|
);
|
||||||
await waitFor(() => userEvent.click(screen.getByLabelText('Confirm Modal Danger Button')));
|
await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Yes' })));
|
||||||
expect(defaultProps.onChange).toHaveBeenCalledWith([]);
|
expect(defaultProps.onChange).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user