D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
search
/
page
/
components
/
Filename :
FolderSection.tsx
back
Copy
import { css } from '@emotion/css'; import React, { useCallback, useId, useState } from 'react'; import { useAsync } from 'react-use'; import { GrafanaTheme2, toIconName } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui'; import { config } from 'app/core/config'; import { t } from 'app/core/internationalization'; import { SearchItem } from '../..'; import { GENERAL_FOLDER_UID, SEARCH_EXPANDED_FOLDER_STORAGE_KEY } from '../../constants'; import { getGrafanaSearcher } from '../../service'; import { getFolderChildren } from '../../service/folders'; import { queryResultToViewItem } from '../../service/utils'; import { DashboardViewItem } from '../../types'; import { SelectionChecker, SelectionToggle } from '../selection'; interface SectionHeaderProps { selection?: SelectionChecker; selectionToggle?: SelectionToggle; onClickItem?: (e: React.MouseEvent<HTMLElement>) => void; onTagSelected: (tag: string) => void; section: DashboardViewItem; renderStandaloneBody?: boolean; // render the body on its own tags?: string[]; } async function getChildren(section: DashboardViewItem, tags: string[] | undefined): Promise<DashboardViewItem[]> { if (config.featureToggles.nestedFolders) { return getFolderChildren(section.uid, section.title); } const query = section.itemsUIDs ? { uid: section.itemsUIDs, } : { query: '*', kind: ['dashboard'], location: section.uid, sort: 'name_sort', limit: 1000, // this component does not have infinite scroll, so we need to load everything upfront }; const raw = await getGrafanaSearcher().search({ ...query, tags }); return raw.view.map((v) => queryResultToViewItem(v, raw.view)); } export const FolderSection = ({ section, selectionToggle, onClickItem, onTagSelected, selection, renderStandaloneBody, tags, }: SectionHeaderProps) => { const uid = section.uid; const editable = selectionToggle != null; const styles = useStyles2(useCallback((theme: GrafanaTheme2) => getSectionHeaderStyles(theme, editable), [editable])); const [sectionExpanded, setSectionExpanded] = useState(() => { const lastExpandedFolder = window.localStorage.getItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY); return lastExpandedFolder === uid; }); const results = useAsync(async () => { if (!sectionExpanded && !renderStandaloneBody) { return Promise.resolve([]); } const childItems = getChildren(section, tags); return childItems; }, [sectionExpanded, tags]); const onSectionExpand = () => { const newExpandedValue = !sectionExpanded; if (newExpandedValue) { // If we've just expanded the section, remember it to local storage window.localStorage.setItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY, uid); } else { // Else, when closing a section, remove it from local storage only if this folder was the most recently opened const lastExpandedFolder = window.localStorage.getItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY); if (lastExpandedFolder === uid) { window.localStorage.removeItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY); } } setSectionExpanded(newExpandedValue); }; 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(item.kind, item.uid!) !== checked) { selectionToggle(item.kind, item.uid!); } } } }; const id = useId(); const labelId = `section-header-label-${id}`; let icon = toIconName(section.icon ?? ''); if (!icon) { icon = sectionExpanded ? 'folder-open' : 'folder'; } const renderResults = () => { if (!results.value) { return null; } else if (results.value.length === 0 && !results.loading) { return ( <Card> <Card.Heading>No results found</Card.Heading> </Card> ); } return results.value.map((item) => { return ( <SearchItem key={item.uid} item={item} onTagSelected={onTagSelected} onToggleChecked={(item) => selectionToggle?.(item.kind, item.uid)} editable={Boolean(selection != null)} onClickItem={onClickItem} isSelected={selection?.(item.kind, item.uid)} /> ); }); }; // Skip the folder wrapper if (renderStandaloneBody) { return ( <div className={styles.folderViewResults}> {!results.value?.length && results.loading ? <Spinner className={styles.spinner} /> : renderResults()} </div> ); } return ( <CollapsableSection headerDataTestId={selectors.components.Search.folderHeader(section.title)} contentDataTestId={selectors.components.Search.folderContent(section.title)} isOpen={sectionExpanded ?? false} onToggle={onSectionExpand} className={styles.wrapper} contentClassName={styles.content} loading={results.loading} labelId={labelId} label={ <> {selectionToggle && selection && ( // TODO: fix keyboard a11y // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions <div onClick={onToggleFolder}> <Checkbox className={styles.checkbox} value={selection(section.kind, section.uid)} aria-label={t('search.folder-view.select-folder', 'Select folder')} /> </div> )} <div className={styles.icon}> <Icon name={icon} /> </div> <div className={styles.text}> <span id={labelId}>{section.title}</span> {section.url && section.uid !== GENERAL_FOLDER_UID && ( <a href={section.url} className={styles.link}> <span className={styles.separator}>|</span> <Icon name="folder-upload" />{' '} {t('search.folder-view.go-to-folder', 'Go to folder')} </a> )} </div> </> } > {results.value && <ul className={styles.sectionItems}>{renderResults()}</ul>} </CollapsableSection> ); }; const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => { const sm = theme.spacing(1); return { wrapper: css` align-items: center; font-size: ${theme.typography.size.base}; padding: 12px; border-bottom: none; color: ${theme.colors.text.secondary}; z-index: 1; &:hover, &.selected { color: ${theme.colors.text}; } &:hover, &:focus-visible, &:focus-within { a { opacity: 1; } } `, sectionItems: css` margin: 0 24px 0 32px; `, 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.text.secondary}; 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; `, checkbox: css({ marginRight: theme.spacing(1), }), }; };