grafana/public/app/features/logs/components/panel/LogListControls.tsx

827 lines
29 KiB
TypeScript

import { css, cx } from '@emotion/css';
import { capitalize } from 'lodash';
import { MouseEvent, useCallback, useMemo } from 'react';
import {
CoreApp,
EventBus,
LogLevel,
LogsDedupDescription,
LogsDedupStrategy,
LogsSortOrder,
store,
} from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data/';
import { t } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { Dropdown, Menu, useStyles2 } from '@grafana/ui';
import { LogsVisualisationType } from '../../../explore/Logs/Logs';
import { DownloadFormat } from '../../utils';
import { useLogListContext } from './LogListContext';
import { LogListControlsOption, LogListControlsSelectOption } from './LogListControlsOption';
import { useLogListSearchContext } from './LogListSearchContext';
import { ScrollToLogsEvent } from './virtualization';
type Props = {
eventBus: EventBus;
visualisationType?: LogsVisualisationType;
};
const DEDUP_OPTIONS = [
LogsDedupStrategy.none,
LogsDedupStrategy.exact,
LogsDedupStrategy.numbers,
LogsDedupStrategy.signature,
];
const FILTER_LEVELS: LogLevel[] = [
LogLevel.info,
LogLevel.debug,
LogLevel.trace,
LogLevel.warning,
LogLevel.error,
LogLevel.critical,
LogLevel.unknown,
];
export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => {
const {
app,
controlsExpanded,
dedupStrategy,
downloadLogs,
filterLevels,
fontSize,
forceEscape,
hasUnescapedContent,
prettifyJSON,
setControlsExpanded,
setDedupStrategy,
setFilterLevels,
setFontSize,
setForceEscape,
setPrettifyJSON,
setShowTime,
setShowUniqueLabels,
setSortOrder,
setSyntaxHighlighting,
setWrapLogMessage,
showTime,
showUniqueLabels,
sortOrder,
syntaxHighlighting,
wrapLogMessage,
logOptionsStorageKey,
} = useLogListContext();
const { hideSearch, searchVisible, showSearch } = useLogListSearchContext();
const styles = useStyles2(getStyles, controlsExpanded);
const onScrollToTopClick = useCallback(() => {
reportInteraction('logs_log_list_controls_scroll_top_clicked');
eventBus.publish(
new ScrollToLogsEvent({
scrollTo: 'top',
})
);
}, [eventBus]);
const onScrollToBottomClick = useCallback(() => {
reportInteraction('logs_log_list_controls_scroll_bottom_clicked');
eventBus.publish(
new ScrollToLogsEvent({
scrollTo: 'bottom',
})
);
}, [eventBus]);
const onExpandControlsClick = useCallback(() => {
reportInteraction('logs_log_list_controls_expand_controls_clicked');
setControlsExpanded(!controlsExpanded);
store.set(`${logOptionsStorageKey}.controlsExpanded`, !controlsExpanded);
}, [controlsExpanded, logOptionsStorageKey, setControlsExpanded]);
const onForceEscapeClick = useCallback(() => {
reportInteraction('logs_log_list_controls_force_escape_clicked');
setForceEscape(!forceEscape);
}, [forceEscape, setForceEscape]);
const onFilterLevelClick = useCallback(
(level?: LogLevel) => {
reportInteraction('logs_log_list_controls_level_clicked', {
level,
});
if (level === undefined) {
setFilterLevels([]);
} else if (!filterLevels.includes(level)) {
setFilterLevels([...filterLevels, level]);
} else {
setFilterLevels(filterLevels.filter((filterLevel) => filterLevel !== level));
}
},
[filterLevels, setFilterLevels]
);
const onFontSizeClick = useCallback(() => {
const newSize = fontSize === 'default' ? 'small' : 'default';
reportInteraction('logs_log_list_controls_font_size_clicked', {
size: newSize,
});
setFontSize(newSize);
}, [fontSize, setFontSize]);
const onShowTimestampsClick = useCallback(() => {
reportInteraction('logs_log_list_controls_show_time_clicked', {
show_time: !showTime,
});
setShowTime(!showTime);
}, [setShowTime, showTime]);
const onShowUniqueLabelsClick = useCallback(() => {
reportInteraction('logs_log_list_controls_show_unique_labels_clicked', {
show_unique_labels: showUniqueLabels,
});
setShowUniqueLabels(!showUniqueLabels);
}, [setShowUniqueLabels, showUniqueLabels]);
const onSortOrderClick = useCallback(() => {
reportInteraction('logs_log_list_controls_sort_order_clicked', {
order: sortOrder === LogsSortOrder.Ascending ? LogsSortOrder.Descending : LogsSortOrder.Ascending,
});
setSortOrder(sortOrder === LogsSortOrder.Ascending ? LogsSortOrder.Descending : LogsSortOrder.Ascending);
}, [setSortOrder, sortOrder]);
const onSetPrettifyJSONClick = useCallback(() => {
reportInteraction('logs_log_list_controls_prettify_json_clicked', {
state: !prettifyJSON,
});
setPrettifyJSON(!prettifyJSON);
}, [prettifyJSON, setPrettifyJSON]);
const onSyntaxHightlightingClick = useCallback(() => {
reportInteraction('logs_log_list_controls_syntax_clicked', {
state: !syntaxHighlighting,
});
setSyntaxHighlighting(!syntaxHighlighting);
}, [setSyntaxHighlighting, syntaxHighlighting]);
const onWrapLogMessageClick = useCallback(
(e: MouseEvent) => {
e.preventDefault();
reportInteraction('logs_log_list_controls_wrap_clicked', {
state: !wrapLogMessage,
});
setWrapLogMessage(!wrapLogMessage);
},
[setWrapLogMessage, wrapLogMessage]
);
const deduplicationMenu = useMemo(
() => (
<Menu>
{DEDUP_OPTIONS.map((option) => (
<Menu.Item
key={option}
className={dedupStrategy === option ? styles.menuItemActive : undefined}
description={LogsDedupDescription[option]}
label={capitalize(option)}
onClick={() => {
setDedupStrategy(option);
reportInteraction('logs_log_list_controls_deduplication_clicked', {
option,
});
}}
/>
))}
</Menu>
),
[dedupStrategy, setDedupStrategy, styles.menuItemActive]
);
const filterLevelsMenu = useMemo(
() => (
<Menu>
<Menu.Item
key={'all'}
className={filterLevels.length === 0 ? styles.menuItemActive : undefined}
label={t('logs.logs-controls.display-level-all', 'All levels')}
onClick={() => onFilterLevelClick()}
/>
{FILTER_LEVELS.map((level) => (
<Menu.Item
key={level}
className={filterLevels.includes(level) ? styles.menuItemActive : undefined}
label={capitalize(level)}
onClick={() => onFilterLevelClick(level)}
/>
))}
</Menu>
),
[filterLevels, onFilterLevelClick, styles.menuItemActive]
);
const downloadMenu = useMemo(
() => (
<Menu>
<Menu.Item
label={t('logs.logs-controls.download-logs.txt', 'txt')}
onClick={() => {
downloadLogs(DownloadFormat.Text);
reportInteraction('logs_log_list_controls_downloaded_logs', {
format: DownloadFormat.Text,
});
}}
/>
<Menu.Item
label={t('logs.logs-controls.download-logs.json', 'json')}
onClick={() => {
downloadLogs(DownloadFormat.Json);
reportInteraction('logs_log_list_controls_downloaded_logs', {
format: DownloadFormat.Json,
});
}}
/>
<Menu.Item
label={t('logs.logs-controls.download-logs.csv', 'csv')}
onClick={() => {
downloadLogs(DownloadFormat.CSV);
reportInteraction('logs_log_list_controls_downloaded_logs', {
format: DownloadFormat.CSV,
});
}}
/>
</Menu>
),
[downloadLogs]
);
const inDashboard = app === CoreApp.Dashboard || app === CoreApp.PanelEditor || app === CoreApp.PanelViewer;
return (
<div className={styles.navContainer}>
<>
<LogListControlsOption
expanded={controlsExpanded}
name="arrow-from-right"
className={cx(styles.controlButton, styles.controlsExpandedButton)}
variant="secondary"
onClick={onExpandControlsClick}
label={
controlsExpanded
? t('logs.logs-controls.label.collapse', 'Expanded')
: t('logs.logs-controls.label.expand', 'Collapsed')
}
tooltip={
controlsExpanded ? t('logs.logs-controls.collapse', 'Collapse') : t('logs.logs-controls.expand', 'Expand')
}
size="lg"
/>
{visualisationType === 'logs' && (
<LogListControlsOption
expanded={controlsExpanded}
name="arrow-down"
className={styles.controlButton}
variant="secondary"
onClick={onScrollToBottomClick}
tooltip={t('logs.logs-controls.scroll-bottom', 'Scroll to bottom')}
size="lg"
/>
)}
</>
{!inDashboard ? (
<>
<LogListControlsOption
expanded={controlsExpanded}
name={sortOrder === LogsSortOrder.Descending ? 'sort-amount-up' : 'sort-amount-down'}
className={styles.controlButton}
onClick={onSortOrderClick}
label={
sortOrder === LogsSortOrder.Descending
? t('logs.logs-controls.labels.newest-first', 'Newest logs first')
: t('logs.logs-controls.labels.oldest-first', 'Oldest logs first')
}
tooltip={
sortOrder === LogsSortOrder.Descending
? t('logs.logs-controls.newest-first', 'Sorted by newest logs first - Click to show oldest first')
: t('logs.logs-controls.oldest-first', 'Sorted by oldest logs first - Click to show newest first')
}
size="lg"
/>
{visualisationType === 'logs' && (
<>
<div className={styles.divider} />
{config.featureToggles.newLogsPanel && (
<LogListControlsOption
expanded={controlsExpanded}
name={'search'}
className={searchVisible ? styles.controlButtonActive : styles.controlButton}
onClick={searchVisible ? hideSearch : showSearch}
label={
searchVisible
? t('logs.logs-controls.labels.hide-search', 'Close search')
: t('logs.logs-controls.labels.show-search', 'Search logs')
}
tooltip={
searchVisible
? t('logs.logs-controls.hide-search', 'Close search')
: t('logs.logs-controls.show-search', 'Search in logs result')
}
size="lg"
/>
)}
<Dropdown overlay={deduplicationMenu} placement="auto-end">
<LogListControlsOption
expanded={controlsExpanded}
name={'filter'}
className={
dedupStrategy !== LogsDedupStrategy.none ? styles.controlButtonActive : styles.controlButton
}
tooltip={t('logs.logs-controls.deduplication', 'Deduplication')}
size="lg"
/>
</Dropdown>
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<LogListControlsOption
expanded={controlsExpanded}
name={'gf-logs'}
className={
filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton
}
label={t('logs.logs-controls.filter-levels', 'Filter levels')}
tooltip={t('logs.logs-controls.tooltip.filter-level', 'Filter logs result by level')}
size="lg"
/>
</Dropdown>
<div className={styles.divider} />
{config.featureToggles.newLogsPanel ? (
<TimestampResolutionButton expanded={controlsExpanded} />
) : (
<LogListControlsOption
expanded={controlsExpanded}
name="clock-nine"
aria-pressed={showTime}
className={showTime ? styles.controlButtonActive : styles.controlButton}
onClick={onShowTimestampsClick}
tooltip={
showTime
? t('logs.logs-controls.hide-timestamps', 'Hide timestamps')
: t('logs.logs-controls.show-timestamps', 'Show timestamps')
}
size="lg"
/>
)}
{/* When this is used in a Plugin context, app is unknown */}
{showUniqueLabels !== undefined && app !== CoreApp.Unknown && (
<LogListControlsOption
expanded={controlsExpanded}
name="tag-alt"
aria-pressed={showUniqueLabels}
className={showUniqueLabels ? styles.controlButtonActive : styles.controlButton}
onClick={onShowUniqueLabelsClick}
tooltip={
showUniqueLabels
? t('logs.logs-controls.hide-unique-labels', 'Hide unique labels')
: t('logs.logs-controls.show-unique-labels', 'Show unique labels')
}
size="lg"
/>
)}
{config.featureToggles.newLogsPanel ? (
<WrapLogMessageButton expanded={controlsExpanded} />
) : (
<LogListControlsOption
expanded={controlsExpanded}
name="wrap-text"
className={wrapLogMessage ? styles.controlButtonActive : styles.controlButton}
aria-pressed={wrapLogMessage}
onClick={onWrapLogMessageClick}
tooltip={
wrapLogMessage
? t('logs.logs-controls.unwrap-lines', 'Unwrap lines')
: t('logs.logs-controls.wrap-lines', 'Wrap lines')
}
size="lg"
/>
)}
{prettifyJSON !== undefined && !config.featureToggles.newLogsPanel && (
<LogListControlsOption
expanded={controlsExpanded}
name="brackets-curly"
aria-pressed={prettifyJSON}
className={prettifyJSON ? styles.controlButtonActive : styles.controlButton}
onClick={onSetPrettifyJSONClick}
tooltip={
prettifyJSON
? t('logs.logs-controls.disable-prettify-json', 'Collapse JSON logs')
: t('logs.logs-controls.prettify-json', 'Expand JSON logs')
}
size="lg"
/>
)}
{syntaxHighlighting !== undefined && (
<LogListControlsOption
expanded={controlsExpanded}
name="brackets-curly"
className={syntaxHighlighting ? styles.controlButtonActive : styles.controlButton}
aria-pressed={syntaxHighlighting}
onClick={onSyntaxHightlightingClick}
label={
syntaxHighlighting
? t('logs.logs-controls.label.disable-highlighting', 'Highlight text')
: t('logs.logs-controls.label.enable-highlighting', 'Plain text')
}
tooltip={
syntaxHighlighting
? t('logs.logs-controls.tooltip.disable-highlighting', 'Disable highlighting')
: t('logs.logs-controls.tooltip.enable-highlighting', 'Enable highlighting')
}
size="lg"
/>
)}
{config.featureToggles.newLogsPanel && (
<LogListControlsOption
expanded={controlsExpanded}
name="text-fields"
className={fontSize === 'small' ? styles.controlButtonActive : styles.controlButton}
aria-pressed={Boolean(fontSize)}
onClick={onFontSizeClick}
label={
fontSize === 'default'
? t('logs.logs-controls.labels.font-large', 'Large font')
: t('logs.logs-controls.labels.font-small', 'Small font')
}
tooltip={
fontSize === 'default'
? t('logs.logs-controls.font-small', 'Set small font')
: t('logs.logs-controls.font-large', 'Set large font')
}
size="lg"
/>
)}
{hasUnescapedContent && (
<LogListControlsOption
expanded={controlsExpanded}
name="enter"
aria-pressed={forceEscape}
className={forceEscape ? styles.controlButtonActive : styles.controlButton}
onClick={onForceEscapeClick}
label={
forceEscape
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
: t('logs.logs-controls.label.escape-newlines', 'Escape newlines')
}
tooltip={
forceEscape
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
: t(
'logs.logs-controls.escape-newlines',
'Fix incorrectly escaped newline and tab sequences in log lines'
)
}
size="lg"
/>
)}
</>
)}
{!config.exploreHideLogsDownload && (
<>
<div className={styles.divider} />
<Dropdown overlay={downloadMenu} placement="auto-end">
<LogListControlsOption
expanded={controlsExpanded}
name="download-alt"
className={styles.controlButton}
label={t('logs.logs-controls.download', 'Download logs')}
tooltip={t('logs.logs-controls.tooltip.download', 'Download')}
size="lg"
/>
</Dropdown>
</>
)}
</>
) : (
<>
{config.featureToggles.newLogsPanel && (
<LogListControlsOption
expanded={controlsExpanded}
name={'search'}
className={searchVisible ? styles.controlButtonActive : styles.controlButton}
onClick={searchVisible ? hideSearch : showSearch}
label={
searchVisible
? t('logs.logs-controls.labels.hide-search', 'Close search')
: t('logs.logs-controls.labels.show-search', 'Search logs')
}
tooltip={
searchVisible
? t('logs.logs-controls.hide-search', 'Close search')
: t('logs.logs-controls.show-search', 'Search in logs result')
}
size="lg"
/>
)}
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<LogListControlsOption
expanded={controlsExpanded}
name={'gf-logs'}
className={filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton}
label={t('logs.logs-controls.filter-levels', 'Filter levels')}
tooltip={t('logs.logs-controls.tooltip.filter-level', 'Filter logs result by level')}
size="lg"
/>
</Dropdown>
{visualisationType === 'logs' && hasUnescapedContent && (
<LogListControlsOption
expanded={controlsExpanded}
name="enter"
aria-pressed={forceEscape}
className={forceEscape ? styles.controlButtonActive : styles.controlButton}
onClick={onForceEscapeClick}
label={
forceEscape
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
: t('logs.logs-controls.label.escape-newlines', 'Escape newlines')
}
tooltip={
forceEscape
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
: t(
'logs.logs-controls.escape-newlines',
'Fix incorrectly escaped newline and tab sequences in log lines'
)
}
size="lg"
/>
)}
</>
)}
{visualisationType === 'logs' && (
<LogListControlsOption
stickToBottom={true}
expanded={controlsExpanded}
name="arrow-up"
data-testid="scrollToTop"
className={styles.scrollToTopButton}
variant="secondary"
onClick={onScrollToTopClick}
tooltip={t('logs.logs-controls.scroll-top', 'Scroll to top')}
size="lg"
/>
)}
</div>
);
};
interface LogSelectOptionProps {
expanded: boolean;
}
const TimestampResolutionButton = ({ expanded }: LogSelectOptionProps) => {
const styles = useStyles2(getWrapButtonStyles, expanded);
const { setTimestampResolution, setShowTime, showTime, timestampResolution } = useLogListContext();
const hide = useCallback(() => {
setShowTime(false);
reportInteraction('logs_log_list_controls_show_time_clicked', {
show_time: false,
});
}, [setShowTime]);
const showMs = useCallback(() => {
setShowTime(true);
setTimestampResolution('ms');
reportInteraction('logs_log_list_controls_show_time_clicked', {
show_time: false,
resolution: 'ms',
});
}, [setShowTime, setTimestampResolution]);
const showNs = useCallback(() => {
setShowTime(true);
setTimestampResolution('ns');
reportInteraction('logs_log_list_controls_show_time_clicked', {
show_time: false,
resolution: 'ns',
});
}, [setShowTime, setTimestampResolution]);
const timestampMenu = useMemo(
() => (
<Menu>
<Menu.Item
label={t('logs.logs-controls.timestamp.hide', 'Hide timestamps')}
className={!showTime ? styles.menuItemActive : undefined}
onClick={hide}
/>
<Menu.Item
label={t('logs.logs-controls.timestamp.milliseconds', 'Show millisecond timestamps')}
className={showTime && timestampResolution === 'ms' ? styles.menuItemActive : undefined}
onClick={showMs}
/>
<Menu.Item
label={t('logs.logs-controls.timestamp.nanoseconds', 'Show nanosecond timestamps')}
className={showTime && timestampResolution === 'ns' ? styles.menuItemActive : undefined}
onClick={showNs}
/>
</Menu>
),
[hide, showMs, showNs, showTime, styles.menuItemActive, timestampResolution]
);
const labelText = !showTime
? t('logs.logs-controls.timestamp.label-hide', 'Hide timestamps')
: timestampResolution === 'ms'
? t('logs.logs-controls.timestamp.label-ms', 'Display ms')
: t('logs.logs-controls.timestamp.label-ns', 'Display ns');
const customTagText =
timestampResolution === 'ms'
? t('logs.logs-controls.resolution-ms', 'ms')
: t('logs.logs-controls.resolution-ns', 'ns');
return (
<LogListControlsSelectOption
expanded={expanded}
name={'clock-nine'}
isActive={showTime}
dropdown={timestampMenu}
tooltip={t('logs.logs-controls.timestamp.tooltip', 'Set timestamp format')}
label={labelText}
buttonAriaLabel={t('logs.logs-controls.timestamp.label', 'Log timestamps')}
customTagText={customTagText}
/>
);
};
const WrapLogMessageButton = ({ expanded }: LogSelectOptionProps) => {
const styles = useStyles2(getWrapButtonStyles, expanded);
const { prettifyJSON, setPrettifyJSON, setWrapLogMessage, wrapLogMessage } = useLogListContext();
/**
* This component currently controls two internal states: line wrapping and JSON formatting.
* The state transition is as follows:
* - Line wrapping and JSON formatting disabled.
* - Line wrapping enabled.
* - Line wrapping and JSON formatting enabled.
*
* Line wrapping also controls JSON formatting, because with line wrapping disabled,
* JSON formatting has no effect, so one is related with the other.
*/
const disable = useCallback(() => {
setWrapLogMessage(false);
setPrettifyJSON(false);
reportInteraction('logs_log_list_controls_wrap_clicked', {
state: false,
prettify: false,
});
}, [setPrettifyJSON, setWrapLogMessage]);
const wrap = useCallback(() => {
setWrapLogMessage(true);
setPrettifyJSON(false);
reportInteraction('logs_log_list_controls_wrap_clicked', {
state: true,
prettify: false,
});
}, [setPrettifyJSON, setWrapLogMessage]);
const wrapAndPrettify = useCallback(() => {
setWrapLogMessage(true);
setPrettifyJSON(true);
reportInteraction('logs_log_list_controls_wrap_clicked', {
state: true,
prettify: true,
});
}, [setPrettifyJSON, setWrapLogMessage]);
const wrappingMenu = useMemo(
() => (
<Menu>
<Menu.Item
label={t('logs.logs-controls.line-wrapping.hide', 'Disable line wrapping')}
className={!wrapLogMessage ? styles.menuItemActive : undefined}
onClick={disable}
/>
<Menu.Item
label={t('logs.logs-controls.line-wrapping.enable', 'Enable line wrapping')}
className={wrapLogMessage && !prettifyJSON ? styles.menuItemActive : undefined}
onClick={wrap}
/>
<Menu.Item
label={t('logs.logs-controls.line-wrapping.enable-prettify', 'Enable line wrapping and prettify JSON')}
className={wrapLogMessage && prettifyJSON ? styles.menuItemActive : undefined}
onClick={wrapAndPrettify}
/>
</Menu>
),
[disable, prettifyJSON, styles.menuItemActive, wrap, wrapAndPrettify, wrapLogMessage]
);
const wrapStateText = !wrapLogMessage
? t('logs.logs-controls.line-wrapping.state.hide', 'Wrap disabled')
: wrapLogMessage && !prettifyJSON
? t('logs.logs-controls.line-wrapping.state.wrap', 'Wrap lines')
: t('logs.logs-controls.line-wrapping.state.json', 'Wrap JSON');
const tooltip = t('logs.logs-controls.line-wrapping.tooltip', 'Set line wrap');
return (
<LogListControlsSelectOption
expanded={expanded}
name={'wrap-text'}
isActive={wrapLogMessage}
dropdown={wrappingMenu}
tooltip={tooltip}
label={wrapStateText}
buttonAriaLabel={tooltip}
customTagText={prettifyJSON ? '+' : ''}
/>
);
};
const getWrapButtonStyles = (theme: GrafanaTheme2, expanded: boolean) => {
return {
menuItemActive: css({
'&:before': {
content: '""',
position: 'absolute',
left: 0,
top: theme.spacing(0.5),
height: `calc(100% - ${theme.spacing(1)})`,
width: '2px',
backgroundColor: theme.colors.warning.main,
},
}),
};
};
export const CONTROLS_WIDTH = 35;
export const CONTROLS_WIDTH_EXPANDED = 176;
const getStyles = (theme: GrafanaTheme2, controlsExpanded: boolean) => {
return {
navContainer: css({
maxHeight: '100%',
display: 'flex',
flex: '1 0 auto',
gap: theme.spacing(3),
flexDirection: 'column',
justifyContent: 'flex-start',
width: controlsExpanded ? CONTROLS_WIDTH_EXPANDED : CONTROLS_WIDTH,
paddingTop: theme.spacing(0.75),
paddingLeft: theme.spacing(1),
borderLeft: `solid 1px ${theme.colors.border.medium}`,
minWidth: theme.spacing(4),
backgroundColor: theme.colors.background.primary,
}),
scrollToTopButton: css({
margin: 0,
marginTop: 'auto',
color: theme.colors.text.secondary,
height: theme.spacing(2),
}),
controlsExpandedButton: css({
transform: !controlsExpanded ? 'rotate(180deg)' : '',
}),
controlButton: css({
margin: 0,
color: theme.colors.text.secondary,
height: theme.spacing(2),
}),
divider: css({
borderTop: `solid 1px ${theme.colors.border.medium}`,
height: 1,
marginTop: theme.spacing(-0.25),
marginBottom: theme.spacing(-1.75),
}),
controlButtonActive: css({
margin: 0,
color: theme.colors.text.secondary,
height: theme.spacing(2),
'&:after': {
display: 'block',
content: '" "',
position: 'absolute',
height: 2,
borderRadius: theme.shape.radius.default,
bottom: theme.spacing(-1),
backgroundImage: theme.colors.gradients.brandHorizontal,
width: theme.spacing(2.25),
opacity: 1,
},
}),
menuItemActive: css({
'&:before': {
content: '""',
position: 'absolute',
left: 0,
top: theme.spacing(0.5),
height: `calc(100% - ${theme.spacing(1)})`,
width: '2px',
backgroundColor: theme.colors.warning.main,
},
}),
};
};