feat: add new expanded state to log options menu (#110725)

* feat: add new expanded state to log options menu
This commit is contained in:
Galen Kistler
2025-09-12 05:37:33 -05:00
committed by GitHub
parent 842ae463b7
commit 076c52e1b2
8 changed files with 623 additions and 192 deletions
@@ -1011,6 +1011,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
hasData && (
<div className={styles.logRowsWrapper} data-testid="logRows">
<ControlledLogRows
ref={logsContainerRef}
logsTableFrames={props.logsFrames}
width={width}
updatePanelState={updatePanelState}
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { useMemo, useRef } from 'react';
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';
@@ -8,7 +8,7 @@ import { LogsTableWrap } from '../../explore/Logs/LogsTableWrap';
import { LogRowsComponentProps } from './ControlledLogRows';
import { useLogListContext } from './panel/LogListContext';
import { LogListControls } from './panel/LogListControls';
import { CONTROLS_WIDTH, CONTROLS_WIDTH_EXPANDED, LogListControls } from './panel/LogListControls';
export const ControlledLogsTable = ({
loading,
@@ -26,8 +26,9 @@ export const ControlledLogsTable = ({
visualisationType,
...rest
}: LogRowsComponentProps) => {
const { sortOrder } = useLogListContext();
const { sortOrder, controlsExpanded } = useLogListContext();
const eventBus = useMemo(() => new EventBusSrv(), []);
const ref = useRef(null);
const theme = useTheme2();
const styles = getStyles(theme);
@@ -37,8 +38,11 @@ export const ControlledLogsTable = ({
return;
}
const tableWidthExpandedControls = width - (CONTROLS_WIDTH_EXPANDED + 12);
const tableWidth = width - (CONTROLS_WIDTH + 12);
return (
<div className={styles.logRowsContainer}>
<div ref={ref} className={styles.logRowsContainer}>
<LogListControls eventBus={eventBus} visualisationType={visualisationType} />
<div className={styles.logRows} data-testid="logRowsTable">
{/* Width should be full width minus logs navigation and padding */}
@@ -47,7 +51,7 @@ export const ControlledLogsTable = ({
range={range}
splitOpen={splitOpen}
timeZone={rest.timeZone}
width={width - 45}
width={controlsExpanded ? tableWidthExpandedControls : tableWidth}
logsFrames={logsTableFrames ?? []}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
@@ -40,6 +40,7 @@ import { getScrollbarWidth, LOG_LIST_CONTROLS_WIDTH, LOG_LIST_MIN_WIDTH } from '
export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> {
closeDetails: () => void;
controlsExpanded: boolean;
detailsDisplayed: (log: LogListModel) => boolean;
detailsMode: LogLineDetailsMode;
detailsWidth: number;
@@ -51,6 +52,7 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo
hasSampledLogs?: boolean;
hasUnescapedContent: boolean;
logLineMenuCustomItems?: LogLineMenuCustomItem[];
setControlsExpanded: (expanded: boolean) => void;
setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void;
setDetailsMode: (mode: LogLineDetailsMode) => void;
setDetailsWidth: (width: number) => void;
@@ -76,6 +78,7 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo
export const LogListContext = createContext<LogListContextData>({
app: CoreApp.Unknown,
closeDetails: () => {},
controlsExpanded: false,
dedupStrategy: LogsDedupStrategy.none,
detailsDisplayed: () => false,
detailsMode: 'sidebar',
@@ -88,6 +91,7 @@ export const LogListContext = createContext<LogListContextData>({
fontSize: 'default',
hasUnescapedContent: false,
noInteractions: false,
setControlsExpanded: () => {},
setDedupStrategy: () => {},
setDetailsMode: () => {},
setDetailsWidth: () => {},
@@ -393,6 +397,13 @@ export const LogListContextProvider = ({
}));
}, [timestampResolution]);
const controlsExpandedFromStore = store.getBool(
`${logOptionsStorageKey}.controlsExpanded`,
getDefaultControlsExpandedMode(containerElement ?? null)
);
// If the user has a large viewport, show the expanded state by default
const [controlsExpanded, setControlsExpanded] = useState<boolean>(controlsExpandedFromStore);
const detailsDisplayed = useCallback(
(log: LogListModel) => !!showDetails.find((shownLog) => shownLog.uid === log.uid),
[showDetails]
@@ -594,6 +605,7 @@ export const LogListContextProvider = ({
value={{
app,
closeDetails,
controlsExpanded,
detailsDisplayed,
dedupStrategy: logListState.dedupStrategy,
detailsMode,
@@ -628,6 +640,7 @@ export const LogListContextProvider = ({
pinLineButtonTooltipTitle,
pinnedLogs: logListState.pinnedLogs,
prettifyJSON,
setControlsExpanded,
setDedupStrategy,
setDetailsMode,
setDetailsWidth,
@@ -754,3 +767,8 @@ export function getDefaultDetailsMode(container: HTMLDivElement | undefined): Lo
const width = container?.clientWidth ?? window.innerWidth;
return width > 1440 ? 'sidebar' : 'inline';
}
export function getDefaultControlsExpandedMode(container: HTMLDivElement | null): boolean {
const width = container?.clientWidth ?? window.innerWidth;
return width > 1200;
}
@@ -12,6 +12,35 @@ import { LogListContextProvider } from './LogListContext';
import { LogListControls } from './LogListControls';
import { ScrollToLogsEvent } from './virtualization';
const FILTER_LEVELS_LABEL_COPY = 'Filter levels';
const SCROLL_BOTTOM_LABEL_COPY = 'Scroll to bottom';
const SCROLL_TOP_LABEL_COPY = 'Scroll to top';
const OLDEST_LOGS_LABEL_COPY = 'Oldest logs first';
const DEDUPE_LABEL_COPY = 'Deduplication';
const SHOW_TIMESTAMP_LABEL_COPY = 'Show timestamps';
const WRAP_LINES_LABEL_COPY = 'Wrap lines';
const WRAP_JSON_TOOLTIP_COPY = 'Enable line wrapping and prettify JSON';
const WRAP_JSON_LABEL_COPY = 'Wrap JSON';
const WRAP_DISABLE_LABEL_COPY = 'Disable line wrapping';
const ENABLE_HIGHLIGHTING_LABEL_COPY = 'Enable highlighting';
const EXPANDED_LABEL_COPY = 'Expanded';
const COLLAPSED_LABEL_COPY = 'Collapsed';
const SHOW_UNIQUE_LABELS_LABEL_COPY = 'Show unique labels';
const HIDE_UNIQUE_LABELS_LABEL_COPY = 'Hide unique labels';
const EXPAND_JSON_LOGS_LABEL_COPY = 'Expand JSON logs';
const COLLAPSE_JSON_LOGS_LABEL_COPY = 'Collapse JSON logs';
const ESCAPE_NEWLINES_TOOLTIP_COPY = 'Fix incorrectly escaped newline and tab sequences in log lines';
const REMOVE_ESCAPE_NEWLINES_LABEL_COPY = 'Remove escaping';
const TIMESTAMP_LABEL_COPY = 'Log timestamps';
const TIMESTAMP_HIDE_LABEL_COPY = 'Hide timestamps';
const FONT_SIZE_LARGE_LABEL_COPY = 'Large font';
const FONT_SIZE_LARGE_TOOLTIP_COPY = 'Set large font';
const FONT_SIZE_SMALL_LABEL_COPY = 'Small font';
const FONT_SIZE_SMALL_TOOLTIP_COPY = 'Set small font';
const DOWNLOAD_LOGS_LABEL_COPY = 'Download logs';
const OLDEST_LOGS_LABEL_REGEX = /oldest logs first/;
jest.mock('../../utils', () => ({
...jest.requireActual('../../utils'),
downloadLogs: jest.fn(),
@@ -42,6 +71,13 @@ const contextProps = {
openAssistantByLog: () => {},
};
const assertExpandedOptionsCopyVisible = () => {
expect(screen.getByText(EXPANDED_LABEL_COPY)).toBeVisible();
expect(screen.getByText(SCROLL_BOTTOM_LABEL_COPY)).toBeVisible();
expect(screen.getByText(OLDEST_LOGS_LABEL_COPY)).toBeVisible();
expect(screen.getByText(DEDUPE_LABEL_COPY)).toBeVisible();
expect(screen.getByText(SCROLL_TOP_LABEL_COPY)).toBeVisible();
};
describe('LogListControls', () => {
test('Renders without errors', () => {
render(
@@ -49,20 +85,18 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument();
expect(screen.getByLabelText(/oldest logs first/)).toBeInTheDocument();
expect(screen.getByLabelText('Deduplication')).toBeInTheDocument();
expect(screen.getByLabelText('Display levels')).toBeInTheDocument();
expect(screen.getByLabelText('Show timestamps')).toBeInTheDocument();
expect(screen.getByLabelText('Wrap lines')).toBeInTheDocument();
expect(screen.getByLabelText('Enable highlighting')).toBeInTheDocument();
expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument();
expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument();
expect(
screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines')
).not.toBeInTheDocument();
expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)).toBeInTheDocument();
expect(screen.getByLabelText(DEDUPE_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(WRAP_LINES_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument();
expect(screen.queryByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY)).not.toBeInTheDocument();
});
test('Renders legacy controls', () => {
@@ -71,8 +105,8 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Show unique labels')).toBeInTheDocument();
expect(screen.getByLabelText('Expand JSON logs')).toBeInTheDocument();
expect(screen.getByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).toBeInTheDocument();
});
test.each([CoreApp.Dashboard, CoreApp.PanelEditor, CoreApp.PanelViewer])(
@@ -83,14 +117,14 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument();
expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument();
expect(screen.getByLabelText('Display levels')).toBeInTheDocument();
expect(screen.queryByLabelText(/oldest logs first/)).not.toBeInTheDocument();
expect(screen.queryByLabelText('Deduplication')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Show timestamps')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Wrap lines')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Enable highlighting')).not.toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument();
expect(screen.queryByLabelText(OLDEST_LOGS_LABEL_REGEX)).not.toBeInTheDocument();
expect(screen.queryByLabelText(DEDUPE_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(WRAP_LINES_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).not.toBeInTheDocument();
}
);
@@ -100,20 +134,18 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument();
expect(screen.getByLabelText(/oldest logs first/)).toBeInTheDocument();
expect(screen.getByLabelText('Deduplication')).toBeInTheDocument();
expect(screen.getByLabelText('Display levels')).toBeInTheDocument();
expect(screen.getByLabelText('Show timestamps')).toBeInTheDocument();
expect(screen.getByLabelText('Wrap lines')).toBeInTheDocument();
expect(screen.getByLabelText('Enable highlighting')).toBeInTheDocument();
expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument();
expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument();
expect(
screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines')
).not.toBeInTheDocument();
expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)).toBeInTheDocument();
expect(screen.getByLabelText(DEDUPE_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(WRAP_LINES_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).toBeInTheDocument();
expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument();
expect(screen.queryByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY)).not.toBeInTheDocument();
expect(screen.queryByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY)).not.toBeInTheDocument();
});
test('Allows to scroll', async () => {
@@ -124,8 +156,8 @@ describe('LogListControls', () => {
<LogListControls eventBus={eventBus} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Scroll to bottom'));
await userEvent.click(screen.getByLabelText('Scroll to top'));
await userEvent.click(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY));
await userEvent.click(screen.getByLabelText(SCROLL_TOP_LABEL_COPY));
expect(eventBus.publish).toHaveBeenCalledTimes(2);
expect(eventBus.publish).toHaveBeenCalledWith(
new ScrollToLogsEvent({
@@ -139,6 +171,45 @@ describe('LogListControls', () => {
);
});
test('Expands options', async () => {
render(
<LogListContextProvider {...contextProps} sortOrder={LogsSortOrder.Ascending}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
// Initial state should be collapsed
expect(screen.getByLabelText(COLLAPSED_LABEL_COPY)).toBeVisible();
// Expanded label should not be visible
expect(screen.queryByText(EXPANDED_LABEL_COPY)).not.toBeInTheDocument();
// Expand options
await userEvent.click(screen.getByLabelText(COLLAPSED_LABEL_COPY));
// Verify that the label (state) is not collapsed
expect(screen.queryByLabelText(COLLAPSED_LABEL_COPY)).not.toBeInTheDocument();
expect(screen.getByLabelText(EXPANDED_LABEL_COPY)).toBeVisible();
// Verify the expanded labels are rendered
assertExpandedOptionsCopyVisible();
});
test('Expands options shown by default with container width > 1200', async () => {
const div = document.createElement('div');
const divSpy = jest.spyOn(div, 'clientWidth', 'get');
//@ts-expect-error
divSpy['clientWidth'] = 1201;
render(
//@ts-expect-error
<LogListContextProvider {...contextProps} sortOrder={LogsSortOrder.Ascending} containerElement={divSpy}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
// Verify the expanded labels are rendered
assertExpandedOptionsCopyVisible();
// Collapse options
await userEvent.click(screen.getByLabelText(EXPANDED_LABEL_COPY));
// State should be collapsed
expect(screen.getByLabelText(COLLAPSED_LABEL_COPY)).toBeVisible();
});
test('Controls sort order', async () => {
const onLogOptionsChange = jest.fn();
render(
@@ -150,7 +221,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText(/oldest logs first/));
await userEvent.click(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('sortOrder', LogsSortOrder.Descending);
});
@@ -162,7 +233,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Deduplication'));
await userEvent.click(screen.getByLabelText(DEDUPE_LABEL_COPY));
await userEvent.click(screen.getByText('Numbers'));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('dedupStrategy', LogsDedupStrategy.numbers);
@@ -175,7 +246,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Display levels'));
await userEvent.click(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY));
expect(await screen.findByText('All levels')).toBeVisible();
expect(screen.getByText('Info')).toBeVisible();
expect(screen.getByText('Debug')).toBeVisible();
@@ -194,7 +265,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Show timestamps'));
await userEvent.click(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', true);
});
@@ -206,7 +277,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Wrap lines'));
await userEvent.click(screen.getByLabelText(WRAP_LINES_LABEL_COPY));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', true);
});
@@ -227,21 +298,21 @@ describe('LogListControls', () => {
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Log line wrapping'));
await userEvent.click(screen.getByLabelText('Wrap disabled'));
await userEvent.click(screen.getByText('Enable line wrapping'));
expect(onLogOptionsChange).toHaveBeenCalledTimes(2);
expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', true);
expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', false);
await userEvent.click(screen.getByLabelText('Log line wrapping'));
await userEvent.click(screen.getByText('Enable line wrapping and prettify JSON'));
await userEvent.click(screen.getByLabelText(WRAP_LINES_LABEL_COPY));
await userEvent.click(screen.getByText(WRAP_JSON_TOOLTIP_COPY));
expect(onLogOptionsChange).toHaveBeenCalledTimes(4);
expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', true);
await userEvent.click(screen.getByLabelText('Log line wrapping'));
await userEvent.click(screen.getByText('Disable line wrapping'));
await userEvent.click(screen.getByLabelText(WRAP_JSON_LABEL_COPY));
await userEvent.click(screen.getByText(WRAP_DISABLE_LABEL_COPY));
expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', false);
expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', false);
@@ -262,19 +333,19 @@ describe('LogListControls', () => {
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Log timestamps'));
await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY));
await userEvent.click(screen.getByText('Show millisecond timestamps'));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', true);
await userEvent.click(screen.getByLabelText('Log timestamps'));
await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY));
await userEvent.click(screen.getByText('Show nanosecond timestamps'));
expect(onLogOptionsChange).toHaveBeenCalledTimes(2);
await userEvent.click(screen.getByLabelText('Log timestamps'));
await userEvent.click(screen.getByText('Hide timestamps'));
await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY));
await userEvent.click(screen.getByText(TIMESTAMP_HIDE_LABEL_COPY));
expect(onLogOptionsChange).toHaveBeenCalledTimes(3);
expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', false);
@@ -289,7 +360,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Enable highlighting'));
await userEvent.click(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY));
expect(onLogOptionsChange).toHaveBeenCalledTimes(1);
expect(onLogOptionsChange).toHaveBeenCalledWith('syntaxHighlighting', true);
});
@@ -300,13 +371,13 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Show unique labels'));
await userEvent.click(screen.getByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY));
rerender(
<LogListContextProvider {...contextProps} app={CoreApp.Explore} showUniqueLabels={false}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Hide unique labels'));
expect(screen.getByLabelText(HIDE_UNIQUE_LABELS_LABEL_COPY));
});
test('Controls Expand JSON logs', async () => {
@@ -315,13 +386,13 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Expand JSON logs'));
await userEvent.click(screen.getByLabelText(EXPAND_JSON_LOGS_LABEL_COPY));
rerender(
<LogListContextProvider {...contextProps} showUniqueLabels={false}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
expect(screen.getByLabelText('Collapse JSON logs'));
expect(screen.getByLabelText(COLLAPSE_JSON_LOGS_LABEL_COPY));
});
test('Controls font size', async () => {
@@ -333,11 +404,11 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Use small font size'));
await screen.findByLabelText('Use default font size');
await userEvent.click(screen.getByLabelText(FONT_SIZE_LARGE_LABEL_COPY));
await screen.findByLabelText(FONT_SIZE_LARGE_TOOLTIP_COPY);
await userEvent.click(screen.getByLabelText('Use default font size'));
await screen.findByLabelText('Use small font size');
await userEvent.click(screen.getByLabelText(FONT_SIZE_SMALL_LABEL_COPY));
await screen.findByLabelText(FONT_SIZE_SMALL_TOOLTIP_COPY);
config.featureToggles.newLogsPanel = originalValue;
});
@@ -353,7 +424,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Download logs'));
await userEvent.click(screen.getByLabelText(DOWNLOAD_LOGS_LABEL_COPY));
await userEvent.click(await screen.findByText(label));
expect(downloadLogs).toHaveBeenCalledTimes(1);
expect(downloadLogs).toHaveBeenCalledWith(format, [], undefined);
@@ -371,7 +442,7 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Download logs'));
await userEvent.click(screen.getByLabelText(DOWNLOAD_LOGS_LABEL_COPY));
await userEvent.click(await screen.findByText('txt'));
expect(downloadLogs).toHaveBeenCalledWith('text', filteredLogs, undefined);
});
@@ -383,12 +454,12 @@ describe('LogListControls', () => {
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Fix incorrectly escaped newline and tab sequences in log lines'));
await userEvent.click(screen.getByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY));
rerender(
<LogListContextProvider {...contextProps} logs={[log]}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Remove escaping'));
await userEvent.click(screen.getByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY));
});
});
@@ -1,17 +1,26 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { capitalize } from 'lodash';
import { MouseEvent, useCallback, useMemo } from 'react';
import { CoreApp, EventBus, LogLevel, LogsDedupDescription, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
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, Icon, IconButton, Menu, Tooltip, useStyles2 } from '@grafana/ui';
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';
@@ -38,9 +47,9 @@ const FILTER_LEVELS: LogLevel[] = [
];
export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => {
const styles = useStyles2(getStyles);
const {
app,
controlsExpanded,
dedupStrategy,
downloadLogs,
filterLevels,
@@ -48,6 +57,7 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
forceEscape,
hasUnescapedContent,
prettifyJSON,
setControlsExpanded,
setDedupStrategy,
setFilterLevels,
setFontSize,
@@ -63,9 +73,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
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(
@@ -84,6 +97,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
);
}, [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);
@@ -242,22 +261,47 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
return (
<div className={styles.navContainer}>
{visualisationType === 'logs' && (
<IconButton
name="arrow-down"
className={styles.controlButton}
<>
<LogListControlsOption
expanded={controlsExpanded}
name="arrow-from-right"
className={cx(styles.controlButton, styles.controlsExpandedButton)}
variant="secondary"
onClick={onScrollToBottomClick}
tooltip={t('logs.logs-controls.scroll-bottom', 'Scroll to bottom')}
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 ? (
<>
<IconButton
<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')
@@ -269,10 +313,16 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
<>
<div className={styles.divider} />
{config.featureToggles.newLogsPanel && (
<IconButton
<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')
@@ -282,7 +332,8 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
)}
<Dropdown overlay={deduplicationMenu} placement="auto-end">
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name={'filter'}
className={
dedupStrategy !== LogsDedupStrategy.none ? styles.controlButtonActive : styles.controlButton
@@ -292,20 +343,23 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
</Dropdown>
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name={'gf-logs'}
className={
filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton
}
tooltip={t('logs.logs-controls.display-level', 'Display levels')}
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 />
<TimestampResolutionButton expanded={controlsExpanded} />
) : (
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name="clock-nine"
aria-pressed={showTime}
className={showTime ? styles.controlButtonActive : styles.controlButton}
@@ -320,7 +374,8 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
)}
{/* When this is used in a Plugin context, app is unknown */}
{showUniqueLabels !== undefined && app !== CoreApp.Unknown && (
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name="tag-alt"
aria-pressed={showUniqueLabels}
className={showUniqueLabels ? styles.controlButtonActive : styles.controlButton}
@@ -334,9 +389,10 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
)}
{config.featureToggles.newLogsPanel ? (
<WrapLogMessageButton />
<WrapLogMessageButton expanded={controlsExpanded} />
) : (
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name="wrap-text"
className={wrapLogMessage ? styles.controlButtonActive : styles.controlButton}
aria-pressed={wrapLogMessage}
@@ -350,7 +406,8 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
)}
{prettifyJSON !== undefined && !config.featureToggles.newLogsPanel && (
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name="brackets-curly"
aria-pressed={prettifyJSON}
className={prettifyJSON ? styles.controlButtonActive : styles.controlButton}
@@ -364,39 +421,57 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
)}
{syntaxHighlighting !== undefined && (
<IconButton
<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.disable-highlighting', 'Disable highlighting')
: t('logs.logs-controls.enable-highlighting', 'Enable highlighting')
? t('logs.logs-controls.tooltip.disable-highlighting', 'Disable highlighting')
: t('logs.logs-controls.tooltip.enable-highlighting', 'Enable highlighting')
}
size="lg"
/>
)}
{config.featureToggles.newLogsPanel && (
<IconButton
<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-size-default', 'Use small font size')
: t('logs.logs-controls.font-size-small', 'Use default font size')
? t('logs.logs-controls.font-small', 'Set small font')
: t('logs.logs-controls.font-large', 'Set large font')
}
size="lg"
/>
)}
{hasUnescapedContent && (
<IconButton
<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')
@@ -414,10 +489,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
<>
<div className={styles.divider} />
<Dropdown overlay={downloadMenu} placement="auto-end">
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name="download-alt"
className={styles.controlButton}
tooltip={t('logs.logs-controls.download', 'Download logs')}
label={t('logs.logs-controls.download', 'Download logs')}
tooltip={t('logs.logs-controls.tooltip.download', 'Download')}
size="lg"
/>
</Dropdown>
@@ -427,10 +504,16 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
) : (
<>
{config.featureToggles.newLogsPanel && (
<IconButton
<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')
@@ -440,19 +523,27 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
/>
)}
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<IconButton
<LogListControlsOption
expanded={controlsExpanded}
name={'gf-logs'}
className={filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton}
tooltip={t('logs.logs-controls.display-level', 'Display levels')}
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 && (
<IconButton
<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')
@@ -467,7 +558,9 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
</>
)}
{visualisationType === 'logs' && (
<IconButton
<LogListControlsOption
stickToBottom={true}
expanded={controlsExpanded}
name="arrow-up"
data-testid="scrollToTop"
className={styles.scrollToTopButton}
@@ -481,8 +574,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
);
};
const TimestampResolutionButton = () => {
const styles = useStyles2(getStyles);
interface LogSelectOptionProps {
expanded: boolean;
}
const TimestampResolutionButton = ({ expanded }: LogSelectOptionProps) => {
const styles = useStyles2(getWrapButtonStyles, expanded);
const { setTimestampResolution, setShowTime, showTime, timestampResolution } = useLogListContext();
const hide = useCallback(() => {
@@ -533,33 +630,32 @@ const TimestampResolutionButton = () => {
[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 (
<Dropdown overlay={timestampMenu} placement="auto-end">
<div>
<Tooltip content={t('logs.logs-controls.timestamp.label', 'Log timestamps')}>
<button
aria-pressed={showTime}
aria-label={t('logs.logs-controls.timestamp.label', 'Log timestamps')}
className={`${styles.customControlButton} ${showTime ? styles.controlButtonActive : styles.controlButton}`}
type="button"
>
<Icon name="clock-nine" size="lg" className={styles.customControlIcon} />
{showTime && (
<span className={styles.customControlTag}>
{timestampResolution === 'ms'
? t('logs.logs-controls.resolution-ms', 'ms')
: t('logs.logs-controls.resolution-ns', 'ns')}
</span>
)}
</button>
</Tooltip>
</div>
</Dropdown>
<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 = () => {
const styles = useStyles2(getStyles);
const WrapLogMessageButton = ({ expanded }: LogSelectOptionProps) => {
const styles = useStyles2(getWrapButtonStyles, expanded);
const { prettifyJSON, setPrettifyJSON, setWrapLogMessage, wrapLogMessage } = useLogListContext();
/**
@@ -622,39 +718,62 @@ const WrapLogMessageButton = () => {
[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 (
<Dropdown overlay={wrappingMenu} placement="auto-end">
<div>
<Tooltip content={t('logs.logs-controls.line-wrapping.label', 'Log line wrapping')}>
<button
aria-label={t('logs.logs-controls.line-wrapping.label', 'Log line wrapping')}
aria-pressed={wrapLogMessage}
className={`${styles.customControlButton} ${wrapLogMessage ? styles.controlButtonActive : styles.controlButton}`}
type="button"
>
<Icon name="wrap-text" size="lg" className={styles.customControlIcon} />
{prettifyJSON && <span className={styles.customControlTag}>+</span>}
</button>
</Tooltip>
</div>
</Dropdown>
<LogListControlsSelectOption
expanded={expanded}
name={'wrap-text'}
isActive={wrapLogMessage}
dropdown={wrappingMenu}
tooltip={tooltip}
label={wrapStateText}
buttonAriaLabel={tooltip}
customTagText={'+'}
/>
);
};
const getStyles = (theme: GrafanaTheme2) => {
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: theme.spacing(4),
width: controlsExpanded ? CONTROLS_WIDTH_EXPANDED : CONTROLS_WIDTH,
paddingTop: theme.spacing(0.75),
paddingLeft: theme.spacing(1),
borderLeft: `solid 1px ${theme.colors.border.medium}`,
overflow: 'hidden',
minWidth: theme.spacing(4),
backgroundColor: theme.colors.background.primary,
}),
scrollToTopButton: css({
margin: 0,
@@ -662,6 +781,9 @@ const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.secondary,
height: theme.spacing(2),
}),
controlsExpandedButton: css({
transform: !controlsExpanded ? 'rotate(180deg)' : '',
}),
controlButton: css({
margin: 0,
color: theme.colors.text.secondary,
@@ -685,7 +807,7 @@ const getStyles = (theme: GrafanaTheme2) => {
borderRadius: theme.shape.radius.default,
bottom: theme.spacing(-1),
backgroundImage: theme.colors.gradients.brandHorizontal,
width: '95%',
width: theme.spacing(2.25),
opacity: 1,
},
}),
@@ -700,32 +822,5 @@ const getStyles = (theme: GrafanaTheme2) => {
backgroundColor: theme.colors.warning.main,
},
}),
customControlButton: css({
position: 'relative',
zIndex: 0,
margin: 0,
boxShadow: 'none',
border: 'none',
display: 'flex',
background: 'transparent',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
overflow: 'visible',
width: '100%',
}),
customControlIcon: css({
verticalAlign: 'baseline',
}),
customControlTag: css({
color: theme.colors.primary.text,
fontSize: 10,
position: 'absolute',
bottom: -4,
right: 1,
lineHeight: '10px',
backgroundColor: theme.colors.background.primary,
paddingLeft: 2,
}),
};
};
@@ -0,0 +1,211 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Dropdown, Icon, IconButton, Tooltip, useStyles2 } from '@grafana/ui';
interface LogControlOptionProps {
label?: string;
expanded: boolean;
tooltip: string;
stickToBottom?: boolean;
}
export type Props = React.ComponentProps<typeof IconButton> & LogControlOptionProps;
export const LogListControlsOption = React.forwardRef<HTMLButtonElement, Props>(
(
{
stickToBottom,
expanded,
label,
tooltip,
className: iconButtonClassName,
name: iconButtonName,
...iconButtonProps
}: Props,
ref
) => {
const styles = useStyles2(getStyles, expanded);
return (
<div className={`${styles.container} ${stickToBottom ? styles.marginTopAuto : ''}`}>
<label className={styles.label}>
<span className={styles.labelText}>{label ?? tooltip}</span>
<span className={styles.iconContainer}>
<IconButton
name={iconButtonName}
tooltip={tooltip}
className={iconButtonClassName}
ref={ref}
{...iconButtonProps}
/>
</span>
</label>
</div>
);
}
);
interface LogControlSelectOptionProps {
label?: string;
expanded: boolean;
tooltip: string;
stickToBottom?: boolean;
dropdown: JSX.Element;
isActive: boolean;
customTagText: string;
buttonAriaLabel: string;
}
export type SelectProps = React.ComponentProps<typeof Icon> & LogControlSelectOptionProps;
export const LogListControlsSelectOption = React.forwardRef<SVGElement, SelectProps>(
(
{
stickToBottom,
expanded,
label,
tooltip,
className: iconButtonClassName,
name: iconButtonName,
dropdown,
isActive: isActive,
customTagText,
buttonAriaLabel,
...iconButtonProps
}: SelectProps,
ref
) => {
const styles = useStyles2(getStyles, expanded);
return (
<div className={styles.container}>
<label className={styles.label}>
<span className={styles.labelText}>{label ?? tooltip}</span>
<span>
<Dropdown overlay={dropdown} placement="auto-end">
<div className={styles.iconContainer}>
<Tooltip content={tooltip}>
<button
aria-pressed={isActive}
aria-label={buttonAriaLabel}
className={`${styles.customControlButton} ${isActive ? styles.controlButtonActive : styles.controlButton}`}
type="button"
>
<Icon
{...iconButtonProps}
ref={ref}
name={iconButtonName}
size="lg"
className={styles.customControlIcon}
/>
{isActive && <span className={styles.customControlTag}>{customTagText}</span>}
</button>
</Tooltip>
</div>
</Dropdown>
</span>
</label>
</div>
);
}
);
LogListControlsSelectOption.displayName = 'LogListControlsSelectOption';
const getStyles = (theme: GrafanaTheme2, expanded: boolean) => {
const hoverSize = '26';
return {
customControlTag: css({
color: theme.colors.primary.text,
fontSize: 10,
position: 'absolute',
bottom: -4,
right: 1,
lineHeight: '10px',
backgroundColor: theme.colors.background.primary,
paddingLeft: 2,
}),
customControlIcon: css({
verticalAlign: 'baseline',
}),
customControlButton: css({
position: 'relative',
zIndex: 0,
margin: 0,
boxShadow: 'none',
border: 'none',
display: 'flex',
background: 'transparent',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
overflow: 'visible',
width: '100%',
}),
controlButtonActive: css({
margin: 0,
color: theme.colors.text.secondary,
height: theme.spacing(2),
'&:hover': {
'&:before': {
backgroundColor: theme.colors.action.hover,
opacity: 1,
},
},
'&:before': {
zIndex: -1,
position: 'absolute',
opacity: 0,
width: `${hoverSize}px`,
height: `${hoverSize}px`,
borderRadius: theme.shape.radius.default,
content: '""',
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transitionDuration: '0.2s',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionProperty: 'opacity',
},
},
'&: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,
},
}),
controlButton: css({
margin: 0,
color: theme.colors.text.secondary,
height: theme.spacing(2),
}),
marginTopAuto: css({
marginTop: 'auto',
marginBottom: theme.spacing(1),
}),
labelText: css({
display: expanded ? 'block' : 'none',
}),
iconContainer: css({
display: 'flex',
alignItems: 'center',
height: '16px',
}),
container: css({
fontSize: theme.typography.pxToRem(12),
height: theme.spacing(2),
width: 'auto',
}),
label: css({
display: 'flex',
justifyContent: expanded ? 'space-between' : 'center',
marginRight: expanded ? '2.5px' : 0,
}),
};
};
LogListControlsOption.displayName = 'LogListControlsOption';
@@ -3,7 +3,6 @@ import { createContext, useContext } from 'react';
import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils';
import { LogLineDetailsMode } from '../LogLineDetails';
import { LogListContextData, Props } from '../LogListContext';
import { LogListModel } from '../processing';
@@ -49,11 +48,11 @@ export const LogListContext = createContext<LogListContextData>({
toggleDetails: () => {},
wrapLogMessage: false,
detailsMode: 'sidebar',
setDetailsMode: function (mode: LogLineDetailsMode): void {
throw new Error('Function not implemented.');
},
setDetailsMode: () => {},
isAssistantAvailable: false,
openAssistantByLog: () => {},
controlsExpanded: false,
setControlsExpanded: () => {},
});
export const useLogListContextData = (key: keyof LogListContextData) => {
@@ -110,8 +109,10 @@ export const defaultValue: LogListContextData = {
sortOrder: LogsSortOrder.Ascending,
wrapLogMessage: false,
isAssistantAvailable: false,
openAssistantByLog: () => {},
openAssistantByLog: jest.fn(),
timestampResolution: 'ns',
controlsExpanded: false,
setControlsExpanded: jest.fn(),
};
export const defaultProps: Props = {
+37 -7
View File
@@ -9633,10 +9633,9 @@
}
},
"logs-controls": {
"collapse": "Collapse",
"deduplication": "Deduplication",
"disable-highlighting": "Disable highlighting",
"disable-prettify-json": "Collapse JSON logs",
"display-level": "Display levels",
"display-level-all": "All levels",
"download": "Download logs",
"download-logs": {
@@ -9644,18 +9643,39 @@
"json": "json",
"txt": "txt"
},
"enable-highlighting": "Enable highlighting",
"escape-newlines": "Fix incorrectly escaped newline and tab sequences in log lines",
"font-size-default": "Use small font size",
"font-size-small": "Use default font size",
"expand": "Expand",
"filter-levels": "Filter levels",
"font-large": "Set large font",
"font-small": "Set small font",
"hide-search": "Close search",
"hide-timestamps": "Hide timestamps",
"hide-unique-labels": "Hide unique labels",
"label": {
"collapse": "Expanded",
"disable-highlighting": "Highlight text",
"enable-highlighting": "Plain text",
"escape-newlines": "Escape newlines",
"expand": "Collapsed"
},
"labels": {
"font-large": "Large font",
"font-small": "Small font",
"hide-search": "Close search",
"newest-first": "Newest logs first",
"oldest-first": "Oldest logs first",
"show-search": "Search logs"
},
"line-wrapping": {
"enable": "Enable line wrapping",
"enable-prettify": "Enable line wrapping and prettify JSON",
"hide": "Disable line wrapping",
"label": "Log line wrapping"
"state": {
"hide": "Wrap disabled",
"json": "Wrap JSON",
"wrap": "Wrap lines"
},
"tooltip": "Set line wrap"
},
"newest-first": "Sorted by newest logs first - Click to show oldest first",
"oldest-first": "Sorted by oldest logs first - Click to show newest first",
@@ -9671,8 +9691,18 @@
"timestamp": {
"hide": "Hide timestamps",
"label": "Log timestamps",
"label-hide": "Hide timestamps",
"label-ms": "Display ms",
"label-ns": "Display ns",
"milliseconds": "Show millisecond timestamps",
"nanoseconds": "Show nanosecond timestamps"
"nanoseconds": "Show nanosecond timestamps",
"tooltip": "Set timestamp format"
},
"tooltip": {
"disable-highlighting": "Disable highlighting",
"download": "Download",
"enable-highlighting": "Enable highlighting",
"filter-level": "Filter logs result by level"
},
"unwrap-lines": "Unwrap lines",
"wrap-lines": "Wrap lines"