mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PluginList: Add plugin list loading skeleton (#79012)
* add plugin skeleton * lineHeight: 1 instead of 0
This commit is contained in:
parent
5efa85e634
commit
7cdddb2790
@ -4258,14 +4258,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginListItem.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/plugins/admin/components/PluginSignatureDetailsBadge.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
@ -4292,11 +4284,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/plugins/admin/pages/Browse.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "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, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/plugins/admin/state/actions.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@ -18,7 +19,7 @@ export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const Badge = React.memo<BadgeProps>(({ icon, color, text, tooltip, className, ...otherProps }) => {
|
||||
const BadgeComponent = React.memo<BadgeProps>(({ icon, color, text, tooltip, className, ...otherProps }) => {
|
||||
const styles = useStyles2(getStyles, color);
|
||||
const badge = (
|
||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||
@ -35,8 +36,27 @@ export const Badge = React.memo<BadgeProps>(({ icon, color, text, tooltip, class
|
||||
badge
|
||||
);
|
||||
});
|
||||
BadgeComponent.displayName = 'Badge';
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
const BadgeSkeleton = () => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} />;
|
||||
};
|
||||
|
||||
interface BadgeWithSkeleton extends React.NamedExoticComponent<BadgeProps> {
|
||||
Skeleton: typeof BadgeSkeleton;
|
||||
}
|
||||
|
||||
export const Badge: BadgeWithSkeleton = Object.assign(BadgeComponent, { Skeleton: BadgeSkeleton });
|
||||
|
||||
Badge.Skeleton = BadgeSkeleton;
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
container: css({
|
||||
lineHeight: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, color: BadgeColor) => {
|
||||
let sourceColor = theme.visualization.getColorByName(color);
|
||||
|
@ -11,18 +11,21 @@ import { PluginListItem } from './PluginListItem';
|
||||
interface Props {
|
||||
plugins: CatalogPlugin[];
|
||||
displayMode: PluginListDisplayMode;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const PluginList = ({ plugins, displayMode }: Props) => {
|
||||
export const PluginList = ({ plugins, displayMode, isLoading }: Props) => {
|
||||
const isList = displayMode === PluginListDisplayMode.List;
|
||||
const { pathname } = useLocation();
|
||||
const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname);
|
||||
|
||||
return (
|
||||
<Grid gap={3} {...(isList ? { columns: 1 } : { minColumnWidth: 34 })} data-testid="plugin-list">
|
||||
{plugins.map((plugin) => (
|
||||
<PluginListItem key={plugin.id} plugin={plugin} pathName={pathName} displayMode={displayMode} />
|
||||
))}
|
||||
{isLoading
|
||||
? new Array(50).fill(null).map((_, index) => <PluginListItem.Skeleton key={index} displayMode={displayMode} />)
|
||||
: plugins.map((plugin) => (
|
||||
<PluginListItem key={plugin.id} plugin={plugin} pathName={pathName} displayMode={displayMode} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
import { Badge, Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CatalogPlugin, PluginIconName, PluginListDisplayMode } from '../types';
|
||||
|
||||
@ -36,67 +37,104 @@ export function PluginListItem({ plugin, pathName, displayMode = PluginListDispl
|
||||
);
|
||||
}
|
||||
|
||||
const PluginListItemSkeleton = ({ displayMode = PluginListDisplayMode.Grid }: Pick<Props, 'displayMode'>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isList = displayMode === PluginListDisplayMode.List;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, { [styles.list]: isList })}>
|
||||
<Skeleton
|
||||
containerClassName={cx(
|
||||
styles.pluginLogo,
|
||||
css({
|
||||
lineHeight: 1,
|
||||
})
|
||||
)}
|
||||
width={LOGO_SIZE}
|
||||
height={LOGO_SIZE}
|
||||
/>
|
||||
<h2 className={styles.name}>
|
||||
<Skeleton width={100} />
|
||||
</h2>
|
||||
<div className={styles.content}>
|
||||
<p>
|
||||
<Skeleton width={120} />
|
||||
</p>
|
||||
<Stack direction="row">
|
||||
<Badge.Skeleton />
|
||||
<Badge.Skeleton />
|
||||
</Stack>
|
||||
</div>
|
||||
<div className={styles.pluginType}>
|
||||
<Skeleton width={16} height={16} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PluginListItem.Skeleton = PluginListItemSkeleton;
|
||||
|
||||
// Styles shared between the different type of list items
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
display: grid;
|
||||
grid-template-columns: ${LOGO_SIZE} 1fr ${theme.spacing(3)};
|
||||
grid-template-rows: auto;
|
||||
gap: ${theme.spacing(2)};
|
||||
grid-auto-flow: row;
|
||||
background: ${theme.colors.background.secondary};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
padding: ${theme.spacing(3)};
|
||||
transition: ${theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
container: css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `${LOGO_SIZE} 1fr ${theme.spacing(3)}`,
|
||||
gridTemplateRows: 'auto',
|
||||
gap: theme.spacing(2),
|
||||
gridAutoFlow: 'row',
|
||||
background: theme.colors.background.secondary,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
padding: theme.spacing(3),
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
})};
|
||||
}),
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)};
|
||||
}
|
||||
`,
|
||||
list: css`
|
||||
row-gap: 0px;
|
||||
'&:hover': {
|
||||
background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
|
||||
},
|
||||
}),
|
||||
list: css({
|
||||
rowGap: 0,
|
||||
|
||||
> img {
|
||||
align-self: start;
|
||||
}
|
||||
'> img': {
|
||||
alignSelf: 'start',
|
||||
},
|
||||
|
||||
> .plugin-content {
|
||||
min-height: 0px;
|
||||
grid-area: 2 / 2 / 4 / 3;
|
||||
'> .plugin-content': {
|
||||
minHeight: 0,
|
||||
gridArea: '2 / 2 / 4 / 3',
|
||||
|
||||
> p {
|
||||
margin: ${theme.spacing(0, 0, 0.5, 0)};
|
||||
}
|
||||
}
|
||||
'> p': {
|
||||
margin: theme.spacing(0, 0, 0.5, 0),
|
||||
},
|
||||
},
|
||||
|
||||
> .plugin-name {
|
||||
align-self: center;
|
||||
grid-area: 1 / 2 / 2 / 3;
|
||||
}
|
||||
`,
|
||||
pluginType: css`
|
||||
grid-area: 1 / 3 / 2 / 4;
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
pluginLogo: css`
|
||||
grid-area: 1 / 1 / 3 / 2;
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
object-fit: contain;
|
||||
`,
|
||||
content: css`
|
||||
grid-area: 3 / 1 / 4 / 3;
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
name: css`
|
||||
grid-area: 1 / 2 / 3 / 3;
|
||||
align-self: center;
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
color: ${theme.colors.text.primary};
|
||||
margin: 0;
|
||||
`,
|
||||
'> .plugin-name': {
|
||||
alignSelf: 'center',
|
||||
gridArea: '1 / 2 / 2 / 3',
|
||||
},
|
||||
}),
|
||||
pluginType: css({
|
||||
gridArea: '1 / 3 / 2 / 4',
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
pluginLogo: css({
|
||||
gridArea: '1 / 1 / 3 / 2',
|
||||
maxWidth: '100%',
|
||||
alignSelf: 'center',
|
||||
objectFit: 'contain',
|
||||
}),
|
||||
content: css({
|
||||
gridArea: '3 / 1 / 4 / 3',
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
name: css({
|
||||
gridArea: '1 / 2 / 3 / 3',
|
||||
alignSelf: 'center',
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
color: theme.colors.text.primary,
|
||||
margin: 0,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -8,6 +8,5 @@ type PluginLogoProps = {
|
||||
};
|
||||
|
||||
export function PluginLogo({ alt, className, src, height }: PluginLogoProps): React.ReactElement {
|
||||
// @ts-ignore - react doesn't know about loading attr.
|
||||
return <img src={src} className={className} alt={alt} loading="lazy" height={height} />;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { SelectableValue, GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { locationSearchToObject } from '@grafana/runtime';
|
||||
import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui';
|
||||
import { Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
@ -159,16 +159,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<div className={styles.listWrap}>
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder
|
||||
className={css`
|
||||
margin-bottom: 0;
|
||||
`}
|
||||
text="Loading results"
|
||||
/>
|
||||
) : (
|
||||
<PluginList plugins={plugins} displayMode={displayMode} />
|
||||
)}
|
||||
<PluginList plugins={plugins} displayMode={displayMode} isLoading={isLoading} />
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
@ -176,17 +167,17 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
actionBar: css`
|
||||
${theme.breakpoints.up('xl')} {
|
||||
margin-left: auto;
|
||||
}
|
||||
`,
|
||||
listWrap: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
displayAs: css`
|
||||
svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
`,
|
||||
actionBar: css({
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
}),
|
||||
listWrap: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
displayAs: css({
|
||||
svg: {
|
||||
marginRight: 0,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user