Data sources: Add skeleton loader (#79016)

* refactor out DataSourcesListCard

* add skeleton

* increase gap between buttons on skeleton to match real component

* lineHeight: 1 instead of 0

* refactor out ternary
This commit is contained in:
Ashley Harrison 2023-12-13 09:50:27 +00:00 committed by GitHub
parent 5aff3389f4
commit 1efd21c08a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 151 additions and 77 deletions

View File

@ -4,16 +4,15 @@ import { useLocation } from 'react-router-dom';
import { DataSourceSettings, GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { LinkButton, Card, Tag, useStyles2 } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { contextSrv } from 'app/core/core';
import { StoreState, AccessControlAction, useSelector } from 'app/types';
import { getDataSources, getDataSourcesCount, useDataSourcesRoutes, useLoadDataSources } from '../state';
import { trackCreateDashboardClicked, trackExploreClicked, trackDataSourcesListViewed } from '../tracking';
import { constructDataSourceExploreUrl } from '../utils';
import { trackDataSourcesListViewed } from '../tracking';
import { DataSourcesListCard } from './DataSourcesListCard';
import { DataSourcesListHeader } from './DataSourcesListHeader';
export function DataSourcesList() {
@ -65,11 +64,7 @@ export function DataSourcesListView({
});
}, [location]);
if (isLoading) {
return <PageLoader />;
}
if (dataSourcesCount === 0) {
if (!isLoading && dataSourcesCount === 0) {
return (
<EmptyListCTA
buttonDisabled={!hasCreateRights}
@ -85,74 +80,31 @@ export function DataSourcesListView({
);
}
const getDataSourcesList = () => {
if (isLoading) {
return new Array(20)
.fill(null)
.map((_, index) => <DataSourcesListCard.Skeleton key={index} hasExploreRights={hasExploreRights} />);
}
return dataSources.map((dataSource) => (
<li key={dataSource.uid}>
<DataSourcesListCard
dataSource={dataSource}
hasWriteRights={hasWriteRights}
hasExploreRights={hasExploreRights}
/>
</li>
));
};
return (
<>
{/* List Header */}
<DataSourcesListHeader />
{/* List */}
<ul className={styles.list}>
{dataSources.map((dataSource) => {
const dsLink = config.appSubUrl + dataSourcesRoutes.Edit.replace(/:uid/gi, dataSource.uid);
return (
<li key={dataSource.uid}>
<Card href={hasWriteRights ? dsLink : undefined}>
<Card.Heading>{dataSource.name}</Card.Heading>
<Card.Figure>
<img src={dataSource.typeLogoUrl} alt="" height="40px" width="40px" className={styles.logo} />
</Card.Figure>
<Card.Meta>
{[
dataSource.typeName,
dataSource.url,
dataSource.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />,
]}
</Card.Meta>
<Card.Tags>
{/* Build Dashboard */}
<LinkButton
icon="apps"
fill="outline"
variant="secondary"
href={`dashboard/new-with-ds/${dataSource.uid}`}
onClick={() => {
trackCreateDashboardClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Build a dashboard
</LinkButton>
{/* Explore */}
{hasExploreRights && (
<LinkButton
icon="compass"
fill="outline"
variant="secondary"
className={styles.button}
href={constructDataSourceExploreUrl(dataSource)}
onClick={() => {
trackExploreClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Explore
</LinkButton>
)}
</Card.Tags>
</Card>
</li>
);
})}
</ul>
<ul className={styles.list}>{getDataSourcesList()}</ul>
</>
);
}
@ -164,11 +116,5 @@ const getStyles = (theme: GrafanaTheme2) => {
display: 'grid',
// gap: '8px', Add back when legacy support for old Card interface is dropped
}),
logo: css({
objectFit: 'contain',
}),
button: css({
marginLeft: theme.spacing(2),
}),
};
};

View File

@ -0,0 +1,128 @@
import { css } from '@emotion/css';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { DataSourceSettings, GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Card, LinkButton, Stack, Tag, useStyles2 } from '@grafana/ui';
import { useDataSourcesRoutes } from '../state';
import { trackCreateDashboardClicked, trackExploreClicked } from '../tracking';
import { constructDataSourceExploreUrl } from '../utils';
export interface Props {
dataSource: DataSourceSettings;
hasWriteRights: boolean;
hasExploreRights: boolean;
}
export function DataSourcesListCard({ dataSource, hasWriteRights, hasExploreRights }: Props) {
const dataSourcesRoutes = useDataSourcesRoutes();
const dsLink = config.appSubUrl + dataSourcesRoutes.Edit.replace(/:uid/gi, dataSource.uid);
const styles = useStyles2(getStyles);
return (
<Card href={hasWriteRights ? dsLink : undefined}>
<Card.Heading>{dataSource.name}</Card.Heading>
<Card.Figure>
<img src={dataSource.typeLogoUrl} alt="" height="40px" width="40px" className={styles.logo} />
</Card.Figure>
<Card.Meta>
{[
dataSource.typeName,
dataSource.url,
dataSource.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />,
]}
</Card.Meta>
<Card.Tags>
{/* Build Dashboard */}
<LinkButton
icon="apps"
fill="outline"
variant="secondary"
href={`dashboard/new-with-ds/${dataSource.uid}`}
onClick={() => {
trackCreateDashboardClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Build a dashboard
</LinkButton>
{/* Explore */}
{hasExploreRights && (
<LinkButton
icon="compass"
fill="outline"
variant="secondary"
className={styles.button}
href={constructDataSourceExploreUrl(dataSource)}
onClick={() => {
trackExploreClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Explore
</LinkButton>
)}
</Card.Tags>
</Card>
);
}
function DataSourcesListCardSkeleton({ hasExploreRights }: Pick<Props, 'hasExploreRights'>) {
const skeletonStyles = useStyles2(getSkeletonStyles);
return (
<Card>
<Card.Heading>
<Skeleton width={140} />
</Card.Heading>
<Card.Figure>
<Skeleton width={40} height={40} containerClassName={skeletonStyles.figure} />
</Card.Figure>
<Card.Meta>
<Skeleton width={120} />
</Card.Meta>
<Card.Tags>
<Stack direction="row" gap={2}>
<Skeleton height={32} width={179} containerClassName={skeletonStyles.button} />
{/* Explore */}
{hasExploreRights && <Skeleton height={32} width={107} containerClassName={skeletonStyles.button} />}
</Stack>
</Card.Tags>
</Card>
);
}
DataSourcesListCard.Skeleton = DataSourcesListCardSkeleton;
const getSkeletonStyles = () => {
return {
button: css({
lineHeight: 1,
}),
figure: css({
lineHeight: 1,
}),
};
};
const getStyles = (theme: GrafanaTheme2) => {
return {
logo: css({
objectFit: 'contain',
}),
button: css({
marginLeft: theme.spacing(2),
}),
};
};