mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
b1e1297bb3
commit
69f888e296
@ -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),
|
||||
|
@ -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"
|
||||
|
@ -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),
|
||||
}),
|
||||
};
|
||||
|
@ -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)})`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user