mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
Explore: Unified Node Graph Container (#72558)
* Improve naming; fix tests * Pass countWarning to titleItems * Center warning text * Revert variable name * Switch from arrow function to direct assignment * Add ability to make component controlled * Replace remaining isOpen with collapsed * Fix test; update story and docs * Add test; add uncontrolled functionality
This commit is contained in:
parent
d293b08e52
commit
19ae937aa8
@ -512,24 +512,37 @@ The panel can be collapsed/expanded by clicking on the chevron or the title.
|
||||
> _Note: `collapsible` and `hoverHeader` props are mutually exclusive and cannot be used in the same panel._
|
||||
|
||||
```tsx
|
||||
<PanelChrome title="My awesome panel title" width={400} height={200} collapsible={true}>
|
||||
{(innerwidth, innerheight) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</PanelChrome>
|
||||
function Container() {
|
||||
const [isCollapsed, setCollapsed] = useState(true);
|
||||
|
||||
return (
|
||||
<PanelChrome
|
||||
title="My awesome panel title"
|
||||
width={400}
|
||||
height={200}
|
||||
collapsible={true}
|
||||
collapsed={isCollapsed}
|
||||
onToggleCollapse={(isCollapsed) => setCollapsed(isCollapsed)}
|
||||
>
|
||||
{(innerwidth, innerheight) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</PanelChrome>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Preview>
|
||||
|
@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { merge } from 'lodash';
|
||||
import React, { CSSProperties, useState, ReactNode } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
import { useInterval, useToggle } from 'react-use';
|
||||
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { Button, Icon, PanelChrome, PanelChromeProps, RadioButtonGroup } from '@grafana/ui';
|
||||
@ -14,6 +14,9 @@ import { Menu } from '../Menu/Menu';
|
||||
|
||||
import mdx from './PanelChrome.mdx';
|
||||
|
||||
const PANEL_WIDTH = 400;
|
||||
const PANEL_HEIGHT = 150;
|
||||
|
||||
const meta: Meta<typeof PanelChrome> = {
|
||||
title: 'Visualizations/PanelChrome',
|
||||
component: PanelChrome,
|
||||
@ -39,8 +42,8 @@ function getContentStyle(): CSSProperties {
|
||||
|
||||
function renderPanel(name: string, overrides?: Partial<PanelChromeProps>) {
|
||||
const props: PanelChromeProps = {
|
||||
width: 400,
|
||||
height: 150,
|
||||
width: PANEL_WIDTH,
|
||||
height: PANEL_HEIGHT,
|
||||
children: () => undefined,
|
||||
};
|
||||
|
||||
@ -57,6 +60,33 @@ function renderPanel(name: string, overrides?: Partial<PanelChromeProps>) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderCollapsiblePanel(name: string, overrides?: Partial<PanelChromeProps>) {
|
||||
const props: PanelChromeProps = {
|
||||
width: PANEL_WIDTH,
|
||||
height: PANEL_HEIGHT,
|
||||
children: () => undefined,
|
||||
collapsible: true,
|
||||
};
|
||||
|
||||
merge(props, overrides);
|
||||
|
||||
const contentStyle = getContentStyle();
|
||||
|
||||
const ControlledCollapseComponent = () => {
|
||||
const [collapsed, toggleCollapsed] = useToggle(false);
|
||||
|
||||
return (
|
||||
<PanelChrome {...props} collapsed={collapsed} onToggleCollapse={toggleCollapsed}>
|
||||
{(innerWidth, innerHeight) => {
|
||||
return <div style={{ width: innerWidth, height: innerHeight, ...contentStyle }}>{name}</div>;
|
||||
}}
|
||||
</PanelChrome>
|
||||
);
|
||||
};
|
||||
|
||||
return <ControlledCollapseComponent />;
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="View" icon="eye" />
|
||||
@ -215,7 +245,7 @@ export const Examples = () => {
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
{renderPanel('Collapsible panel', {
|
||||
{renderCollapsiblePanel('Collapsible panel', {
|
||||
title: 'Default title',
|
||||
collapsible: true,
|
||||
})}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { screen, render, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
@ -18,6 +19,27 @@ const setup = (propOverrides?: Partial<PanelChromeProps>) => {
|
||||
return render(<PanelChrome {...props} />);
|
||||
};
|
||||
|
||||
const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) => {
|
||||
const props: PanelChromeProps = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
children: (innerWidth, innerHeight) => {
|
||||
return <div style={{ width: innerWidth, height: innerHeight, color: 'pink' }}>Panel's Content</div>;
|
||||
},
|
||||
collapsible: true,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const ControlledCollapseComponent = () => {
|
||||
const [collapsed, toggleCollapsed] = useToggle(false);
|
||||
|
||||
return <PanelChrome {...props} collapsed={collapsed} onToggleCollapse={toggleCollapsed} />;
|
||||
};
|
||||
|
||||
return render(<ControlledCollapseComponent />);
|
||||
};
|
||||
|
||||
it('renders an empty panel with required props only', () => {
|
||||
setup();
|
||||
|
||||
@ -125,8 +147,25 @@ it('renders streaming indicator in the panel header if loadingState is streaming
|
||||
expect(screen.getByTestId('panel-streaming')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('collapes the panel when user clicks on the chevron or the title', () => {
|
||||
setup({ collapsible: true, title: 'Default title' });
|
||||
it('collapses the controlled panel when user clicks on the chevron or the title', () => {
|
||||
setupWithToggleCollapsed({ title: 'Default title' });
|
||||
|
||||
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
||||
|
||||
const button = screen.getByText('Default title');
|
||||
// collapse button should have same aria-controls as the panel's content
|
||||
expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id);
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
|
||||
// aria-controls should be removed when panel is collapsed
|
||||
expect(button).not.toHaveAttribute('aria-controlls');
|
||||
expect(button.parentElement?.parentElement?.nextElementSibling?.id).toBe(undefined);
|
||||
});
|
||||
|
||||
it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => {
|
||||
setup({ title: 'Default title', collapsible: true });
|
||||
|
||||
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
|
||||
|
||||
|
@ -71,12 +71,19 @@ interface AutoSize extends BaseProps {
|
||||
|
||||
interface Collapsible {
|
||||
collapsible: boolean;
|
||||
collapsed?: boolean;
|
||||
/**
|
||||
* callback when collapsing or expanding the panel
|
||||
*/
|
||||
onToggleCollapse?: (collapsed: boolean) => void;
|
||||
hoverHeader?: never;
|
||||
hoverHeaderOffset?: never;
|
||||
}
|
||||
|
||||
interface HoverHeader {
|
||||
collapsible?: never;
|
||||
collapsed?: never;
|
||||
onToggleCollapse?: never;
|
||||
hoverHeader?: boolean;
|
||||
hoverHeaderOffset?: number;
|
||||
}
|
||||
@ -111,14 +118,21 @@ export function PanelChrome({
|
||||
onCancelQuery,
|
||||
onOpenMenu,
|
||||
collapsible = false,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
}: PanelChromeProps) {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelContentId = useId();
|
||||
|
||||
const hasHeader = !hoverHeader;
|
||||
|
||||
const [isOpen, toggleOpen] = useToggle(true);
|
||||
|
||||
const hasHeader = !hoverHeader;
|
||||
// if collapsed is not defined, then component is uncontrolled and state is managed internally
|
||||
if (collapsed === undefined) {
|
||||
collapsed = !isOpen;
|
||||
}
|
||||
|
||||
// hover menu is only shown on hover when not on touch devices
|
||||
const showOnHoverClass = 'show-on-hover';
|
||||
@ -129,7 +143,7 @@ export function PanelChrome({
|
||||
padding,
|
||||
theme,
|
||||
headerHeight,
|
||||
isOpen,
|
||||
collapsed,
|
||||
height,
|
||||
width
|
||||
);
|
||||
@ -139,7 +153,7 @@ export function PanelChrome({
|
||||
cursor: dragClass ? 'move' : 'auto',
|
||||
};
|
||||
|
||||
const containerStyles: CSSProperties = { width, height: isOpen ? height : headerHeight };
|
||||
const containerStyles: CSSProperties = { width, height: !collapsed ? height : headerHeight };
|
||||
const [ref, { width: loadingBarWidth }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
/** Old property name now maps to actions */
|
||||
@ -154,12 +168,17 @@ export function PanelChrome({
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButtonStyles}
|
||||
onClick={toggleOpen}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={isOpen ? panelContentId : undefined}
|
||||
onClick={() => {
|
||||
toggleOpen();
|
||||
if (onToggleCollapse) {
|
||||
onToggleCollapse(!collapsed);
|
||||
}
|
||||
}}
|
||||
aria-expanded={!collapsed}
|
||||
aria-controls={!collapsed ? panelContentId : undefined}
|
||||
>
|
||||
<Icon
|
||||
name={isOpen ? 'angle-down' : 'angle-right'}
|
||||
name={!collapsed ? 'angle-down' : 'angle-right'}
|
||||
aria-hidden={!!title}
|
||||
aria-label={!title ? 'toggle collapse panel' : undefined}
|
||||
/>
|
||||
@ -265,7 +284,7 @@ export function PanelChrome({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
{!collapsed && (
|
||||
<div
|
||||
id={panelContentId}
|
||||
className={cx(styles.content, height === undefined && styles.containNone)}
|
||||
@ -295,7 +314,7 @@ const getContentStyle = (
|
||||
padding: string,
|
||||
theme: GrafanaTheme2,
|
||||
headerHeight: number,
|
||||
isOpen: boolean,
|
||||
collapsed: boolean,
|
||||
height?: number,
|
||||
width?: number
|
||||
) => {
|
||||
@ -314,7 +333,7 @@ const getContentStyle = (
|
||||
innerHeight = height - headerHeight - panelPadding - panelBorder;
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
if (collapsed) {
|
||||
innerHeight = headerHeight;
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,8 @@ describe('NodeGraphContainer', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
// Make sure we only show header in the collapsible
|
||||
expect(container.firstChild?.childNodes.length).toBe(1);
|
||||
// Make sure we only show header and loading bar container from PanelChrome in the collapsible
|
||||
expect(container.firstChild?.childNodes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('shows the graph if not with trace view', async () => {
|
||||
@ -33,7 +33,7 @@ describe('NodeGraphContainer', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.firstChild?.childNodes.length).toBe(2);
|
||||
expect(container.firstChild?.childNodes.length).toBe(3);
|
||||
expect(container.querySelector('svg')).toBeInTheDocument();
|
||||
await screen.findByLabelText(/Node: tempo-querier/);
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import { useToggle, useWindowSize } from 'react-use';
|
||||
|
||||
import { applyFieldOverrides, DataFrame, GrafanaTheme2, SplitOpen } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Collapse, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { useStyles2, useTheme2, PanelChrome } from '@grafana/ui';
|
||||
|
||||
import { NodeGraph } from '../../../plugins/panel/nodeGraph';
|
||||
import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames';
|
||||
@ -15,6 +15,8 @@ import { useLinks } from '../utils/links';
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
warningText: css`
|
||||
label: warningText;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
@ -53,9 +55,10 @@ export function UnconnectedNodeGraphContainer(props: Props) {
|
||||
});
|
||||
|
||||
const { nodes } = useCategorizeFrames(frames);
|
||||
const [open, toggleOpen] = useToggle(false);
|
||||
const [collapsed, toggleCollapsed] = useToggle(true);
|
||||
|
||||
const toggled = () => {
|
||||
toggleOpen();
|
||||
toggleCollapsed();
|
||||
reportInteraction('grafana_traces_node_graph_panel_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
@ -81,12 +84,13 @@ export function UnconnectedNodeGraphContainer(props: Props) {
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
label={<span>Node graph{countWarning} </span>}
|
||||
collapsible={withTraceView}
|
||||
<PanelChrome
|
||||
title={`Node graph`}
|
||||
titleItems={countWarning}
|
||||
// We allow collapsing this only when it is shown together with trace view.
|
||||
isOpen={withTraceView ? open : true}
|
||||
onToggle={withTraceView ? () => toggled() : undefined}
|
||||
collapsible={!!withTraceView}
|
||||
collapsed={withTraceView ? collapsed : false}
|
||||
onToggleCollapse={withTraceView ? toggled : undefined}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
@ -101,7 +105,7 @@ export function UnconnectedNodeGraphContainer(props: Props) {
|
||||
>
|
||||
<NodeGraph dataFrames={frames} getLinks={getLinks} />
|
||||
</div>
|
||||
</Collapse>
|
||||
</PanelChrome>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user