mirror of https://github.com/grafana/grafana.git
New Logs Panel: Render new panel using the current visualization (#105968)
Actionlint / Lint GitHub Actions files (push) Waiting to run
Details
Backend Code Checks / Validate Backend Configs (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
CodeQL checks / Analyze (actions) (push) Waiting to run
Details
CodeQL checks / Analyze (go) (push) Waiting to run
Details
CodeQL checks / Analyze (javascript) (push) Waiting to run
Details
CodeQL checks / Analyze (python) (push) Waiting to run
Details
Lint Frontend / Verify i18n (push) Waiting to run
Details
Lint Frontend / Lint (push) Waiting to run
Details
Lint Frontend / Typecheck (push) Waiting to run
Details
Lint Frontend / Betterer (push) Waiting to run
Details
End-to-end tests / Build & Package Grafana (push) Waiting to run
Details
End-to-end tests / ${{ matrix.suite }} (dashboards-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (panels-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (smoke-tests-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (various-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/dashboards-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/panels-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/smoke-tests-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/various-suite) (push) Blocked by required conditions
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (1) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (2) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (3) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (4) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (5) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (6) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (7) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Reject GitHub secrets / reject-gh-secrets (push) Waiting to run
Details
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Waiting to run
Details
Dispatch sync to mirror / dispatch-job (push) Waiting to run
Details
publish-kinds-next / main (push) Has been cancelled
Details
Actionlint / Lint GitHub Actions files (push) Waiting to run
Details
Backend Code Checks / Validate Backend Configs (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
CodeQL checks / Analyze (actions) (push) Waiting to run
Details
CodeQL checks / Analyze (go) (push) Waiting to run
Details
CodeQL checks / Analyze (javascript) (push) Waiting to run
Details
CodeQL checks / Analyze (python) (push) Waiting to run
Details
Lint Frontend / Verify i18n (push) Waiting to run
Details
Lint Frontend / Lint (push) Waiting to run
Details
Lint Frontend / Typecheck (push) Waiting to run
Details
Lint Frontend / Betterer (push) Waiting to run
Details
End-to-end tests / Build & Package Grafana (push) Waiting to run
Details
End-to-end tests / ${{ matrix.suite }} (dashboards-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (panels-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (smoke-tests-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (various-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/dashboards-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/panels-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/smoke-tests-suite) (push) Blocked by required conditions
Details
End-to-end tests / ${{ matrix.suite }} (old arch) (old-arch/various-suite) (push) Blocked by required conditions
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (1) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (2) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (3) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (4) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (5) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (6) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (7) (push) Waiting to run
Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / Sqlite (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / MySQL (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (1/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (2/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (3/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (4/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (5/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (6/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (7/8) (push) Waiting to run
Details
Integration Tests / Postgres (${{ matrix.shard }}) (8/8) (push) Waiting to run
Details
Reject GitHub secrets / reject-gh-secrets (push) Waiting to run
Details
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Waiting to run
Details
Dispatch sync to mirror / dispatch-job (push) Waiting to run
Details
publish-kinds-next / main (push) Has been cancelled
Details
* LogsPanel: integrate new panel via feature flag * Log Line: move sampled/errors/deduplication count outside of log line body * LogList: increase overscan count * Logs Panel: enable deduplication for infinite scrolling * Logs Panel: remove margin overflowing drilldown * Logs Panel: add missing dependency to effect * Logs Panel: pass missing callback * Remove console log * LogLine: show cursor pointer only when interactable * LogLineDetails: make resize handler more obvious * LogsPanel: add missing props to from panel * LogLineMenu: add support for custom items * LogsPanel: pass custom menu items to LogList * Fix imports * Chore: comments and missing argument * LogLineMenu: pass log to event listener * LogListContext: filter log details when no longer present in the new response * chore: log * LogsPanel: conditionally show options per feature flag status * LogLine: align logs when some of them are sampled or with errors * Chore: update tests * LogLineMenu: test custom options * LogsSamplePanel: show controls * LogsPanel: move return after hooks to prevent bugs
This commit is contained in:
parent
5b4d188638
commit
b3596e8c72
|
@ -19,6 +19,7 @@ export interface Options {
|
|||
enableInfiniteScrolling?: boolean;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
logLineMenuCustomItems?: unknown;
|
||||
logRowMenuIconsAfter?: unknown;
|
||||
logRowMenuIconsBefore?: unknown;
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ import { LogRows } from '../../logs/components/LogRows';
|
|||
import { dataFrameToLogsModel } from '../../logs/logsModel';
|
||||
import { SupplementaryResultError } from '../SupplementaryResultError';
|
||||
|
||||
import { SETTINGS_KEYS } from './utils/logs';
|
||||
import { SETTING_KEY_ROOT, SETTINGS_KEYS } from './utils/logs';
|
||||
|
||||
type Props = {
|
||||
queryResponse: DataQueryResponse | undefined;
|
||||
|
@ -118,8 +118,9 @@ export function LogsSamplePanel(props: Props) {
|
|||
enableLogDetails
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
displayedFields={[]}
|
||||
logOptionsStorageKey={SETTING_KEY_ROOT}
|
||||
logs={logs.rows}
|
||||
showControls={false}
|
||||
showControls
|
||||
showTime={store.getBool(SETTINGS_KEYS.showTime, true)}
|
||||
sortOrder={store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending}
|
||||
timeRange={props.timeRange}
|
||||
|
|
|
@ -22,7 +22,6 @@ const contextProps = {
|
|||
app: CoreApp.Unknown,
|
||||
dedupStrategy: LogsDedupStrategy.exact,
|
||||
displayedFields: [],
|
||||
logs: [],
|
||||
showControls: false,
|
||||
showTime: false,
|
||||
sortOrder: LogsSortOrder.Ascending,
|
||||
|
@ -33,6 +32,7 @@ describe('LogLine', () => {
|
|||
let log: LogListModel, defaultProps: Props;
|
||||
beforeEach(() => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||
contextProps.logs = [log];
|
||||
defaultProps = {
|
||||
displayedFields: [],
|
||||
index: 0,
|
||||
|
@ -106,9 +106,10 @@ describe('LogLine', () => {
|
|||
|
||||
test('Shows log lines with errors', async () => {
|
||||
log.hasError = true;
|
||||
log.labels.__error__ = 'error message';
|
||||
jest.spyOn(log, 'errorMessage', 'get').mockReturnValue('error message');
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} dedupStrategy={LogsDedupStrategy.signature}>
|
||||
<LogListContextProvider {...contextProps} dedupStrategy={LogsDedupStrategy.signature} logs={[log]}>
|
||||
<LogLine {...defaultProps} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
|
@ -118,9 +119,10 @@ describe('LogLine', () => {
|
|||
|
||||
test('Shows sampled log lines', async () => {
|
||||
log.isSampled = true;
|
||||
log.labels.__adaptive_logs_sampled__ = 'true';
|
||||
jest.spyOn(log, 'sampledMessage', 'get').mockReturnValue('sampled message');
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} dedupStrategy={LogsDedupStrategy.signature}>
|
||||
<LogListContextProvider {...contextProps} dedupStrategy={LogsDedupStrategy.signature} logs={[log]}>
|
||||
<LogLine {...defaultProps} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
|
|
|
@ -45,7 +45,8 @@ export const LogLine = ({
|
|||
variant,
|
||||
wrapLogMessage,
|
||||
}: Props) => {
|
||||
const { detailsDisplayed, onLogLineHover } = useLogListContext();
|
||||
const { detailsDisplayed, dedupStrategy, enableLogDetails, hasLogsWithErrors, hasSampledLogs, onLogLineHover } =
|
||||
useLogListContext();
|
||||
const [collapsed, setCollapsed] = useState<boolean | undefined>(
|
||||
wrapLogMessage && log.collapsed !== undefined ? log.collapsed : undefined
|
||||
);
|
||||
|
@ -99,10 +100,49 @@ export const LogLine = ({
|
|||
onFocus={handleMouseOver}
|
||||
>
|
||||
<LogLineMenu styles={styles} log={log} />
|
||||
{dedupStrategy !== LogsDedupStrategy.none && (
|
||||
<div className={`${styles.duplicates}`}>
|
||||
{log.duplicates && log.duplicates > 0 ? `${log.duplicates + 1}x` : null}
|
||||
</div>
|
||||
)}
|
||||
{hasLogsWithErrors && (
|
||||
<div className={`${styles.hasError}`}>
|
||||
{log.hasError && (
|
||||
<Tooltip
|
||||
content={t('logs.log-line.tooltip-error', 'Error: {{errorMessage}}', {
|
||||
errorMessage: log.errorMessage,
|
||||
})}
|
||||
placement="right"
|
||||
theme="error"
|
||||
>
|
||||
<Icon
|
||||
className={styles.logIconError}
|
||||
name="exclamation-triangle"
|
||||
aria-label={t('logs.log-line.has-error', 'Has errors')}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSampledLogs && (
|
||||
<div className={`${styles.isSampled}`}>
|
||||
{log.isSampled && (
|
||||
<Tooltip content={log.sampledMessage ?? ''} placement="right" theme="info">
|
||||
<Icon
|
||||
className={styles.logIconInfo}
|
||||
name="info-circle"
|
||||
size="xs"
|
||||
aria-label={t('logs.log-line.is-sampled', 'Is sampled')}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* A button element could be used but in Safari it prevents text selection. Fallback available for a11y in LogLineMenu */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''}`}
|
||||
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''} ${enableLogDetails ? styles.clickable : ''}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Log
|
||||
|
@ -153,43 +193,8 @@ interface LogProps {
|
|||
}
|
||||
|
||||
const Log = ({ displayedFields, log, showTime, styles, wrapLogMessage }: LogProps) => {
|
||||
const { dedupStrategy } = useLogListContext();
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<>
|
||||
{dedupStrategy !== LogsDedupStrategy.none && (
|
||||
<span className={`${styles.duplicates} field`}>
|
||||
{log.duplicates && log.duplicates > 0 ? `${log.duplicates + 1}x` : null}
|
||||
</span>
|
||||
)}
|
||||
{log.hasError && (
|
||||
<span className={`${styles.hasError} field`}>
|
||||
<Tooltip
|
||||
content={t('logs.log-line.tooltip-error', 'Error: {{errorMessage}}', { errorMessage: log.errorMessage })}
|
||||
placement="right"
|
||||
theme="error"
|
||||
>
|
||||
<Icon
|
||||
className={styles.logIconError}
|
||||
name="exclamation-triangle"
|
||||
aria-label={t('logs.log-line.has-error', 'Has errors')}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
{log.isSampled && (
|
||||
<span className={`${styles.isSampled} field`}>
|
||||
<Tooltip content={log.sampledMessage ?? ''} placement="right" theme="info">
|
||||
<Icon
|
||||
className={styles.logIconInfo}
|
||||
name="info-circle"
|
||||
size="xs"
|
||||
aria-label={t('logs.log-line.is-sampled', 'Is sampled')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
{showTime && <span className={`${styles.timestamp} level-${log.logLevel} field`}>{log.timestamp}</span>}
|
||||
{
|
||||
// When logs are unwrapped, we want an empty column space to align with other log lines.
|
||||
|
@ -334,12 +339,12 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||
display: 'inline-block',
|
||||
}),
|
||||
duplicates: css({
|
||||
display: 'inline-block',
|
||||
flexShrink: 0,
|
||||
textAlign: 'center',
|
||||
width: theme.spacing(4.5),
|
||||
}),
|
||||
hasError: css({
|
||||
display: 'inline-block',
|
||||
flexShrink: 0,
|
||||
width: theme.spacing(2),
|
||||
'& svg': {
|
||||
position: 'relative',
|
||||
|
@ -347,7 +352,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||
},
|
||||
}),
|
||||
isSampled: css({
|
||||
display: 'inline-block',
|
||||
flexShrink: 0,
|
||||
width: theme.spacing(2),
|
||||
'& svg': {
|
||||
position: 'relative',
|
||||
|
@ -389,15 +394,16 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||
overflows: css({
|
||||
outline: 'solid 1px red',
|
||||
}),
|
||||
unwrappedLogLine: css({
|
||||
clickable: css({
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
unwrappedLogLine: css({
|
||||
display: 'grid',
|
||||
gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER),
|
||||
whiteSpace: 'pre',
|
||||
paddingBottom: theme.spacing(0.75),
|
||||
}),
|
||||
wrappedLogLine: css({
|
||||
cursor: 'pointer',
|
||||
alignSelf: 'flex-start',
|
||||
paddingBottom: theme.spacing(0.75),
|
||||
whiteSpace: 'pre-wrap',
|
||||
|
|
|
@ -54,7 +54,7 @@ export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize
|
|||
return (
|
||||
<Resizable
|
||||
onResize={handleResize}
|
||||
handleClasses={{ left: dragStyles.dragHandleBaseVertical }}
|
||||
handleClasses={{ left: dragStyles.dragHandleVertical }}
|
||||
defaultSize={{ width: detailsWidth, height: containerElement.clientHeight }}
|
||||
enable={{ left: true }}
|
||||
minWidth={40}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { CoreApp, createTheme, LogsDedupStrategy, LogsSortOrder } from '@grafana
|
|||
import { createLogLine } from '../__mocks__/logRow';
|
||||
|
||||
import { getStyles } from './LogLine';
|
||||
import { LogLineMenu } from './LogLineMenu';
|
||||
import { LogLineMenu, LogLineMenuCustomItem } from './LogLineMenu';
|
||||
import { LogListContextProvider } from './LogListContext';
|
||||
import { defaultProps, defaultValue } from './__mocks__/LogListContext';
|
||||
import { LogListModel } from './processing';
|
||||
|
@ -54,6 +54,33 @@ describe('LogLineMenu', () => {
|
|||
expect(onPermalinkClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Allows to copy a permalink', async () => {
|
||||
const customOption1onClick = jest.fn();
|
||||
const logLineMenuCustomItems: LogLineMenuCustomItem[] = [
|
||||
{
|
||||
label: 'Custom option 1',
|
||||
onClick: customOption1onClick,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Custom option 2',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
];
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} logLineMenuCustomItems={logLineMenuCustomItems}>
|
||||
<LogLineMenu log={log} styles={styles} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText('Log menu'));
|
||||
await screen.findByText('Custom option 1');
|
||||
await screen.findByText('Custom option 2');
|
||||
await userEvent.click(screen.getByText('Custom option 1'));
|
||||
expect(customOption1onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Allows to open show context', async () => {
|
||||
const onOpenContext = jest.fn();
|
||||
const logSupportsContext = jest.fn().mockReturnValue(true);
|
||||
|
|
|
@ -17,6 +17,17 @@ export type GetRowContextQueryFn = (
|
|||
cacheFilters?: boolean
|
||||
) => Promise<DataQuery | null>;
|
||||
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
onClick(log: LogListModel): void;
|
||||
};
|
||||
|
||||
type MenuItemDivider = {
|
||||
divider: true;
|
||||
};
|
||||
|
||||
export type LogLineMenuCustomItem = MenuItem | MenuItemDivider;
|
||||
|
||||
interface Props {
|
||||
log: LogListModel;
|
||||
styles: LogLineStyles;
|
||||
|
@ -31,6 +42,7 @@ export const LogLineMenu = ({ log, styles }: Props) => {
|
|||
onPermalinkClick,
|
||||
onPinLine,
|
||||
onUnpinLine,
|
||||
logLineMenuCustomItems = [],
|
||||
logSupportsContext,
|
||||
toggleDetails,
|
||||
} = useLogListContext();
|
||||
|
@ -98,6 +110,15 @@ export const LogLineMenu = ({ log, styles }: Props) => {
|
|||
{onPermalinkClick && log.rowId !== undefined && log.uid && (
|
||||
<Menu.Item onClick={copyLinkToLogLine} label={t('logs.log-line-menu.copy-link', 'Copy link to log line')} />
|
||||
)}
|
||||
{logLineMenuCustomItems.map((item, i) => {
|
||||
if (isDivider(item)) {
|
||||
return <Menu.Divider key={i} />;
|
||||
}
|
||||
if (isItem(item)) {
|
||||
return <Menu.Item onClick={() => item.onClick(log)} label={item.label} key={i} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Menu>
|
||||
),
|
||||
[
|
||||
|
@ -106,6 +127,7 @@ export const LogLineMenu = ({ log, styles }: Props) => {
|
|||
detailsDisplayed,
|
||||
enableLogDetails,
|
||||
log,
|
||||
logLineMenuCustomItems,
|
||||
onPermalinkClick,
|
||||
onPinLine,
|
||||
onUnpinLine,
|
||||
|
@ -128,3 +150,11 @@ export const LogLineMenu = ({ log, styles }: Props) => {
|
|||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
function isDivider(item: LogLineMenuCustomItem) {
|
||||
return 'divider' in item && item.divider;
|
||||
}
|
||||
|
||||
function isItem(item: LogLineMenuCustomItem) {
|
||||
return 'onClick' in item && 'label' in item;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
|||
import { InfiniteScroll } from './InfiniteScroll';
|
||||
import { getGridTemplateColumns } from './LogLine';
|
||||
import { LogLineDetails } from './LogLineDetails';
|
||||
import { GetRowContextQueryFn } from './LogLineMenu';
|
||||
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
||||
import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext';
|
||||
import { LogListControls } from './LogListControls';
|
||||
import { preProcessLogs, LogListModel } from './processing';
|
||||
|
@ -53,6 +53,7 @@ export interface Props {
|
|||
isLabelFilterActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
loading?: boolean;
|
||||
loadMore?: (range: AbsoluteTimeRange) => void;
|
||||
logLineMenuCustomItems?: LogLineMenuCustomItem[];
|
||||
logOptionsStorageKey?: string;
|
||||
logs: LogRowModel[];
|
||||
logsMeta?: LogsMetaItem[];
|
||||
|
@ -111,6 +112,7 @@ export const LogList = ({
|
|||
isLabelFilterActive,
|
||||
loading,
|
||||
loadMore,
|
||||
logLineMenuCustomItems,
|
||||
logOptionsStorageKey,
|
||||
logs,
|
||||
logsMeta,
|
||||
|
@ -150,6 +152,7 @@ export const LogList = ({
|
|||
isLabelFilterActive={isLabelFilterActive}
|
||||
logs={logs}
|
||||
logsMeta={logsMeta}
|
||||
logLineMenuCustomItems={logLineMenuCustomItems}
|
||||
logOptionsStorageKey={logOptionsStorageKey}
|
||||
logSupportsContext={logSupportsContext}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
|
@ -209,6 +212,8 @@ const LogListComponent = ({
|
|||
dedupStrategy,
|
||||
filterLevels,
|
||||
forceEscape,
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
permalinkedLogId,
|
||||
showDetails,
|
||||
showTime,
|
||||
|
@ -265,7 +270,7 @@ const LogListComponent = ({
|
|||
|
||||
useEffect(() => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, [wrapLogMessage, showDetails, displayedFields]);
|
||||
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = debounce(() => {
|
||||
|
@ -362,6 +367,8 @@ const LogListComponent = ({
|
|||
height={listHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={getLogLineSize.bind(null, filteredLogs, widthContainer, displayedFields, {
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
showDuplicates: dedupStrategy !== LogsDedupStrategy.none,
|
||||
showTime,
|
||||
wrap: wrapLogMessage,
|
||||
|
@ -370,6 +377,7 @@ const LogListComponent = ({
|
|||
layout="vertical"
|
||||
onItemsRendered={onItemsRendered}
|
||||
outerRef={scrollRef}
|
||||
overscanCount={5}
|
||||
ref={listRef}
|
||||
style={{ overflowY: 'scroll' }}
|
||||
width="100%"
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
|
@ -22,9 +23,9 @@ import {
|
|||
} from '@grafana/data';
|
||||
import { PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { DownloadFormat, downloadLogs as download } from '../../utils';
|
||||
import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils';
|
||||
|
||||
import { GetRowContextQueryFn } from './LogLineMenu';
|
||||
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> {
|
||||
|
@ -34,7 +35,10 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo
|
|||
downloadLogs: (format: DownloadFormat) => void;
|
||||
enableLogDetails: boolean;
|
||||
filterLevels: LogLevel[];
|
||||
hasLogsWithErrors?: boolean;
|
||||
hasSampledLogs?: boolean;
|
||||
hasUnescapedContent?: boolean;
|
||||
logLineMenuCustomItems?: LogLineMenuCustomItem[];
|
||||
setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void;
|
||||
setDetailsWidth: (width: number) => void;
|
||||
setFilterLevels: (filterLevels: LogLevel[]) => void;
|
||||
|
@ -129,6 +133,7 @@ export interface Props {
|
|||
getRowContextQuery?: GetRowContextQueryFn;
|
||||
isLabelFilterActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
logs: LogRowModel[];
|
||||
logLineMenuCustomItems?: LogLineMenuCustomItem[];
|
||||
logsMeta?: LogsMetaItem[];
|
||||
logOptionsStorageKey?: string;
|
||||
logSupportsContext?: (row: LogRowModel) => boolean;
|
||||
|
@ -169,6 +174,7 @@ export const LogListContextProvider = ({
|
|||
isLabelFilterActive,
|
||||
getRowContextQuery,
|
||||
logs,
|
||||
logLineMenuCustomItems,
|
||||
logsMeta,
|
||||
logOptionsStorageKey,
|
||||
logSupportsContext,
|
||||
|
@ -260,6 +266,18 @@ export const LogListContextProvider = ({
|
|||
}
|
||||
}, [logListState, pinnedLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showDetails.length) {
|
||||
return;
|
||||
}
|
||||
const newShowDetails = showDetails.filter(
|
||||
(expandedLog) => logs.findIndex((log) => log.uid === expandedLog.uid) >= 0
|
||||
);
|
||||
if (newShowDetails.length !== showDetails.length) {
|
||||
setShowDetails(newShowDetails);
|
||||
}
|
||||
}, [logs, showDetails]);
|
||||
|
||||
const detailsDisplayed = useCallback(
|
||||
(log: LogListModel) => !!showDetails.find((shownLog) => shownLog.uid === log.uid),
|
||||
[showDetails]
|
||||
|
@ -403,6 +421,9 @@ export const LogListContextProvider = ({
|
|||
[logOptionsStorageKey]
|
||||
);
|
||||
|
||||
const hasLogsWithErrors = useMemo(() => logs.some((log) => !!checkLogsError(log)), [logs]);
|
||||
const hasSampledLogs = useMemo(() => logs.some((log) => !!checkLogsSampled(log)), [logs]);
|
||||
|
||||
const defaultWidth = (containerElement?.clientWidth ?? 0) * 0.4;
|
||||
const detailsWidth = logOptionsStorageKey
|
||||
? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`), 10)
|
||||
|
@ -421,10 +442,13 @@ export const LogListContextProvider = ({
|
|||
enableLogDetails,
|
||||
filterLevels: logListState.filterLevels,
|
||||
forceEscape: logListState.forceEscape,
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
hasUnescapedContent: logListState.hasUnescapedContent,
|
||||
isLabelFilterActive,
|
||||
getRowContextQuery,
|
||||
logSupportsContext,
|
||||
logLineMenuCustomItems,
|
||||
onClickFilterLabel,
|
||||
onClickFilterOutLabel,
|
||||
onClickFilterString,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
|
||||
import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils';
|
||||
|
||||
import { LogListContextData, Props } from '../LogListContext';
|
||||
import { LogListModel } from '../processing';
|
||||
|
@ -114,6 +115,8 @@ export const LogListContextProvider = ({
|
|||
enableLogDetails = false,
|
||||
filterLevels = [],
|
||||
getRowContextQuery = jest.fn(),
|
||||
logLineMenuCustomItems = undefined,
|
||||
logs = [],
|
||||
logSupportsContext = jest.fn(),
|
||||
onLogLineHover,
|
||||
onPermalinkClick = jest.fn(),
|
||||
|
@ -127,6 +130,9 @@ export const LogListContextProvider = ({
|
|||
syntaxHighlighting = true,
|
||||
wrapLogMessage = true,
|
||||
}: Partial<Props>) => {
|
||||
const hasLogsWithErrors = logs.some((log) => !!checkLogsError(log));
|
||||
const hasSampledLogs = logs.some((log) => !!checkLogsSampled(log));
|
||||
|
||||
return (
|
||||
<LogListContext.Provider
|
||||
value={{
|
||||
|
@ -136,8 +142,11 @@ export const LogListContextProvider = ({
|
|||
displayedFields,
|
||||
downloadLogs: jest.fn(),
|
||||
enableLogDetails,
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
filterLevels,
|
||||
getRowContextQuery,
|
||||
logLineMenuCustomItems,
|
||||
logSupportsContext,
|
||||
onLogLineHover,
|
||||
onPermalinkClick,
|
||||
|
|
|
@ -14,6 +14,13 @@ const THREE_LINES_HEIGHT = 3 * LINE_HEIGHT + PADDING_BOTTOM;
|
|||
let LETTER_WIDTH: number;
|
||||
let CONTAINER_SIZE = 200;
|
||||
let TWO_LINES_OF_CHARACTERS: number;
|
||||
const defaultOptions = {
|
||||
wrap: false,
|
||||
showTime: false,
|
||||
showDuplicates: false,
|
||||
hasLogsWithErrors: false,
|
||||
hasSampledLogs: false,
|
||||
};
|
||||
|
||||
describe('Virtualization', () => {
|
||||
let log: LogListModel, container: HTMLDivElement;
|
||||
|
@ -28,7 +35,7 @@ describe('Virtualization', () => {
|
|||
|
||||
describe('getLogLineSize', () => {
|
||||
test('Returns the a single line if the display mode is unwrapped', () => {
|
||||
const size = getLogLineSize([log], container, [], { wrap: false, showTime: true, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, showTime: true }, 0);
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
|
@ -38,7 +45,7 @@ describe('Virtualization', () => {
|
|||
logs,
|
||||
container,
|
||||
[],
|
||||
{ wrap: true, showTime: true, showDuplicates: false },
|
||||
{ ...defaultOptions, wrap: true, showTime: true },
|
||||
logs.length + 1
|
||||
);
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
|
@ -48,12 +55,18 @@ describe('Virtualization', () => {
|
|||
// Very small container
|
||||
log.collapsed = true;
|
||||
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10);
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: true, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
expect(size).toBe((TRUNCATION_LINE_COUNT + 1) * LINE_HEIGHT);
|
||||
});
|
||||
|
||||
test.each([true, false])('Measures a log line with controls %s and displayed time %s', (showTime: boolean) => {
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize(
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ wrap: true, showTime, showDuplicates: false, hasLogsWithErrors: false, hasSampledLogs: false },
|
||||
0
|
||||
);
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
|
@ -64,14 +77,14 @@ describe('Virtualization', () => {
|
|||
logLevel: undefined,
|
||||
});
|
||||
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: false, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true }, 0);
|
||||
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with level, controls, and displayed time', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: true, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
// Two lines for the log and one extra for level and time
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
@ -87,7 +100,7 @@ describe('Virtualization', () => {
|
|||
[log],
|
||||
container,
|
||||
['place', LOG_LINE_BODY_FIELD_NAME],
|
||||
{ wrap: true, showTime: false, showDuplicates: false },
|
||||
{ ...defaultOptions, wrap: true },
|
||||
0
|
||||
);
|
||||
// Two lines for the log and one extra for the displayed fields
|
||||
|
@ -97,13 +110,7 @@ describe('Virtualization', () => {
|
|||
test('Measures displayed fields in a log line with level, controls, and displayed time', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
|
||||
const size = getLogLineSize(
|
||||
[log],
|
||||
container,
|
||||
['place'],
|
||||
{ wrap: true, showTime: true, showDuplicates: false },
|
||||
0
|
||||
);
|
||||
const size = getLogLineSize([log], container, ['place'], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
// Only renders a short displayed field, so a single line
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
});
|
||||
|
@ -112,25 +119,23 @@ describe('Virtualization', () => {
|
|||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log.duplicates = 1;
|
||||
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: false, showDuplicates: true }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showDuplicates: true }, 0);
|
||||
// Two lines for the log and one extra for duplicates
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with errors', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log.hasError = true;
|
||||
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: false, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, hasLogsWithErrors: true }, 0);
|
||||
// Two lines for the log and one extra for the error icon
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line sampled log line', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log.isSampled = true;
|
||||
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: false, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, hasSampledLogs: true }, 0);
|
||||
// Two lines for the log and one extra for the sampled icon
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
@ -138,7 +143,7 @@ describe('Virtualization', () => {
|
|||
test('Adds an extra line for the expand/collapse controls if present', () => {
|
||||
jest.spyOn(log, 'updateCollapsedState').mockImplementation(() => undefined);
|
||||
log.collapsed = false;
|
||||
const size = getLogLineSize([log], container, [], { wrap: true, showTime: false, showDuplicates: false }, 0);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true }, 0);
|
||||
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -147,6 +147,8 @@ export function measureTextHeight(text: string, maxWidth: number, beforeWidth =
|
|||
}
|
||||
|
||||
interface DisplayOptions {
|
||||
hasLogsWithErrors?: boolean;
|
||||
hasSampledLogs?: boolean;
|
||||
showDuplicates: boolean;
|
||||
showTime: boolean;
|
||||
wrap: boolean;
|
||||
|
@ -156,7 +158,7 @@ export function getLogLineSize(
|
|||
logs: LogListModel[],
|
||||
container: HTMLDivElement | null,
|
||||
displayedFields: string[],
|
||||
{ showDuplicates, showTime, wrap }: DisplayOptions,
|
||||
{ hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions,
|
||||
index: number
|
||||
) {
|
||||
if (!container) {
|
||||
|
@ -179,15 +181,16 @@ export function getLogLineSize(
|
|||
|
||||
let textToMeasure = '';
|
||||
const gap = gridSize * FIELD_GAP_MULTIPLIER;
|
||||
const iconsGap = gridSize * 0.5;
|
||||
let optionsWidth = 0;
|
||||
if (showDuplicates) {
|
||||
optionsWidth += gridSize * 4.5 + gap;
|
||||
optionsWidth += gridSize * 4.5 + iconsGap;
|
||||
}
|
||||
if (logs[index].hasError) {
|
||||
optionsWidth += gridSize * 2 + gap;
|
||||
if (hasLogsWithErrors) {
|
||||
optionsWidth += gridSize * 2 + iconsGap;
|
||||
}
|
||||
if (logs[index].isSampled) {
|
||||
optionsWidth += gridSize * 2 + gap;
|
||||
if (hasSampledLogs) {
|
||||
optionsWidth += gridSize * 2 + iconsGap;
|
||||
}
|
||||
if (showTime) {
|
||||
optionsWidth += gap;
|
||||
|
|
|
@ -37,6 +37,7 @@ 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 { LogList } from 'app/features/logs/components/panel/LogList';
|
||||
import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView';
|
||||
import { combineResponses } from 'app/plugins/datasource/loki/mergeResponses';
|
||||
|
||||
|
@ -49,6 +50,7 @@ import {
|
|||
GetFieldLinksFn,
|
||||
isCoreApp,
|
||||
isIsFilterLabelActive,
|
||||
isLogLineMenuCustomItems,
|
||||
isOnClickFilterLabel,
|
||||
isOnClickFilterOutLabel,
|
||||
isOnClickFilterOutString,
|
||||
|
@ -108,6 +110,10 @@ interface LogsPanelProps extends PanelProps<Options> {
|
|||
*
|
||||
* 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;
|
||||
*
|
||||
* When the feature toggle newLogsPanel is enabled, you can pass extra options to the LogLineMenu component.
|
||||
* These options are an array of items with { label, onClick } or { divider: true } for dividers.
|
||||
* logLineMenuCustomItems?: LogLineMenuCustomItem[];
|
||||
*/
|
||||
}
|
||||
interface LogsPermalinkUrlState {
|
||||
|
@ -142,6 +148,7 @@ export const LogsPanel = ({
|
|||
isFilterLabelActive,
|
||||
logRowMenuIconsBefore,
|
||||
logRowMenuIconsAfter,
|
||||
logLineMenuCustomItems,
|
||||
enableInfiniteScrolling,
|
||||
onNewLogsReceived,
|
||||
...options
|
||||
|
@ -285,13 +292,19 @@ export const LogsPanel = ({
|
|||
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
|
||||
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
|
||||
const logs = panelData
|
||||
? dataFrameToLogsModel(panelData.series, panelData.request?.intervalMs, undefined, panelData.request?.targets)
|
||||
? dataFrameToLogsModel(
|
||||
panelData.series,
|
||||
panelData.request?.intervalMs,
|
||||
undefined,
|
||||
panelData.request?.targets,
|
||||
Boolean(enableInfiniteScrolling)
|
||||
)
|
||||
: null;
|
||||
const logRows = logs?.rows || [];
|
||||
const commonLabels = logs?.meta?.find((m) => m.label === COMMON_LABELS);
|
||||
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
|
||||
return [logRows, deduplicatedRows, commonLabels];
|
||||
}, [dedupStrategy, panelData]);
|
||||
}, [dedupStrategy, enableInfiniteScrolling, panelData]);
|
||||
|
||||
const onPermalinkClick = useCallback(
|
||||
async (row: LogRowModel) => {
|
||||
|
@ -307,6 +320,9 @@ export const LogsPanel = ({
|
|||
}, [data]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (config.featureToggles.newLogsPanel) {
|
||||
return;
|
||||
}
|
||||
if (!logsContainerRef.current || !scrollElement || keepScrollPositionRef.current) {
|
||||
keepScrollPositionRef.current =
|
||||
keepScrollPositionRef.current === 'infinite-scroll' ? null : keepScrollPositionRef.current;
|
||||
|
@ -449,10 +465,6 @@ export const LogsPanel = ({
|
|||
[data.request, dataSourcesMap, onNewLogsReceived, panelData, timeZone]
|
||||
);
|
||||
|
||||
if (!data || logRows.length === 0) {
|
||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||
}
|
||||
|
||||
const renderCommonLabels = () => (
|
||||
<div className={cx(style.labelContainer, isAscending && style.labelContainerAscending)}>
|
||||
<span className={style.label}>
|
||||
|
@ -465,6 +477,31 @@ export const LogsPanel = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
const initialScrollPosition = useMemo(() => {
|
||||
/**
|
||||
* In dashboards, users with newest logs at the bottom have the expectation of keeping the scroll at the bottom
|
||||
* when new data is received. See https://github.com/grafana/grafana/pull/37634
|
||||
*/
|
||||
if (app === CoreApp.Dashboard || app === CoreApp.PanelEditor) {
|
||||
return sortOrder === LogsSortOrder.Ascending ? 'bottom' : 'top';
|
||||
}
|
||||
return 'top';
|
||||
}, [app, sortOrder]);
|
||||
|
||||
const storageKey = useMemo(() => {
|
||||
if (controlsStorageKey) {
|
||||
return controlsStorageKey;
|
||||
}
|
||||
if (!data.request) {
|
||||
return undefined;
|
||||
}
|
||||
return `${data.request?.dashboardUID}.${id}`;
|
||||
}, [controlsStorageKey, data.request, id]);
|
||||
|
||||
if (!data || logRows.length === 0) {
|
||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||
}
|
||||
|
||||
// Passing callbacks control the display of the filtering buttons. We want to pass it only if onAddAdHocFilter is defined.
|
||||
const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined;
|
||||
const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined;
|
||||
|
@ -485,7 +522,55 @@ export const LogsPanel = ({
|
|||
getLogRowContextUi={getLogRowContextUi}
|
||||
/>
|
||||
)}
|
||||
{!showControls ? (
|
||||
{config.featureToggles.newLogsPanel && (
|
||||
<div
|
||||
onMouseLeave={onLogContainerMouseLeave}
|
||||
className={style.logListContainer}
|
||||
ref={(element: HTMLDivElement) => setScrollElement(element)}
|
||||
>
|
||||
{deduplicatedRows.length > 0 && scrollElement && (
|
||||
<LogList
|
||||
app={isCoreApp(app) ? app : CoreApp.Dashboard}
|
||||
containerElement={scrollElement}
|
||||
dedupStrategy={dedupStrategy}
|
||||
displayedFields={displayedFields}
|
||||
enableLogDetails={enableLogDetails}
|
||||
getFieldLinks={getFieldLinks}
|
||||
isLabelFilterActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
initialScrollPosition={initialScrollPosition}
|
||||
loading={infiniteScrolling}
|
||||
logLineMenuCustomItems={
|
||||
isLogLineMenuCustomItems(logLineMenuCustomItems) ? logLineMenuCustomItems : undefined
|
||||
}
|
||||
logs={deduplicatedRows}
|
||||
logSupportsContext={showContextToggle}
|
||||
loadMore={enableInfiniteScrolling ? loadMoreLogs : undefined}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||
onLogLineHover={onLogRowHover}
|
||||
onLogOptionsChange={isOnLogOptionsChange(onLogOptionsChange) ? onLogOptionsChange : undefined}
|
||||
onOpenContext={onOpenContext}
|
||||
onPermalinkClick={showPermaLink() ? onPermalinkClick : undefined}
|
||||
permalinkedLogId={getLogsPanelState()?.logs?.id ?? undefined}
|
||||
showControls={Boolean(showControls)}
|
||||
showTime={showTime}
|
||||
sortOrder={sortOrder}
|
||||
logOptionsStorageKey={storageKey}
|
||||
syntaxHighlighting={prettifyLogMessage}
|
||||
timeRange={data.timeRange}
|
||||
timeZone={timeZone}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!config.featureToggles.newLogsPanel && !showControls && (
|
||||
<ScrollContainer ref={(scrollElement) => setScrollElement(scrollElement)}>
|
||||
<div onMouseLeave={onLogContainerMouseLeave} className={style.container} ref={logsContainerRef}>
|
||||
{showCommonLabels && !isAscending && renderCommonLabels()}
|
||||
|
@ -542,7 +627,8 @@ export const LogsPanel = ({
|
|||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
) : (
|
||||
)}
|
||||
{!config.featureToggles.newLogsPanel && showControls && (
|
||||
<div onMouseLeave={onLogContainerMouseLeave} className={style.controlledLogsContainer}>
|
||||
{showCommonLabels && !isAscending && renderCommonLabels()}
|
||||
<ControlledLogRows
|
||||
|
@ -588,7 +674,7 @@ export const LogsPanel = ({
|
|||
onLogOptionsChange={isOnLogOptionsChange(onLogOptionsChange) ? onLogOptionsChange : undefined}
|
||||
logOptionsStorageKey={controlsStorageKey}
|
||||
// Ascending order causes scroll to stick to the bottom, so previewing is futile
|
||||
renderPreview={false}
|
||||
renderPreview={isAscending ? false : true}
|
||||
/>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
|
@ -601,6 +687,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||
container: css({
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}),
|
||||
logListContainer: css({
|
||||
minHeight: '100%',
|
||||
maxHeight: '100%',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
controlledLogsContainer: css({
|
||||
height: '100%',
|
||||
}),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { LogsPanel } from './LogsPanel';
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
@ -6,25 +7,30 @@ import { LogsPanelSuggestionsSupplier } from './suggestions';
|
|||
|
||||
export const plugin = new PanelPlugin<Options>(LogsPanel)
|
||||
.setPanelOptions((builder) => {
|
||||
builder.addBooleanSwitch({
|
||||
path: 'showTime',
|
||||
name: 'Time',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
if (!config.featureToggles.newLogsPanel) {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
name: 'Unique labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showCommonLabels',
|
||||
name: 'Common labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showTime',
|
||||
name: 'Time',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
name: 'Unique labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showCommonLabels',
|
||||
name: 'Common labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapLogMessage',
|
||||
name: 'Wrap lines',
|
||||
|
@ -33,7 +39,7 @@ export const plugin = new PanelPlugin<Options>(LogsPanel)
|
|||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'prettifyLogMessage',
|
||||
name: 'Prettify JSON',
|
||||
name: config.featureToggles.newLogsPanel ? 'Enable log message highlighting' : 'Prettify JSON',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
|
|
|
@ -49,6 +49,7 @@ composableKinds: PanelCfg: {
|
|||
onLogOptionsChange?: _
|
||||
logRowMenuIconsBefore?: _
|
||||
logRowMenuIconsAfter?: _
|
||||
logLineMenuCustomItems?: _
|
||||
onNewLogsReceived?: _
|
||||
displayedFields?: [...string]
|
||||
} @cuetsy(kind="interface")
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface Options {
|
|||
enableInfiniteScrolling?: boolean;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
logLineMenuCustomItems?: unknown;
|
||||
logRowMenuIconsAfter?: unknown;
|
||||
logRowMenuIconsBefore?: unknown;
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { CoreApp, DataFrame, Field, LinkModel, ScopedVars } from '@grafana/data';
|
||||
import { LogLineMenuCustomItem } from 'app/features/logs/components/panel/LogLineMenu';
|
||||
import { LogListControlOptions } from 'app/features/logs/components/panel/LogList';
|
||||
|
||||
export type { Options } from './panelcfg.gen';
|
||||
|
@ -66,3 +67,7 @@ export function isCoreApp(app: unknown): app is CoreApp {
|
|||
const apps = Object.values(CoreApp).map((coreApp) => coreApp.toString());
|
||||
return typeof app === 'string' && apps.includes(app);
|
||||
}
|
||||
|
||||
export function isLogLineMenuCustomItems(items: unknown): items is LogLineMenuCustomItem[] {
|
||||
return Array.isArray(items) && items.every((item) => 'divider' in item || ('onClick' in item && 'label' in item));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue