From 0ca1febb776778bb9bc95ea2dbe75c90ab48216b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 5 Feb 2025 15:21:32 +0000 Subject: [PATCH] FolderPicker: Make lazy in prep for exposing publicly (#100118) * Make lazy NestedFolderPicker * Change permission prop to use string union instead of enum * reword comment --- .../NestedFolderPicker/LazyFolderPicker.tsx | 16 +++++++++ .../NestedFolderPicker.test.tsx | 7 ++-- .../NestedFolderPicker/NestedFolderPicker.tsx | 23 ++++++++---- .../NestedFolderPicker/Skeleton.tsx | 36 +++++++++++++++++++ .../components/NestedFolderPicker/Trigger.tsx | 11 +++--- public/app/plugins/panel/dashlist/module.tsx | 8 +---- public/app/types/acl.ts | 3 ++ 7 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 public/app/core/components/NestedFolderPicker/LazyFolderPicker.tsx create mode 100644 public/app/core/components/NestedFolderPicker/Skeleton.tsx diff --git a/public/app/core/components/NestedFolderPicker/LazyFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/LazyFolderPicker.tsx new file mode 100644 index 00000000000..988b2c9d558 --- /dev/null +++ b/public/app/core/components/NestedFolderPicker/LazyFolderPicker.tsx @@ -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[0]) => { + return ( + }> + + + ); +}; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index caea1db92d3..f5c90e5f403 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -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(); + render(); 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(); + render(); // 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(); + render(); const button = await screen.findByRole('button', { name: 'Select folder' }); await userEvent.click(button); diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 9e345b32712..2656b96b6a2 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -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(undefined); // TODO: error not populated anymore const lastSearchTimestamp = useRef(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 = [ diff --git a/public/app/core/components/NestedFolderPicker/Skeleton.tsx b/public/app/core/components/NestedFolderPicker/Skeleton.tsx new file mode 100644 index 00000000000..98faf98b6a4 --- /dev/null +++ b/public/app/core/components/NestedFolderPicker/Skeleton.tsx @@ -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 ( +
+
+ +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + const baseStyles = getInputStyles({ theme }); + + return { + wrapper: baseStyles.wrapper, + inputWrapper: baseStyles.inputWrapper, + fakeInput: css([ + baseStyles.input, + { + textAlign: 'left', + }, + ]), + }; +}; diff --git a/public/app/core/components/NestedFolderPicker/Trigger.tsx b/public/app/core/components/NestedFolderPicker/Trigger.tsx index 942beab6e96..fe352b806d3 100644 --- a/public/app/core/components/NestedFolderPicker/Trigger.tsx +++ b/public/app/core/components/NestedFolderPicker/Trigger.tsx @@ -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 { isLoading: boolean; handleClearSelection?: (event: React.MouseEvent | React.KeyboardEvent) => void; @@ -28,6 +29,10 @@ function Trigger( } }; + if (isLoading) { + return ; + } + return (
@@ -43,9 +48,7 @@ function Trigger( {...rest} ref={ref} > - {isLoading ? ( - - ) : label ? ( + {label ? ( {label} ) : ( diff --git a/public/app/plugins/panel/dashlist/module.tsx b/public/app/plugins/panel/dashlist/module.tsx index 8cb3c286e16..a4033ebc7dc 100644 --- a/public/app/plugins/panel/dashlist/module.tsx +++ b/public/app/plugins/panel/dashlist/module.tsx @@ -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(DashList) defaultValue: undefined, editor: function RenderFolderPicker({ value, onChange }) { return ( - onChange(folderUID)} - /> + onChange(folderUID)} /> ); }, }) diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index b939d67c726..684a3a158b1 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -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',