Logs Table: Add new Controls component to Explore (#103467)

* Add logs options to Table

---------

Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
Galen Kistler
2025-04-07 10:35:45 -05:00
committed by GitHub
co-authored by Matias Chomicki
parent 71c66acb2d
commit 369dc28ca2
4 changed files with 223 additions and 116 deletions
+12 -4
View File
@@ -213,7 +213,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
const [contextOpen, setContextOpen] = useState<boolean>(false);
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
const [visualisationType, setVisualisationType] = useState<LogsVisualisationType | undefined>(
const [visualisationType, setVisualisationType] = useState<LogsVisualisationType>(
panelState?.logs?.visualisationType ?? getDefaultVisualisationType()
);
const logsContainerRef = useRef<HTMLDivElement | null>(null);
@@ -229,7 +229,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
const logLevelsRef = useRef<LogLevel[] | null>(null);
const tableHeight = getLogsTableHeight();
const styles = getStyles(theme, wrapLogMessage, tableHeight);
const setWrapperLineWrapStyles = wrapLogMessage || visualisationType === 'table';
const styles = getStyles(theme, setWrapperLineWrapStyles, tableHeight);
const hasData = logRows && logRows.length > 0;
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
@@ -1007,7 +1008,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
/>
</div>
<div className={cx(styles.logsSection, visualisationType === 'table' ? styles.logsTable : undefined)}>
{visualisationType === 'table' && hasData && (
{!config.featureToggles.logsPanelControls && visualisationType === 'table' && hasData && (
<div className={styles.logRows} data-testid="logRowsTable">
{/* Width should be full width minus logs navigation and padding */}
<LogsTableWrap
@@ -1026,9 +1027,16 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
/>
</div>
)}
{config.featureToggles.logsPanelControls && visualisationType === 'logs' && hasData && (
{config.featureToggles.logsPanelControls && hasData && (
<div className={styles.logRowsWrapper} data-testid="logRows">
<ControlledLogRows
logsTableFrames={props.logsFrames}
width={width}
updatePanelState={updatePanelState}
panelState={panelState?.logs}
datasourceType={props.datasourceType}
splitOpen={splitOpen}
visualisationType={visualisationType}
loading={loading}
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined}
range={props.range}
@@ -1,9 +1,22 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useRef } from 'react';
import { AbsoluteTimeRange, CoreApp, EventBusSrv, LogsMetaItem, LogsSortOrder, TimeRange } from '@grafana/data';
import {
AbsoluteTimeRange,
CoreApp,
DataFrame,
EventBusSrv,
ExploreLogsPanelState,
LogsMetaItem,
LogsSortOrder,
SplitOpen,
TimeRange,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { LogsVisualisationType } from '../../explore/Logs/Logs';
import { ControlledLogsTable } from './ControlledLogsTable';
import { InfiniteScroll } from './InfiniteScroll';
import { LogRows, Props } from './LogRows';
import { LogListControlOptions } from './panel/LogList';
@@ -11,16 +24,25 @@ import { LogListContextProvider, useLogListContext } from './panel/LogListContex
import { LogListControls } from './panel/LogListControls';
import { ScrollToLogsEvent } from './panel/virtualization';
interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
loading: boolean;
logsMeta?: LogsMetaItem[];
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
logOptionsStorageKey?: string;
onLogOptionsChange?: (option: keyof LogListControlOptions, value: string | boolean | string[]) => void;
range: TimeRange;
/** Props added for Table **/
visualisationType: LogsVisualisationType;
splitOpen: SplitOpen;
panelState: ExploreLogsPanelState | undefined;
updatePanelState: (panelState: Partial<ExploreLogsPanelState>) => void;
datasourceType?: string;
width: number;
logsTableFrames: DataFrame[] | undefined;
}
type LogRowsComponentProps = Omit<
export type LogRowsComponentProps = Omit<
ControlledLogRowsProps,
'app' | 'dedupStrategy' | 'showLabels' | 'showTime' | 'logsSortOrder' | 'prettifyLogMessage' | 'wrapLogMessage'
>;
@@ -54,7 +76,8 @@ export const ControlledLogRows = ({
onLogOptionsChange={onLogOptionsChange}
wrapLogMessage={wrapLogMessage}
>
<LogRowsComponent {...rest} deduplicatedRows={deduplicatedRows} />
{rest.visualisationType === 'logs' && <LogRowsComponent {...rest} deduplicatedRows={deduplicatedRows} />}
{rest.visualisationType === 'table' && <ControlledLogsTable {...rest} />}
</LogListContextProvider>
);
};
@@ -0,0 +1,69 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';
import { LogsTableWrap } from '../../explore/Logs/LogsTableWrap';
import { LogRowsComponentProps } from './ControlledLogRows';
import { useLogListContext } from './panel/LogListContext';
import { LogListControls } from './panel/LogListControls';
export const ControlledLogsTable = ({
loading,
loadMoreLogs,
deduplicatedRows = [],
range,
splitOpen,
onClickFilterLabel,
onClickFilterOutLabel,
panelState,
datasourceType,
updatePanelState,
width,
logsTableFrames,
visualisationType,
...rest
}: LogRowsComponentProps) => {
const { sortOrder } = useLogListContext();
const eventBus = useMemo(() => new EventBusSrv(), []);
const theme = useTheme2();
const styles = getStyles(theme);
return (
<div className={styles.logRowsContainer}>
<div className={styles.logRows} data-testid="logRowsTable">
{/* Width should be full width minus logs navigation and padding */}
<LogsTableWrap
logsSortOrder={sortOrder}
range={range}
splitOpen={splitOpen}
timeZone={rest.timeZone}
width={width - 45}
logsFrames={logsTableFrames ?? []}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
panelState={panelState}
theme={theme}
updatePanelState={updatePanelState}
datasourceType={datasourceType}
/>
</div>
<LogListControls eventBus={eventBus} visualisationType={visualisationType} />
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
logRows: css({
overflowY: 'visible',
width: '100%',
}),
logRowsContainer: css({
display: 'flex',
}),
};
};
@@ -2,19 +2,13 @@ import { css } from '@emotion/css';
import { capitalize } from 'lodash';
import { MouseEvent, useCallback, useMemo } from 'react';
import {
CoreApp,
EventBus,
GrafanaTheme2,
LogLevel,
LogsDedupDescription,
LogsDedupStrategy,
LogsSortOrder,
} from '@grafana/data';
import { CoreApp, EventBus, LogLevel, LogsDedupDescription, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data/';
import { config, reportInteraction } from '@grafana/runtime';
import { Dropdown, IconButton, Menu, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { LogsVisualisationType } from '../../../explore/Logs/Logs';
import { DownloadFormat } from '../../utils';
import { useLogListContext } from './LogListContext';
@@ -22,6 +16,7 @@ import { ScrollToLogsEvent } from './virtualization';
type Props = {
eventBus: EventBus;
visualisationType?: LogsVisualisationType;
};
const DEDUP_OPTIONS = [
@@ -40,7 +35,7 @@ const FILTER_LEVELS: LogLevel[] = [
LogLevel.critical,
];
export const LogListControls = ({ eventBus }: Props) => {
export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => {
const styles = useStyles2(getStyles);
const {
app,
@@ -204,14 +199,16 @@ export const LogListControls = ({ eventBus }: Props) => {
return (
<div className={styles.navContainer}>
<IconButton
name="arrow-down"
className={styles.controlButton}
variant="secondary"
onClick={onScrollToBottomClick}
tooltip={t('logs.logs-controls.scroll-bottom', 'Scroll to bottom')}
size="lg"
/>
{visualisationType === 'logs' && (
<IconButton
name="arrow-down"
className={styles.controlButton}
variant="secondary"
onClick={onScrollToBottomClick}
tooltip={t('logs.logs-controls.scroll-bottom', 'Scroll to bottom')}
size="lg"
/>
)}
{!inDashboard ? (
<>
<IconButton
@@ -225,87 +222,95 @@ export const LogListControls = ({ eventBus }: Props) => {
}
size="lg"
/>
<Dropdown overlay={deduplicationMenu} placement="auto-end">
<IconButton
name={'filter'}
className={dedupStrategy !== LogsDedupStrategy.none ? styles.controlButtonActive : styles.controlButton}
tooltip={t('logs.logs-controls.deduplication', 'Deduplication')}
size="lg"
/>
</Dropdown>
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<IconButton
name={'gf-logs'}
className={filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton}
tooltip={t('logs.logs-controls.display-level', 'Display levels')}
size="lg"
/>
</Dropdown>
<IconButton
name="clock-nine"
aria-pressed={showTime}
className={showTime ? styles.controlButtonActive : styles.controlButton}
onClick={onShowTimestampsClick}
tooltip={
showTime
? t('logs.logs-controls.hide-timestamps', 'Hide timestamps')
: t('logs.logs-controls.show-timestamps', 'Show timestamps')
}
size="lg"
/>
{showUniqueLabels !== undefined && (
<IconButton
name="tag-alt"
aria-pressed={showUniqueLabels}
className={showUniqueLabels ? styles.controlButtonActive : styles.controlButton}
onClick={onShowUniqueLabelsClick}
tooltip={
showUniqueLabels
? t('logs.logs-controls.hide-unique-labels', 'Hide unique labels')
: t('logs.logs-controls.show-unique-labels', 'Show unique labels')
}
size="lg"
/>
)}
<IconButton
name="wrap-text"
className={wrapLogMessage ? styles.controlButtonActive : styles.controlButton}
aria-pressed={wrapLogMessage}
onClick={onWrapLogMessageClick}
tooltip={
wrapLogMessage
? t('logs.logs-controls.unwrap-lines', 'Unwrap lines')
: t('logs.logs-controls.wrap-lines', 'Wrap lines')
}
size="lg"
/>
{prettifyJSON !== undefined && (
<IconButton
name="brackets-curly"
aria-pressed={prettifyJSON}
className={prettifyJSON ? styles.controlButtonActive : styles.controlButton}
onClick={onSetPrettifyJSONClick}
tooltip={
prettifyJSON
? t('logs.logs-controls.disable-prettify-json', 'Collapse JSON logs')
: t('logs.logs-controls.prettify-json', 'Expand JSON logs')
}
size="lg"
/>
)}
{syntaxHighlighting !== undefined && (
<IconButton
name="brackets-curly"
className={syntaxHighlighting ? styles.controlButtonActive : styles.controlButton}
aria-pressed={syntaxHighlighting}
onClick={onSyntaxHightlightingClick}
tooltip={
syntaxHighlighting
? t('logs.logs-controls.disable-highlighting', 'Disable highlighting')
: t('logs.logs-controls.enable-highlighting', 'Enable highlighting')
}
size="lg"
/>
{visualisationType === 'logs' && (
<>
<Dropdown overlay={deduplicationMenu} placement="auto-end">
<IconButton
name={'filter'}
className={
dedupStrategy !== LogsDedupStrategy.none ? styles.controlButtonActive : styles.controlButton
}
tooltip={t('logs.logs-controls.deduplication', 'Deduplication')}
size="lg"
/>
</Dropdown>
<Dropdown overlay={filterLevelsMenu} placement="auto-end">
<IconButton
name={'gf-logs'}
className={
filterLevels && filterLevels.length > 0 ? styles.controlButtonActive : styles.controlButton
}
tooltip={t('logs.logs-controls.display-level', 'Display levels')}
size="lg"
/>
</Dropdown>
<IconButton
name="clock-nine"
aria-pressed={showTime}
className={showTime ? styles.controlButtonActive : styles.controlButton}
onClick={onShowTimestampsClick}
tooltip={
showTime
? t('logs.logs-controls.hide-timestamps', 'Hide timestamps')
: t('logs.logs-controls.show-timestamps', 'Show timestamps')
}
size="lg"
/>
{showUniqueLabels !== undefined && (
<IconButton
name="tag-alt"
aria-pressed={showUniqueLabels}
className={showUniqueLabels ? styles.controlButtonActive : styles.controlButton}
onClick={onShowUniqueLabelsClick}
tooltip={
showUniqueLabels
? t('logs.logs-controls.hide-unique-labels', 'Hide unique labels')
: t('logs.logs-controls.show-unique-labels', 'Show unique labels')
}
size="lg"
/>
)}
<IconButton
name="wrap-text"
className={wrapLogMessage ? styles.controlButtonActive : styles.controlButton}
aria-pressed={wrapLogMessage}
onClick={onWrapLogMessageClick}
tooltip={
wrapLogMessage
? t('logs.logs-controls.unwrap-lines', 'Unwrap lines')
: t('logs.logs-controls.wrap-lines', 'Wrap lines')
}
size="lg"
/>
{prettifyJSON !== undefined && (
<IconButton
name="brackets-curly"
aria-pressed={prettifyJSON}
className={prettifyJSON ? styles.controlButtonActive : styles.controlButton}
onClick={onSetPrettifyJSONClick}
tooltip={
prettifyJSON
? t('logs.logs-controls.disable-prettify-json', 'Collapse JSON logs')
: t('logs.logs-controls.prettify-json', 'Expand JSON logs')
}
size="lg"
/>
)}
{syntaxHighlighting !== undefined && (
<IconButton
name="brackets-curly"
className={syntaxHighlighting ? styles.controlButtonActive : styles.controlButton}
aria-pressed={syntaxHighlighting}
onClick={onSyntaxHightlightingClick}
tooltip={
syntaxHighlighting
? t('logs.logs-controls.disable-highlighting', 'Disable highlighting')
: t('logs.logs-controls.enable-highlighting', 'Enable highlighting')
}
size="lg"
/>
)}
</>
)}
{!config.exploreHideLogsDownload && (
<>
@@ -332,15 +337,17 @@ export const LogListControls = ({ eventBus }: Props) => {
/>
</Dropdown>
)}
<IconButton
name="arrow-up"
data-testid="scrollToTop"
className={styles.scrollToTopButton}
variant="secondary"
onClick={onScrollToTopClick}
tooltip={t('logs.logs-controls.scroll-top', 'Scroll to top')}
size="lg"
/>
{visualisationType === 'logs' && (
<IconButton
name="arrow-up"
data-testid="scrollToTop"
className={styles.scrollToTopButton}
variant="secondary"
onClick={onScrollToTopClick}
tooltip={t('logs.logs-controls.scroll-top', 'Scroll to top')}
size="lg"
/>
)}
</div>
);
};