RecentlyViewedDashboards: Set up container on browsing dashboards page (#115164)
* RecentlyViewedDashboards: Set up container on browsing dashboards page
This commit is contained in:
@@ -27,6 +27,7 @@ import { BrowseFilters } from './components/BrowseFilters';
|
||||
import { BrowseView } from './components/BrowseView';
|
||||
import CreateNewButton from './components/CreateNewButton';
|
||||
import { FolderActionsButton } from './components/FolderActionsButton';
|
||||
import { RecentlyViewedDashboards } from './components/RecentlyViewedDashboards';
|
||||
import { SearchView } from './components/SearchView';
|
||||
import { getFolderPermissions } from './permissions';
|
||||
import { useHasSelection } from './state/hooks';
|
||||
@@ -178,6 +179,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
>
|
||||
<Page.Contents className={styles.pageContents}>
|
||||
<ProvisionedFolderPreviewBanner queryParams={queryParams} />
|
||||
{/* only show recently viewed dashboards when in root */}
|
||||
{!folderUID && <RecentlyViewedDashboards />}
|
||||
<div>
|
||||
<FilterInput
|
||||
placeholder={getSearchPlaceholder(searchState.includePanels)}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { css } from '@emotion/css';
|
||||
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 { getRecentlyViewedDashboards } from './utils';
|
||||
|
||||
const MAX_RECENT = 5;
|
||||
|
||||
export function RecentlyViewedDashboards() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { value: recentDashboards = [], loading } = useAsync(async () => {
|
||||
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
|
||||
return [];
|
||||
}
|
||||
return getRecentlyViewedDashboards(MAX_RECENT);
|
||||
}, []);
|
||||
|
||||
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableSection
|
||||
headerDataTestId="browseDashboardsRecentlyViewedTitle"
|
||||
label={
|
||||
<Text variant="h5" element="h3">
|
||||
<Trans i18nKey="browse-dashboards.recently-viewed.title">Recently viewed</Trans>
|
||||
</Text>
|
||||
}
|
||||
isOpen={true}
|
||||
className={styles.title}
|
||||
contentClassName={styles.content}
|
||||
>
|
||||
{/* placeholder */}
|
||||
{loading && <Spinner />}
|
||||
{/* TODO: Better empty state https://github.com/grafana/grafana/issues/114804 */}
|
||||
{!loading && recentDashboards.length === 0 && (
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</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,
|
||||
},
|
||||
}),
|
||||
content: css({
|
||||
paddingTop: theme.spacing(0),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { ResourceRef } from 'app/features/provisioning/components/BulkActions/useBulkActionJob';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
import { DashboardQueryResult } from 'app/features/search/service/types';
|
||||
|
||||
import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types';
|
||||
|
||||
@@ -60,3 +63,36 @@ export function canSelectItems(permissions: BrowseDashboardsPermissions) {
|
||||
const canSelectDashboards = canEditDashboards || canDeleteDashboards;
|
||||
return Boolean(canSelectFolders || canSelectDashboards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dashboard search results ordered the same way the user opened them.
|
||||
*/
|
||||
export async function getRecentlyViewedDashboards(maxItems = 5): Promise<DashboardQueryResult[]> {
|
||||
try {
|
||||
const recentlyOpened = (await impressionSrv.getDashboardOpened()).slice(0, maxItems);
|
||||
if (!recentlyOpened.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResults = await getGrafanaSearcher().search({
|
||||
kind: ['dashboard'],
|
||||
limit: recentlyOpened.length,
|
||||
uid: recentlyOpened,
|
||||
});
|
||||
|
||||
const dashboards = searchResults.view.toArray();
|
||||
// Keep dashboards in the same order the user opened them.
|
||||
// When a UID is missing from the search response
|
||||
// push it to the end instead of letting indexOf return -1
|
||||
const order = (uid: string) => {
|
||||
const idx = recentlyOpened.indexOf(uid);
|
||||
return idx === -1 ? recentlyOpened.length : idx;
|
||||
};
|
||||
|
||||
dashboards.sort((a, b) => order(a.uid) - order(b.uid));
|
||||
return dashboards;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recently viewed dashboards', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { getRecentlyViewedDashboards } from 'app/features/browse-dashboards/components/utils';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
|
||||
import { CommandPaletteAction } from '../types';
|
||||
@@ -20,20 +20,7 @@ export async function getRecentDashboardActions(): Promise<CommandPaletteAction[
|
||||
return [];
|
||||
}
|
||||
|
||||
const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS);
|
||||
const resultsDataFrame = await getGrafanaSearcher().search({
|
||||
kind: ['dashboard'],
|
||||
limit: MAX_RECENT_DASHBOARDS,
|
||||
uid: recentUids,
|
||||
});
|
||||
|
||||
// Search results are alphabetical, so reorder them according to recently viewed
|
||||
const recentResults = resultsDataFrame.view.toArray();
|
||||
recentResults.sort((resultA, resultB) => {
|
||||
const orderA = recentUids.indexOf(resultA.uid);
|
||||
const orderB = recentUids.indexOf(resultB.uid);
|
||||
return orderA - orderB;
|
||||
});
|
||||
const recentResults = await getRecentlyViewedDashboards(MAX_RECENT_DASHBOARDS);
|
||||
|
||||
const recentDashboardActions: CommandPaletteAction[] = recentResults.map((item) => {
|
||||
const { url, name } = item; // items are backed by DataFrameView, so must hold the url in a closure
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Clear search and filters",
|
||||
"text": "No results found for your query"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "Nothing viewed yet",
|
||||
"title": "Recently viewed"
|
||||
},
|
||||
"restore": {
|
||||
"all-failed_one": "Failed to restore {{count}} dashboard",
|
||||
"all-failed_other": "Failed to restore {{count}} dashboards",
|
||||
|
||||
Reference in New Issue
Block a user