feat: add new expanded state to log options menu (#110725)
* feat: add new expanded state to log options menu
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user