123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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 />;
- }
- 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;
- `,
- };
- });
|