From 6a02863cc9c3a18e10459a7e69488fbf2f87a545 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 7 Dec 2023 09:56:12 +0000 Subject: [PATCH] Library panels: Add loading skeleton (#79087) * add library panel card skeleton * lineHeight: 1 instead of 0 --- .betterer.results | 23 +-- .../LibraryPanelCard/LibraryPanelCard.tsx | 35 ++-- .../LibraryPanelsView/LibraryPanelsView.tsx | 79 +++----- .../VizTypePicker/PanelTypeCard.tsx | 185 +++++++++++------- 4 files changed, 168 insertions(+), 154 deletions(-) diff --git a/.betterer.results b/.betterer.results index 86d3ab34569..9d7bce2be57 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3942,9 +3942,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "9"], [0, 0, 0, "Styles should be written using objects.", "10"] ], - "public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -3961,14 +3958,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "7"], [0, 0, 0, "Styles should be written using objects.", "8"] ], - "public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"] - ], "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4160,17 +4149,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"] + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], "public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx index fd7c2e79b25..3955d939f05 100644 --- a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx +++ b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import React, { ReactElement, useState } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; @@ -52,6 +53,18 @@ export const LibraryPanelCard = ({ libraryPanel, onClick, onDelete, showSecondar ); }; +const LibraryPanelCardSkeleton = ({ showSecondaryActions }: Pick) => { + const styles = useStyles2(getStyles); + + return ( + + + + ); +}; + +LibraryPanelCard.Skeleton = LibraryPanelCardSkeleton; + interface FolderLinkProps { libraryPanel: LibraryElementDTO; } @@ -84,17 +97,17 @@ function FolderLink({ libraryPanel }: FolderLinkProps): ReactElement | null { function getStyles(theme: GrafanaTheme2) { return { - metaContainer: css` - display: flex; - align-items: center; - color: ${theme.colors.text.secondary}; - font-size: ${theme.typography.bodySmall.fontSize}; - padding-top: ${theme.spacing(0.5)}; + metaContainer: css({ + display: 'flex', + alignItems: 'center', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + paddingTop: theme.spacing(0.5), - svg { - margin-right: ${theme.spacing(0.5)}; - margin-bottom: 3px; - } - `, + svg: { + marginRight: theme.spacing(0.5), + marginBottom: 3, + }, + }), }; } diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx index 7cfd999aade..85123c7f7d4 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx @@ -1,9 +1,9 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React, { useMemo, useReducer } from 'react'; import { useDebounce } from 'react-use'; import { GrafanaTheme2, LoadingState } from '@grafana/data'; -import { Pagination, useStyles2 } from '@grafana/ui'; +import { Pagination, Stack, useStyles2 } from '@grafana/ui'; import { LibraryElementDTO } from '../../types'; import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; @@ -12,7 +12,6 @@ import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './a import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer'; interface LibraryPanelViewProps { - className?: string; onClickCard: (panel: LibraryElementDTO) => void; showSecondaryActions?: boolean; currentPanelId?: string; @@ -25,7 +24,6 @@ interface LibraryPanelViewProps { } export const LibraryPanelsView = ({ - className, onClickCard, searchString, sortDirection, @@ -77,24 +75,26 @@ export const LibraryPanelsView = ({ const onPageChange = (page: number) => asyncDispatch(changePage({ page })); return ( -
-
- {loadingState === LoadingState.Loading ? ( -

Loading library panels...

- ) : libraryPanels.length < 1 ? ( -

No library panels found.

- ) : ( - libraryPanels?.map((item, i) => ( - - )) - )} -
+ + {loadingState === LoadingState.Loading ? ( + <> + + + + + ) : libraryPanels.length < 1 ? ( +

No library panels found.

+ ) : ( + libraryPanels?.map((item, i) => ( + + )) + )} {libraryPanels.length ? (
) : null} -
+ ); }; const getPanelViewStyles = (theme: GrafanaTheme2) => { return { - container: css` - display: flex; - flex-direction: column; - flex-wrap: nowrap; - `, - libraryPanelList: css` - max-width: 100%; - display: grid; - grid-gap: ${theme.spacing(1)}; - `, - searchHeader: css` - display: flex; - `, - newPanelButton: css` - margin-top: 10px; - align-self: flex-start; - `, - pagination: css` - align-self: center; - margin-top: ${theme.spacing(1)}; - `, - noPanelsFound: css` - label: noPanelsFound; - min-height: 200px; - `, + pagination: css({ + alignSelf: 'center', + marginTop: theme.spacing(1), + }), + noPanelsFound: css({ + label: 'noPanelsFound', + minHeight: 200, + }), }; }; diff --git a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx index 0065c697210..17f225b33c0 100644 --- a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import React, { MouseEventHandler } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -17,6 +18,8 @@ interface Props { description?: string; } +const IMAGE_SIZE = 38; + export const PanelTypeCard = ({ isCurrent, title, @@ -74,86 +77,122 @@ export const PanelTypeCard = ({ ); }; +interface SkeletonProps { + hasDescription?: boolean; + hasDelete?: boolean; +} + +const PanelTypeCardSkeleton = ({ children, hasDescription, hasDelete }: React.PropsWithChildren) => { + const styles = useStyles2(getStyles); + const skeletonStyles = useStyles2(getSkeletonStyles); + return ( +
+ + +
+
+ +
+ {hasDescription ? : null} + {children} +
+ {hasDelete && ( + + )} +
+ ); +}; + PanelTypeCard.displayName = 'PanelTypeCard'; +PanelTypeCard.Skeleton = PanelTypeCardSkeleton; + +const getSkeletonStyles = () => { + return { + deleteButton: css({ + lineHeight: 1, + }), + image: css({ + lineHeight: 1, + }), + }; +}; const getStyles = (theme: GrafanaTheme2) => { return { - item: css` - position: relative; - display: flex; - flex-shrink: 0; - cursor: pointer; - background: ${theme.colors.background.secondary}; - border-radius: ${theme.shape.radius.default}; - box-shadow: ${theme.shadows.z1}; - border: 1px solid ${theme.colors.background.secondary}; - align-items: center; - padding: 8px; - width: 100%; - position: relative; - overflow: hidden; - transition: ${theme.transitions.create(['background'], { + item: css({ + position: 'relative', + display: 'flex', + flexShrink: 0, + cursor: 'pointer', + background: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z1, + border: `1px solid ${theme.colors.background.secondary}`, + alignItems: 'center', + padding: theme.spacing(1), + width: '100%', + overflow: 'hidden', + transition: theme.transitions.create(['background'], { duration: theme.transitions.duration.short, - })}; + }), - &:hover { - background: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)}; - } - `, - itemContent: css` - overflow: hidden; - position: relative; - padding: ${theme.spacing(0, 1)}; - `, - itemDisabled: css` - cursor: default; + '&:hover': { + background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + }, + }), + itemContent: css({ + overflow: 'hidden', + position: 'relative', + padding: theme.spacing(0, 1), + }), + itemDisabled: css({ + cursor: 'default', - &, - &:hover { - background: ${theme.colors.action.disabledBackground}; - } - `, - current: css` - label: currentVisualizationItem; - border: 1px solid ${theme.colors.primary.border}; - background: ${theme.colors.action.selected}; - `, - disabled: css` - opacity: 0.6; - filter: grayscale(1); - cursor: default; - pointer-events: none; - `, - name: css` - text-overflow: ellipsis; - overflow: hidden; - font-size: ${theme.typography.size.sm}; - font-weight: ${theme.typography.fontWeightMedium}; - width: 100%; - `, - description: css` - display: block; - text-overflow: ellipsis; - overflow: hidden; - color: ${theme.colors.text.secondary}; - font-size: ${theme.typography.bodySmall.fontSize}; - font-weight: ${theme.typography.fontWeightLight}; - width: 100%; - max-height: 4.5em; - `, - img: css` - max-height: 38px; - width: 38px; - display: flex; - align-items: center; - `, - badge: css` - background: ${theme.colors.background.primary}; - `, - deleteButton: css` - cursor: pointer; - margin-left: auto; - `, + '&, &:hover': { + background: theme.colors.action.disabledBackground, + }, + }), + current: css({ + label: 'currentVisualizationItem', + border: `1px solid ${theme.colors.primary.border}`, + background: theme.colors.action.selected, + }), + disabled: css({ + opacity: 0.6, + filter: 'grayscale(1)', + cursor: 'default', + pointerEvents: 'none', + }), + name: css({ + textOverflow: 'ellipsis', + overflow: 'hidden', + fontSize: theme.typography.size.sm, + fontWeight: theme.typography.fontWeightMedium, + width: '100%', + }), + description: css({ + display: 'block', + textOverflow: 'ellipsis', + overflow: 'hidden', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightLight, + width: '100%', + maxHeight: '4.5em', + }), + img: css({ + maxHeight: IMAGE_SIZE, + width: IMAGE_SIZE, + display: 'flex', + alignItems: 'center', + }), + badge: css({ + background: theme.colors.background.primary, + }), + deleteButton: css({ + cursor: 'pointer', + marginLeft: 'auto', + }), }; };