mirror of
https://github.com/grafana/grafana.git
synced 2025-09-19 22:34:09 +08:00
Logs: Add show context to dashboard panel (#80403)
* Logs: Add show context to dashboard panel * add prop to enable show context toggle * update test * adjust tests * add query targets as a dependency * extract `useDatasourcesFromTargets` hook * add tests * remove comment
This commit is contained in:
@ -25,12 +25,13 @@ title: LogsPanelCfg kind
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Property | Type | Required | Default | Description |
|
| Property | Type | Required | Default | Description |
|
||||||
|----------------------|---------|----------|---------|---------------------------------------------------------------|
|
|------------------------|---------|----------|---------|---------------------------------------------------------------|
|
||||||
| `dedupStrategy` | string | **Yes** | | Possible values are: `none`, `exact`, `numbers`, `signature`. |
|
| `dedupStrategy` | string | **Yes** | | Possible values are: `none`, `exact`, `numbers`, `signature`. |
|
||||||
| `enableLogDetails` | boolean | **Yes** | | |
|
| `enableLogDetails` | boolean | **Yes** | | |
|
||||||
| `prettifyLogMessage` | boolean | **Yes** | | |
|
| `prettifyLogMessage` | boolean | **Yes** | | |
|
||||||
| `showCommonLabels` | boolean | **Yes** | | |
|
| `showCommonLabels` | boolean | **Yes** | | |
|
||||||
| `showLabels` | boolean | **Yes** | | |
|
| `showLabels` | boolean | **Yes** | | |
|
||||||
|
| `showLogContextToggle` | boolean | **Yes** | | |
|
||||||
| `showTime` | boolean | **Yes** | | |
|
| `showTime` | boolean | **Yes** | | |
|
||||||
| `sortOrder` | string | **Yes** | | Possible values are: `Descending`, `Ascending`. |
|
| `sortOrder` | string | **Yes** | | Possible values are: `Descending`, `Ascending`. |
|
||||||
| `wrapLogMessage` | boolean | **Yes** | | |
|
| `wrapLogMessage` | boolean | **Yes** | | |
|
||||||
|
@ -19,6 +19,7 @@ export interface Options {
|
|||||||
prettifyLogMessage: boolean;
|
prettifyLogMessage: boolean;
|
||||||
showCommonLabels: boolean;
|
showCommonLabels: boolean;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
showLogContextToggle: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
sortOrder: common.LogsSortOrder;
|
sortOrder: common.LogsSortOrder;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
|
@ -1,11 +1,42 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
|
import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv';
|
||||||
|
|
||||||
import { LoadingState, createDataFrame, FieldType, LogsSortOrder } from '@grafana/data';
|
import { LoadingState, createDataFrame, FieldType, LogsSortOrder, CoreApp } from '@grafana/data';
|
||||||
|
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
|
||||||
|
|
||||||
import { LogsPanel } from './LogsPanel';
|
import { LogsPanel } from './LogsPanel';
|
||||||
|
|
||||||
type LogsPanelProps = ComponentProps<typeof LogsPanel>;
|
type LogsPanelProps = ComponentProps<typeof LogsPanel>;
|
||||||
|
type LogRowContextModalProps = ComponentProps<typeof LogRowContextModal>;
|
||||||
|
|
||||||
|
const logRowContextModalMock = jest.fn().mockReturnValue(<div>LogRowContextModal</div>);
|
||||||
|
jest.mock('app/features/logs/components/log-context/LogRowContextModal', () => ({
|
||||||
|
LogRowContextModal: (props: LogRowContextModalProps) => logRowContextModalMock(props),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultDs = new MockDataSourceApi('default datasource', { data: ['default data'] });
|
||||||
|
const noShowContextDs = new MockDataSourceApi('no-show-context');
|
||||||
|
const showContextDs = new MockDataSourceApi('show-context') as MockDataSourceApi & { getLogRowContext: jest.Mock };
|
||||||
|
|
||||||
|
const datasourceSrv = new DatasourceSrvMock(defaultDs, {
|
||||||
|
'no-show-context': noShowContextDs,
|
||||||
|
'show-context': showContextDs,
|
||||||
|
});
|
||||||
|
const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv);
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getDataSourceSrv: () => getDataSourceSrvMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hasLogsContextSupport = jest.fn().mockImplementation((ds) => {
|
||||||
|
return ds.name === 'show-context';
|
||||||
|
});
|
||||||
|
jest.mock('@grafana/data', () => ({
|
||||||
|
...jest.requireActual('@grafana/data'),
|
||||||
|
hasLogsContextSupport: (ds: MockDataSourceApi) => hasLogsContextSupport(ds),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('LogsPanel', () => {
|
describe('LogsPanel', () => {
|
||||||
describe('when returned series include common labels', () => {
|
describe('when returned series include common labels', () => {
|
||||||
@ -33,35 +64,37 @@ describe('LogsPanel', () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
it('shows common labels when showCommonLabels is set to true', () => {
|
it('shows common labels when showCommonLabels is set to true', async () => {
|
||||||
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: true } });
|
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: true } });
|
||||||
|
|
||||||
expect(screen.getByText(/common labels:/i)).toBeInTheDocument();
|
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/common_app/i)).toBeInTheDocument();
|
expect(await screen.findByText(/common_app/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/common_job/i)).toBeInTheDocument();
|
expect(await screen.findByText(/common_job/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('shows common labels on top when descending sort order', () => {
|
it('shows common labels on top when descending sort order', async () => {
|
||||||
const { container } = setup({
|
const { container } = setup({
|
||||||
data: { series: seriesWithCommonLabels },
|
data: { series: seriesWithCommonLabels },
|
||||||
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
|
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
|
||||||
});
|
});
|
||||||
|
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||||
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:common_appcommon_job/);
|
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:common_appcommon_job/);
|
||||||
});
|
});
|
||||||
it('shows common labels on bottom when ascending sort order', () => {
|
it('shows common labels on bottom when ascending sort order', async () => {
|
||||||
const { container } = setup({
|
const { container } = setup({
|
||||||
data: { series: seriesWithCommonLabels },
|
data: { series: seriesWithCommonLabels },
|
||||||
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
|
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
|
||||||
});
|
});
|
||||||
|
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
|
||||||
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:common_appcommon_job$/);
|
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:common_appcommon_job$/);
|
||||||
});
|
});
|
||||||
it('does not show common labels when showCommonLabels is set to false', () => {
|
it('does not show common labels when showCommonLabels is set to false', async () => {
|
||||||
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: false } });
|
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: false } });
|
||||||
|
|
||||||
expect(screen.queryByText(/common labels:/i)).not.toBeInTheDocument();
|
await waitFor(async () => {
|
||||||
expect(screen.queryByText(/common_app/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/common labels:/i)).toBeNull();
|
||||||
expect(screen.queryByText(/common_job/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/common_app/i)).toBeNull();
|
||||||
|
expect(screen.queryByText(/common_job/i)).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when returned series does not include common labels', () => {
|
describe('when returned series does not include common labels', () => {
|
||||||
@ -84,15 +117,139 @@ describe('LogsPanel', () => {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
it('shows (no common labels) when showCommonLabels is set to true', () => {
|
it('shows (no common labels) when showCommonLabels is set to true', async () => {
|
||||||
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: true } });
|
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: true } });
|
||||||
expect(screen.getByText(/common labels:/i)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/(no common labels)/i)).toBeInTheDocument();
|
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', () => {
|
it('does not show common labels when showCommonLabels is set to false', async () => {
|
||||||
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: false } });
|
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: false } });
|
||||||
expect(screen.queryByText(/common labels:/i)).not.toBeInTheDocument();
|
await waitFor(async () => {
|
||||||
expect(screen.queryByText(/(no common labels)/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/common labels:/i)).toBeNull();
|
||||||
|
expect(screen.queryByText(/(no common labels)/i)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('log context', () => {
|
||||||
|
const series = [
|
||||||
|
createDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['logline text'],
|
||||||
|
labels: {
|
||||||
|
app: 'common_app',
|
||||||
|
job: 'common_job',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
showContextDs.getLogRowContext = jest.fn().mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show the toggle if the datasource does not support show context', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
options: { showCommonLabels: false },
|
||||||
|
request: {
|
||||||
|
app: CoreApp.Dashboard,
|
||||||
|
targets: [{ refId: 'A', datasource: { uid: 'no-show-context' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(/logline text/i));
|
||||||
|
expect(screen.queryByLabelText(/show context/i)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the toggle if the datasource does support show context', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
options: { showCommonLabels: false },
|
||||||
|
request: {
|
||||||
|
app: CoreApp.Dashboard,
|
||||||
|
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(/logline text/i));
|
||||||
|
expect(screen.getByLabelText(/show context/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show the toggle if the datasource does support show context but the app is not Dashboard', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
options: { showCommonLabels: false },
|
||||||
|
request: {
|
||||||
|
app: CoreApp.CloudAlerting,
|
||||||
|
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(/logline text/i));
|
||||||
|
expect(screen.queryByLabelText(/show context/i)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the mocked `LogRowContextModal` after click', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
options: { showCommonLabels: false },
|
||||||
|
request: {
|
||||||
|
app: CoreApp.Dashboard,
|
||||||
|
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitFor(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(/logline text/i));
|
||||||
|
await userEvent.click(screen.getByLabelText(/show context/i));
|
||||||
|
expect(screen.getByText(/LogRowContextModal/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `getLogRowContext` if the user clicks the show context toggle', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
options: { showCommonLabels: false },
|
||||||
|
request: {
|
||||||
|
app: CoreApp.Dashboard,
|
||||||
|
targets: [{ refId: 'A', datasource: { uid: 'show-context' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitFor(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(/logline text/i));
|
||||||
|
await userEvent.click(screen.getByLabelText(/show context/i));
|
||||||
|
|
||||||
|
const getRowContextCb = logRowContextModalMock.mock.calls[0][0].getRowContext;
|
||||||
|
getRowContextCb();
|
||||||
|
expect(showContextDs.getLogRowContext).toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,9 +11,13 @@ import {
|
|||||||
DataHoverClearEvent,
|
DataHoverClearEvent,
|
||||||
DataHoverEvent,
|
DataHoverEvent,
|
||||||
CoreApp,
|
CoreApp,
|
||||||
|
DataQueryResponse,
|
||||||
|
LogRowContextOptions,
|
||||||
|
hasLogsContextSupport,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { CustomScrollbar, useStyles2, usePanelContext } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2, usePanelContext } from '@grafana/ui';
|
||||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
|
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
|
||||||
import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView';
|
import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView';
|
||||||
|
|
||||||
import { LogLabels } from '../../../features/logs/components/LogLabels';
|
import { LogLabels } from '../../../features/logs/components/LogLabels';
|
||||||
@ -21,6 +25,7 @@ import { LogRows } from '../../../features/logs/components/LogRows';
|
|||||||
import { dataFrameToLogsModel, dedupLogRows, COMMON_LABELS } from '../../../features/logs/logsModel';
|
import { dataFrameToLogsModel, dedupLogRows, COMMON_LABELS } from '../../../features/logs/logsModel';
|
||||||
|
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
|
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
||||||
|
|
||||||
interface LogsPanelProps extends PanelProps<Options> {}
|
interface LogsPanelProps extends PanelProps<Options> {}
|
||||||
|
|
||||||
@ -37,14 +42,18 @@ export const LogsPanel = ({
|
|||||||
sortOrder,
|
sortOrder,
|
||||||
dedupStrategy,
|
dedupStrategy,
|
||||||
enableLogDetails,
|
enableLogDetails,
|
||||||
|
showLogContextToggle,
|
||||||
},
|
},
|
||||||
title,
|
|
||||||
id,
|
id,
|
||||||
}: LogsPanelProps) => {
|
}: LogsPanelProps) => {
|
||||||
const isAscending = sortOrder === LogsSortOrder.Ascending;
|
const isAscending = sortOrder === LogsSortOrder.Ascending;
|
||||||
const style = useStyles2(getStyles);
|
const style = useStyles2(getStyles);
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
const logsContainerRef = useRef<HTMLDivElement>(null);
|
const logsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [contextRow, setContextRow] = useState<LogRowModel | null>(null);
|
||||||
|
const [closeCallback, setCloseCallback] = useState<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets);
|
||||||
|
|
||||||
const { eventBus } = usePanelContext();
|
const { eventBus } = usePanelContext();
|
||||||
const onLogRowHover = useCallback(
|
const onLogRowHover = useCallback(
|
||||||
@ -64,6 +73,58 @@ export const LogsPanel = ({
|
|||||||
[eventBus]
|
[eventBus]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onCloseContext = useCallback(() => {
|
||||||
|
setContextRow(null);
|
||||||
|
if (closeCallback) {
|
||||||
|
closeCallback();
|
||||||
|
}
|
||||||
|
}, [closeCallback]);
|
||||||
|
|
||||||
|
const onOpenContext = useCallback((row: LogRowModel, onClose: () => void) => {
|
||||||
|
setContextRow(row);
|
||||||
|
setCloseCallback(onClose);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showContextToggle = useCallback(
|
||||||
|
(row: LogRowModel): boolean => {
|
||||||
|
if (
|
||||||
|
!row.dataFrame.refId ||
|
||||||
|
!dataSourcesMap ||
|
||||||
|
(!showLogContextToggle &&
|
||||||
|
data.request?.app !== CoreApp.Dashboard &&
|
||||||
|
data.request?.app !== CoreApp.PanelEditor &&
|
||||||
|
data.request?.app !== CoreApp.PanelViewer)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = dataSourcesMap.get(row.dataFrame.refId);
|
||||||
|
return hasLogsContextSupport(dataSource);
|
||||||
|
},
|
||||||
|
[dataSourcesMap, showLogContextToggle, data.request?.app]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLogRowContext = useCallback(
|
||||||
|
async (row: LogRowModel, origRow: LogRowModel, options: LogRowContextOptions): Promise<DataQueryResponse> => {
|
||||||
|
if (!origRow.dataFrame.refId || !dataSourcesMap) {
|
||||||
|
return Promise.resolve({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = data.request?.targets[0];
|
||||||
|
if (!query) {
|
||||||
|
return Promise.resolve({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = dataSourcesMap.get(origRow.dataFrame.refId);
|
||||||
|
if (!hasLogsContextSupport(dataSource)) {
|
||||||
|
return Promise.resolve({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource.getLogRowContext(row, options, query);
|
||||||
|
},
|
||||||
|
[data.request?.targets, dataSourcesMap]
|
||||||
|
);
|
||||||
|
|
||||||
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
|
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
|
||||||
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
|
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
|
||||||
const logs = data
|
const logs = data
|
||||||
@ -102,11 +163,23 @@ export const LogsPanel = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{contextRow && (
|
||||||
|
<LogRowContextModal
|
||||||
|
open={contextRow !== null}
|
||||||
|
row={contextRow}
|
||||||
|
onClose={onCloseContext}
|
||||||
|
getRowContext={(row, options) => getLogRowContext(row, contextRow, options)}
|
||||||
|
logsSortOrder={sortOrder}
|
||||||
|
timeZone={timeZone}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<CustomScrollbar autoHide scrollTop={scrollTop}>
|
<CustomScrollbar autoHide scrollTop={scrollTop}>
|
||||||
<div className={style.container} ref={logsContainerRef}>
|
<div className={style.container} ref={logsContainerRef}>
|
||||||
{showCommonLabels && !isAscending && renderCommonLabels()}
|
{showCommonLabels && !isAscending && renderCommonLabels()}
|
||||||
<LogRows
|
<LogRows
|
||||||
logRows={logRows}
|
logRows={logRows}
|
||||||
|
showContextToggle={showContextToggle}
|
||||||
deduplicatedRows={deduplicatedRows}
|
deduplicatedRows={deduplicatedRows}
|
||||||
dedupStrategy={dedupStrategy}
|
dedupStrategy={dedupStrategy}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
@ -120,10 +193,12 @@ export const LogsPanel = ({
|
|||||||
previewLimit={isAscending ? logRows.length : undefined}
|
previewLimit={isAscending ? logRows.length : undefined}
|
||||||
onLogRowHover={onLogRowHover}
|
onLogRowHover={onLogRowHover}
|
||||||
app={CoreApp.Dashboard}
|
app={CoreApp.Dashboard}
|
||||||
|
onOpenContext={onOpenContext}
|
||||||
/>
|
/>
|
||||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ composableKinds: PanelCfg: {
|
|||||||
showLabels: bool
|
showLabels: bool
|
||||||
showCommonLabels: bool
|
showCommonLabels: bool
|
||||||
showTime: bool
|
showTime: bool
|
||||||
|
showLogContextToggle: bool
|
||||||
wrapLogMessage: bool
|
wrapLogMessage: bool
|
||||||
prettifyLogMessage: bool
|
prettifyLogMessage: bool
|
||||||
enableLogDetails: bool
|
enableLogDetails: bool
|
||||||
|
@ -16,6 +16,7 @@ export interface Options {
|
|||||||
prettifyLogMessage: boolean;
|
prettifyLogMessage: boolean;
|
||||||
showCommonLabels: boolean;
|
showCommonLabels: boolean;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
showLogContextToggle: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
sortOrder: common.LogsSortOrder;
|
sortOrder: common.LogsSortOrder;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
// CustomHook.test.js
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { MockDataSourceApi, DatasourceSrvMock } from 'test/mocks/datasource_srv';
|
||||||
|
|
||||||
|
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets'; // Update the path accordingly
|
||||||
|
|
||||||
|
const defaultDs = new MockDataSourceApi('default datasource', { data: ['default data'] });
|
||||||
|
const ds1 = new MockDataSourceApi('dataSource1');
|
||||||
|
const ds2 = new MockDataSourceApi('dataSource2') as MockDataSourceApi;
|
||||||
|
|
||||||
|
const datasourceSrv = new DatasourceSrvMock(defaultDs, {
|
||||||
|
dataSource1: ds1,
|
||||||
|
dataSource2: ds2,
|
||||||
|
});
|
||||||
|
const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv);
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getDataSourceSrv: () => getDataSourceSrvMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useDatasourcesFromTargets', () => {
|
||||||
|
it('returns an empty map when targets are not provided', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useDatasourcesFromTargets(undefined));
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches and returns the data sources map', async () => {
|
||||||
|
const mockTargets = [
|
||||||
|
{ refId: '1', datasource: { uid: 'dataSource1' } },
|
||||||
|
{ refId: '2', datasource: { uid: 'dataSource2' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useDatasourcesFromTargets(mockTargets));
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current.size).toBe(2);
|
||||||
|
expect(result.current.get('1')).toEqual(ds1);
|
||||||
|
expect(result.current.get('2')).toEqual(ds2);
|
||||||
|
});
|
||||||
|
});
|
31
public/app/plugins/panel/logs/useDatasourcesFromTargets.ts
Normal file
31
public/app/plugins/panel/logs/useDatasourcesFromTargets.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { DataSourceApi } from '@grafana/data';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
|
||||||
|
export const useDatasourcesFromTargets = (targets: DataQuery[] | undefined): Map<string, DataSourceApi> => {
|
||||||
|
const [dataSourcesMap, setDataSourcesMap] = useState(new Map<string, DataSourceApi>());
|
||||||
|
|
||||||
|
useAsync(async () => {
|
||||||
|
if (!targets) {
|
||||||
|
setDataSourcesMap(new Map<string, DataSourceApi>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await Promise.all(
|
||||||
|
targets
|
||||||
|
.filter((target) => !!target.datasource?.uid)
|
||||||
|
.map((target) =>
|
||||||
|
getDataSourceSrv()
|
||||||
|
.get(target.datasource?.uid)
|
||||||
|
.then((ds) => ({ key: target.refId, ds }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setDataSourcesMap(new Map<string, DataSourceApi>(raw.map(({ key, ds }) => [key, ds])));
|
||||||
|
}, [targets]);
|
||||||
|
|
||||||
|
return dataSourcesMap;
|
||||||
|
};
|
Reference in New Issue
Block a user