Library panels: Add loading skeleton (#79087)

* add library panel card skeleton

* lineHeight: 1 instead of 0
This commit is contained in:
Ashley Harrison 2023-12-07 09:56:12 +00:00 committed by GitHub
parent f6bd390bc1
commit 6a02863cc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 154 deletions

View File

@ -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.", "9"],
[0, 0, 0, "Styles should be written using objects.", "10"] [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": [ "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.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [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.", "7"],
[0, 0, 0, "Styles should be written using objects.", "8"] [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": [ "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [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"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
"public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx:5381": [ "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, "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"]
], ],
"public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx:5381": [ "public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],

View File

@ -1,5 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -52,6 +53,18 @@ export const LibraryPanelCard = ({ libraryPanel, onClick, onDelete, showSecondar
); );
}; };
const LibraryPanelCardSkeleton = ({ showSecondaryActions }: Pick<Props, 'showSecondaryActions'>) => {
const styles = useStyles2(getStyles);
return (
<PanelTypeCard.Skeleton hasDelete={showSecondaryActions}>
<Skeleton containerClassName={styles.metaContainer} width={80} />
</PanelTypeCard.Skeleton>
);
};
LibraryPanelCard.Skeleton = LibraryPanelCardSkeleton;
interface FolderLinkProps { interface FolderLinkProps {
libraryPanel: LibraryElementDTO; libraryPanel: LibraryElementDTO;
} }
@ -84,17 +97,17 @@ function FolderLink({ libraryPanel }: FolderLinkProps): ReactElement | null {
function getStyles(theme: GrafanaTheme2) { function getStyles(theme: GrafanaTheme2) {
return { return {
metaContainer: css` metaContainer: css({
display: flex; display: 'flex',
align-items: center; alignItems: 'center',
color: ${theme.colors.text.secondary}; color: theme.colors.text.secondary,
font-size: ${theme.typography.bodySmall.fontSize}; fontSize: theme.typography.bodySmall.fontSize,
padding-top: ${theme.spacing(0.5)}; paddingTop: theme.spacing(0.5),
svg { svg: {
margin-right: ${theme.spacing(0.5)}; marginRight: theme.spacing(0.5),
margin-bottom: 3px; marginBottom: 3,
} },
`, }),
}; };
} }

View File

@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useMemo, useReducer } from 'react'; import React, { useMemo, useReducer } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { GrafanaTheme2, LoadingState } from '@grafana/data'; import { GrafanaTheme2, LoadingState } from '@grafana/data';
import { Pagination, useStyles2 } from '@grafana/ui'; import { Pagination, Stack, useStyles2 } from '@grafana/ui';
import { LibraryElementDTO } from '../../types'; import { LibraryElementDTO } from '../../types';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
@ -12,7 +12,6 @@ import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './a
import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer'; import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer';
interface LibraryPanelViewProps { interface LibraryPanelViewProps {
className?: string;
onClickCard: (panel: LibraryElementDTO) => void; onClickCard: (panel: LibraryElementDTO) => void;
showSecondaryActions?: boolean; showSecondaryActions?: boolean;
currentPanelId?: string; currentPanelId?: string;
@ -25,7 +24,6 @@ interface LibraryPanelViewProps {
} }
export const LibraryPanelsView = ({ export const LibraryPanelsView = ({
className,
onClickCard, onClickCard,
searchString, searchString,
sortDirection, sortDirection,
@ -77,24 +75,26 @@ export const LibraryPanelsView = ({
const onPageChange = (page: number) => asyncDispatch(changePage({ page })); const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
return ( return (
<div className={cx(styles.container, className)}> <Stack direction="column" wrap="nowrap">
<div className={styles.libraryPanelList}> {loadingState === LoadingState.Loading ? (
{loadingState === LoadingState.Loading ? ( <>
<p>Loading library panels...</p> <LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
) : libraryPanels.length < 1 ? ( <LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
<p className={styles.noPanelsFound}>No library panels found.</p> <LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
) : ( </>
libraryPanels?.map((item, i) => ( ) : libraryPanels.length < 1 ? (
<LibraryPanelCard <p className={styles.noPanelsFound}>No library panels found.</p>
key={`library-panel=${i}`} ) : (
libraryPanel={item} libraryPanels?.map((item, i) => (
onDelete={onDelete} <LibraryPanelCard
onClick={onClickCard} key={`library-panel=${i}`}
showSecondaryActions={showSecondaryActions} libraryPanel={item}
/> onDelete={onDelete}
)) onClick={onClickCard}
)} showSecondaryActions={showSecondaryActions}
</div> />
))
)}
{libraryPanels.length ? ( {libraryPanels.length ? (
<div className={styles.pagination}> <div className={styles.pagination}>
<Pagination <Pagination
@ -105,36 +105,19 @@ export const LibraryPanelsView = ({
/> />
</div> </div>
) : null} ) : null}
</div> </Stack>
); );
}; };
const getPanelViewStyles = (theme: GrafanaTheme2) => { const getPanelViewStyles = (theme: GrafanaTheme2) => {
return { return {
container: css` pagination: css({
display: flex; alignSelf: 'center',
flex-direction: column; marginTop: theme.spacing(1),
flex-wrap: nowrap; }),
`, noPanelsFound: css({
libraryPanelList: css` label: 'noPanelsFound',
max-width: 100%; minHeight: 200,
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;
`,
}; };
}; };

View File

@ -1,5 +1,6 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { MouseEventHandler } from 'react'; import React, { MouseEventHandler } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data'; import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -17,6 +18,8 @@ interface Props {
description?: string; description?: string;
} }
const IMAGE_SIZE = 38;
export const PanelTypeCard = ({ export const PanelTypeCard = ({
isCurrent, isCurrent,
title, title,
@ -74,86 +77,122 @@ export const PanelTypeCard = ({
); );
}; };
interface SkeletonProps {
hasDescription?: boolean;
hasDelete?: boolean;
}
const PanelTypeCardSkeleton = ({ children, hasDescription, hasDelete }: React.PropsWithChildren<SkeletonProps>) => {
const styles = useStyles2(getStyles);
const skeletonStyles = useStyles2(getSkeletonStyles);
return (
<div className={styles.item}>
<Skeleton className={cx(styles.img, skeletonStyles.image)} width={IMAGE_SIZE} height={IMAGE_SIZE} />
<div className={styles.itemContent}>
<div className={styles.name}>
<Skeleton width={160} />
</div>
{hasDescription ? <Skeleton containerClassName={styles.description} width={80} /> : null}
{children}
</div>
{hasDelete && (
<Skeleton containerClassName={cx(styles.deleteButton, skeletonStyles.deleteButton)} width={16} height={16} />
)}
</div>
);
};
PanelTypeCard.displayName = 'PanelTypeCard'; PanelTypeCard.displayName = 'PanelTypeCard';
PanelTypeCard.Skeleton = PanelTypeCardSkeleton;
const getSkeletonStyles = () => {
return {
deleteButton: css({
lineHeight: 1,
}),
image: css({
lineHeight: 1,
}),
};
};
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
item: css` item: css({
position: relative; position: 'relative',
display: flex; display: 'flex',
flex-shrink: 0; flexShrink: 0,
cursor: pointer; cursor: 'pointer',
background: ${theme.colors.background.secondary}; background: theme.colors.background.secondary,
border-radius: ${theme.shape.radius.default}; borderRadius: theme.shape.radius.default,
box-shadow: ${theme.shadows.z1}; boxShadow: theme.shadows.z1,
border: 1px solid ${theme.colors.background.secondary}; border: `1px solid ${theme.colors.background.secondary}`,
align-items: center; alignItems: 'center',
padding: 8px; padding: theme.spacing(1),
width: 100%; width: '100%',
position: relative; overflow: 'hidden',
overflow: hidden; transition: theme.transitions.create(['background'], {
transition: ${theme.transitions.create(['background'], {
duration: theme.transitions.duration.short, duration: theme.transitions.duration.short,
})}; }),
&:hover { '&:hover': {
background: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)}; background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
} },
`, }),
itemContent: css` itemContent: css({
overflow: hidden; overflow: 'hidden',
position: relative; position: 'relative',
padding: ${theme.spacing(0, 1)}; padding: theme.spacing(0, 1),
`, }),
itemDisabled: css` itemDisabled: css({
cursor: default; cursor: 'default',
&, '&, &:hover': {
&:hover { background: theme.colors.action.disabledBackground,
background: ${theme.colors.action.disabledBackground}; },
} }),
`, current: css({
current: css` label: 'currentVisualizationItem',
label: currentVisualizationItem; border: `1px solid ${theme.colors.primary.border}`,
border: 1px solid ${theme.colors.primary.border}; background: theme.colors.action.selected,
background: ${theme.colors.action.selected}; }),
`, disabled: css({
disabled: css` opacity: 0.6,
opacity: 0.6; filter: 'grayscale(1)',
filter: grayscale(1); cursor: 'default',
cursor: default; pointerEvents: 'none',
pointer-events: none; }),
`, name: css({
name: css` textOverflow: 'ellipsis',
text-overflow: ellipsis; overflow: 'hidden',
overflow: hidden; fontSize: theme.typography.size.sm,
font-size: ${theme.typography.size.sm}; fontWeight: theme.typography.fontWeightMedium,
font-weight: ${theme.typography.fontWeightMedium}; width: '100%',
width: 100%; }),
`, description: css({
description: css` display: 'block',
display: block; textOverflow: 'ellipsis',
text-overflow: ellipsis; overflow: 'hidden',
overflow: hidden; color: theme.colors.text.secondary,
color: ${theme.colors.text.secondary}; fontSize: theme.typography.bodySmall.fontSize,
font-size: ${theme.typography.bodySmall.fontSize}; fontWeight: theme.typography.fontWeightLight,
font-weight: ${theme.typography.fontWeightLight}; width: '100%',
width: 100%; maxHeight: '4.5em',
max-height: 4.5em; }),
`, img: css({
img: css` maxHeight: IMAGE_SIZE,
max-height: 38px; width: IMAGE_SIZE,
width: 38px; display: 'flex',
display: flex; alignItems: 'center',
align-items: center; }),
`, badge: css({
badge: css` background: theme.colors.background.primary,
background: ${theme.colors.background.primary}; }),
`, deleteButton: css({
deleteButton: css` cursor: 'pointer',
cursor: pointer; marginLeft: 'auto',
margin-left: auto; }),
`,
}; };
}; };