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';
|
import { useStyles2 } from '../../../themes';
|
||||||
|
|
||||||
export interface RadioButtonDotProps<T> {
|
export interface RadioButtonDotProps<T>
|
||||||
|
extends Omit<React.HTMLProps<HTMLInputElement>, 'label' | 'value' | 'onChange' | 'type'> {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
@ -25,12 +26,14 @@ export const RadioButtonDot = <T extends string | number | readonly string[]>({
|
|||||||
disabled,
|
disabled,
|
||||||
description,
|
description,
|
||||||
onChange,
|
onChange,
|
||||||
|
...props
|
||||||
}: RadioButtonDotProps<T>) => {
|
}: RadioButtonDotProps<T>) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label title={description} className={styles.label}>
|
<label title={description} className={styles.label}>
|
||||||
<input
|
<input
|
||||||
|
{...props}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -238,6 +238,7 @@ export * from './Select/types';
|
|||||||
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
|
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
|
||||||
export { Badge, type BadgeColor, type BadgeProps } from './Badge/Badge';
|
export { Badge, type BadgeColor, type BadgeProps } from './Badge/Badge';
|
||||||
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
|
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
|
||||||
|
export { RadioButtonDot } from './Forms/RadioButtonList/RadioButtonDot';
|
||||||
export { RadioButtonList } from './Forms/RadioButtonList/RadioButtonList';
|
export { RadioButtonList } from './Forms/RadioButtonList/RadioButtonList';
|
||||||
|
|
||||||
export { Input, getInputStyles } from './Input/Input';
|
export { Input, getInputStyles } from './Input/Input';
|
||||||
|
@ -131,14 +131,14 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
|
|||||||
public toggleNodeSelect(path: string[]) {
|
public toggleNodeSelect(path: string[]) {
|
||||||
let treeScopes = [...this.state.treeScopes];
|
let treeScopes = [...this.state.treeScopes];
|
||||||
|
|
||||||
let siblings = this.state.nodes;
|
let parentNode = this.state.nodes[''];
|
||||||
|
|
||||||
for (let idx = 0; idx < path.length - 1; idx++) {
|
for (let idx = 1; idx < path.length - 1; idx++) {
|
||||||
siblings = siblings[path[idx]].nodes;
|
parentNode = parentNode.nodes[path[idx]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeName = path[path.length - 1];
|
const nodeName = path[path.length - 1];
|
||||||
const { linkId } = siblings[nodeName];
|
const { linkId } = parentNode.nodes[nodeName];
|
||||||
|
|
||||||
const selectedIdx = treeScopes.findIndex(({ scopeName }) => scopeName === linkId);
|
const selectedIdx = treeScopes.findIndex(({ scopeName }) => scopeName === linkId);
|
||||||
|
|
||||||
@ -146,14 +146,17 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
|
|||||||
fetchScope(linkId!);
|
fetchScope(linkId!);
|
||||||
|
|
||||||
const selectedFromSameNode =
|
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 = {
|
const treeScope = {
|
||||||
scopeName: linkId!,
|
scopeName: linkId!,
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ treeScopes: !selectedFromSameNode ? [treeScope] : [...treeScopes, treeScope] });
|
this.setState({
|
||||||
|
treeScopes: parentNode?.disableMultiSelect || !selectedFromSameNode ? [treeScope] : [...treeScopes, treeScope],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
treeScopes.splice(selectedIdx, 1);
|
treeScopes.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ import {
|
|||||||
getFiltersInput,
|
getFiltersInput,
|
||||||
getClustersExpand,
|
getClustersExpand,
|
||||||
getClustersSelect,
|
getClustersSelect,
|
||||||
getClustersSlothClusterNorthSelect,
|
getClustersSlothClusterNorthRadio,
|
||||||
getClustersSlothClusterSouthSelect,
|
getClustersSlothClusterSouthRadio,
|
||||||
getDashboard,
|
getDashboard,
|
||||||
getDashboardsContainer,
|
getDashboardsContainer,
|
||||||
getDashboardsExpand,
|
getDashboardsExpand,
|
||||||
@ -158,8 +158,12 @@ describe('ScopesScene', () => {
|
|||||||
it('Respects only one select per container', async () => {
|
it('Respects only one select per container', async () => {
|
||||||
await userEvents.click(getFiltersInput());
|
await userEvents.click(getFiltersInput());
|
||||||
await userEvents.click(getClustersExpand());
|
await userEvents.click(getClustersExpand());
|
||||||
await userEvents.click(getClustersSlothClusterNorthSelect());
|
await userEvents.click(getClustersSlothClusterNorthRadio());
|
||||||
expect(getClustersSlothClusterSouthSelect()).toBeDisabled();
|
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 () => {
|
it('Search works', async () => {
|
||||||
|
@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
|||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { t, Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { NodesMap, TreeScope } from './types';
|
import { NodesMap, TreeScope } from './types';
|
||||||
@ -72,18 +72,32 @@ export function ScopesTreeLevel({
|
|||||||
|
|
||||||
const childNodePath = [...nodePath, childNode.name];
|
const childNodePath = [...nodePath, childNode.name];
|
||||||
|
|
||||||
|
const radioName = childNodePath.join('.');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={childNode.name} role="treeitem" aria-selected={childNode.isExpanded}>
|
<div key={childNode.name} role="treeitem" aria-selected={childNode.isExpanded}>
|
||||||
<div className={styles.itemTitle}>
|
<div className={styles.itemTitle}>
|
||||||
{childNode.isSelectable && !childNode.isExpanded ? (
|
{childNode.isSelectable && !childNode.isExpanded ? (
|
||||||
<Checkbox
|
node.disableMultiSelect ? (
|
||||||
checked={isSelected}
|
<RadioButtonDot
|
||||||
disabled={anyChildSelected && !isSelected && node.disableMultiSelect}
|
id={radioName}
|
||||||
data-testid={`scopes-tree-${childNode.name}-checkbox`}
|
name={radioName}
|
||||||
onChange={() => {
|
checked={isSelected}
|
||||||
onNodeSelectToggle(childNodePath);
|
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}
|
) : null}
|
||||||
|
|
||||||
{childNode.isExpandable && (
|
{childNode.isExpandable && (
|
||||||
@ -142,6 +156,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
fontSize: theme.typography.pxToRem(14),
|
fontSize: theme.typography.pxToRem(14),
|
||||||
lineHeight: theme.typography.pxToRem(22),
|
lineHeight: theme.typography.pxToRem(22),
|
||||||
padding: theme.spacing(0.5, 0),
|
padding: theme.spacing(0.5, 0),
|
||||||
|
|
||||||
|
'& > label': css({
|
||||||
|
gap: 0,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
itemChildren: css({
|
itemChildren: css({
|
||||||
paddingLeft: theme.spacing(4),
|
paddingLeft: theme.spacing(4),
|
||||||
|
@ -295,6 +295,7 @@ const selectors = {
|
|||||||
tree: {
|
tree: {
|
||||||
search: (nodeId: string) => `scopes-tree-${nodeId}-search`,
|
search: (nodeId: string) => `scopes-tree-${nodeId}-search`,
|
||||||
select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
|
select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
|
||||||
|
radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
|
||||||
expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
|
expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
|
||||||
title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
|
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 getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters'));
|
||||||
export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters'));
|
export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters'));
|
||||||
export const getClustersSlothClusterNorthSelect = () =>
|
export const getClustersSlothClusterNorthRadio = () =>
|
||||||
screen.getByTestId(selectors.tree.select('clusters-slothClusterNorth'));
|
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterNorth'));
|
||||||
export const getClustersSlothClusterSouthSelect = () =>
|
export const getClustersSlothClusterSouthRadio = () =>
|
||||||
screen.getByTestId(selectors.tree.select('clusters-slothClusterSouth'));
|
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterSouth'));
|
||||||
|
|
||||||
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
|
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
|
||||||
return new DashboardScene({
|
return new DashboardScene({
|
||||||
|
Loading…
Reference in New Issue
Block a user