Scopes: Fix rendering of the scopes list for large chunk of items (#95327)

* Scopes: Fix rendering of the scopes list for large chunk of items

* Fix outline for overflow

* Fix outline for overflow
This commit is contained in:
Bogdan Matei 2024-10-25 12:27:30 +03:00 committed by GitHub
parent b1e1297bb3
commit 69f888e296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 51 deletions

View File

@ -351,39 +351,44 @@ export function ScopesSelectorSceneRenderer({ model }: SceneComponentProps<Scope
model.resetDirtyScopeNames();
}}
>
{isLoadingScopes ? (
<Spinner data-testid="scopes-selector-loading" />
) : (
<ScopesTree
nodes={nodes}
nodePath={['']}
loadingNodeName={loadingNodeName}
scopes={treeScopes}
onNodeUpdate={(path, isExpanded, query) => model.updateNode(path, isExpanded, query)}
onNodeSelectToggle={(path) => model.toggleNodeSelect(path)}
/>
)}
<div className={styles.buttonGroup}>
<Button
variant="primary"
data-testid="scopes-selector-apply"
onClick={() => {
model.closePicker();
model.updateScopes();
}}
>
<Trans i18nKey="scopes.selector.apply">Apply</Trans>
</Button>
<Button
variant="secondary"
data-testid="scopes-selector-cancel"
onClick={() => {
model.closePicker();
model.resetDirtyScopeNames();
}}
>
<Trans i18nKey="scopes.selector.cancel">Cancel</Trans>
</Button>
<div className={styles.drawerContainer}>
<div className={styles.treeContainer}>
{isLoadingScopes ? (
<Spinner data-testid="scopes-selector-loading" />
) : (
<ScopesTree
nodes={nodes}
nodePath={['']}
loadingNodeName={loadingNodeName}
scopes={treeScopes}
onNodeUpdate={(path, isExpanded, query) => model.updateNode(path, isExpanded, query)}
onNodeSelectToggle={(path) => model.toggleNodeSelect(path)}
/>
)}
</div>
<div className={styles.buttonsContainer}>
<Button
variant="primary"
data-testid="scopes-selector-apply"
onClick={() => {
model.closePicker();
model.updateScopes();
}}
>
<Trans i18nKey="scopes.selector.apply">Apply</Trans>
</Button>
<Button
variant="secondary"
data-testid="scopes-selector-cancel"
onClick={() => {
model.closePicker();
model.resetDirtyScopeNames();
}}
>
<Trans i18nKey="scopes.selector.cancel">Cancel</Trans>
</Button>
</div>
</div>
</Drawer>
)}
@ -410,7 +415,20 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => {
color: theme.colors.text.primary,
}),
}),
buttonGroup: css({
drawerContainer: css({
display: 'flex',
flexDirection: 'column',
height: '100%',
}),
treeContainer: css({
display: 'flex',
flexDirection: 'column',
maxHeight: '100%',
overflowY: 'hidden',
// Fix for top level search outline overflow due to scrollbars
paddingLeft: theme.spacing(0.5),
}),
buttonsContainer: css({
display: 'flex',
gap: theme.spacing(1),
marginTop: theme.spacing(8),

View File

@ -1,11 +1,11 @@
import { groupBy } from 'lodash';
import { Dictionary, groupBy } from 'lodash';
import { useMemo } from 'react';
import { ScopesTreeHeadline } from './ScopesTreeHeadline';
import { ScopesTreeItem } from './ScopesTreeItem';
import { ScopesTreeLoading } from './ScopesTreeLoading';
import { ScopesTreeSearch } from './ScopesTreeSearch';
import { NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
import { Node, NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
export interface ScopesTreeProps {
nodes: NodesMap;
@ -30,7 +30,8 @@ export function ScopesTree({
const isNodeLoading = loadingNodeName === nodeId;
const scopeNames = scopes.map(({ scopeName }) => scopeName);
const anyChildExpanded = childNodes.some(({ isExpanded }) => isExpanded);
const groupedNodes = useMemo(() => groupBy(childNodes, 'reason'), [childNodes]);
const groupedNodes: Dictionary<Node[]> = useMemo(() => groupBy(childNodes, 'reason'), [childNodes]);
const isLastExpandedNode = !anyChildExpanded && node.isExpanded;
return (
<>
@ -44,11 +45,12 @@ export function ScopesTree({
<ScopesTreeLoading isNodeLoading={isNodeLoading}>
<ScopesTreeItem
anyChildExpanded={anyChildExpanded}
isNodeLoading={isNodeLoading}
groupedNodes={groupedNodes}
isLastExpandedNode={isLastExpandedNode}
loadingNodeName={loadingNodeName}
node={node}
nodePath={nodePath}
nodes={groupedNodes[NodeReason.Persisted] ?? []}
nodeReason={NodeReason.Persisted}
scopes={scopes}
scopeNames={scopeNames}
type="persisted"
@ -64,11 +66,12 @@ export function ScopesTree({
<ScopesTreeItem
anyChildExpanded={anyChildExpanded}
isNodeLoading={isNodeLoading}
groupedNodes={groupedNodes}
isLastExpandedNode={isLastExpandedNode}
loadingNodeName={loadingNodeName}
node={node}
nodePath={nodePath}
nodes={groupedNodes[NodeReason.Result] ?? []}
nodeReason={NodeReason.Result}
scopes={scopes}
scopeNames={scopeNames}
type="result"

View File

@ -1,19 +1,21 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { Dictionary } from 'lodash';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
import { Checkbox, CustomScrollbar, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ScopesTree } from './ScopesTree';
import { Node, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
import { Node, NodeReason, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
export interface ScopesTreeItemProps {
anyChildExpanded: boolean;
isNodeLoading: boolean;
groupedNodes: Dictionary<Node[]>;
isLastExpandedNode: boolean;
loadingNodeName: string | undefined;
node: Node;
nodePath: string[];
nodes: Node[];
nodeReason: NodeReason;
scopeNames: string[];
scopes: TreeScope[];
type: 'persisted' | 'result';
@ -23,10 +25,12 @@ export interface ScopesTreeItemProps {
export function ScopesTreeItem({
anyChildExpanded,
groupedNodes,
isLastExpandedNode,
loadingNodeName,
node,
nodePath,
nodes,
nodeReason,
scopeNames,
scopes,
type,
@ -35,8 +39,14 @@ export function ScopesTreeItem({
}: ScopesTreeItemProps) {
const styles = useStyles2(getStyles);
return (
<div role="tree">
const nodes = groupedNodes[nodeReason] || [];
if (nodes.length === 0) {
return null;
}
const children = (
<div role="tree" className={anyChildExpanded ? styles.expandedContainer : undefined}>
{nodes.map((childNode) => {
const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
@ -49,8 +59,13 @@ export function ScopesTreeItem({
const radioName = childNodePath.join('.');
return (
<div key={childNode.name} role="treeitem" aria-selected={childNode.isExpanded}>
<div className={styles.title}>
<div
key={childNode.name}
role="treeitem"
aria-selected={childNode.isExpanded}
className={anyChildExpanded ? styles.expandedContainer : undefined}
>
<div className={cx(styles.title, childNode.isSelectable && !childNode.isExpanded && styles.titlePadding)}>
{childNode.isSelectable && !childNode.isExpanded ? (
node.disableMultiSelect ? (
<RadioButtonDot
@ -111,10 +126,28 @@ export function ScopesTreeItem({
})}
</div>
);
if (isLastExpandedNode) {
return (
<CustomScrollbar
autoHeightMin={`${Math.min(5, nodes.length) * 30}px`}
autoHeightMax={nodeReason === NodeReason.Persisted ? `${Math.min(5, nodes.length) * 30}px` : '100%'}
>
{children}
</CustomScrollbar>
);
}
return children;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
expandedContainer: css({
display: 'flex',
flexDirection: 'column',
maxHeight: '100%',
}),
title: css({
alignItems: 'center',
display: 'flex',
@ -127,6 +160,10 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: 0,
}),
}),
titlePadding: css({
// Fix for checkboxes and radios outline overflow due to scrollbars
paddingLeft: theme.spacing(0.5),
}),
expand: css({
alignItems: 'center',
background: 'none',
@ -137,6 +174,10 @@ const getStyles = (theme: GrafanaTheme2) => {
padding: 0,
}),
children: css({
display: 'flex',
flexDirection: 'column',
overflowY: 'hidden',
maxHeight: '100%',
paddingLeft: theme.spacing(4),
}),
};

View File

@ -57,6 +57,10 @@ const getStyles = (theme: GrafanaTheme2) => {
return {
input: css({
margin: theme.spacing(1, 0),
minHeight: theme.spacing(4),
height: theme.spacing(4),
maxHeight: theme.spacing(4),
width: `calc(100% - ${theme.spacing(0.5)})`,
}),
};
};