2023-04-14 17:05:43 +02:00
|
|
|
import { css, cx } from '@emotion/css';
|
2023-04-20 14:21:14 +02:00
|
|
|
import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
|
|
|
import { useAsync, useAsyncFn } from 'react-use';
|
2023-04-14 17:05:43 +02:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
DataQueryResponse,
|
|
|
|
|
DataSourceWithLogsContextSupport,
|
|
|
|
|
GrafanaTheme2,
|
|
|
|
|
LogRowContextOptions,
|
|
|
|
|
LogRowContextQueryDirection,
|
|
|
|
|
LogRowModel,
|
|
|
|
|
LogsDedupStrategy,
|
|
|
|
|
LogsSortOrder,
|
|
|
|
|
SelectableValue,
|
2023-04-20 14:21:14 +02:00
|
|
|
rangeUtil,
|
2023-04-14 17:05:43 +02:00
|
|
|
} from '@grafana/data';
|
|
|
|
|
import { config } from '@grafana/runtime';
|
2023-04-20 14:21:14 +02:00
|
|
|
import { DataQuery, TimeZone } from '@grafana/schema';
|
|
|
|
|
import { Button, LoadingBar, Modal, useTheme2 } from '@grafana/ui';
|
2023-04-14 17:05:43 +02:00
|
|
|
import { dataFrameToLogsModel } from 'app/core/logsModel';
|
|
|
|
|
import store from 'app/core/store';
|
2023-04-20 14:21:14 +02:00
|
|
|
import { splitOpen } from 'app/features/explore/state/main';
|
2023-04-14 17:05:43 +02:00
|
|
|
import { SETTINGS_KEYS } from 'app/features/explore/utils/logs';
|
2023-04-20 14:21:14 +02:00
|
|
|
import { useDispatch } from 'app/types';
|
2023-04-14 17:05:43 +02:00
|
|
|
|
|
|
|
|
import { LogRows } from '../LogRows';
|
|
|
|
|
|
|
|
|
|
import { LoadMoreOptions, LogContextButtons } from './LogContextButtons';
|
|
|
|
|
|
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => {
|
|
|
|
|
return {
|
|
|
|
|
modal: css`
|
|
|
|
|
width: 85vw;
|
|
|
|
|
${theme.breakpoints.down('md')} {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
`,
|
|
|
|
|
entry: css`
|
|
|
|
|
position: sticky;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
top: -1px;
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
& > td {
|
|
|
|
|
padding: ${theme.spacing(1)} 0 ${theme.spacing(1)} 0;
|
|
|
|
|
}
|
|
|
|
|
background: ${theme.colors.emphasize(theme.colors.background.secondary)};
|
|
|
|
|
|
|
|
|
|
& > table {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
datasourceUi: css`
|
|
|
|
|
padding-bottom: ${theme.spacing(1.25)};
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
`,
|
|
|
|
|
logRowGroups: css`
|
|
|
|
|
overflow: auto;
|
|
|
|
|
max-height: 75%;
|
|
|
|
|
align-self: stretch;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
& > table {
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
flexColumn: css`
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 0 ${theme.spacing(3)} ${theme.spacing(3)} ${theme.spacing(3)};
|
|
|
|
|
`,
|
|
|
|
|
flexRow: css`
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
align-items: center;
|
|
|
|
|
& > div:last-child {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
noMarginBottom: css`
|
|
|
|
|
& > table {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
hidden: css`
|
|
|
|
|
display: none;
|
|
|
|
|
`,
|
|
|
|
|
paddingTop: css`
|
|
|
|
|
padding-top: ${theme.spacing(1)};
|
|
|
|
|
`,
|
|
|
|
|
paddingBottom: css`
|
|
|
|
|
padding-bottom: ${theme.spacing(1)};
|
|
|
|
|
`,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export enum LogGroupPosition {
|
|
|
|
|
Bottom = 'bottom',
|
|
|
|
|
Top = 'top',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LogRowContextModalProps {
|
|
|
|
|
row: LogRowModel;
|
|
|
|
|
open: boolean;
|
|
|
|
|
timeZone: TimeZone;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
|
2023-04-20 14:21:14 +02:00
|
|
|
|
|
|
|
|
getRowContextQuery?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQuery | null>;
|
2023-04-14 17:05:43 +02:00
|
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
|
|
|
runContextQuery?: () => void;
|
|
|
|
|
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps> = ({
|
|
|
|
|
row,
|
|
|
|
|
open,
|
|
|
|
|
logsSortOrder,
|
2023-04-20 14:21:14 +02:00
|
|
|
timeZone,
|
2023-04-14 17:05:43 +02:00
|
|
|
getLogRowContextUi,
|
2023-04-20 14:21:14 +02:00
|
|
|
getRowContextQuery,
|
2023-04-14 17:05:43 +02:00
|
|
|
onClose,
|
|
|
|
|
getRowContext,
|
|
|
|
|
}) => {
|
|
|
|
|
const scrollElement = React.createRef<HTMLDivElement>();
|
|
|
|
|
const entryElement = React.createRef<HTMLTableRowElement>();
|
2023-04-17 13:05:56 +02:00
|
|
|
// We can not use `entryElement` to scroll to the right element because it's
|
|
|
|
|
// sticky. That's why we add another row and use this ref to scroll to that
|
|
|
|
|
// first.
|
|
|
|
|
const preEntryElement = React.createRef<HTMLTableRowElement>();
|
2023-04-14 17:05:43 +02:00
|
|
|
|
2023-04-20 14:21:14 +02:00
|
|
|
const dispatch = useDispatch();
|
2023-04-14 17:05:43 +02:00
|
|
|
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]);
|
2023-04-20 14:21:14 +02:00
|
|
|
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]);
|
2023-04-14 17:05:43 +02:00
|
|
|
|
|
|
|
|
const onChangeLimitOption = (option: SelectableValue<number>) => {
|
|
|
|
|
setLoadMoreOption(option);
|
|
|
|
|
setLimit(option.value!);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [{ loading }, fetchResults] = useAsyncFn(async () => {
|
|
|
|
|
if (open && row && limit) {
|
|
|
|
|
const rawResults = await Promise.all([
|
|
|
|
|
getRowContext(row, {
|
|
|
|
|
limit: logsSortOrder === LogsSortOrder.Descending ? limit + 1 : limit,
|
|
|
|
|
direction:
|
|
|
|
|
logsSortOrder === LogsSortOrder.Descending
|
|
|
|
|
? LogRowContextQueryDirection.Forward
|
|
|
|
|
: LogRowContextQueryDirection.Backward,
|
|
|
|
|
}),
|
|
|
|
|
getRowContext(row, {
|
|
|
|
|
limit: logsSortOrder === LogsSortOrder.Ascending ? limit + 1 : limit,
|
|
|
|
|
direction:
|
|
|
|
|
logsSortOrder === LogsSortOrder.Ascending
|
|
|
|
|
? LogRowContextQueryDirection.Forward
|
|
|
|
|
: LogRowContextQueryDirection.Backward,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const logsModels = rawResults.map((result) => {
|
|
|
|
|
return dataFrameToLogsModel(result.data);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const afterRows = logsSortOrder === LogsSortOrder.Ascending ? logsModels[0].rows.reverse() : logsModels[0].rows;
|
|
|
|
|
const beforeRows = logsSortOrder === LogsSortOrder.Ascending ? logsModels[1].rows.reverse() : logsModels[1].rows;
|
|
|
|
|
|
|
|
|
|
setContext({
|
|
|
|
|
after: afterRows.filter((r) => {
|
|
|
|
|
return r.timeEpochNs !== row.timeEpochNs && r.entry !== row.entry;
|
|
|
|
|
}),
|
|
|
|
|
before: beforeRows.filter((r) => {
|
|
|
|
|
return r.timeEpochNs !== row.timeEpochNs && r.entry !== row.entry;
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setContext({ after: [], before: [] });
|
|
|
|
|
}
|
|
|
|
|
}, [row, open, limit]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
fetchResults();
|
|
|
|
|
}
|
|
|
|
|
}, [fetchResults, open]);
|
|
|
|
|
|
|
|
|
|
const [displayedFields, setDisplayedFields] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
const showField = (key: string) => {
|
|
|
|
|
const index = displayedFields.indexOf(key);
|
|
|
|
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
setDisplayedFields([...displayedFields, key]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hideField = (key: string) => {
|
|
|
|
|
const index = displayedFields.indexOf(key);
|
|
|
|
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
displayedFields.splice(index, 1);
|
|
|
|
|
setDisplayedFields([...displayedFields]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
2023-04-17 13:05:56 +02:00
|
|
|
if (!loading && entryElement.current && preEntryElement.current) {
|
|
|
|
|
preEntryElement.current.scrollIntoView({ block: 'center' });
|
2023-04-14 17:05:43 +02:00
|
|
|
entryElement.current.scrollIntoView({ block: 'center' });
|
|
|
|
|
}
|
2023-04-17 13:05:56 +02:00
|
|
|
}, [entryElement, preEntryElement, context, loading]);
|
2023-04-14 17:05:43 +02:00
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
const width = scrollElement?.current?.parentElement?.clientWidth;
|
|
|
|
|
if (width && width > 0) {
|
|
|
|
|
setLoadingWidth(width);
|
|
|
|
|
}
|
|
|
|
|
}, [scrollElement]);
|
|
|
|
|
|
2023-04-20 14:21:14 +02:00
|
|
|
useAsync(async () => {
|
|
|
|
|
const contextQuery = getRowContextQuery ? await getRowContextQuery(row) : null;
|
|
|
|
|
setContextQuery(contextQuery);
|
|
|
|
|
}, [getRowContextQuery, row]);
|
|
|
|
|
|
2023-04-14 17:05:43 +02:00
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
isOpen={open}
|
|
|
|
|
title="Log context"
|
|
|
|
|
contentClassName={styles.flexColumn}
|
|
|
|
|
className={styles.modal}
|
|
|
|
|
onDismiss={onClose}
|
|
|
|
|
>
|
|
|
|
|
{config.featureToggles.logsContextDatasourceUi && getLogRowContextUi && (
|
|
|
|
|
<div className={styles.datasourceUi}>{getLogRowContextUi(row, fetchResults)}</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className={cx(styles.flexRow, styles.paddingBottom)}>
|
|
|
|
|
<div className={loading ? styles.hidden : ''}>
|
|
|
|
|
Showing {context.after.length} lines {logsSortOrder === LogsSortOrder.Ascending ? 'after' : 'before'} match.
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<LogContextButtons onChangeOption={onChangeLimitOption} option={loadMoreOption} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={loading ? '' : styles.hidden}>
|
|
|
|
|
<LoadingBar width={loadingWidth} />
|
|
|
|
|
</div>
|
|
|
|
|
<div ref={scrollElement} className={styles.logRowGroups}>
|
|
|
|
|
<table>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td className={styles.noMarginBottom}>
|
|
|
|
|
<LogRows
|
|
|
|
|
logRows={context.after}
|
|
|
|
|
dedupStrategy={LogsDedupStrategy.none}
|
|
|
|
|
showLabels={store.getBool(SETTINGS_KEYS.showLabels, false)}
|
|
|
|
|
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
|
|
|
|
wrapLogMessage={store.getBool(SETTINGS_KEYS.wrapLogMessage, true)}
|
|
|
|
|
prettifyLogMessage={store.getBool(SETTINGS_KEYS.prettifyLogMessage, false)}
|
|
|
|
|
enableLogDetails={true}
|
|
|
|
|
timeZone={timeZone}
|
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
|
onClickShowField={showField}
|
|
|
|
|
onClickHideField={hideField}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
2023-04-17 13:05:56 +02:00
|
|
|
<tr ref={preEntryElement}></tr>
|
2023-04-14 17:05:43 +02:00
|
|
|
<tr ref={entryElement} className={styles.entry}>
|
|
|
|
|
<td className={styles.noMarginBottom}>
|
|
|
|
|
<LogRows
|
|
|
|
|
logRows={[row]}
|
|
|
|
|
dedupStrategy={LogsDedupStrategy.none}
|
|
|
|
|
showLabels={store.getBool(SETTINGS_KEYS.showLabels, false)}
|
|
|
|
|
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
|
|
|
|
wrapLogMessage={store.getBool(SETTINGS_KEYS.wrapLogMessage, true)}
|
|
|
|
|
prettifyLogMessage={store.getBool(SETTINGS_KEYS.prettifyLogMessage, false)}
|
|
|
|
|
enableLogDetails={true}
|
|
|
|
|
timeZone={timeZone}
|
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
|
onClickShowField={showField}
|
|
|
|
|
onClickHideField={hideField}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>
|
|
|
|
|
<LogRows
|
|
|
|
|
logRows={context.before}
|
|
|
|
|
dedupStrategy={LogsDedupStrategy.none}
|
|
|
|
|
showLabels={store.getBool(SETTINGS_KEYS.showLabels, false)}
|
|
|
|
|
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
|
|
|
|
wrapLogMessage={store.getBool(SETTINGS_KEYS.wrapLogMessage, true)}
|
|
|
|
|
prettifyLogMessage={store.getBool(SETTINGS_KEYS.prettifyLogMessage, false)}
|
|
|
|
|
enableLogDetails={true}
|
|
|
|
|
timeZone={timeZone}
|
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
|
onClickShowField={showField}
|
|
|
|
|
onClickHideField={hideField}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className={cx(styles.paddingTop, loading ? styles.hidden : '')}>
|
|
|
|
|
Showing {context.before.length} lines {logsSortOrder === LogsSortOrder.Descending ? 'after' : 'before'} match.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2023-04-20 14:21:14 +02:00
|
|
|
{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>
|
|
|
|
|
)}
|
2023-04-14 17:05:43 +02:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|