mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NestedFolders: API improvements for NestedFolderPicker (#72093)
* NestedFolders: Prepare nested folder picker for more usage * fix betterer results * Update usage of NestedFolderPicker * fix tests * fix betterer
This commit is contained in:
parent
cfa1a2c55f
commit
a5016c9e88
@ -15,8 +15,6 @@ import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { FolderUID } from './types';
|
||||
|
||||
const ROW_HEIGHT = 40;
|
||||
const CHEVRON_SIZE = 'md';
|
||||
|
||||
@ -27,7 +25,7 @@ interface NestedFolderListProps {
|
||||
focusedItemIndex: number;
|
||||
foldersAreOpenable: boolean;
|
||||
idPrefix: string;
|
||||
selectedFolder: FolderUID | undefined;
|
||||
selectedFolder: string | undefined;
|
||||
onFolderExpand: (uid: string, newOpenState: boolean) => void;
|
||||
onFolderSelect: (item: DashboardViewItem) => void;
|
||||
isItemLoaded: (itemIndex: number) => boolean;
|
||||
|
@ -109,10 +109,7 @@ describe('NestedFolderPicker', () => {
|
||||
await screen.findByLabelText(folderA.item.title);
|
||||
|
||||
await userEvent.click(screen.getByLabelText(folderA.item.title));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
uid: folderA.item.uid,
|
||||
title: folderA.item.title,
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title);
|
||||
});
|
||||
|
||||
it('can select a folder from the picker with the keyboard', async () => {
|
||||
@ -122,10 +119,7 @@ describe('NestedFolderPicker', () => {
|
||||
await userEvent.click(button);
|
||||
|
||||
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}');
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
uid: folderA.item.uid,
|
||||
title: folderA.item.title,
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title);
|
||||
});
|
||||
|
||||
it('can expand and collapse a folder to show its children', async () => {
|
||||
@ -156,10 +150,7 @@ describe('NestedFolderPicker', () => {
|
||||
|
||||
// Select the first child
|
||||
await userEvent.click(screen.getByLabelText(folderA_folderA.item.title));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
uid: folderA_folderA.item.uid,
|
||||
title: folderA_folderA.item.title,
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title);
|
||||
});
|
||||
|
||||
it('can expand and collapse a folder to show its children with the keyboard', async () => {
|
||||
@ -185,9 +176,6 @@ describe('NestedFolderPicker', () => {
|
||||
|
||||
// Select the first child
|
||||
await userEvent.keyboard('{ArrowDown}{Enter}');
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
uid: folderA_folderA.item.uid,
|
||||
title: folderA_folderA.item.title,
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title);
|
||||
});
|
||||
});
|
||||
|
@ -26,19 +26,24 @@ import { useDispatch, useSelector } from 'app/types/store';
|
||||
import { getDOMId, NestedFolderList } from './NestedFolderList';
|
||||
import Trigger from './Trigger';
|
||||
import { useTreeInteractions } from './hooks';
|
||||
import { FolderChange, FolderUID } from './types';
|
||||
|
||||
interface NestedFolderPickerProps {
|
||||
value?: FolderUID;
|
||||
// TODO: think properly (and pragmatically) about how to communicate moving to general folder,
|
||||
// vs removing selection (if possible?)
|
||||
onChange?: (folder: FolderChange) => void;
|
||||
export interface NestedFolderPickerProps {
|
||||
/* Folder UID to show as selected */
|
||||
value?: string;
|
||||
|
||||
/* Whether to show the root 'Dashboards' (formally General) folder as selectable */
|
||||
showRootFolder?: boolean;
|
||||
|
||||
/* Folder UIDs to exclude from the picker, to prevent invalid operations */
|
||||
excludeUIDs?: string[];
|
||||
|
||||
/* Callback for when the user selects a folder */
|
||||
onChange?: (folderUID: string, folderName: string) => void;
|
||||
}
|
||||
|
||||
const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const];
|
||||
|
||||
export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: NestedFolderPickerProps) {
|
||||
export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, onChange }: NestedFolderPickerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const selectedFolder = useGetFolderQuery(value || skipToken);
|
||||
@ -101,10 +106,7 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested
|
||||
const handleFolderSelect = useCallback(
|
||||
(item: DashboardViewItem) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
uid: item.uid,
|
||||
title: item.title,
|
||||
});
|
||||
onChange(item.uid, item.title);
|
||||
}
|
||||
setOverlayOpen(false);
|
||||
},
|
||||
@ -150,20 +152,22 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested
|
||||
excludeUIDs
|
||||
);
|
||||
|
||||
// Increase the level of each item to 'make way' for the fake root Dashboards item
|
||||
for (const item of flatTree) {
|
||||
item.level += 1;
|
||||
}
|
||||
if (showRootFolder) {
|
||||
// Increase the level of each item to 'make way' for the fake root Dashboards item
|
||||
for (const item of flatTree) {
|
||||
item.level += 1;
|
||||
}
|
||||
|
||||
flatTree.unshift({
|
||||
isOpen: true,
|
||||
level: 0,
|
||||
item: {
|
||||
kind: 'folder',
|
||||
title: 'Dashboards',
|
||||
uid: '',
|
||||
},
|
||||
});
|
||||
flatTree.unshift({
|
||||
isOpen: true,
|
||||
level: 0,
|
||||
item: {
|
||||
kind: 'folder',
|
||||
title: 'Dashboards',
|
||||
uid: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the root collection hasn't loaded yet, create loading placeholders
|
||||
if (!rootCollection) {
|
||||
@ -171,7 +175,7 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested
|
||||
}
|
||||
|
||||
return flatTree;
|
||||
}, [search, searchState.value, rootCollection, childrenCollections, folderOpenState, excludeUIDs]);
|
||||
}, [search, searchState.value, rootCollection, childrenCollections, folderOpenState, excludeUIDs, showRootFolder]);
|
||||
|
||||
const isItemLoaded = useCallback(
|
||||
(itemIndex: number) => {
|
||||
|
@ -1,2 +0,0 @@
|
||||
export type FolderUID = string;
|
||||
export type FolderChange = { title: string; uid: FolderUID };
|
@ -5,7 +5,6 @@ import { config } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, Modal } from '@grafana/ui';
|
||||
import { Text } from '@grafana/ui/src/unstable';
|
||||
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
|
||||
import { FolderChange } from 'app/core/components/NestedFolderPicker/types';
|
||||
import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
@ -25,7 +24,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
const handleFolderChange = (newFolder: FolderChange) => {
|
||||
const handleFolderChange = (newFolder: { uid: string; title: string }) => {
|
||||
setMoveTarget(newFolder.uid);
|
||||
};
|
||||
|
||||
@ -61,7 +60,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
||||
|
||||
<Field label={t('browse-dashboards.action.move-modal-field-label', 'Folder name')}>
|
||||
{config.featureToggles.nestedFolderPicker ? (
|
||||
<NestedFolderPicker value={moveTarget} onChange={handleFolderChange} excludeUIDs={selectedFolders} />
|
||||
<NestedFolderPicker value={moveTarget} excludeUIDs={selectedFolders} onChange={setMoveTarget} />
|
||||
) : (
|
||||
<OldFolderPicker allowEmpty onChange={handleFolderChange} />
|
||||
)}
|
||||
|
@ -5,7 +5,6 @@ import { TimeZone } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CollapsableSection, Field, Input, RadioButtonGroup, TagsInput } from '@grafana/ui';
|
||||
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
|
||||
import { FolderChange } from 'app/core/components/NestedFolderPicker/types';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker';
|
||||
import { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions';
|
||||
@ -31,13 +30,20 @@ export function GeneralSettingsUnconnected({
|
||||
}: Props): JSX.Element {
|
||||
const [renderCounter, setRenderCounter] = useState(0);
|
||||
|
||||
const onFolderChange = (newFolder: FolderChange) => {
|
||||
const onFolderChange = (newFolder: { uid: string; title: string }) => {
|
||||
dashboard.meta.folderUid = newFolder.uid;
|
||||
dashboard.meta.folderTitle = newFolder.title;
|
||||
dashboard.meta.hasUnsavedFolderChange = true;
|
||||
setRenderCounter(renderCounter + 1);
|
||||
};
|
||||
|
||||
const onNestedFolderChange = (newUID: string, newTitle: string) => {
|
||||
dashboard.meta.folderUid = newUID;
|
||||
dashboard.meta.folderTitle = newTitle;
|
||||
dashboard.meta.hasUnsavedFolderChange = true;
|
||||
setRenderCounter(renderCounter + 1);
|
||||
};
|
||||
|
||||
const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (event.currentTarget.name === 'title' || event.currentTarget.name === 'description') {
|
||||
dashboard[event.currentTarget.name] = event.currentTarget.value;
|
||||
@ -110,7 +116,7 @@ export function GeneralSettingsUnconnected({
|
||||
|
||||
<Field label="Folder">
|
||||
{config.featureToggles.nestedFolderPicker ? (
|
||||
<NestedFolderPicker value={dashboard.meta.folderUid} onChange={onFolderChange} />
|
||||
<NestedFolderPicker value={dashboard.meta.folderUid} onChange={onNestedFolderChange} />
|
||||
) : (
|
||||
<OldFolderPicker
|
||||
inputId="dashboard-folder-input"
|
||||
|
@ -112,7 +112,11 @@ export const SaveDashboardAsForm = ({
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) =>
|
||||
config.featureToggles.nestedFolderPicker ? (
|
||||
<NestedFolderPicker {...field} value={field.value?.uid} />
|
||||
<NestedFolderPicker
|
||||
{...field}
|
||||
onChange={(uid: string, title: string) => field.onChange({ uid, title })}
|
||||
value={field.value?.uid}
|
||||
/>
|
||||
) : (
|
||||
<OldFolderPicker
|
||||
{...field}
|
||||
|
Loading…
Reference in New Issue
Block a user