Log Controls: Allow plugins to use Log Controls (#104237)
* Logs Panel: add showControls option * Make showControls optional * Logs Panel: expose storage option * Controlled log rows: allow table props to be possibly undefined * Logs panel: expose controlled options change callback * Logs panel: pass new callback * Logs Panel: pass the correct field callbacks * LogListControls: disallow unique labels in apps * LogListControls: allow to filter by unknown level * LogListControls: fix wrong aria-pressed state * LogListControls: hide overflow * ControlledLogRows: make scroll auto * Controlled Logs Panel: forward scroll ref * chore: generalize isCoreApp * Formatting * LogListControls: update test * Logs Panel: make sure tests pass with and without controls * formatting * Losg Panel: add comments for the new options * Log list controls: Add comment
This commit is contained in:
@@ -13,6 +13,7 @@ import * as common from '@grafana/schema';
|
||||
export const pluginVersion = "12.0.0-pre";
|
||||
|
||||
export interface Options {
|
||||
controlsStorageKey?: string;
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
displayedFields?: Array<string>;
|
||||
enableInfiniteScrolling?: boolean;
|
||||
@@ -29,9 +30,11 @@ export interface Options {
|
||||
onClickFilterString?: unknown;
|
||||
onClickHideField?: unknown;
|
||||
onClickShowField?: unknown;
|
||||
onLogOptionsChange?: unknown;
|
||||
onNewLogsReceived?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showControls?: boolean;
|
||||
showLabels: boolean;
|
||||
showLogContextToggle: boolean;
|
||||
showTime: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
@@ -35,12 +35,12 @@ export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
|
||||
|
||||
/** Props added for Table **/
|
||||
visualisationType: LogsVisualisationType;
|
||||
splitOpen: SplitOpen;
|
||||
panelState: ExploreLogsPanelState | undefined;
|
||||
updatePanelState: (panelState: Partial<ExploreLogsPanelState>) => void;
|
||||
splitOpen?: SplitOpen;
|
||||
panelState?: ExploreLogsPanelState;
|
||||
updatePanelState?: (panelState: Partial<ExploreLogsPanelState>) => void;
|
||||
datasourceType?: string;
|
||||
width: number;
|
||||
logsTableFrames: DataFrame[] | undefined;
|
||||
width?: number;
|
||||
logsTableFrames?: DataFrame[];
|
||||
}
|
||||
|
||||
export type LogRowsComponentProps = Omit<
|
||||
@@ -48,108 +48,129 @@ export type LogRowsComponentProps = Omit<
|
||||
'app' | 'dedupStrategy' | 'showLabels' | 'showTime' | 'logsSortOrder' | 'prettifyLogMessage' | 'wrapLogMessage'
|
||||
>;
|
||||
|
||||
export const ControlledLogRows = ({
|
||||
deduplicatedRows,
|
||||
dedupStrategy,
|
||||
hasUnescapedContent,
|
||||
showLabels,
|
||||
showTime,
|
||||
logsMeta,
|
||||
logOptionsStorageKey,
|
||||
logsSortOrder,
|
||||
prettifyLogMessage,
|
||||
onLogOptionsChange,
|
||||
wrapLogMessage,
|
||||
...rest
|
||||
}: ControlledLogRowsProps) => {
|
||||
return (
|
||||
<LogListContextProvider
|
||||
app={rest.app || CoreApp.Unknown}
|
||||
displayedFields={[]}
|
||||
dedupStrategy={dedupStrategy}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
logOptionsStorageKey={logOptionsStorageKey}
|
||||
logs={deduplicatedRows ?? []}
|
||||
logsMeta={logsMeta}
|
||||
prettifyJSON={prettifyLogMessage}
|
||||
showControls
|
||||
showTime={showTime}
|
||||
showUniqueLabels={showLabels}
|
||||
sortOrder={logsSortOrder || LogsSortOrder.Descending}
|
||||
onLogOptionsChange={onLogOptionsChange}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
>
|
||||
{rest.visualisationType === 'logs' && <LogRowsComponent {...rest} deduplicatedRows={deduplicatedRows} />}
|
||||
{rest.visualisationType === 'table' && <ControlledLogsTable {...rest} />}
|
||||
</LogListContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const LogRowsComponent = ({ loading, loadMoreLogs, deduplicatedRows = [], range, ...rest }: LogRowsComponentProps) => {
|
||||
const {
|
||||
app,
|
||||
dedupStrategy,
|
||||
filterLevels,
|
||||
forceEscape,
|
||||
prettifyJSON,
|
||||
sortOrder,
|
||||
showTime,
|
||||
showUniqueLabels,
|
||||
wrapLogMessage,
|
||||
} = useLogListContext();
|
||||
const eventBus = useMemo(() => new EventBusSrv(), []);
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
||||
handleScrollToEvent(e, scrollElementRef.current)
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [eventBus]);
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() =>
|
||||
filterLevels.length === 0
|
||||
? deduplicatedRows
|
||||
: deduplicatedRows.filter((log) => filterLevels.includes(log.logLevel)),
|
||||
[filterLevels, deduplicatedRows]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.logRowsContainer}>
|
||||
<LogListControls eventBus={eventBus} />
|
||||
<div
|
||||
ref={scrollElementRef}
|
||||
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
|
||||
export const ControlledLogRows = forwardRef<HTMLDivElement | null, ControlledLogRowsProps>(
|
||||
(
|
||||
{
|
||||
deduplicatedRows,
|
||||
dedupStrategy,
|
||||
hasUnescapedContent,
|
||||
showLabels,
|
||||
showTime,
|
||||
logsMeta,
|
||||
logOptionsStorageKey,
|
||||
logsSortOrder,
|
||||
prettifyLogMessage,
|
||||
onLogOptionsChange,
|
||||
wrapLogMessage,
|
||||
...rest
|
||||
}: ControlledLogRowsProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<LogListContextProvider
|
||||
app={rest.app || CoreApp.Unknown}
|
||||
displayedFields={[]}
|
||||
dedupStrategy={dedupStrategy}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
logOptionsStorageKey={logOptionsStorageKey}
|
||||
logs={deduplicatedRows ?? []}
|
||||
logsMeta={logsMeta}
|
||||
prettifyJSON={prettifyLogMessage}
|
||||
showControls
|
||||
showTime={showTime}
|
||||
showUniqueLabels={showLabels}
|
||||
sortOrder={logsSortOrder || LogsSortOrder.Descending}
|
||||
onLogOptionsChange={onLogOptionsChange}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
>
|
||||
<InfiniteScroll
|
||||
loading={loading}
|
||||
loadMoreLogs={loadMoreLogs}
|
||||
range={range}
|
||||
timeZone={rest.timeZone}
|
||||
rows={filteredLogs}
|
||||
scrollElement={scrollElementRef.current}
|
||||
sortOrder={sortOrder}
|
||||
>
|
||||
<LogRows
|
||||
{...rest}
|
||||
app={app}
|
||||
dedupStrategy={dedupStrategy}
|
||||
deduplicatedRows={filteredLogs}
|
||||
forceEscape={forceEscape}
|
||||
logRows={filteredLogs}
|
||||
logsSortOrder={sortOrder}
|
||||
{rest.visualisationType === 'logs' && (
|
||||
<LogRowsComponent ref={ref} {...rest} deduplicatedRows={deduplicatedRows} />
|
||||
)}
|
||||
{rest.visualisationType === 'table' && rest.panelState && rest.updatePanelState && (
|
||||
<ControlledLogsTable {...rest} />
|
||||
)}
|
||||
</LogListContextProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ControlledLogRows.displayName = 'ControlledLogRows';
|
||||
|
||||
const LogRowsComponent = forwardRef<HTMLDivElement | null, LogRowsComponentProps>(
|
||||
({ loading, loadMoreLogs, deduplicatedRows = [], range, ...rest }: LogRowsComponentProps, ref) => {
|
||||
const {
|
||||
app,
|
||||
dedupStrategy,
|
||||
filterLevels,
|
||||
forceEscape,
|
||||
prettifyJSON,
|
||||
sortOrder,
|
||||
showTime,
|
||||
showUniqueLabels,
|
||||
wrapLogMessage,
|
||||
} = useLogListContext();
|
||||
const eventBus = useMemo(() => new EventBusSrv(), []);
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
||||
handleScrollToEvent(e, scrollElementRef.current)
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [eventBus]);
|
||||
|
||||
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => scrollElementRef.current);
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() =>
|
||||
filterLevels.length === 0
|
||||
? deduplicatedRows
|
||||
: deduplicatedRows.filter((log) => filterLevels.includes(log.logLevel)),
|
||||
[filterLevels, deduplicatedRows]
|
||||
);
|
||||
|
||||
const scrollElementClassName = useMemo(() => {
|
||||
if (ref) {
|
||||
return styles.forwardedScrollableLogRows;
|
||||
}
|
||||
return config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows;
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<div className={styles.logRowsContainer}>
|
||||
<LogListControls eventBus={eventBus} />
|
||||
<div ref={scrollElementRef} className={scrollElementClassName}>
|
||||
<InfiniteScroll
|
||||
loading={loading}
|
||||
loadMoreLogs={loadMoreLogs}
|
||||
range={range}
|
||||
timeZone={rest.timeZone}
|
||||
rows={filteredLogs}
|
||||
scrollElement={scrollElementRef.current}
|
||||
prettifyLogMessage={Boolean(prettifyJSON)}
|
||||
showLabels={Boolean(showUniqueLabels)}
|
||||
showTime={showTime}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
sortOrder={sortOrder}
|
||||
>
|
||||
<LogRows
|
||||
{...rest}
|
||||
app={app}
|
||||
dedupStrategy={dedupStrategy}
|
||||
deduplicatedRows={filteredLogs}
|
||||
forceEscape={forceEscape}
|
||||
logRows={filteredLogs}
|
||||
logsSortOrder={sortOrder}
|
||||
scrollElement={scrollElementRef.current}
|
||||
prettifyLogMessage={Boolean(prettifyJSON)}
|
||||
showLabels={Boolean(showUniqueLabels)}
|
||||
showTime={showTime}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LogRowsComponent.displayName = 'LogRowsComponent';
|
||||
|
||||
function handleScrollToEvent(event: ScrollToLogsEvent, scrollElement: HTMLDivElement | null) {
|
||||
if (event.payload.scrollTo === 'top') {
|
||||
@@ -161,10 +182,15 @@ function handleScrollToEvent(event: ScrollToLogsEvent, scrollElement: HTMLDivEle
|
||||
|
||||
const styles = {
|
||||
scrollableLogRows: css({
|
||||
overflowY: 'scroll',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxHeight: '75vh',
|
||||
}),
|
||||
forwardedScrollableLogRows: css({
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxHeight: '100%',
|
||||
}),
|
||||
logRows: css({
|
||||
overflowX: 'scroll',
|
||||
overflowY: 'visible',
|
||||
@@ -173,5 +199,6 @@ const styles = {
|
||||
logRowsContainer: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -32,6 +32,11 @@ export const ControlledLogsTable = ({
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
if (!splitOpen || !width || !updatePanelState) {
|
||||
console.error('<ControlledLogsTable>: Missing required props.');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.logRowsContainer}>
|
||||
<LogListControls eventBus={eventBus} visualisationType={visualisationType} />
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('LogListControls', () => {
|
||||
|
||||
test('Renders legacy controls', () => {
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} showUniqueLabels={false} prettifyJSON={false}>
|
||||
<LogListContextProvider {...contextProps} app={CoreApp.Explore} showUniqueLabels={false} prettifyJSON={false}>
|
||||
<LogListControls eventBus={new EventBusSrv()} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
@@ -76,6 +76,28 @@ describe('LogListControls', () => {
|
||||
}
|
||||
);
|
||||
|
||||
test('Renders a subset of options for plugins', () => {
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} app={CoreApp.Unknown}>
|
||||
<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();
|
||||
});
|
||||
|
||||
test('Allows to scroll', async () => {
|
||||
const eventBus = new EventBusSrv();
|
||||
jest.spyOn(eventBus, 'publish');
|
||||
@@ -185,13 +207,13 @@ describe('LogListControls', () => {
|
||||
|
||||
test('Controls unique labels', async () => {
|
||||
const { rerender } = render(
|
||||
<LogListContextProvider {...contextProps} showUniqueLabels={false}>
|
||||
<LogListContextProvider {...contextProps} app={CoreApp.Explore} showUniqueLabels={false}>
|
||||
<LogListControls eventBus={new EventBusSrv()} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText('Show unique labels'));
|
||||
rerender(
|
||||
<LogListContextProvider {...contextProps} showUniqueLabels={false}>
|
||||
<LogListContextProvider {...contextProps} app={CoreApp.Explore} showUniqueLabels={false}>
|
||||
<LogListControls eventBus={new EventBusSrv()} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ const FILTER_LEVELS: LogLevel[] = [
|
||||
LogLevel.warning,
|
||||
LogLevel.error,
|
||||
LogLevel.critical,
|
||||
LogLevel.unknown,
|
||||
];
|
||||
|
||||
export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => {
|
||||
@@ -264,7 +265,8 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
{showUniqueLabels !== undefined && (
|
||||
{/* When this is used in a Plugin context, app is unknown */}
|
||||
{showUniqueLabels !== undefined && app !== CoreApp.Unknown && (
|
||||
<IconButton
|
||||
name="tag-alt"
|
||||
aria-pressed={showUniqueLabels}
|
||||
@@ -344,7 +346,6 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
|
||||
<IconButton
|
||||
name="download-alt"
|
||||
className={styles.controlButton}
|
||||
aria-pressed={wrapLogMessage}
|
||||
tooltip={t('logs.logs-controls.download', 'Download logs')}
|
||||
size="lg"
|
||||
/>
|
||||
@@ -389,6 +390,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
paddingTop: theme.spacing(0.75),
|
||||
paddingLeft: theme.spacing(1),
|
||||
borderLeft: `solid 1px ${theme.colors.border.medium}`,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
scrollToTopButton: css({
|
||||
margin: 0,
|
||||
|
||||
@@ -19,14 +19,6 @@ export function isLogsGrammar(grammar: unknown): grammar is Grammar {
|
||||
}
|
||||
|
||||
export function isCoreApp(app: unknown): app is CoreApp {
|
||||
return (
|
||||
app === CoreApp.CloudAlerting ||
|
||||
app === CoreApp.Correlations ||
|
||||
app === CoreApp.Dashboard ||
|
||||
app === CoreApp.Explore ||
|
||||
app === CoreApp.PanelEditor ||
|
||||
app === CoreApp.PanelViewer ||
|
||||
app === CoreApp.UnifiedAlerting ||
|
||||
app === CoreApp.Unknown
|
||||
);
|
||||
const apps = Object.values(CoreApp).map((coreApp) => coreApp.toString());
|
||||
return typeof app === 'string' && apps.includes(app);
|
||||
}
|
||||
|
||||
@@ -145,10 +145,10 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('LogsPanel', () => {
|
||||
describe.each([false, true])('LogsPanel with controls = %s', (showControls: boolean) => {
|
||||
it('publishes an event with the current sort order', async () => {
|
||||
publishMock.mockClear();
|
||||
setup();
|
||||
setup({}, showControls);
|
||||
|
||||
await screen.findByText('logline text');
|
||||
|
||||
@@ -199,36 +199,52 @@ describe('LogsPanel', () => {
|
||||
];
|
||||
|
||||
it('shows common labels when showCommonLabels is set to true', async () => {
|
||||
setup({
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true },
|
||||
});
|
||||
setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/common_app/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/common_job/i)).toBeInTheDocument();
|
||||
});
|
||||
it('shows common labels on top when descending sort order', async () => {
|
||||
const { container } = setup({
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
|
||||
});
|
||||
const { container } = setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:app=common_appjob=common_job/);
|
||||
});
|
||||
it('shows common labels on bottom when ascending sort order', async () => {
|
||||
const { container } = setup({
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
|
||||
});
|
||||
const { container } = setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:app=common_appjob=common_job$/);
|
||||
if (!showControls) {
|
||||
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:app=common_appjob=common_job$/);
|
||||
} else {
|
||||
expect(container.childNodes[0].textContent).toMatch(/Common labels:app=common_appjob=common_job$/);
|
||||
}
|
||||
});
|
||||
it('does not show common labels when showCommonLabels is set to false', async () => {
|
||||
setup({
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: false },
|
||||
});
|
||||
setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: false },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.queryByText(/common labels:/i)).toBeNull();
|
||||
@@ -261,19 +277,25 @@ describe('LogsPanel', () => {
|
||||
}),
|
||||
];
|
||||
it('shows (no common labels) when showCommonLabels is set to true', async () => {
|
||||
setup({
|
||||
data: { ...defaultProps.data, series: seriesWithoutCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true },
|
||||
});
|
||||
setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithoutCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: true },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/(no common labels)/i)).toBeInTheDocument();
|
||||
});
|
||||
it('does not show common labels when showCommonLabels is set to false', async () => {
|
||||
setup({
|
||||
data: { ...defaultProps.data, series: seriesWithoutCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: false },
|
||||
});
|
||||
setup(
|
||||
{
|
||||
data: { ...defaultProps.data, series: seriesWithoutCommonLabels },
|
||||
options: { ...defaultProps.options, showCommonLabels: false },
|
||||
},
|
||||
showControls
|
||||
);
|
||||
await waitFor(async () => {
|
||||
expect(screen.queryByText(/common labels:/i)).toBeNull();
|
||||
expect(screen.queryByText(/(no common labels)/i)).toBeNull();
|
||||
@@ -322,17 +344,20 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should not show the toggle if the datasource does not support show context', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'no-show-context' } }],
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'no-show-context' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
@@ -341,17 +366,20 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should show the toggle if the datasource does support show context', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
@@ -360,17 +388,20 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should not show the toggle if the datasource does support show context but the app is not Dashboard', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.CloudAlerting,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.CloudAlerting,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
@@ -379,17 +410,20 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should render the mocked `LogRowContextModal` after click', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
await userEvent.click(screen.getByLabelText(/show context/i));
|
||||
@@ -398,17 +432,20 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should call `getLogRowContext` if the user clicks the show context toggle', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
request: {
|
||||
...defaultProps.data.request,
|
||||
app: CoreApp.Dashboard,
|
||||
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
await userEvent.click(screen.getByLabelText(/show context/i));
|
||||
@@ -427,17 +464,20 @@ describe('LogsPanel', () => {
|
||||
<grafanaUI.IconButton name="rss" tooltip="Addon after" aria-label="Addon after" key={1} />,
|
||||
];
|
||||
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
logRowMenuIconsBefore,
|
||||
logRowMenuIconsAfter,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
logRowMenuIconsBefore,
|
||||
logRowMenuIconsAfter,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.hover(screen.getByText(/logline text/i));
|
||||
@@ -488,12 +528,15 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('does not rerender without changes', async () => {
|
||||
const { rerender, props } = setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
const { rerender, props } = setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -504,12 +547,15 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('rerenders when prop changes', async () => {
|
||||
const { rerender, props } = setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
const { rerender, props } = setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -520,12 +566,15 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('does not re-render when data is loading', async () => {
|
||||
const { rerender, props } = setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
const { rerender, props } = setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -571,26 +620,29 @@ describe('LogsPanel', () => {
|
||||
const filterForMock = jest.fn();
|
||||
const filterOutMock = jest.fn();
|
||||
const isFilterLabelActiveMock = jest.fn();
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickFilterLabel: filterForMock,
|
||||
onClickFilterOutLabel: filterOutMock,
|
||||
isFilterLabelActive: isFilterLabelActiveMock,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickFilterLabel: filterForMock,
|
||||
onClickFilterOutLabel: filterOutMock,
|
||||
isFilterLabelActive: isFilterLabelActiveMock,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -610,16 +662,19 @@ describe('LogsPanel', () => {
|
||||
eventBus: new EventBusSrv(),
|
||||
});
|
||||
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
enableLogDetails: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
enableLogDetails: true,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -635,16 +690,19 @@ describe('LogsPanel', () => {
|
||||
onAddAdHocFilter: jest.fn(),
|
||||
});
|
||||
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
enableLogDetails: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
enableLogDetails: true,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -690,26 +748,29 @@ describe('LogsPanel', () => {
|
||||
];
|
||||
|
||||
it('displays the provided fields instead of the log line', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
displayedFields: ['app'],
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
displayedFields: ['app'],
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
expect(screen.queryByText('logline text')).not.toBeInTheDocument();
|
||||
@@ -724,25 +785,28 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('updates the provided fields instead of the log line', async () => {
|
||||
const { rerender, props } = setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
const { rerender, props } = setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
expect(screen.getByText('logline text')).toBeInTheDocument();
|
||||
@@ -753,26 +817,29 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('enables the behavior with a default implementation', async () => {
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
displayedFields: [],
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
displayedFields: [],
|
||||
onClickHideField: undefined,
|
||||
onClickShowField: undefined,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -789,25 +856,28 @@ describe('LogsPanel', () => {
|
||||
it('overrides the default implementation when the callbacks are provided', async () => {
|
||||
const onClickShowFieldMock = jest.fn();
|
||||
|
||||
setup({
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
setup(
|
||||
{
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickHideField: jest.fn(),
|
||||
onClickShowField: onClickShowFieldMock,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickHideField: jest.fn(),
|
||||
onClickShowField: onClickShowFieldMock,
|
||||
},
|
||||
});
|
||||
showControls
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
@@ -819,7 +889,7 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const setup = (propsOverrides?: Partial<LogsPanelProps>) => {
|
||||
const setup = (propsOverrides?: Partial<LogsPanelProps>, showControls = false) => {
|
||||
const props: LogsPanelProps = {
|
||||
...defaultProps,
|
||||
data: {
|
||||
@@ -827,6 +897,7 @@ const setup = (propsOverrides?: Partial<LogsPanelProps>) => {
|
||||
},
|
||||
options: {
|
||||
...(propsOverrides?.options || defaultProps.options),
|
||||
showControls,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { config, getAppEvents } from '@grafana/runtime';
|
||||
import { ScrollContainer, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||
import { ControlledLogRows } from 'app/features/logs/components/ControlledLogRows';
|
||||
import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll';
|
||||
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
|
||||
import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView';
|
||||
@@ -45,6 +46,7 @@ import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../feat
|
||||
|
||||
import {
|
||||
GetFieldLinksFn,
|
||||
isCoreApp,
|
||||
isIsFilterLabelActive,
|
||||
isOnClickFilterLabel,
|
||||
isOnClickFilterOutLabel,
|
||||
@@ -52,6 +54,7 @@ import {
|
||||
isOnClickFilterString,
|
||||
isOnClickHideField,
|
||||
isOnClickShowField,
|
||||
isOnLogOptionsChange,
|
||||
isOnNewLogsReceivedType,
|
||||
isReactNodeArray,
|
||||
onNewLogsReceivedType,
|
||||
@@ -93,6 +96,17 @@ interface LogsPanelProps extends PanelProps<Options> {
|
||||
*
|
||||
* Callback to be invoked when enableInfiniteScrolling and new logs have been received after an scroll event.
|
||||
* onNewLogsReceived?: (allLogs: DataFrame[], newLogs: DataFrame[]) => void;
|
||||
*
|
||||
* Log Controls props:
|
||||
*
|
||||
* Enables a sidebar with controls for scrolling, sort order, deduplication, filtering, timestamps, wrapping, etc.
|
||||
* showControls?: boolean
|
||||
*
|
||||
* If controls are enabled, the component will use this key to store changes to the aforementioned options.
|
||||
* controlsStorageKey?: string
|
||||
*
|
||||
* If controls are enabled, this function is called when a change is made in one of the options from the controls.
|
||||
* onLogOptionsChange?: (option: keyof LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
*/
|
||||
}
|
||||
interface LogsPermalinkUrlState {
|
||||
@@ -108,6 +122,8 @@ export const LogsPanel = ({
|
||||
timeZone,
|
||||
fieldConfig,
|
||||
options: {
|
||||
showControls,
|
||||
controlsStorageKey,
|
||||
showLabels,
|
||||
showTime,
|
||||
wrapLogMessage,
|
||||
@@ -121,6 +137,7 @@ export const LogsPanel = ({
|
||||
onClickFilterOutLabel,
|
||||
onClickFilterOutString,
|
||||
onClickFilterString,
|
||||
onLogOptionsChange,
|
||||
isFilterLabelActive,
|
||||
logRowMenuIconsBefore,
|
||||
logRowMenuIconsAfter,
|
||||
@@ -145,7 +162,7 @@ export const LogsPanel = ({
|
||||
// Prevents the scroll position to change when new data from infinite scrolling is received
|
||||
const keepScrollPositionRef = useRef<null | 'infinite-scroll' | 'user'>(null);
|
||||
let closeCallback = useRef<() => void>();
|
||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||
const { app, eventBus, onAddAdHocFilter } = usePanelContext();
|
||||
|
||||
useEffect(() => {
|
||||
getAppEvents().publish(
|
||||
@@ -465,62 +482,114 @@ export const LogsPanel = ({
|
||||
getLogRowContextUi={getLogRowContextUi}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer ref={(scrollElement) => setScrollElement(scrollElement)}>
|
||||
<div onMouseLeave={onLogContainerMouseLeave} className={style.container} ref={logsContainerRef}>
|
||||
{!showControls ? (
|
||||
<ScrollContainer ref={(scrollElement) => setScrollElement(scrollElement)}>
|
||||
<div onMouseLeave={onLogContainerMouseLeave} className={style.container} ref={logsContainerRef}>
|
||||
{showCommonLabels && !isAscending && renderCommonLabels()}
|
||||
<InfiniteScroll
|
||||
loading={infiniteScrolling}
|
||||
loadMoreLogs={enableInfiniteScrolling ? loadMoreLogs : undefined}
|
||||
range={data.timeRange}
|
||||
timeZone={timeZone}
|
||||
rows={logRows}
|
||||
scrollElement={scrollElement}
|
||||
sortOrder={sortOrder}
|
||||
>
|
||||
<LogRows
|
||||
scrollElement={scrollElement}
|
||||
scrollIntoView={scrollIntoView}
|
||||
permalinkedRowId={getLogsPanelState()?.logs?.id ?? undefined}
|
||||
onPermalinkClick={showPermaLink() ? onPermalinkClick : undefined}
|
||||
logRows={logRows}
|
||||
showContextToggle={showContextToggle}
|
||||
deduplicatedRows={deduplicatedRows}
|
||||
dedupStrategy={dedupStrategy}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={sortOrder}
|
||||
enableLogDetails={enableLogDetails}
|
||||
previewLimit={isAscending ? logRows.length : undefined}
|
||||
onLogRowHover={onLogRowHover}
|
||||
app={isCoreApp(app) ? app : CoreApp.Dashboard}
|
||||
onOpenContext={onOpenContext}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
onClickFilterString={isOnClickFilterString(onClickFilterString) ? onClickFilterString : undefined}
|
||||
onClickFilterOutString={
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
displayedFields={displayedFields}
|
||||
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||
logRowMenuIconsBefore={isReactNodeArray(logRowMenuIconsBefore) ? logRowMenuIconsBefore : undefined}
|
||||
logRowMenuIconsAfter={isReactNodeArray(logRowMenuIconsAfter) ? logRowMenuIconsAfter : undefined}
|
||||
// Ascending order causes scroll to stick to the bottom, so previewing is futile
|
||||
renderPreview={isAscending ? false : true}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
) : (
|
||||
<div onMouseLeave={onLogContainerMouseLeave} className={style.controlledLogsContainer}>
|
||||
{showCommonLabels && !isAscending && renderCommonLabels()}
|
||||
<InfiniteScroll
|
||||
<ControlledLogRows
|
||||
ref={(scrollElement: HTMLDivElement | null) => setScrollElement(scrollElement)}
|
||||
visualisationType="logs"
|
||||
loading={infiniteScrolling}
|
||||
loadMoreLogs={enableInfiniteScrolling ? loadMoreLogs : undefined}
|
||||
range={data.timeRange}
|
||||
logRows={logRows}
|
||||
deduplicatedRows={deduplicatedRows}
|
||||
dedupStrategy={dedupStrategy}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
showContextToggle={showContextToggle}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
enableLogDetails={enableLogDetails}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
rows={logRows}
|
||||
scrollElement={scrollElement}
|
||||
sortOrder={sortOrder}
|
||||
>
|
||||
<LogRows
|
||||
scrollElement={scrollElement}
|
||||
scrollIntoView={scrollIntoView}
|
||||
permalinkedRowId={getLogsPanelState()?.logs?.id ?? undefined}
|
||||
onPermalinkClick={showPermaLink() ? onPermalinkClick : undefined}
|
||||
logRows={logRows}
|
||||
showContextToggle={showContextToggle}
|
||||
deduplicatedRows={deduplicatedRows}
|
||||
dedupStrategy={dedupStrategy}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={sortOrder}
|
||||
enableLogDetails={enableLogDetails}
|
||||
previewLimit={isAscending ? logRows.length : undefined}
|
||||
onLogRowHover={onLogRowHover}
|
||||
app={CoreApp.Dashboard}
|
||||
onOpenContext={onOpenContext}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
onClickFilterString={isOnClickFilterString(onClickFilterString) ? onClickFilterString : undefined}
|
||||
onClickFilterOutString={
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
displayedFields={displayedFields}
|
||||
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||
logRowMenuIconsBefore={isReactNodeArray(logRowMenuIconsBefore) ? logRowMenuIconsBefore : undefined}
|
||||
logRowMenuIconsAfter={isReactNodeArray(logRowMenuIconsAfter) ? logRowMenuIconsAfter : undefined}
|
||||
// Ascending order causes scroll to stick to the bottom, so previewing is futile
|
||||
renderPreview={isAscending ? false : true}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={sortOrder}
|
||||
displayedFields={displayedFields}
|
||||
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||
app={isCoreApp(app) ? app : CoreApp.Dashboard}
|
||||
onLogRowHover={onLogRowHover}
|
||||
onOpenContext={onOpenContext}
|
||||
onPermalinkClick={onPermalinkClick}
|
||||
permalinkedRowId={getLogsPanelState()?.logs?.id ?? undefined}
|
||||
scrollIntoView={scrollIntoView}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
onClickFilterString={isOnClickFilterString(onClickFilterString) ? onClickFilterString : undefined}
|
||||
onClickFilterOutString={
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
logRowMenuIconsBefore={isReactNodeArray(logRowMenuIconsBefore) ? logRowMenuIconsBefore : undefined}
|
||||
logRowMenuIconsAfter={isReactNodeArray(logRowMenuIconsAfter) ? logRowMenuIconsAfter : undefined}
|
||||
onLogOptionsChange={isOnLogOptionsChange(onLogOptionsChange) ? onLogOptionsChange : undefined}
|
||||
logOptionsStorageKey={controlsStorageKey}
|
||||
// Ascending order causes scroll to stick to the bottom, so previewing is futile
|
||||
renderPreview={false}
|
||||
/>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -529,6 +598,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}),
|
||||
controlledLogsContainer: css({
|
||||
height: '100%',
|
||||
}),
|
||||
labelContainer: css({
|
||||
margin: theme.spacing(0, 0, 0.5, 0.5),
|
||||
display: 'flex',
|
||||
|
||||
@@ -30,6 +30,8 @@ composableKinds: PanelCfg: {
|
||||
showCommonLabels: bool
|
||||
showTime: bool
|
||||
showLogContextToggle: bool
|
||||
showControls?: bool
|
||||
controlsStorageKey?: string
|
||||
wrapLogMessage: bool
|
||||
prettifyLogMessage: bool
|
||||
enableLogDetails: bool
|
||||
@@ -44,6 +46,7 @@ composableKinds: PanelCfg: {
|
||||
onClickFilterOutString?: _
|
||||
onClickShowField?: _
|
||||
onClickHideField?: _
|
||||
onLogOptionsChange?: _
|
||||
logRowMenuIconsBefore?: _
|
||||
logRowMenuIconsAfter?: _
|
||||
onNewLogsReceived?: _
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export interface Options {
|
||||
controlsStorageKey?: string;
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
displayedFields?: Array<string>;
|
||||
enableInfiniteScrolling?: boolean;
|
||||
@@ -27,9 +28,11 @@ export interface Options {
|
||||
onClickFilterString?: unknown;
|
||||
onClickHideField?: unknown;
|
||||
onClickShowField?: unknown;
|
||||
onLogOptionsChange?: unknown;
|
||||
onNewLogsReceived?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showControls?: boolean;
|
||||
showLabels: boolean;
|
||||
showLogContextToggle: boolean;
|
||||
showTime: boolean;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CoreApp } from '@grafana/data';
|
||||
|
||||
import { isCoreApp } from './types';
|
||||
|
||||
describe('isCoreApp', () => {
|
||||
test('Identifies core apps', () => {
|
||||
expect(isCoreApp(CoreApp.Explore)).toBe(true);
|
||||
expect(isCoreApp(CoreApp.Unknown)).toBe(true);
|
||||
expect(isCoreApp(CoreApp.PanelEditor)).toBe(true);
|
||||
expect(isCoreApp(CoreApp.PanelViewer)).toBe(true);
|
||||
expect(isCoreApp(CoreApp.Dashboard)).toBe(true);
|
||||
});
|
||||
|
||||
test('Identifies non-apps', () => {
|
||||
expect(isCoreApp('the explore')).toBe(false);
|
||||
expect(isCoreApp('nope')).toBe(false);
|
||||
expect(isCoreApp('drilldown')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { DataFrame, Field, LinkModel, ScopedVars } from '@grafana/data';
|
||||
import { CoreApp, DataFrame, Field, LinkModel, ScopedVars } from '@grafana/data';
|
||||
import { LogListControlOptions } from 'app/features/logs/components/panel/LogList';
|
||||
|
||||
export type { Options } from './panelcfg.gen';
|
||||
|
||||
@@ -12,6 +13,7 @@ type isFilterLabelActiveType = (key: string, value: string, refId?: string) => P
|
||||
type isOnClickShowFieldType = (value: string) => void;
|
||||
type isOnClickHideFieldType = (value: string) => void;
|
||||
export type onNewLogsReceivedType = (allLogs: DataFrame[], newLogs: DataFrame[]) => void;
|
||||
type onLogOptionsChangeType = (option: keyof LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
|
||||
export type GetFieldLinksFn = (
|
||||
field: Field,
|
||||
@@ -52,6 +54,15 @@ export function isOnNewLogsReceivedType(callback: unknown): callback is onNewLog
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnLogOptionsChange(callback: unknown): callback is onLogOptionsChangeType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isReactNodeArray(node: unknown): node is ReactNode[] {
|
||||
return Array.isArray(node) && node.every(React.isValidElement);
|
||||
}
|
||||
|
||||
export function isCoreApp(app: unknown): app is CoreApp {
|
||||
const apps = Object.values(CoreApp).map((coreApp) => coreApp.toString());
|
||||
return typeof app === 'string' && apps.includes(app);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user