mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Skeleton: Abstract out attach/animation logic (#79309)
* apply styles globally to skeleton * use abstraction everywhere * just use withSkeleton * add comment * update docs * use it in News as well * rename withSkeleton to attachSkeleton * move to @grafana/ui/src/unstable * rename skeletonProps to rootProps
This commit is contained in:
parent
5f5ed3187c
commit
ffda25f4a3
@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { IconName } from '../../types';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
@ -38,19 +39,13 @@ const BadgeComponent = React.memo<BadgeProps>(({ icon, color, text, tooltip, cla
|
||||
});
|
||||
BadgeComponent.displayName = 'Badge';
|
||||
|
||||
const BadgeSkeleton = () => {
|
||||
const BadgeSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} />;
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} {...rootProps} />;
|
||||
};
|
||||
|
||||
interface BadgeWithSkeleton extends React.NamedExoticComponent<BadgeProps> {
|
||||
Skeleton: typeof BadgeSkeleton;
|
||||
}
|
||||
|
||||
export const Badge: BadgeWithSkeleton = Object.assign(BadgeComponent, { Skeleton: BadgeSkeleton });
|
||||
|
||||
Badge.Skeleton = BadgeSkeleton;
|
||||
export const Badge = attachSkeleton(BadgeComponent, BadgeSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
container: css({
|
||||
|
@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { getTagColor, getTagColorsFromName } from '../../utils';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
/**
|
||||
@ -50,18 +51,12 @@ const TagComponent = forwardRef<HTMLElement, Props>(({ name, onClick, icon, clas
|
||||
});
|
||||
TagComponent.displayName = 'Tag';
|
||||
|
||||
const TagSkeleton = () => {
|
||||
const TagSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} />;
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} {...rootProps} />;
|
||||
};
|
||||
|
||||
interface TagWithSkeleton extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLElement>> {
|
||||
Skeleton: typeof TagSkeleton;
|
||||
}
|
||||
|
||||
export const Tag: TagWithSkeleton = Object.assign(TagComponent, {
|
||||
Skeleton: TagSkeleton,
|
||||
});
|
||||
export const Tag = attachSkeleton(TagComponent, TagSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
container: css({
|
||||
|
@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
|
||||
import { OnTagClick, Tag } from './Tag';
|
||||
|
||||
@ -56,23 +57,17 @@ const TagListComponent = memo(
|
||||
);
|
||||
TagListComponent.displayName = 'TagList';
|
||||
|
||||
const TagListSkeleton = () => {
|
||||
const TagListSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} {...rootProps}>
|
||||
<Tag.Skeleton />
|
||||
<Tag.Skeleton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TagListWithSkeleton extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLUListElement>> {
|
||||
Skeleton: typeof TagListSkeleton;
|
||||
}
|
||||
|
||||
export const TagList: TagListWithSkeleton = Object.assign(TagListComponent, {
|
||||
Skeleton: TagListSkeleton,
|
||||
});
|
||||
export const TagList = attachSkeleton(TagListComponent, TagListSkeleton);
|
||||
|
||||
const getSkeletonStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
|
@ -10,6 +10,7 @@ import { getExtraStyles } from './extra';
|
||||
import { getFormElementStyles } from './forms';
|
||||
import { getMarkdownStyles } from './markdownStyles';
|
||||
import { getPageStyles } from './page';
|
||||
import { getSkeletonStyles } from './skeletonStyles';
|
||||
|
||||
/** @internal */
|
||||
export function GlobalStyles() {
|
||||
@ -25,6 +26,7 @@ export function GlobalStyles() {
|
||||
getCardStyles(theme),
|
||||
getAgularPanelStyles(theme),
|
||||
getMarkdownStyles(theme),
|
||||
getSkeletonStyles(theme),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { skeletonAnimation } from '../../utils/skeleton';
|
||||
|
||||
export const getSkeletonStyles = (theme: GrafanaTheme2) => {
|
||||
return css({
|
||||
'.react-loading-skeleton': skeletonAnimation,
|
||||
});
|
||||
};
|
@ -8,3 +8,5 @@
|
||||
* Once mature, they will be moved to the main export, be available to plugins, and
|
||||
* be subject to the standard policies
|
||||
*/
|
||||
|
||||
export * from './utils/skeleton';
|
||||
|
51
packages/grafana-ui/src/utils/skeleton.tsx
Normal file
51
packages/grafana-ui/src/utils/skeleton.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { keyframes } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
const fadeIn = keyframes({
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export const skeletonAnimation = {
|
||||
animationName: fadeIn,
|
||||
animationDelay: '100ms',
|
||||
animationTimingFunction: 'ease-in',
|
||||
animationDuration: '100ms',
|
||||
animationFillMode: 'backwards',
|
||||
};
|
||||
|
||||
interface SkeletonProps {
|
||||
/**
|
||||
* Spread these props at the root of your skeleton to handle animation logic
|
||||
*/
|
||||
rootProps: {
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
}
|
||||
|
||||
export type SkeletonComponent<P = {}> = React.ComponentType<P & SkeletonProps>;
|
||||
|
||||
/**
|
||||
* Use this to attach a skeleton as a static property on the component.
|
||||
* e.g. if you render a component with `<Component />`, you can render the skeleton with `<Component.Skeleton />`.
|
||||
* @param Component A functional or class component
|
||||
* @param Skeleton A functional or class skeleton component
|
||||
* @returns A wrapped component with a static skeleton property
|
||||
*/
|
||||
export const attachSkeleton = <C extends object, P>(Component: C, Skeleton: SkeletonComponent<P>) => {
|
||||
const skeletonWrapper = (props: P) => {
|
||||
return (
|
||||
<Skeleton
|
||||
{...props}
|
||||
rootProps={{
|
||||
style: skeletonAnimation,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return Object.assign(Component, { Skeleton: skeletonWrapper });
|
||||
};
|
@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, ConfirmModal, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, Organization } from 'app/types';
|
||||
|
||||
@ -22,7 +23,7 @@ const getTableHeader = () => (
|
||||
</thead>
|
||||
);
|
||||
|
||||
export function AdminOrgsTable({ orgs, onDelete }: Props) {
|
||||
function AdminOrgsTableComponent({ orgs, onDelete }: Props) {
|
||||
const canDeleteOrgs = contextSrv.hasPermission(AccessControlAction.OrgsDelete);
|
||||
|
||||
const [deleteOrg, setDeleteOrg] = useState<Organization>();
|
||||
@ -74,10 +75,10 @@ export function AdminOrgsTable({ orgs, onDelete }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const AdminOrgsTableSkeleton = () => {
|
||||
const AdminOrgsTableSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<table className="filter-table" {...rootProps}>
|
||||
{getTableHeader()}
|
||||
<tbody>
|
||||
{new Array(3).fill(null).map((_, index) => (
|
||||
@ -98,7 +99,7 @@ const AdminOrgsTableSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
AdminOrgsTable.Skeleton = AdminOrgsTableSkeleton;
|
||||
export const AdminOrgsTable = attachSkeleton(AdminOrgsTableComponent, AdminOrgsTableSkeleton);
|
||||
|
||||
const getSkeletonStyles = (theme: GrafanaTheme2) => ({
|
||||
deleteButton: css({
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
|
||||
import { Settings } from './AdminSettings';
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const AdminSettingsTable = ({ settings }: Props) => {
|
||||
const AdminSettingsTableComponent = ({ settings }: Props) => {
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<tbody>
|
||||
@ -33,9 +35,9 @@ export const AdminSettingsTable = ({ settings }: Props) => {
|
||||
// note: don't want to put this in render function else it will get regenerated
|
||||
const randomValues = new Array(50).fill(null).map(() => Math.random());
|
||||
|
||||
const AdminSettingsTableSkeleton = () => {
|
||||
const AdminSettingsTableSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<table className="filter-table" {...rootProps}>
|
||||
<tbody>
|
||||
{randomValues.map((randomValue, index) => {
|
||||
const isSection = index === 0 || randomValue > 0.9;
|
||||
@ -70,4 +72,4 @@ function getRandomInRange(min: number, max: number, randomSeed: number) {
|
||||
return randomSeed * (max - min) + min;
|
||||
}
|
||||
|
||||
AdminSettingsTable.Skeleton = AdminSettingsTableSkeleton;
|
||||
export const AdminSettingsTable = attachSkeleton(AdminSettingsTableComponent, AdminSettingsTableSkeleton);
|
||||
|
@ -5,6 +5,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Icon, Link, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { PanelTypeCard } from 'app/features/panel/components/VizTypePicker/PanelTypeCard';
|
||||
|
||||
@ -20,7 +21,7 @@ export interface LibraryPanelCardProps {
|
||||
|
||||
type Props = LibraryPanelCardProps & { children?: JSX.Element | JSX.Element[] };
|
||||
|
||||
export const LibraryPanelCard = ({ libraryPanel, onClick, onDelete, showSecondaryActions }: Props) => {
|
||||
const LibraryPanelCardComponent = ({ libraryPanel, onClick, onDelete, showSecondaryActions }: Props) => {
|
||||
const [showDeletionModal, setShowDeletionModal] = useState(false);
|
||||
|
||||
const onDeletePanel = () => {
|
||||
@ -53,17 +54,20 @@ export const LibraryPanelCard = ({ libraryPanel, onClick, onDelete, showSecondar
|
||||
);
|
||||
};
|
||||
|
||||
const LibraryPanelCardSkeleton = ({ showSecondaryActions }: Pick<Props, 'showSecondaryActions'>) => {
|
||||
const LibraryPanelCardSkeleton: SkeletonComponent<Pick<Props, 'showSecondaryActions'>> = ({
|
||||
showSecondaryActions,
|
||||
rootProps,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<PanelTypeCard.Skeleton hasDelete={showSecondaryActions}>
|
||||
<PanelTypeCard.Skeleton hasDelete={showSecondaryActions} {...rootProps}>
|
||||
<Skeleton containerClassName={styles.metaContainer} width={80} />
|
||||
</PanelTypeCard.Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
LibraryPanelCard.Skeleton = LibraryPanelCardSkeleton;
|
||||
export const LibraryPanelCard = attachSkeleton(LibraryPanelCardComponent, LibraryPanelCardSkeleton);
|
||||
|
||||
interface FolderLinkProps {
|
||||
libraryPanel: LibraryElementDTO;
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { Button, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv';
|
||||
|
||||
@ -11,7 +12,7 @@ export interface Props {
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
export const SnapshotListTableRow = ({ snapshot, onRemove }: Props) => {
|
||||
const SnapshotListTableRowComponent = ({ snapshot, onRemove }: Props) => {
|
||||
const url = snapshot.externalUrl || snapshot.url;
|
||||
return (
|
||||
<tr>
|
||||
@ -40,10 +41,10 @@ export const SnapshotListTableRow = ({ snapshot, onRemove }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SnapshotListTableRowSkeleton = () => {
|
||||
const SnapshotListTableRowSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<tr>
|
||||
<tr {...rootProps}>
|
||||
<td>
|
||||
<Skeleton width={80} />
|
||||
</td>
|
||||
@ -61,7 +62,7 @@ const SnapshotListTableRowSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
SnapshotListTableRow.Skeleton = SnapshotListTableRowSkeleton;
|
||||
export const SnapshotListTableRow = attachSkeleton(SnapshotListTableRowComponent, SnapshotListTableRowSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
blockSkeleton: css({
|
||||
|
@ -5,6 +5,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
|
||||
interface Props {
|
||||
@ -20,7 +21,7 @@ interface Props {
|
||||
|
||||
const IMAGE_SIZE = 38;
|
||||
|
||||
export const PanelTypeCard = ({
|
||||
const PanelTypeCardComponent = ({
|
||||
isCurrent,
|
||||
title,
|
||||
plugin,
|
||||
@ -76,17 +77,23 @@ export const PanelTypeCard = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
PanelTypeCardComponent.displayName = 'PanelTypeCard';
|
||||
|
||||
interface SkeletonProps {
|
||||
hasDescription?: boolean;
|
||||
hasDelete?: boolean;
|
||||
}
|
||||
|
||||
const PanelTypeCardSkeleton = ({ children, hasDescription, hasDelete }: React.PropsWithChildren<SkeletonProps>) => {
|
||||
const PanelTypeCardSkeleton: SkeletonComponent<React.PropsWithChildren<SkeletonProps>> = ({
|
||||
children,
|
||||
hasDescription,
|
||||
hasDelete,
|
||||
rootProps,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const skeletonStyles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.item} {...rootProps}>
|
||||
<Skeleton className={cx(styles.img, skeletonStyles.image)} width={IMAGE_SIZE} height={IMAGE_SIZE} />
|
||||
|
||||
<div className={styles.itemContent}>
|
||||
@ -103,8 +110,7 @@ const PanelTypeCardSkeleton = ({ children, hasDescription, hasDelete }: React.Pr
|
||||
);
|
||||
};
|
||||
|
||||
PanelTypeCard.displayName = 'PanelTypeCard';
|
||||
PanelTypeCard.Skeleton = PanelTypeCardSkeleton;
|
||||
export const PanelTypeCard = attachSkeleton(PanelTypeCardComponent, PanelTypeCardSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => {
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Card, LinkButton, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { attachSkeleton, SkeletonComponent } from '@grafana/ui/src/unstable';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
@ -17,7 +18,7 @@ interface Props {
|
||||
playlist: Playlist;
|
||||
}
|
||||
|
||||
export const PlaylistCard = ({ playlist, setStartPlaylist, setPlaylistToDelete }: Props) => {
|
||||
const PlaylistCardComponent = ({ playlist, setStartPlaylist, setPlaylistToDelete }: Props) => {
|
||||
return (
|
||||
<Card>
|
||||
<Card.Heading>
|
||||
@ -62,10 +63,10 @@ export const PlaylistCard = ({ playlist, setStartPlaylist, setPlaylistToDelete }
|
||||
);
|
||||
};
|
||||
|
||||
const PlaylistCardSkeleton = () => {
|
||||
const PlaylistCardSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const skeletonStyles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<Card>
|
||||
<Card {...rootProps}>
|
||||
<Card.Heading>
|
||||
<Skeleton width={140} />
|
||||
</Card.Heading>
|
||||
@ -84,7 +85,7 @@ const PlaylistCardSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
PlaylistCard.Skeleton = PlaylistCardSkeleton;
|
||||
export const PlaylistCard = attachSkeleton(PlaylistCardComponent, PlaylistCardSkeleton);
|
||||
|
||||
function getSkeletonStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
|
||||
import { PlaylistCard } from './PlaylistCard';
|
||||
import { Playlist } from './types';
|
||||
@ -13,7 +14,7 @@ interface Props {
|
||||
playlists: Playlist[];
|
||||
}
|
||||
|
||||
export const PlaylistPageList = ({ playlists, setStartPlaylist, setPlaylistToDelete }: Props) => {
|
||||
const PlaylistPageListComponent = ({ playlists, setStartPlaylist, setPlaylistToDelete }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
@ -30,10 +31,10 @@ export const PlaylistPageList = ({ playlists, setStartPlaylist, setPlaylistToDel
|
||||
);
|
||||
};
|
||||
|
||||
const PlaylistPageListSkeleton = () => {
|
||||
const PlaylistPageListSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div data-testid="playlist-page-list-skeleton" className={styles.list}>
|
||||
<div data-testid="playlist-page-list-skeleton" className={styles.list} {...rootProps}>
|
||||
<PlaylistCard.Skeleton />
|
||||
<PlaylistCard.Skeleton />
|
||||
<PlaylistCard.Skeleton />
|
||||
@ -41,7 +42,7 @@ const PlaylistPageListSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
PlaylistPageList.Skeleton = PlaylistPageListSkeleton;
|
||||
export const PlaylistPageList = attachSkeleton(PlaylistPageListComponent, PlaylistPageListSkeleton);
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
|
||||
import { CatalogPlugin, PluginIconName, PluginListDisplayMode } from '../types';
|
||||
|
||||
@ -18,7 +19,7 @@ type Props = {
|
||||
displayMode?: PluginListDisplayMode;
|
||||
};
|
||||
|
||||
export function PluginListItem({ plugin, pathName, displayMode = PluginListDisplayMode.Grid }: Props) {
|
||||
function PluginListItemComponent({ plugin, pathName, displayMode = PluginListDisplayMode.Grid }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isList = displayMode === PluginListDisplayMode.List;
|
||||
|
||||
@ -37,12 +38,15 @@ export function PluginListItem({ plugin, pathName, displayMode = PluginListDispl
|
||||
);
|
||||
}
|
||||
|
||||
const PluginListItemSkeleton = ({ displayMode = PluginListDisplayMode.Grid }: Pick<Props, 'displayMode'>) => {
|
||||
const PluginListItemSkeleton: SkeletonComponent<Pick<Props, 'displayMode'>> = ({
|
||||
displayMode = PluginListDisplayMode.Grid,
|
||||
rootProps,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isList = displayMode === PluginListDisplayMode.List;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, { [styles.list]: isList })}>
|
||||
<div className={cx(styles.container, { [styles.list]: isList })} {...rootProps}>
|
||||
<Skeleton
|
||||
containerClassName={cx(
|
||||
styles.pluginLogo,
|
||||
@ -72,7 +76,7 @@ const PluginListItemSkeleton = ({ displayMode = PluginListDisplayMode.Grid }: Pi
|
||||
);
|
||||
};
|
||||
|
||||
PluginListItem.Skeleton = PluginListItemSkeleton;
|
||||
export const PluginListItem = attachSkeleton(PluginListItemComponent, PluginListItemSkeleton);
|
||||
|
||||
// Styles shared between the different type of list items
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
|
@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||
import { Button, Icon, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { OrgRolePicker } from 'app/features/admin/OrgRolePicker';
|
||||
@ -162,11 +163,11 @@ const ServiceAccountListItemComponent = memo(
|
||||
);
|
||||
ServiceAccountListItemComponent.displayName = 'ServiceAccountListItem';
|
||||
|
||||
const ServiceAccountsListItemSkeleton = () => {
|
||||
const ServiceAccountsListItemSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<tr {...rootProps}>
|
||||
<td className="width-4 text-center">
|
||||
<Skeleton containerClassName={styles.blockSkeleton} circle width={25} height={25} />
|
||||
</td>
|
||||
@ -193,12 +194,7 @@ const ServiceAccountsListItemSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface ServiceAccountsListItemWithSkeleton extends React.NamedExoticComponent<ServiceAccountListItemProps> {
|
||||
Skeleton: typeof ServiceAccountsListItemSkeleton;
|
||||
}
|
||||
const ServiceAccountListItem: ServiceAccountsListItemWithSkeleton = Object.assign(ServiceAccountListItemComponent, {
|
||||
Skeleton: ServiceAccountsListItemSkeleton,
|
||||
});
|
||||
const ServiceAccountListItem = attachSkeleton(ServiceAccountListItemComponent, ServiceAccountsListItemSkeleton);
|
||||
|
||||
const getSkeletonStyles = (theme: GrafanaTheme2) => ({
|
||||
blockSkeleton: css({
|
||||
|
@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { DataFrameView, GrafanaTheme2, textUtil, dateTimeFormat } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { attachSkeleton, SkeletonComponent } from '@grafana/ui/src/unstable';
|
||||
|
||||
import { NewsItem } from '../types';
|
||||
|
||||
@ -14,7 +15,7 @@ interface NewsItemProps {
|
||||
data: DataFrameView<NewsItem>;
|
||||
}
|
||||
|
||||
export function News({ width, showImage, data, index }: NewsItemProps) {
|
||||
function NewsComponent({ width, showImage, data, index }: NewsItemProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const useWideLayout = width > 600;
|
||||
const newsItem = data.get(index);
|
||||
@ -46,12 +47,16 @@ export function News({ width, showImage, data, index }: NewsItemProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const NewsSkeleton = ({ width, showImage }: Pick<NewsItemProps, 'width' | 'showImage'>) => {
|
||||
const NewsSkeleton: SkeletonComponent<Pick<NewsItemProps, 'width' | 'showImage'>> = ({
|
||||
width,
|
||||
showImage,
|
||||
rootProps,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const useWideLayout = width > 600;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.item, useWideLayout && styles.itemWide)}>
|
||||
<div className={cx(styles.item, useWideLayout && styles.itemWide)} {...rootProps}>
|
||||
{showImage && (
|
||||
<Skeleton
|
||||
containerClassName={cx(styles.socialImage, useWideLayout && styles.socialImageWide)}
|
||||
@ -68,7 +73,7 @@ const NewsSkeleton = ({ width, showImage }: Pick<NewsItemProps, 'width' | 'showI
|
||||
);
|
||||
};
|
||||
|
||||
News.Skeleton = NewsSkeleton;
|
||||
export const News = attachSkeleton(NewsComponent, NewsSkeleton);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
|
Loading…
Reference in New Issue
Block a user