deprecate collapsible prop, improve Collapse to allow for buttons in the header

This commit is contained in:
Ashley Harrison
2025-10-29 12:53:20 +00:00
parent e75610ed03
commit 02543bd5ce
24 changed files with 81 additions and 68 deletions
@@ -111,7 +111,6 @@ export const QueryPatternsModal = (props: Props) => {
}
)}
isOpen={isOpen}
collapsible={true}
onToggle={() => {
const action = isOpen ? 'close' : 'open';
reportInteraction(`grafana_prom_kickstart_toggle_pattern_card`, {
@@ -20,7 +20,6 @@ export function QueryOptionGroup({ title, children, collapsedInfo }: Props) {
<div className={styles.wrapper}>
<Collapse
className={styles.collapse}
collapsible
isOpen={isOpen}
onToggle={toggleOpen}
label={
@@ -2,6 +2,9 @@ import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import { Meta, StoryFn } from '@storybook/react';
import { IconButton } from '../IconButton/IconButton';
import { Stack } from '../Layout/Stack/Stack';
import { Collapse, ControlledCollapse } from './Collapse';
import mdx from './Collapse.mdx';
@@ -22,7 +25,6 @@ const meta: Meta<typeof Collapse> = {
children: 'Panel data',
isOpen: false,
label: 'Collapse panel',
collapsible: true,
},
argTypes: {
onToggle: { action: 'toggled' },
@@ -57,4 +59,39 @@ Controlled.parameters = {
},
};
export const WithCustomLabel: StoryFn<typeof Collapse> = (args) => {
const [, updateArgs] = useArgs();
return (
<Collapse
{...args}
onToggle={() => {
action('onToggle')({ isOpen: !args.isOpen });
updateArgs({ isOpen: !args.isOpen });
}}
label={
<Stack flex={1} alignItems="center" justifyContent="space-between">
Collapse panel
<Stack alignItems="center">
<IconButton
onClick={(event) => {
event.stopPropagation();
action('onDeleteClick')();
}}
aria-label="Delete"
name="trash-alt"
/>
</Stack>
</Stack>
}
>
<p>{args.children}</p>
</Collapse>
);
};
WithCustomLabel.parameters = {
controls: {
exclude: [...EXCLUDED_PROPS, 'label'],
},
};
export default meta;
@@ -42,7 +42,7 @@ describe('Collapse', () => {
const onToggle = jest.fn();
const { user } = setup(
<Collapse label={TEST_LABEL} onToggle={onToggle} collapsible>
<Collapse label={TEST_LABEL} onToggle={onToggle}>
<div>{contentText}</div>
</Collapse>
);
@@ -1,12 +1,12 @@
import { css, cx } from '@emotion/css';
import { useState } from 'react';
import { useId, useState } from 'react';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { useStyles2 } from '../../themes/ThemeContext';
import { clearButtonStyles } from '../Button/Button';
import { Icon } from '../Icon/Icon';
import { IconButton } from '../IconButton/IconButton';
const getStyles = (theme: GrafanaTheme2) => ({
collapse: css({
@@ -74,25 +74,21 @@ const getStyles = (theme: GrafanaTheme2) => ({
},
}),
header: css({
cursor: 'pointer',
label: 'collapse__header',
padding: theme.spacing(1, 2, 1, 2),
padding: theme.spacing(1),
display: 'flex',
gap: theme.spacing(1),
}),
headerCollapsed: css({
label: 'collapse__header--collapsed',
padding: theme.spacing(1, 2, 1, 2),
button: css({
marginRight: 0,
}),
headerLabel: css({
label: 'collapse__header-label',
fontWeight: theme.typography.fontWeightMedium,
marginRight: theme.spacing(1),
fontSize: theme.typography.size.md,
display: 'flex',
flex: '0 0 100%',
}),
icon: css({
label: 'collapse__icon',
margin: theme.spacing(0.25, 1, 0, -1),
flex: 1,
}),
});
@@ -103,12 +99,12 @@ export interface Props {
label: React.ReactNode;
/** Indicates loading state of the content */
loading?: boolean;
/** Toggle collapsed header icon */
collapsible?: boolean;
/** Callback for the toggle functionality */
onToggle?: (isOpen: boolean) => void;
/** Additional class name for the root element */
className?: string;
/** @deprecated this prop is no longer used and will be removed in Grafana 13 */
collapsible?: boolean;
}
export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.PropsWithChildren<Props>) => {
@@ -116,7 +112,6 @@ export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.Pr
return (
<Collapse
isOpen={open}
collapsible
{...otherProps}
onToggle={() => {
setOpen(!open);
@@ -128,35 +123,39 @@ export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.Pr
);
};
export const Collapse = ({
isOpen,
label,
loading,
collapsible,
onToggle,
className,
children,
}: React.PropsWithChildren<Props>) => {
const buttonStyles = useStyles2(clearButtonStyles);
export const Collapse = ({ isOpen, label, loading, onToggle, className, children }: React.PropsWithChildren<Props>) => {
const style = useStyles2(getStyles);
const labelId = useId();
const onClickToggle = () => {
if (onToggle) {
onToggle(!isOpen);
}
};
const panelClass = cx([style.collapse, className]);
const loaderClass = loading ? cx([style.loader, style.loaderActive]) : cx([style.loader]);
const headerClass = collapsible ? cx([style.header]) : cx([style.headerCollapsed]);
const loaderClass = loading ? cx([style.loader, style.loaderActive]) : style.loader;
return (
<div className={panelClass}>
<button type="button" className={cx(buttonStyles, headerClass)} onClick={onClickToggle}>
{collapsible && <Icon className={style.icon} name={isOpen ? 'angle-down' : 'angle-right'} />}
<div className={cx([style.headerLabel])}>{label}</div>
</button>
{/* the inner button handles keyboard a11y. this is a convenience for mouse users */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div className={style.header} onClick={onClickToggle}>
<IconButton
aria-describedby={labelId}
className={style.button}
aria-label={
isOpen
? t('grafana-ui.collapse.aria-label-collapse', 'Collapse panel')
: t('grafana-ui.collapse.aria-label-expand', 'Expand panel')
}
name={isOpen ? 'angle-down' : 'angle-right'}
/>
<div id={labelId} className={style.headerLabel}>
{label}
</div>
</div>
{isOpen && (
<div className={cx([style.collapseBody])}>
<div className={style.collapseBody}>
<div className={loaderClass} />
<div className={style.bodyContentWrapper}>{children}</div>
</div>
@@ -385,7 +385,6 @@ function TargetFolderNotEmptyWarning({ targetFolderRules }: { targetFolderRules:
)}
isOpen={showTargetRules}
onToggle={toggleShowTargetRules}
collapsible={true}
>
<RulesPreview rules={targetFolderRules} />
</Collapse>
@@ -150,7 +150,6 @@ const ImportToGMARules = () => {
label={t('alerting.import-to-gma.additional-settings', 'Additional settings')}
isOpen={optionsShowing}
onToggle={toggleOptions}
collapsible={true}
>
<Box marginLeft={1}>
<Box marginBottom={2}>
@@ -130,7 +130,6 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
/>
</Field>
<Collapse
collapsible
className={styles.collapse}
isOpen={isTimingOptionsExpanded}
label={t('alerting.am-root-route-form.label-timing-options', 'Timing options')}
@@ -171,7 +171,6 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
})}
</pre>
<Collapse
collapsible
isOpen={isLabelDescOpen}
onToggle={() => {
setIsLabelDescOpen(!isLabelDescOpen);
@@ -201,7 +200,6 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
</Field>
</Collapse>
<Collapse
collapsible
isOpen={isTransformOpen}
onToggle={() => {
setIsTransformOpen(!isTransformOpen);
@@ -24,7 +24,7 @@ import {
import { t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { Collapse } from '@grafana/ui';
import { PanelChrome } from '@grafana/ui';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
import { ExploreItemState } from 'app/types/explore';
@@ -308,8 +308,8 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
return (
<>
<LogsCrossFadeTransition visible={isLive}>
<Collapse label={t('explore.logs-container.label-logs', 'Logs')} loading={false} isOpen>
<LogsCrossFadeTransition visible={!isLive}>
<PanelChrome title={t('explore.logs-container.label-logs', 'Logs')}>
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveLogsWithTheme
@@ -324,9 +324,9 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
/>
)}
</LiveTailControls>
</Collapse>
</PanelChrome>
</LogsCrossFadeTransition>
<LogsCrossFadeTransition visible={!isLive}>
<LogsCrossFadeTransition visible={isLive}>
<Logs
exploreId={exploreId}
datasourceType={this.props.datasourceInstance?.type}
@@ -156,7 +156,6 @@ export function LogsSamplePanel(props: Props) {
</div>
}
isOpen={enabled}
collapsible={true}
onToggle={onToggleLogsSampleCollapse}
>
<OpenInSplitViewButton />
@@ -151,7 +151,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
return (
<div className={styles.container}>
<Collapse label={collapseLabel} collapsible={true} isOpen={showSpanFilters} onToggle={setShowSpanFilters}>
<Collapse label={collapseLabel} isOpen={showSpanFilters} onToggle={setShowSpanFilters}>
<InlineFieldRow className={styles.flexContainer}>
<InlineField label={t('explore.span-filters.label-service-name', 'Service name')} labelWidth={16}>
<Stack gap={0.5}>
@@ -329,7 +329,6 @@ export const LogLineContext = memo(
<div>{getLogRowContextUi(log, updateResults)}</div>
)}
<Collapse
collapsible={true}
isOpen={showLog}
onToggle={() => setShowLog(!showLog)}
className={styles.referenceLogLine}
@@ -131,7 +131,6 @@ export const LogLineDetailsComponent = memo(
<ControlledCollapse
className={styles.collapsable}
label={t('logs.log-line-details.log-line-section', 'Log line')}
collapsible
isOpen={logLineOpen}
onToggle={(isOpen: boolean) => handleToggle('logLineOpen', isOpen)}
>
@@ -140,7 +139,6 @@ export const LogLineDetailsComponent = memo(
{displayedFields.length > 0 && setDisplayedFields && (
<ControlledCollapse
label={t('logs.log-line-details.displayed-fields-section', 'Organize displayed fields')}
collapsible
isOpen={displayedFieldsOpen}
onToggle={(isOpen: boolean) => handleToggle('displayedFieldsOpen', isOpen)}
>
@@ -151,7 +149,6 @@ export const LogLineDetailsComponent = memo(
<ControlledCollapse
className={styles.collapsable}
label={t('logs.log-line-details.links-section', 'Links')}
collapsible
isOpen={linksOpen}
onToggle={(isOpen: boolean) => handleToggle('linksOpen', isOpen)}
>
@@ -161,7 +158,6 @@ export const LogLineDetailsComponent = memo(
{trace && (
<ControlledCollapse
label={t('logs.log-line-details.trace-section', 'Trace')}
collapsible
isOpen={traceOpen}
onToggle={(isOpen: boolean) => handleToggle('traceOpen', isOpen)}
>
@@ -174,7 +170,6 @@ export const LogLineDetailsComponent = memo(
className={styles.collapsable}
key={'fields'}
label={t('logs.log-line-details.fields-section', 'Fields')}
collapsible
isOpen={fieldsOpen}
onToggle={(isOpen: boolean) => handleToggle('fieldsOpen', isOpen)}
>
@@ -186,7 +181,6 @@ export const LogLineDetailsComponent = memo(
className={styles.collapsable}
key={group}
label={group}
collapsible
isOpen={store.getBool(`${logOptionsStorageKey}.log-details.${groupOptionName(group)}`, true)}
onToggle={(isOpen: boolean) => handleToggle(groupOptionName(group), isOpen)}
>
@@ -199,7 +193,6 @@ export const LogLineDetailsComponent = memo(
className={styles.collapsable}
key={'fields'}
label={t('logs.log-line-details.fields-section', 'Fields')}
collapsible
isOpen={fieldsOpen}
onToggle={(isOpen: boolean) => handleToggle('fieldsOpen', isOpen)}
>
@@ -38,7 +38,6 @@ export const DataHoverRows = ({ layers, activeTabIndex }: Props) => {
return shouldDisplayCollapse ? (
<Collapse
key={key}
collapsible
label={generateLabel(feature, idx)}
isOpen={rowMap.get(key)}
onToggle={() => {
@@ -186,7 +186,6 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
return (
<Collapse
label={category + ' ' + `(${visibleQueries[category]!.length})`}
collapsible={true}
isOpen={areDropdownsOpen[category]}
onToggle={(isOpen) => setAreDropdownsOpen({ ...areDropdownsOpen, [category]: isOpen })}
key={category}
@@ -19,7 +19,6 @@ const AdvancedMulti = ({ resources, onChange, renderAdvanced }: ResourcePickerPr
return (
<div data-testid={selectors.components.queryEditor.resourcePicker.advanced.collapse}>
<Collapse
collapsible
label={t('components.advanced-multi.label-advanced', 'Advanced')}
isOpen={isAdvancedOpen}
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
@@ -80,7 +80,7 @@ interface CollapseProps {
const CheatSheetCollapse = (props: CollapseProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Collapse label={props.label} isOpen={isOpen} onToggle={setIsOpen} key={props.key} collapsible>
<Collapse label={props.label} isOpen={isOpen} onToggle={setIsOpen} key={props.key}>
{props.children}
</Collapse>
);
@@ -113,7 +113,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P
<Divider />
<Collapse collapsible label="User Permissions" isOpen={isOpen} onToggle={() => setIsOpen((x) => !x)}>
<Collapse label="User Permissions" isOpen={isOpen} onToggle={() => setIsOpen((x) => !x)}>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to
query. <br />
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
@@ -19,7 +19,6 @@ export function QueryOptionGroup({ title, children, collapsedInfo }: Props) {
<div className={styles.wrapper}>
<Collapse
className={styles.collapse}
collapsible
isOpen={isOpen}
onToggle={toggleOpen}
label={
@@ -296,7 +296,6 @@ export function LokiContextUi(props: LokiContextUiProps) {
</div>
<Collapse
collapsible={true}
isOpen={isOpen}
onToggle={() => {
window.localStorage.setItem(IS_LOKI_LOG_CONTEXT_UI_OPEN, (!isOpen).toString());
@@ -99,7 +99,6 @@ export const QueryPatternsModal = (props: Props) => {
key={patternType}
label={`${capitalize(patternType)} query starters`}
isOpen={openTabs.includes(patternType)}
collapsible={true}
onToggle={() =>
setOpenTabs((tabs) =>
// close tab if it's already open, otherwise open it
@@ -60,7 +60,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<My
<Divider />
<Collapse collapsible label="User Permission" isOpen={isOpen} onToggle={() => setIsOpen((x) => !x)}>
<Collapse label="User Permission" isOpen={isOpen} onToggle={() => setIsOpen((x) => !x)}>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to
query. <br />
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
@@ -25,7 +25,6 @@ export function QueryOptionGroup({ title, children, collapsedInfo, queryStats, o
<div className={styles.wrapper}>
<Collapse
className={styles.collapse}
collapsible
isOpen={propsIsOpen ?? isOpen}
onToggle={onToggle ?? toggleOpen}
label={