mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
* Add and configure eslint-plugin-import * Fix the lint:ts npm command * Autofix + prettier all the files * Manually fix remaining files * Move jquery code in jest-setup to external file to safely reorder imports * Resolve issue caused by circular dependencies within Prometheus * Update .betterer.results * Fix missing // @ts-ignore * ignore iconBundle.ts * Fix missing // @ts-ignore
246 lines
7.5 KiB
TypeScript
246 lines
7.5 KiB
TypeScript
import { css, cx } from '@emotion/css';
|
|
import { take } from 'lodash';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import { GrafanaTheme2, InterpolateFunction, PanelProps } from '@grafana/data';
|
|
import { CustomScrollbar, stylesFactory, useStyles2 } from '@grafana/ui';
|
|
import { Icon, IconProps } from '@grafana/ui/src/components/Icon/Icon';
|
|
import { getFocusStyles } from '@grafana/ui/src/themes/mixins';
|
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
|
import impressionSrv from 'app/core/services/impression_srv';
|
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
|
import { SearchCard } from 'app/features/search/components/SearchCard';
|
|
import { DashboardSearchHit } from 'app/features/search/types';
|
|
|
|
import { PanelLayout, PanelOptions } from './models.gen';
|
|
import { getStyles } from './styles';
|
|
|
|
type Dashboard = DashboardSearchHit & { isSearchResult?: boolean; isRecent?: boolean };
|
|
|
|
interface DashboardGroup {
|
|
show: boolean;
|
|
header: string;
|
|
dashboards: Dashboard[];
|
|
}
|
|
|
|
async function fetchDashboards(options: PanelOptions, replaceVars: InterpolateFunction) {
|
|
let starredDashboards: Promise<Dashboard[]> = Promise.resolve([]);
|
|
if (options.showStarred) {
|
|
const params = { limit: options.maxItems, starred: 'true' };
|
|
starredDashboards = getBackendSrv().search(params);
|
|
}
|
|
|
|
let recentDashboards: Promise<Dashboard[]> = Promise.resolve([]);
|
|
let dashIds: number[] = [];
|
|
if (options.showRecentlyViewed) {
|
|
dashIds = take<number>(impressionSrv.getDashboardOpened(), options.maxItems);
|
|
recentDashboards = getBackendSrv().search({ dashboardIds: dashIds, limit: options.maxItems });
|
|
}
|
|
|
|
let searchedDashboards: Promise<Dashboard[]> = Promise.resolve([]);
|
|
if (options.showSearch) {
|
|
const params = {
|
|
limit: options.maxItems,
|
|
query: replaceVars(options.query, {}, 'text'),
|
|
folderIds: options.folderId,
|
|
tag: options.tags.map((tag: string) => replaceVars(tag, {}, 'text')),
|
|
type: 'dash-db',
|
|
};
|
|
|
|
searchedDashboards = getBackendSrv().search(params);
|
|
}
|
|
|
|
const [starred, searched, recent] = await Promise.all([starredDashboards, searchedDashboards, recentDashboards]);
|
|
|
|
// We deliberately deal with recent dashboards first so that the order of dash IDs is preserved
|
|
let dashMap = new Map<number, Dashboard>();
|
|
for (const dashId of dashIds) {
|
|
const dash = recent.find((d) => d.id === dashId);
|
|
if (dash) {
|
|
dashMap.set(dashId, { ...dash, isRecent: true });
|
|
}
|
|
}
|
|
|
|
searched.forEach((dash) => {
|
|
if (dashMap.has(dash.id)) {
|
|
dashMap.get(dash.id)!.isSearchResult = true;
|
|
} else {
|
|
dashMap.set(dash.id, { ...dash, isSearchResult: true });
|
|
}
|
|
});
|
|
|
|
starred.forEach((dash) => {
|
|
if (dashMap.has(dash.id)) {
|
|
dashMap.get(dash.id)!.isStarred = true;
|
|
} else {
|
|
dashMap.set(dash.id, { ...dash, isStarred: true });
|
|
}
|
|
});
|
|
|
|
return dashMap;
|
|
}
|
|
|
|
export function DashList(props: PanelProps<PanelOptions>) {
|
|
const [dashboards, setDashboards] = useState(new Map<number, Dashboard>());
|
|
useEffect(() => {
|
|
fetchDashboards(props.options, props.replaceVariables).then((dashes) => {
|
|
setDashboards(dashes);
|
|
});
|
|
}, [props.options, props.replaceVariables, props.renderCounter]);
|
|
|
|
const toggleDashboardStar = async (e: React.SyntheticEvent, dash: Dashboard) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const isStarred = await getDashboardSrv().starDashboard(dash.id.toString(), dash.isStarred);
|
|
const updatedDashboards = new Map(dashboards);
|
|
updatedDashboards.set(dash.id, { ...dash, isStarred });
|
|
setDashboards(updatedDashboards);
|
|
};
|
|
|
|
const [starredDashboards, recentDashboards, searchedDashboards] = useMemo(() => {
|
|
const dashboardList = [...dashboards.values()];
|
|
return [
|
|
dashboardList.filter((dash) => dash.isStarred).sort((a, b) => a.title.localeCompare(b.title)),
|
|
dashboardList.filter((dash) => dash.isRecent),
|
|
dashboardList.filter((dash) => dash.isSearchResult).sort((a, b) => a.title.localeCompare(b.title)),
|
|
];
|
|
}, [dashboards]);
|
|
|
|
const { showStarred, showRecentlyViewed, showHeadings, showSearch, layout } = props.options;
|
|
|
|
const dashboardGroups: DashboardGroup[] = [
|
|
{
|
|
header: 'Starred dashboards',
|
|
dashboards: starredDashboards,
|
|
show: showStarred,
|
|
},
|
|
{
|
|
header: 'Recently viewed dashboards',
|
|
dashboards: recentDashboards,
|
|
show: showRecentlyViewed,
|
|
},
|
|
{
|
|
header: 'Search',
|
|
dashboards: searchedDashboards,
|
|
show: showSearch,
|
|
},
|
|
];
|
|
|
|
const css = useStyles2(getStyles);
|
|
|
|
const renderList = (dashboards: Dashboard[]) => (
|
|
<ul>
|
|
{dashboards.map((dash) => (
|
|
<li className={css.dashlistItem} key={`dash-${dash.id}`}>
|
|
<div className={css.dashlistLink}>
|
|
<div className={css.dashlistLinkBody}>
|
|
<a className={css.dashlistTitle} href={dash.url}>
|
|
{dash.title}
|
|
</a>
|
|
{dash.folderTitle && <div className={css.dashlistFolder}>{dash.folderTitle}</div>}
|
|
</div>
|
|
<IconToggle
|
|
aria-label={`Star dashboard "${dash.title}".`}
|
|
className={css.dashlistStar}
|
|
enabled={{ name: 'favorite', type: 'mono' }}
|
|
disabled={{ name: 'star', type: 'default' }}
|
|
checked={dash.isStarred}
|
|
onClick={(e) => toggleDashboardStar(e, dash)}
|
|
/>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
const renderPreviews = (dashboards: Dashboard[]) => (
|
|
<ul className={css.gridContainer}>
|
|
{dashboards.map((dash) => (
|
|
<li key={dash.uid}>
|
|
<SearchCard item={dash} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
return (
|
|
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
|
|
{dashboardGroups.map(
|
|
({ show, header, dashboards }, i) =>
|
|
show && (
|
|
<div className={css.dashlistSection} key={`dash-group-${i}`}>
|
|
{showHeadings && <h6 className={css.dashlistSectionHeader}>{header}</h6>}
|
|
{layout === PanelLayout.Previews ? renderPreviews(dashboards) : renderList(dashboards)}
|
|
</div>
|
|
)
|
|
)}
|
|
</CustomScrollbar>
|
|
);
|
|
}
|
|
|
|
interface IconToggleProps extends Partial<IconProps> {
|
|
enabled: IconProps;
|
|
disabled: IconProps;
|
|
checked: boolean;
|
|
}
|
|
|
|
function IconToggle({
|
|
enabled,
|
|
disabled,
|
|
checked,
|
|
onClick,
|
|
className,
|
|
'aria-label': ariaLabel,
|
|
...otherProps
|
|
}: IconToggleProps) {
|
|
const toggleCheckbox = useCallback(
|
|
(e: React.MouseEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
onClick?.(e);
|
|
},
|
|
[onClick]
|
|
);
|
|
|
|
const iconPropsOverride = checked ? enabled : disabled;
|
|
const iconProps = { ...otherProps, ...iconPropsOverride };
|
|
const styles = useStyles2(getCheckboxStyles);
|
|
return (
|
|
<label className={styles.wrapper}>
|
|
<input
|
|
type="checkbox"
|
|
defaultChecked={checked}
|
|
onClick={toggleCheckbox}
|
|
className={styles.checkBox}
|
|
aria-label={ariaLabel}
|
|
/>
|
|
<Icon className={cx(styles.icon, className)} {...iconProps} />
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|
return {
|
|
wrapper: css({
|
|
display: 'flex',
|
|
alignSelf: 'center',
|
|
cursor: 'pointer',
|
|
zIndex: 100,
|
|
}),
|
|
checkBox: css({
|
|
appearance: 'none',
|
|
'&:focus-visible + *': {
|
|
...getFocusStyles(theme),
|
|
borderRadius: theme.shape.borderRadius(1),
|
|
},
|
|
}),
|
|
icon: css({
|
|
marginBottom: 0,
|
|
verticalAlign: 'baseline',
|
|
display: 'flex',
|
|
}),
|
|
};
|
|
});
|