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/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx

185 lines
5.8 KiB

import { css } from '@emotion/css';
import { debounce } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, FilterInput, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { NodesMap, TreeScope } from './types';
export interface ScopesTreeLevelProps {
nodes: NodesMap;
nodePath: string[];
loadingNodeName: string | undefined;
scopes: TreeScope[];
onNodeUpdate: (path: string[], isExpanded: boolean, query: string) => void;
onNodeSelectToggle: (path: string[]) => void;
}
export function ScopesTreeLevel({
nodes,
nodePath,
loadingNodeName,
scopes,
onNodeUpdate,
onNodeSelectToggle,
}: ScopesTreeLevelProps) {
const styles = useStyles2(getStyles);
const nodeId = nodePath[nodePath.length - 1];
const node = nodes[nodeId];
const childNodes = node.nodes;
const childNodesArr = Object.values(childNodes);
const isNodeLoading = loadingNodeName === nodeId;
const scopeNames = scopes.map(({ scopeName }) => scopeName);
const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded);
const [queryValue, setQueryValue] = useState(node.query);
useEffect(() => {
setQueryValue(node.query);
}, [node.query]);
const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
return (
<>
{!anyChildExpanded && (
<FilterInput
placeholder={t('scopes.tree.search', 'Search')}
value={queryValue}
className={styles.searchInput}
data-testid={`scopes-tree-${nodeId}-search`}
onChange={(value) => {
setQueryValue(value);
onQueryUpdate(nodePath, true, value);
}}
/>
)}
{!anyChildExpanded && !node.query && (
<h6 className={styles.headline}>
<Trans i18nKey="scopes.tree.headline">Recommended</Trans>
</h6>
)}
<div role="tree">
{isNodeLoading && <Skeleton count={5} className={styles.loader} />}
{!isNodeLoading &&
childNodesArr.map((childNode) => {
const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
return null;
}
const childNodePath = [...nodePath, childNode.name];
const radioName = childNodePath.join('.');
return (
<div key={childNode.name} role="treeitem" aria-selected={childNode.isExpanded}>
<div className={styles.itemTitle}>
{childNode.isSelectable && !childNode.isExpanded ? (
node.disableMultiSelect ? (
<RadioButtonDot
id={radioName}
name={radioName}
checked={isSelected}
label=""
data-testid={`scopes-tree-${childNode.name}-radio`}
onClick={() => {
onNodeSelectToggle(childNodePath);
}}
/>
) : (
<Checkbox
checked={isSelected}
data-testid={`scopes-tree-${childNode.name}-checkbox`}
onChange={() => {
onNodeSelectToggle(childNodePath);
}}
/>
)
) : null}
{childNode.isExpandable ? (
<button
className={styles.itemExpand}
data-testid={`scopes-tree-${childNode.name}-expand`}
aria-label={
childNode.isExpanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')
}
onClick={() => {
onNodeUpdate(childNodePath, !childNode.isExpanded, childNode.query);
}}
>
<Icon name={!childNode.isExpanded ? 'angle-right' : 'angle-down'} />
{childNode.title}
</button>
) : (
<span data-testid={`scopes-tree-${childNode.name}-title`}>{childNode.title}</span>
)}
</div>
<div className={styles.itemChildren}>
{childNode.isExpanded && (
<ScopesTreeLevel
nodes={node.nodes}
nodePath={childNodePath}
loadingNodeName={loadingNodeName}
scopes={scopes}
onNodeUpdate={onNodeUpdate}
onNodeSelectToggle={onNodeSelectToggle}
/>
)}
</div>
</div>
);
})}
</div>
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
searchInput: css({
margin: theme.spacing(1, 0),
}),
headline: css({
color: theme.colors.text.secondary,
margin: theme.spacing(1, 0),
}),
loader: css({
margin: theme.spacing(0.5, 0),
}),
itemTitle: css({
alignItems: 'center',
display: 'flex',
gap: theme.spacing(1),
fontSize: theme.typography.pxToRem(14),
lineHeight: theme.typography.pxToRem(22),
padding: theme.spacing(0.5, 0),
'& > label': css({
gap: 0,
}),
}),
itemExpand: css({
alignItems: 'center',
background: 'none',
border: 0,
display: 'flex',
gap: theme.spacing(1),
margin: 0,
padding: 0,
}),
itemChildren: css({
paddingLeft: theme.spacing(4),
}),
};
};