mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scopes: Implement radio buttons for containers with single node selection (#89674)
This commit is contained in:
parent
71270f3203
commit
e4b9f356bc
@ -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"
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 () => {
|
||||
|
@ -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),
|
||||
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user