diff --git a/.betterer.results b/.betterer.results index 9648cb3d1e3..96fc9868c1e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index d8cb863dea7..e05b9056517 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -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 { tooltip?: string; } -export const Badge = React.memo(({ icon, color, text, tooltip, className, ...otherProps }) => { +const BadgeComponent = React.memo(({ icon, color, text, tooltip, className, ...otherProps }) => { const styles = useStyles2(getStyles, color); const badge = (
@@ -35,8 +36,27 @@ export const Badge = React.memo(({ icon, color, text, tooltip, class badge ); }); +BadgeComponent.displayName = 'Badge'; -Badge.displayName = 'Badge'; +const BadgeSkeleton = () => { + const styles = useStyles2(getSkeletonStyles); + + return ; +}; + +interface BadgeWithSkeleton extends React.NamedExoticComponent { + 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); diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index 428b2e2fffe..bbe2c567cc0 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -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 ( - {plugins.map((plugin) => ( - - ))} + {isLoading + ? new Array(50).fill(null).map((_, index) => ) + : plugins.map((plugin) => ( + + ))} ); }; diff --git a/public/app/features/plugins/admin/components/PluginListItem.tsx b/public/app/features/plugins/admin/components/PluginListItem.tsx index 31de4c21133..57434ed32a0 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.tsx @@ -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) => { + const styles = useStyles2(getStyles); + const isList = displayMode === PluginListDisplayMode.List; + + return ( +
+ +

+ +

+
+

+ +

+ + + + +
+
+ +
+
+ ); +}; + +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, + }), }; }; diff --git a/public/app/features/plugins/admin/components/PluginLogo.tsx b/public/app/features/plugins/admin/components/PluginLogo.tsx index 7c864020f34..2c063bd97d1 100644 --- a/public/app/features/plugins/admin/components/PluginLogo.tsx +++ b/public/app/features/plugins/admin/components/PluginLogo.tsx @@ -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 {alt}; } diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 7208cc7d8dc..9ec3179e834 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -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
- {isLoading ? ( - - ) : ( - - )} +
@@ -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, + }, + }), });