The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/search/page/components/FolderSection.tsx

256 lines
6.8 KiB

import { css, cx } from '@emotion/css';
import React, { FC } from 'react';
import { useAsync, useLocalStorage } from 'react-use';
import { GrafanaTheme } from '@grafana/data';
import { Card, Checkbox, CollapsableSection, Icon, Spinner, stylesFactory, useTheme } from '@grafana/ui';
import { getSectionStorageKey } from 'app/features/search/utils';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
import { SearchItem } from '../..';
import { getGrafanaSearcher, SearchQuery } from '../../service';
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
import { SelectionChecker, SelectionToggle } from '../selection';
export interface DashboardSection {
kind: string; // folder | query!
uid: string;
title: string;
selected?: boolean; // not used ? keyboard
url?: string;
icon?: string;
itemsUIDs?: string[]; // for pseudo folders
}
interface SectionHeaderProps {
selection?: SelectionChecker;
selectionToggle?: SelectionToggle;
onTagSelected: (tag: string) => void;
section: DashboardSection;
renderStandaloneBody?: boolean; // render the body on its own
tags?: string[];
}
export const FolderSection: FC<SectionHeaderProps> = ({
section,
selectionToggle,
onTagSelected,
selection,
renderStandaloneBody,
tags,
}) => {
const editable = selectionToggle != null;
const theme = useTheme();
const styles = getSectionHeaderStyles(theme, section.selected, editable);
const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false);
const results = useAsync(async () => {
if (!sectionExpanded && !renderStandaloneBody) {
return Promise.resolve([] as DashboardSectionItem[]);
}
let folderUid: string | undefined = section.uid;
let folderTitle: string | undefined = section.title;
let query: SearchQuery = {
query: '*',
kind: ['dashboard'],
location: section.uid,
sort: 'name_sort',
};
if (section.itemsUIDs) {
query = {
uid: section.itemsUIDs, // array of UIDs
};
folderUid = undefined;
folderTitle = undefined;
}
const raw = await getGrafanaSearcher().search({ ...query, tags });
const v = raw.view.map(
(item) =>
({
uid: item.uid,
title: item.name,
url: item.url,
uri: item.url,
type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
id: 666, // do not use me!
isStarred: false,
tags: item.tags ?? [],
folderUid,
folderTitle,
} as DashboardSectionItem)
);
return v;
}, [sectionExpanded, section, tags]);
const onSectionExpand = () => {
setSectionExpanded(!sectionExpanded);
};
const onToggleFolder = (evt: React.FormEvent) => {
evt.preventDefault();
evt.stopPropagation();
if (selectionToggle && selection) {
const checked = !selection(section.kind, section.uid);
selectionToggle(section.kind, section.uid);
const sub = results.value ?? [];
for (const item of sub) {
if (selection('dashboard', item.uid!) !== checked) {
selectionToggle('dashboard', item.uid!);
}
}
}
};
const onToggleChecked = (item: DashboardSectionItem) => {
if (selectionToggle) {
selectionToggle('dashboard', item.uid!);
}
};
const id = useUniqueId();
const labelId = `section-header-label-${id}`;
let icon = section.icon;
if (!icon) {
icon = sectionExpanded ? 'folder-open' : 'folder';
}
const renderResults = () => {
if (!results.value?.length) {
if (results.loading) {
return <Spinner className={styles.spinner} />;
}
return (
<Card>
<Card.Heading>No results found</Card.Heading>
</Card>
);
}
return results.value.map((v) => {
if (selection && selectionToggle) {
const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
v = {
...v,
checked: selection(type, v.uid!),
};
}
return (
<SearchItem
key={v.uid}
item={v}
onTagSelected={onTagSelected}
onToggleChecked={onToggleChecked as any}
editable={Boolean(selection != null)}
/>
);
});
};
// Skip the folder wrapper
if (renderStandaloneBody) {
return <div className={styles.folderViewResults}>{renderResults()}</div>;
}
return (
<CollapsableSection
isOpen={sectionExpanded ?? false}
onToggle={onSectionExpand}
className={styles.wrapper}
contentClassName={styles.content}
loading={results.loading}
labelId={labelId}
label={
<>
{selectionToggle && selection && (
<div className={styles.checkbox} onClick={onToggleFolder}>
<Checkbox value={selection(section.kind, section.uid)} aria-label="Select folder" />
</div>
)}
<div className={styles.icon}>
<Icon name={icon as any} />
</div>
<div className={styles.text}>
<span id={labelId}>{section.title}</span>
{section.url && section.uid !== 'general' && (
<a href={section.url} className={styles.link}>
<span className={styles.separator}>|</span> <Icon name="folder-upload" /> Go to folder
</a>
)}
</div>
</>
}
>
{results.value && <ul className={styles.sectionItems}>{renderResults()}</ul>}
</CollapsableSection>
);
};
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => {
const { sm } = theme.spacing;
return {
wrapper: cx(
css`
align-items: center;
font-size: ${theme.typography.size.base};
padding: 12px;
border-bottom: none;
color: ${theme.colors.textWeak};
z-index: 1;
&:hover,
&.selected {
color: ${theme.colors.text};
}
&:hover,
&:focus-visible,
&:focus-within {
a {
opacity: 1;
}
}
`,
'pointer',
{ selected }
),
sectionItems: css`
margin: 0 24px 0 32px;
`,
checkbox: css`
padding: 0 ${sm} 0 0;
`,
icon: css`
padding: 0 ${sm} 0 ${editable ? 0 : sm};
`,
folderViewResults: css`
overflow: auto;
`,
text: css`
flex-grow: 1;
line-height: 24px;
`,
link: css`
padding: 2px 10px 0;
color: ${theme.colors.textWeak};
opacity: 0;
transition: opacity 150ms ease-in-out;
`,
separator: css`
margin-right: 6px;
`,
content: css`
padding-top: 0px;
padding-bottom: 0px;
`,
spinner: css`
display: grid;
place-content: center;
padding-bottom: 1rem;
`,
};
});