DashListItem: Added DashListItem shared component (#115384)

* DashListItem: Add DashListItem shared component and shared with DashList and RecentlyViewedDashboards
This commit is contained in:
Yunwen Zheng
2025-12-17 09:40:04 -05:00
committed by GitHub
parent 8a160a8ca1
commit 3672d9c41d
5 changed files with 168 additions and 53 deletions
@@ -4,7 +4,9 @@ import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { CollapsableSection, Link, Spinner, Text, useStyles2 } from '@grafana/ui';
import { CollapsableSection, Grid, Spinner, Text, useStyles2 } from '@grafana/ui';
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
import { DashListItem } from 'app/plugins/panel/dashlist/DashListItem';
import { getRecentlyViewedDashboards } from './utils';
@@ -19,6 +21,7 @@ export function RecentlyViewedDashboards() {
}
return getRecentlyViewedDashboards(MAX_RECENT);
}, []);
const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0);
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
return null;
@@ -43,35 +46,53 @@ export function RecentlyViewedDashboards() {
<Text>{t('browse-dashboards.recently-viewed.empty', 'Nothing viewed yet')}</Text>
)}
{/* TODO: implement actual card content */}
{!loading && recentDashboards.length > 0 && (
<>
{recentDashboards.map((dash) => (
<div key={dash.uid}>
<Link href={dash.url}>{dash.name}</Link>
</div>
))}
</>
<ul className={styles.list}>
<Grid columns={{ xs: 1, sm: 2, md: 3, lg: 5 }} gap={2}>
{recentDashboards.map((dash) => (
<li key={dash.uid} className={styles.listItem}>
<DashListItem
key={dash.uid}
dashboard={dash}
url={dash.url}
showFolderNames={true}
locationInfo={foldersByUid[dash.location]}
layoutMode="card"
/>
</li>
))}
</Grid>
</ul>
)}
</CollapsableSection>
);
}
const getStyles = (theme: GrafanaTheme2) => {
const accent = theme.visualization.getColorByName('purple'); // or your own hex
return {
title: css({
background: `linear-gradient(90deg, ${accent} 0%, #e478eaff 100%)`,
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
color: 'transparent',
'& button svg': {
color: accent,
color: theme.colors.primary.text,
},
h3: {
background: `linear-gradient(90deg, ${theme.colors.primary.text} 0%, ${theme.colors.secondary.text} 100%)`,
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
color: 'transparent',
},
}),
content: css({
paddingTop: theme.spacing(0),
}),
list: css({
listStyle: 'none',
margin: 0,
padding: 0,
display: 'grid',
gap: theme.spacing(2),
}),
listItem: css({
margin: 0,
}),
};
};
@@ -0,0 +1,29 @@
import { useAsync } from 'react-use';
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
import { LocationInfo } from 'app/features/search/service/types';
/**
*
* @description Hook to fetch dashboard location info (folders).
* @returns An object containing a mapping of folder UIDs to LocationInfo, loading state, and error state.
*/
export function useDashboardLocationInfo(enabled: boolean) {
const searcher = getGrafanaSearcher();
const {
value: foldersByUid,
loading,
error,
} = useAsync(async (): Promise<Record<string, LocationInfo>> => {
if (!enabled) {
return {};
}
return searcher.getLocationInfo();
}, [enabled, searcher]);
return {
foldersByUid: foldersByUid ?? {},
loading,
error,
};
}
+15 -36
View File
@@ -4,18 +4,18 @@ import { useThrottle } from 'react-use';
import { InterpolateFunction, PanelProps, textUtil } from '@grafana/data';
import { t } from '@grafana/i18n';
import { useStyles2, ScrollContainer, Box, Text, EmptyState, Link } from '@grafana/ui';
import { ScrollContainer, Box, Text, EmptyState } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import impressionSrv from 'app/core/services/impression_srv';
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
import { DashboardQueryResult, LocationInfo, QueryResponse, SearchQuery } from 'app/features/search/service/types';
import { StarToolbarButton } from 'app/features/stars/StarToolbarButton';
import { DashboardQueryResult, QueryResponse, SearchQuery } from 'app/features/search/service/types';
import { DashListItem } from './DashListItem';
import { Options } from './panelcfg.gen';
import { getStyles } from './styles';
import { useDashListUrlParams } from './utils';
type Dashboard = DashboardQueryResult & {
export type Dashboard = DashboardQueryResult & {
isSearchResult?: boolean;
isRecent?: boolean;
isStarred?: boolean;
@@ -107,15 +107,10 @@ async function fetchDashboards(options: Options, replaceVars: InterpolateFunctio
return dashMap;
}
async function fetchDashboardFolders() {
return getGrafanaSearcher().getLocationInfo();
}
const collator = new Intl.Collator();
export function DashList(props: PanelProps<Options>) {
const [dashboards, setDashboards] = useState(new Map<string, Dashboard>());
const [foldersTitleMap, setFoldersTitleMap] = useState<Record<string, LocationInfo>>({});
const throttledRenderCount = useThrottle(props.renderCounter, 5000);
@@ -125,13 +120,7 @@ export function DashList(props: PanelProps<Options>) {
});
}, [props.options, props.replaceVariables, throttledRenderCount]);
useEffect(() => {
if (props.options.showFolderNames && dashboards.size > 0) {
fetchDashboardFolders().then((locationInfo) => {
setFoldersTitleMap(locationInfo);
});
}
}, [props.options.showFolderNames, dashboards]);
const { foldersByUid } = useDashboardLocationInfo(props.options.showFolderNames && dashboards.size > 0);
const [starredDashboards, recentDashboards, searchedDashboards] = useMemo(() => {
const dashboardList = [...dashboards.values()];
@@ -185,7 +174,6 @@ export function DashList(props: PanelProps<Options>) {
setDashboards(updatedDashboards);
};
const css = useStyles2(getStyles);
const urlParams = useDashListUrlParams(props);
const renderList = (dashboards: Dashboard[]) => (
@@ -194,26 +182,17 @@ export function DashList(props: PanelProps<Options>) {
let url = dash.url + urlParams;
url = getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
const locationInfo = showFolderNames && dash.location ? foldersTitleMap[dash.location] : undefined;
const locationInfo = showFolderNames && dash.location ? foldersByUid[dash.location] : undefined;
return (
<li key={`dash-${dash.uid}`}>
<div className={css.dashlistLink}>
<Box flex={1}>
<Link href={url}>{dash.name}</Link>
{showFolderNames && locationInfo && (
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
)}
</Box>
<StarToolbarButton
title={dash.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dash.uid}
onStarChange={handleStarChange}
/>
</div>
<DashListItem
dashboard={dash}
url={url}
showFolderNames={showFolderNames}
locationInfo={locationInfo}
layoutMode="list"
onStarChange={handleStarChange}
/>
</li>
);
})}
@@ -0,0 +1,64 @@
import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
import { LocationInfo } from 'app/features/search/service/types';
import { StarToolbarButton } from 'app/features/stars/StarToolbarButton';
import { Dashboard } from './DashList';
import { getStyles } from './styles';
interface Props {
dashboard: Dashboard;
url: string;
showFolderNames: boolean;
locationInfo?: LocationInfo;
layoutMode: 'list' | 'card';
onStarChange?: (id: string, isStarred: boolean) => void;
}
export function DashListItem({ dashboard, url, showFolderNames, locationInfo, layoutMode, onStarChange }: Props) {
const css = useStyles2(getStyles);
return (
<>
{layoutMode === 'list' ? (
<div className={css.dashlistLink}>
<Box flex={1}>
<Link href={url}>{dashboard.name}</Link>
{showFolderNames && locationInfo && (
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
)}
</Box>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</div>
) : (
<Card className={css.dashlistCard} noMargin>
<Stack justifyContent="space-between" alignItems="center">
<Link href={url}>{dashboard.name}</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</Stack>
{showFolderNames && locationInfo && (
<Stack alignItems="center" direction="row" gap={0}>
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
</Stack>
)}
</Card>
)}
</>
);
}
+23 -1
View File
@@ -1,8 +1,13 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
export const getStyles = (theme: GrafanaTheme2) => {
const gradient = `linear-gradient(
90deg,
${colorManipulator.alpha(theme.colors.primary.text, 0.1)} 0%,
${colorManipulator.alpha(theme.colors.secondary.text, 0.1)} 100%
)`;
return {
dashlistLink: css({
display: 'flex',
@@ -19,5 +24,22 @@ export const getStyles = (theme: GrafanaTheme2) => {
},
},
}),
dashlistCard: css({
display: 'flex',
flexDirection: 'column',
'&:hover a': {
color: theme.colors.text.link,
textDecoration: 'underline',
},
height: '100%',
'&:hover': {
backgroundImage: gradient,
color: theme.colors.text.primary,
},
}),
dashlistCardIcon: css({
marginRight: theme.spacing(0.5),
}),
};
};