Log Context: Add button to open the context query in a split view (#66777)

* add show context button

* improve type definition

* change to default `maxLines`

* remove context query

* add provider to tests

* add test for split view button

* improve documentation

* add tests for `getLogRowContextQuery`

* refactor LogsContainer functions

* fix spelling

* add `contextQuery` as state

* fix tests

* fix lint

* do not use callIfDefined

* make button secondary
This commit is contained in:
Sven Grossmann
2023-04-20 14:21:14 +02:00
committed by GitHub
parent 40c7b3126e
commit 1e53a87d76
9 changed files with 282 additions and 35 deletions

View File

@ -1,7 +1,8 @@
import { render, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { render } from 'test/redux-rtl';
import { createLogRow } from '../__mocks__/logRow';
@ -9,6 +10,18 @@ import { LogRowContextModal } from './LogRowContextModal';
const getRowContext = jest.fn().mockResolvedValue({ data: { fields: [], rows: [] } });
const dispatchMock = jest.fn();
jest.mock('app/types', () => ({
...jest.requireActual('app/types'),
useDispatch: () => dispatchMock,
}));
const splitOpen = Symbol('splitOpen');
jest.mock('app/features/explore/state/main', () => ({
...jest.requireActual('app/features/explore/state/main'),
splitOpen: () => splitOpen,
}));
const row = createLogRow({ uid: '1' });
const timeZone = 'UTC';
@ -24,12 +37,20 @@ describe('LogRowContextModal', () => {
jest.clearAllMocks();
});
it('should not render modal when it is closed', () => {
render(
<LogRowContextModal row={row} open={false} onClose={() => {}} getRowContext={getRowContext} timeZone={timeZone} />
);
it('should not render modal when it is closed', async () => {
act(() => {
render(
<LogRowContextModal
row={row}
open={false}
onClose={() => {}}
getRowContext={getRowContext}
timeZone={timeZone}
/>
);
});
expect(screen.queryByText('Log context')).not.toBeInTheDocument();
await waitFor(() => expect(screen.queryByText('Log context')).not.toBeInTheDocument());
});
it('should render modal when it is open', async () => {
@ -48,12 +69,20 @@ describe('LogRowContextModal', () => {
await waitFor(() => expect(screen.queryByText('Log context')).toBeInTheDocument());
});
it('should call getRowContext on open and change of row', () => {
render(
<LogRowContextModal row={row} open={false} onClose={() => {}} getRowContext={getRowContext} timeZone={timeZone} />
);
it('should call getRowContext on open and change of row', async () => {
act(() => {
render(
<LogRowContextModal
row={row}
open={false}
onClose={() => {}}
getRowContext={getRowContext}
timeZone={timeZone}
/>
);
});
expect(getRowContext).not.toHaveBeenCalled();
await waitFor(() => expect(getRowContext).not.toHaveBeenCalled());
});
it('should call getRowContext on open', async () => {
act(() => {
@ -97,4 +126,102 @@ describe('LogRowContextModal', () => {
await waitFor(() => expect(getRowContext).toHaveBeenCalledTimes(4));
});
it('should show a split view button', async () => {
const getRowContextQuery = jest.fn().mockResolvedValue({ datasource: { uid: 'test-uid' } });
render(
<LogRowContextModal
row={row}
open={true}
onClose={() => {}}
getRowContext={getRowContext}
getRowContextQuery={getRowContextQuery}
timeZone={timeZone}
/>
);
await waitFor(() =>
expect(
screen.getByRole('button', {
name: /open in split view/i,
})
).toBeInTheDocument()
);
});
it('should not show a split view button', async () => {
render(
<LogRowContextModal row={row} open={true} onClose={() => {}} getRowContext={getRowContext} timeZone={timeZone} />
);
expect(
screen.queryByRole('button', {
name: /open in split view/i,
})
).not.toBeInTheDocument();
});
it('should call getRowContextQuery', async () => {
const getRowContextQuery = jest.fn().mockResolvedValue({ datasource: { uid: 'test-uid' } });
render(
<LogRowContextModal
row={row}
open={true}
onClose={() => {}}
getRowContext={getRowContext}
getRowContextQuery={getRowContextQuery}
timeZone={timeZone}
/>
);
await waitFor(() => expect(getRowContextQuery).toHaveBeenCalledTimes(1));
});
it('should close modal', async () => {
const getRowContextQuery = jest.fn().mockResolvedValue({ datasource: { uid: 'test-uid' } });
const onClose = jest.fn();
render(
<LogRowContextModal
row={row}
open={true}
onClose={onClose}
getRowContext={getRowContext}
getRowContextQuery={getRowContextQuery}
timeZone={timeZone}
/>
);
const splitViewButton = await screen.findByRole('button', {
name: /open in split view/i,
});
await userEvent.click(splitViewButton);
expect(onClose).toHaveBeenCalled();
});
it('should dispatch splitOpen', async () => {
const getRowContextQuery = jest.fn().mockResolvedValue({ datasource: { uid: 'test-uid' } });
const onClose = jest.fn();
render(
<LogRowContextModal
row={row}
open={true}
onClose={onClose}
getRowContext={getRowContext}
getRowContextQuery={getRowContextQuery}
timeZone={timeZone}
/>
);
const splitViewButton = await screen.findByRole('button', {
name: /open in split view/i,
});
await userEvent.click(splitViewButton);
await waitFor(() => expect(dispatchMock).toHaveBeenCalledWith(splitOpen));
});
});

View File

@ -1,6 +1,6 @@
import { css, cx } from '@emotion/css';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { useAsyncFn } from 'react-use';
import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { useAsync, useAsyncFn } from 'react-use';
import {
DataQueryResponse,
@ -12,13 +12,16 @@ import {
LogsDedupStrategy,
LogsSortOrder,
SelectableValue,
rangeUtil,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
import { LoadingBar, Modal, useTheme2 } from '@grafana/ui';
import { DataQuery, TimeZone } from '@grafana/schema';
import { Button, LoadingBar, Modal, useTheme2 } from '@grafana/ui';
import { dataFrameToLogsModel } from 'app/core/logsModel';
import store from 'app/core/store';
import { splitOpen } from 'app/features/explore/state/main';
import { SETTINGS_KEYS } from 'app/features/explore/utils/logs';
import { useDispatch } from 'app/types';
import { LogRows } from '../LogRows';
@ -104,6 +107,8 @@ interface LogRowContextModalProps {
timeZone: TimeZone;
onClose: () => void;
getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
getRowContextQuery?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQuery | null>;
logsSortOrder?: LogsSortOrder | null;
runContextQuery?: () => void;
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'];
@ -113,10 +118,11 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
row,
open,
logsSortOrder,
timeZone,
getLogRowContextUi,
getRowContextQuery,
onClose,
getRowContext,
timeZone,
}) => {
const scrollElement = React.createRef<HTMLDivElement>();
const entryElement = React.createRef<HTMLTableRowElement>();
@ -125,12 +131,28 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
// first.
const preEntryElement = React.createRef<HTMLTableRowElement>();
const dispatch = useDispatch();
const theme = useTheme2();
const styles = getStyles(theme);
const [context, setContext] = useState<{ after: LogRowModel[]; before: LogRowModel[] }>({ after: [], before: [] });
const [limit, setLimit] = useState<number>(LoadMoreOptions[0].value!);
const [loadingWidth, setLoadingWidth] = useState(0);
const [loadMoreOption, setLoadMoreOption] = useState<SelectableValue<number>>(LoadMoreOptions[0]);
const [contextQuery, setContextQuery] = useState<DataQuery | null>(null);
const getFullTimeRange = useCallback(() => {
const { before, after } = context;
const allRows = [...before, row, ...after].sort((a, b) => a.timeEpochMs - b.timeEpochMs);
const first = allRows[0];
const last = allRows[allRows.length - 1];
return rangeUtil.convertRawToRange(
{
from: first.timeUtc,
to: last.timeUtc,
},
'utc'
);
}, [context, row]);
const onChangeLimitOption = (option: SelectableValue<number>) => {
setLoadMoreOption(option);
@ -215,6 +237,11 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
}
}, [scrollElement]);
useAsync(async () => {
const contextQuery = getRowContextQuery ? await getRowContextQuery(row) : null;
setContextQuery(contextQuery);
}, [getRowContextQuery, row]);
return (
<Modal
isOpen={open}
@ -300,6 +327,25 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
Showing {context.before.length} lines {logsSortOrder === LogsSortOrder.Descending ? 'after' : 'before'} match.
</div>
</div>
{contextQuery?.datasource?.uid && (
<Modal.ButtonRow>
<Button
variant="secondary"
onClick={async () => {
dispatch(
splitOpen({
queries: [contextQuery],
range: getFullTimeRange(),
datasourceUid: contextQuery.datasource!.uid!,
})
);
onClose();
}}
>
Open in split view
</Button>
</Modal.ButtonRow>
)}
</Modal>
);
};