mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Library panels: Add loading skeleton (#79087)
* add library panel card skeleton * lineHeight: 1 instead of 0
This commit is contained in:
parent
f6bd390bc1
commit
6a02863cc9
@ -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"],
|
||||
|
@ -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<Props, 'showSecondaryActions'>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<PanelTypeCard.Skeleton hasDelete={showSecondaryActions}>
|
||||
<Skeleton containerClassName={styles.metaContainer} width={80} />
|
||||
</PanelTypeCard.Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className={cx(styles.container, className)}>
|
||||
<div className={styles.libraryPanelList}>
|
||||
{loadingState === LoadingState.Loading ? (
|
||||
<p>Loading library panels...</p>
|
||||
) : libraryPanels.length < 1 ? (
|
||||
<p className={styles.noPanelsFound}>No library panels found.</p>
|
||||
) : (
|
||||
libraryPanels?.map((item, i) => (
|
||||
<LibraryPanelCard
|
||||
key={`library-panel=${i}`}
|
||||
libraryPanel={item}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
showSecondaryActions={showSecondaryActions}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<Stack direction="column" wrap="nowrap">
|
||||
{loadingState === LoadingState.Loading ? (
|
||||
<>
|
||||
<LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
|
||||
<LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
|
||||
<LibraryPanelCard.Skeleton showSecondaryActions={showSecondaryActions} />
|
||||
</>
|
||||
) : libraryPanels.length < 1 ? (
|
||||
<p className={styles.noPanelsFound}>No library panels found.</p>
|
||||
) : (
|
||||
libraryPanels?.map((item, i) => (
|
||||
<LibraryPanelCard
|
||||
key={`library-panel=${i}`}
|
||||
libraryPanel={item}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
showSecondaryActions={showSecondaryActions}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{libraryPanels.length ? (
|
||||
<div className={styles.pagination}>
|
||||
<Pagination
|
||||
@ -105,36 +105,19 @@ export const LibraryPanelsView = ({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -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<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.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',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user