Scopes: Implement radio buttons for containers with single node selection (#89674)

This commit is contained in:
Bogdan Matei 2024-06-25 17:10:36 +03:00 committed by GitHub
parent 71270f3203
commit e4b9f356bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 54 additions and 24 deletions

View File

@ -5,7 +5,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes';
export interface RadioButtonDotProps<T> {
export interface RadioButtonDotProps<T>
extends Omit<React.HTMLProps<HTMLInputElement>, 'label' | 'value' | 'onChange' | 'type'> {
id: string;
name: string;
checked?: boolean;
@ -25,12 +26,14 @@ export const RadioButtonDot = <T extends string | number | readonly string[]>({
disabled,
description,
onChange,
...props
}: RadioButtonDotProps<T>) => {
const styles = useStyles2(getStyles);
return (
<label title={description} className={styles.label}>
<input
{...props}
id={id}
name={name}
type="radio"

View File

@ -238,6 +238,7 @@ export * from './Select/types';
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
export { Badge, type BadgeColor, type BadgeProps } from './Badge/Badge';
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
export { RadioButtonDot } from './Forms/RadioButtonList/RadioButtonDot';
export { RadioButtonList } from './Forms/RadioButtonList/RadioButtonList';
export { Input, getInputStyles } from './Input/Input';

View File

@ -131,14 +131,14 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
public toggleNodeSelect(path: string[]) {
let treeScopes = [...this.state.treeScopes];
let siblings = this.state.nodes;
let parentNode = this.state.nodes[''];
for (let idx = 0; idx < path.length - 1; idx++) {
siblings = siblings[path[idx]].nodes;
for (let idx = 1; idx < path.length - 1; idx++) {
parentNode = parentNode.nodes[path[idx]];
}
const nodeName = path[path.length - 1];
const { linkId } = siblings[nodeName];
const { linkId } = parentNode.nodes[nodeName];
const selectedIdx = treeScopes.findIndex(({ scopeName }) => scopeName === linkId);
@ -146,14 +146,17 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
fetchScope(linkId!);
const selectedFromSameNode =
treeScopes.length === 0 || Object.values(siblings).some(({ linkId }) => linkId === treeScopes[0].scopeName);
treeScopes.length === 0 ||
Object.values(parentNode.nodes).some(({ linkId }) => linkId === treeScopes[0].scopeName);
const treeScope = {
scopeName: linkId!,
path,
};
this.setState({ treeScopes: !selectedFromSameNode ? [treeScope] : [...treeScopes, treeScope] });
this.setState({
treeScopes: parentNode?.disableMultiSelect || !selectedFromSameNode ? [treeScope] : [...treeScopes, treeScope],
});
} else {
treeScopes.splice(selectedIdx, 1);

View File

@ -28,8 +28,8 @@ import {
getFiltersInput,
getClustersExpand,
getClustersSelect,
getClustersSlothClusterNorthSelect,
getClustersSlothClusterSouthSelect,
getClustersSlothClusterNorthRadio,
getClustersSlothClusterSouthRadio,
getDashboard,
getDashboardsContainer,
getDashboardsExpand,
@ -158,8 +158,12 @@ describe('ScopesScene', () => {
it('Respects only one select per container', async () => {
await userEvents.click(getFiltersInput());
await userEvents.click(getClustersExpand());
await userEvents.click(getClustersSlothClusterNorthSelect());
expect(getClustersSlothClusterSouthSelect()).toBeDisabled();
await userEvents.click(getClustersSlothClusterNorthRadio());
expect(getClustersSlothClusterNorthRadio().checked).toBe(true);
expect(getClustersSlothClusterSouthRadio().checked).toBe(false);
await userEvents.click(getClustersSlothClusterSouthRadio());
expect(getClustersSlothClusterNorthRadio().checked).toBe(false);
expect(getClustersSlothClusterSouthRadio().checked).toBe(true);
});
it('Search works', async () => {

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Icon, IconButton, Input, useStyles2 } from '@grafana/ui';
import { Checkbox, Icon, IconButton, Input, RadioButtonDot, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { NodesMap, TreeScope } from './types';
@ -72,18 +72,32 @@ export function ScopesTreeLevel({
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 ? (
<Checkbox
checked={isSelected}
disabled={anyChildSelected && !isSelected && node.disableMultiSelect}
data-testid={`scopes-tree-${childNode.name}-checkbox`}
onChange={() => {
onNodeSelectToggle(childNodePath);
}}
/>
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 && (
@ -142,6 +156,10 @@ const getStyles = (theme: GrafanaTheme2) => {
fontSize: theme.typography.pxToRem(14),
lineHeight: theme.typography.pxToRem(22),
padding: theme.spacing(0.5, 0),
'& > label': css({
gap: 0,
}),
}),
itemChildren: css({
paddingLeft: theme.spacing(4),

View File

@ -295,6 +295,7 @@ const selectors = {
tree: {
search: (nodeId: string) => `scopes-tree-${nodeId}-search`,
select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
},
@ -354,10 +355,10 @@ export const getApplicationsClustersSlothClusterSouthSelect = () =>
export const getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters'));
export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters'));
export const getClustersSlothClusterNorthSelect = () =>
screen.getByTestId(selectors.tree.select('clusters-slothClusterNorth'));
export const getClustersSlothClusterSouthSelect = () =>
screen.getByTestId(selectors.tree.select('clusters-slothClusterSouth'));
export const getClustersSlothClusterNorthRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterNorth'));
export const getClustersSlothClusterSouthRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterSouth'));
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
return new DashboardScene({