mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FolderPicker: Make lazy in prep for exposing publicly (#100118)
* Make lazy NestedFolderPicker * Change permission prop to use string union instead of enum * reword comment
This commit is contained in:
parent
39d94eabcd
commit
0ca1febb77
@ -0,0 +1,16 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
import { FolderPickerSkeleton } from './Skeleton';
|
||||
|
||||
const SuspendingNestedFolderPicker = lazy(() =>
|
||||
import('./NestedFolderPicker').then((module) => ({ default: module.NestedFolderPicker }))
|
||||
);
|
||||
|
||||
// Lazily load folder picker, is what is exposed to plugins through @grafana/runtime
|
||||
export const LazyFolderPicker = (props: Parameters<typeof SuspendingNestedFolderPicker>[0]) => {
|
||||
return (
|
||||
<Suspense fallback={<FolderPickerSkeleton />}>
|
||||
<SuspendingNestedFolderPicker {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
@ -6,7 +6,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { PermissionLevelString } from 'app/types';
|
||||
|
||||
import {
|
||||
treeViewersCanEdit,
|
||||
@ -187,7 +186,7 @@ describe('NestedFolderPicker', () => {
|
||||
});
|
||||
|
||||
it('shows items the user can view, with the prop', async () => {
|
||||
render(<NestedFolderPicker permission={PermissionLevelString.View} onChange={mockOnChange} />);
|
||||
render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
await userEvent.click(button);
|
||||
@ -209,7 +208,7 @@ describe('NestedFolderPicker', () => {
|
||||
});
|
||||
|
||||
it('can expand and collapse a folder to show its children', async () => {
|
||||
render(<NestedFolderPicker permission={PermissionLevelString.View} onChange={mockOnChange} />);
|
||||
render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
|
||||
// Open the picker and wait for children to load
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
@ -240,7 +239,7 @@ describe('NestedFolderPicker', () => {
|
||||
});
|
||||
|
||||
it('can expand and collapse a folder to show its children with the keyboard', async () => {
|
||||
render(<NestedFolderPicker permission={PermissionLevelString.View} onChange={mockOnChange} />);
|
||||
render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
|
||||
await userEvent.click(button);
|
||||
|
@ -35,7 +35,7 @@ export interface NestedFolderPickerProps {
|
||||
excludeUIDs?: string[];
|
||||
|
||||
/* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */
|
||||
permission?: PermissionLevelString.View | PermissionLevelString.Edit;
|
||||
permission?: 'view' | 'edit';
|
||||
|
||||
/* Callback for when the user selects a folder */
|
||||
onChange?: (folderUID: string | undefined, folderName: string | undefined) => void;
|
||||
@ -51,7 +51,7 @@ async function getSearchResults(searchQuery: string, permission?: PermissionLeve
|
||||
query: searchQuery,
|
||||
kind: ['folder'],
|
||||
limit: 100,
|
||||
permission: permission,
|
||||
permission,
|
||||
});
|
||||
|
||||
const items = queryResponse.view.map((v) => queryResultToViewItem(v, queryResponse.view));
|
||||
@ -64,7 +64,7 @@ export function NestedFolderPicker({
|
||||
showRootFolder = true,
|
||||
clearable = false,
|
||||
excludeUIDs,
|
||||
permission = PermissionLevelString.Edit,
|
||||
permission = 'edit',
|
||||
onChange,
|
||||
}: NestedFolderPickerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -82,12 +82,23 @@ export function NestedFolderPicker({
|
||||
const [error] = useState<Error | undefined>(undefined); // TODO: error not populated anymore
|
||||
const lastSearchTimestamp = useRef<number>(0);
|
||||
|
||||
// Map the permission string union to enum value for compatibility
|
||||
const permissionLevel = useMemo(() => {
|
||||
if (permission === 'view') {
|
||||
return PermissionLevelString.View;
|
||||
} else if (permission === 'edit') {
|
||||
return PermissionLevelString.Edit;
|
||||
}
|
||||
|
||||
throw new Error('Invalid permission');
|
||||
}, [permission]);
|
||||
|
||||
const isBrowsing = Boolean(overlayOpen && !(search && searchResults));
|
||||
const {
|
||||
items: browseFlatTree,
|
||||
isLoading: isBrowseLoading,
|
||||
requestNextPage: fetchFolderPage,
|
||||
} = useFoldersQuery(isBrowsing, foldersOpenState, permission);
|
||||
} = useFoldersQuery(isBrowsing, foldersOpenState, permissionLevel);
|
||||
|
||||
useEffect(() => {
|
||||
if (!search) {
|
||||
@ -98,7 +109,7 @@ export function NestedFolderPicker({
|
||||
const timestamp = Date.now();
|
||||
setIsFetchingSearchResults(true);
|
||||
|
||||
debouncedSearch(search, permission).then((queryResponse) => {
|
||||
debouncedSearch(search, permissionLevel).then((queryResponse) => {
|
||||
// Only keep the results if it's was issued after the most recently resolved search.
|
||||
// This prevents results showing out of order if first request is slower than later ones.
|
||||
// We don't need to worry about clearing the isFetching state either - if there's a later
|
||||
@ -110,7 +121,7 @@ export function NestedFolderPicker({
|
||||
lastSearchTimestamp.current = timestamp;
|
||||
}
|
||||
});
|
||||
}, [search, permission]);
|
||||
}, [search, permissionLevel]);
|
||||
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
|
36
public/app/core/components/NestedFolderPicker/Skeleton.tsx
Normal file
36
public/app/core/components/NestedFolderPicker/Skeleton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { css } from '@emotion/css';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import type { GrafanaTheme2 } from '@grafana/data';
|
||||
import { getInputStyles, useStyles2 } from '@grafana/ui';
|
||||
|
||||
// This component is used as a fallback for codesplitting, so aim to keep
|
||||
// the bundle size of it as small as possible :)
|
||||
export function FolderPickerSkeleton() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<button type="button" className={styles.fakeInput} aria-disabled>
|
||||
<Skeleton width={100} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const baseStyles = getInputStyles({ theme });
|
||||
|
||||
return {
|
||||
wrapper: baseStyles.wrapper,
|
||||
inputWrapper: baseStyles.inputWrapper,
|
||||
fakeInput: css([
|
||||
baseStyles.input,
|
||||
{
|
||||
textAlign: 'left',
|
||||
},
|
||||
]),
|
||||
};
|
||||
};
|
@ -1,13 +1,14 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { forwardRef, ReactNode, ButtonHTMLAttributes } from 'react';
|
||||
import * as React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, getInputStyles, useTheme2, Text } from '@grafana/ui';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { FolderPickerSkeleton } from './Skeleton';
|
||||
|
||||
interface TriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
isLoading: boolean;
|
||||
handleClearSelection?: (event: React.MouseEvent<SVGElement> | React.KeyboardEvent<SVGElement>) => void;
|
||||
@ -28,6 +29,10 @@ function Trigger(
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <FolderPickerSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.inputWrapper}>
|
||||
@ -43,9 +48,7 @@ function Trigger(
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Skeleton width={100} />
|
||||
) : label ? (
|
||||
{label ? (
|
||||
<Text truncate>{label}</Text>
|
||||
) : (
|
||||
<Text truncate color="secondary">
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { TagsInput } from '@grafana/ui';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { PermissionLevelString } from 'app/types';
|
||||
|
||||
import { DashList } from './DashList';
|
||||
import { dashlistMigrationHandler } from './migrations';
|
||||
@ -62,12 +61,7 @@ export const plugin = new PanelPlugin<Options>(DashList)
|
||||
defaultValue: undefined,
|
||||
editor: function RenderFolderPicker({ value, onChange }) {
|
||||
return (
|
||||
<FolderPicker
|
||||
clearable
|
||||
permission={PermissionLevelString.View}
|
||||
value={value}
|
||||
onChange={(folderUID) => onChange(folderUID)}
|
||||
/>
|
||||
<FolderPicker clearable permission="view" value={value} onChange={(folderUID) => onChange(folderUID)} />
|
||||
);
|
||||
},
|
||||
})
|
||||
|
@ -7,6 +7,9 @@ export enum TeamPermissionLevel {
|
||||
|
||||
export { OrgRole as OrgRole };
|
||||
|
||||
export type PermissionLevel = 'view' | 'edit' | 'admin';
|
||||
|
||||
/** @deprecated Use PermissionLevel instead */
|
||||
export enum PermissionLevelString {
|
||||
View = 'View',
|
||||
Edit = 'Edit',
|
||||
|
Loading…
Reference in New Issue
Block a user