Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logs: Add permalink to log lines #69464

Merged
merged 29 commits into from Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f6e422f
create explore panel state for logs
svennergr Jun 12, 2023
ebc0ed9
add props to LogRows and unify
svennergr Jun 13, 2023
ae6d68f
pass properties from explore to logs
svennergr Jun 13, 2023
3a11cae
add css
svennergr Jun 13, 2023
674b783
implement button and scrolling
svennergr Jun 13, 2023
cfef0a1
export and use `getUrlStateFromPaneState`
svennergr Jun 15, 2023
a089992
make `scrollIntoView` optional
svennergr Jun 15, 2023
bc4dc4c
change state handling for permalinks
svennergr Jun 15, 2023
0d0b393
change link icon
svennergr Jun 15, 2023
49d31ab
removed unused state
svennergr Jun 15, 2023
d3f4dfc
add tests for `LogRowMessage`
svennergr Jun 15, 2023
f9da55d
remove unused prop
svennergr Jun 15, 2023
db84171
fix name
svennergr Jun 15, 2023
1d85433
reorg component
svennergr Jun 15, 2023
08d8acf
add `LogRow` tests
svennergr Jun 15, 2023
8ab7678
add test for `Logs`
svennergr Jun 15, 2023
3d39f29
Update public/app/features/logs/components/LogRow.test.tsx
svennergr Jun 15, 2023
b17cbeb
Update public/app/features/explore/Logs/Logs.test.tsx
svennergr Jun 15, 2023
e0f9b76
improve types in test
svennergr Jun 15, 2023
1607fff
fix props export in Logs.tsx
svennergr Jun 15, 2023
5db7ab3
fix props export in LogRowMessage.tsx
svennergr Jun 15, 2023
b00cebb
fix props export in LogRow.tsx
svennergr Jun 15, 2023
08f6560
fixed import
svennergr Jun 15, 2023
0749352
fix theme import
svennergr Jun 15, 2023
05f5b85
remove hidden style
svennergr Jun 15, 2023
34a8b11
add better test names
svennergr Jun 15, 2023
5a142ab
change to `log line` rather logline
svennergr Jun 16, 2023
ad6baaf
fix tooltips
svennergr Jun 16, 2023
5134a52
remove unused css
svennergr Jun 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .betterer.results
Expand Up @@ -2563,6 +2563,11 @@ exports[`better eslint`] = {
"public/app/features/explore/ExploreQueryInspector.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/Logs/Logs.test.tsx:5381": [
svennergr marked this conversation as resolved.
Show resolved Hide resolved
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/explore/Logs/Logs.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
Expand Down
5 changes: 5 additions & 0 deletions packages/grafana-data/src/types/explore.ts
Expand Up @@ -15,12 +15,17 @@ export interface ExploreUrlState<T extends DataQuery = AnyQuery> {

export interface ExplorePanelsState extends Partial<Record<PreferredVisualisationType, {}>> {
trace?: ExploreTracePanelState;
logs?: ExploreLogsPanelState;
}

export interface ExploreTracePanelState {
spanId?: string;
}

export interface ExploreLogsPanelState {
id?: string;
}

export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
datasourceUid: string;
/** @deprecated Will be removed in a future version. Use queries instead. */
Expand Down
1 change: 1 addition & 0 deletions public/app/features/explore/Explore.tsx
Expand Up @@ -347,6 +347,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
onStopScanning={this.onStopScanning}
eventBus={this.logsEventBus}
splitOpenFn={this.onSplitOpen('logs')}
scrollElement={this.scrollElement}
/>
);
}
Expand Down
54 changes: 49 additions & 5 deletions public/app/features/explore/Logs/Logs.test.tsx
Expand Up @@ -2,20 +2,32 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { LoadingState, LogLevel, LogRowModel, MutableDataFrame, toUtc, EventBusSrv } from '@grafana/data';
import { EventBusSrv, LoadingState, LogLevel, LogRowModel, MutableDataFrame, toUtc } from '@grafana/data';
import { ExploreId } from 'app/types';

import { Logs } from './Logs';
import { Logs, Props } from './Logs';

const changePanelState = jest.fn();
jest.mock('../state/explorePane', () => ({
...jest.requireActual('../state/explorePane'),
changePanelState: (...args: any) => {
return changePanelState(...args);
},
}));

describe('Logs', () => {
const setup = (logs?: LogRowModel[]) => {
beforeEach(() => {
jest.clearAllMocks();
});

const getComponent = (partialProps?: Partial<Props>, logs?: LogRowModel[]) => {
svennergr marked this conversation as resolved.
Show resolved Hide resolved
const rows = [
makeLog({ uid: '1', timeEpochMs: 1 }),
makeLog({ uid: '2', timeEpochMs: 2 }),
makeLog({ uid: '3', timeEpochMs: 3 }),
];

return render(
return (
<Logs
exploreId={ExploreId.left}
splitOpen={() => undefined}
Expand All @@ -41,9 +53,13 @@ describe('Logs', () => {
return [];
}}
eventBus={new EventBusSrv()}
{...partialProps}
/>
);
};
const setup = (partialProps?: Partial<Props>, logs?: LogRowModel[]) => {
return render(getComponent(partialProps, logs));
};

it('should render logs', () => {
setup();
Expand All @@ -55,7 +71,7 @@ describe('Logs', () => {
});

it('should render no logs found', () => {
setup([]);
setup({}, []);

expect(screen.getByText(/no logs found\./i)).toBeInTheDocument();
expect(
Expand Down Expand Up @@ -192,6 +208,34 @@ describe('Logs', () => {
expect(logRows[0].textContent).toContain('log message 1');
expect(logRows[2].textContent).toContain('log message 3');
});

describe('for permalinking', () => {
it('should dispatch a `changePanelState` event without the id', () => {
const panelState = { logs: { id: '1' } };
const { rerender } = setup({ loading: false, panelState });

rerender(getComponent({ loading: true, exploreId: ExploreId.right, panelState }));
rerender(getComponent({ loading: false, exploreId: ExploreId.right, panelState }));

expect(changePanelState).toHaveBeenCalledWith(ExploreId.right, 'logs', { logs: {} });
});

it('should scroll the scrollElement into view if rows contain id', () => {
const panelState = { logs: { id: '3' } };
const scrollElementMock = { scroll: jest.fn() };
setup({ loading: false, scrollElement: scrollElementMock as any, panelState });

expect(scrollElementMock.scroll).toHaveBeenCalled();
});

it('should not scroll the scrollElement into view if rows contain id', () => {
svennergr marked this conversation as resolved.
Show resolved Hide resolved
const panelState = { logs: { id: 'not-included' } };
const scrollElementMock = { scroll: jest.fn() };
setup({ loading: false, scrollElement: scrollElementMock as any, panelState });

expect(scrollElementMock.scroll).not.toHaveBeenCalled();
});
});
});

const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
Expand Down
53 changes: 52 additions & 1 deletion public/app/features/explore/Logs/Logs.tsx
Expand Up @@ -26,6 +26,9 @@ import {
DataHoverClearEvent,
EventBus,
LogRowContextOptions,
ExplorePanelsState,
serializeStateToUrlParam,
urlUtil,
} from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
Expand All @@ -41,17 +44,21 @@ import {
} from '@grafana/ui';
import { dedupLogRows, filterLogLevels } from 'app/core/logsModel';
import store from 'app/core/store';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { getState, dispatch } from 'app/store/store';
import { ExploreId } from 'app/types/explore';

import { LogRows } from '../../logs/components/LogRows';
import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal';
import { getUrlStateFromPaneState } from '../hooks/useStateSync';
import { changePanelState } from '../state/explorePane';

import { LogsMetaRow } from './LogsMetaRow';
import LogsNavigation from './LogsNavigation';
import { LogsVolumePanelList } from './LogsVolumePanelList';
import { SETTINGS_KEYS } from './utils/logs';

interface Props extends Themeable2 {
export interface Props extends Themeable2 {
width: number;
splitOpen: SplitOpen;
logRows: LogRowModel[];
Expand Down Expand Up @@ -85,6 +92,8 @@ interface Props extends Themeable2 {
addResultsToCache: () => void;
clearCache: () => void;
eventBus: EventBus;
panelState?: ExplorePanelsState;
scrollElement?: HTMLDivElement;
}

interface State {
Expand Down Expand Up @@ -158,6 +167,18 @@ class UnthemedLogs extends PureComponent<Props, State> {
}
}

componentDidUpdate(prevProps: Readonly<Props>): void {
if (this.props.loading && !prevProps.loading && this.props.panelState?.logs?.id) {
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
// loading stopped, so we need to remove any permalinked log lines
delete this.props.panelState.logs.id;
dispatch(
changePanelState(this.props.exploreId, 'logs', {
...this.props.panelState,
})
);
}
}

onLogRowHover = (row?: LogRowModel) => {
if (!row) {
this.props.eventBus.publish(new DataHoverClearEvent());
Expand Down Expand Up @@ -332,6 +353,33 @@ class UnthemedLogs extends PureComponent<Props, State> {
};
};

onPermalinkClick = async (row: LogRowModel) => {
// get explore state, add log-row-id and make timerange absolute
const urlState = getUrlStateFromPaneState(getState().explore.panes[this.props.exploreId]!);
urlState.panelsState = { ...this.props.panelState, logs: { id: row.uid } };
urlState.range = {
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
from: new Date(this.props.absoluteRange.from).toISOString(),
to: new Date(this.props.absoluteRange.to).toISOString(),
};

// append changed urlState to baseUrl
const serializedState = serializeStateToUrlParam(urlState);
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0];
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState });
await createAndCopyShortLink(url);
};

scrollIntoView = (element: HTMLElement) => {
const { scrollElement } = this.props;

if (scrollElement) {
scrollElement.scroll({
behavior: 'smooth',
top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2,
});
}
};

checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => {
return !!logRows.some((r) => r.hasUnescapedContent);
});
Expand Down Expand Up @@ -557,6 +605,9 @@ class UnthemedLogs extends PureComponent<Props, State> {
app={CoreApp.Explore}
onLogRowHover={this.onLogRowHover}
onOpenContext={this.onOpenContext}
onPermalinkClick={this.onPermalinkClick}
permalinkedRowId={this.props.panelState?.logs?.id}
scrollIntoView={this.scrollIntoView}
/>
{!loading && !hasData && !scanning && (
<div className={styles.noData}>
Expand Down
6 changes: 6 additions & 0 deletions public/app/features/explore/Logs/LogsContainer.tsx
Expand Up @@ -50,6 +50,7 @@ interface LogsContainerProps extends PropsFromRedux {
onStopScanning: () => void;
eventBus: EventBus;
splitOpenFn: SplitOpen;
scrollElement?: HTMLDivElement;
}

class LogsContainer extends PureComponent<LogsContainerProps> {
Expand Down Expand Up @@ -144,6 +145,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
addResultsToCache,
clearCache,
logsVolume,
scrollElement,
} = this.props;

if (!logRows) {
Expand Down Expand Up @@ -206,6 +208,8 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
addResultsToCache={() => addResultsToCache(exploreId)}
clearCache={() => clearCache(exploreId)}
eventBus={this.props.eventBus}
panelState={this.props.panelState}
scrollElement={scrollElement}
/>
</LogsCrossFadeTransition>
</>
Expand All @@ -228,6 +232,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI
absoluteRange,
supplementaryQueries,
} = item;
const panelState = item.panelsState;
const timeZone = getTimeZone(state.user);
const logsVolume = supplementaryQueries[SupplementaryQueryType.LogsVolume];

Expand All @@ -247,6 +252,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI
range,
absoluteRange,
logsVolume,
panelState,
};
}

Expand Down
2 changes: 1 addition & 1 deletion public/app/features/explore/hooks/useStateSync.ts
Expand Up @@ -366,7 +366,7 @@ const urlDiff = (
};
};

function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
return {
// datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined
// lets just fallback instead of crashing.
Expand Down
98 changes: 98 additions & 0 deletions public/app/features/logs/components/LogRow.test.tsx
@@ -0,0 +1,98 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import tinycolor from 'tinycolor2';

import { CoreApp, createTheme, LogLevel, LogRowModel } from '@grafana/data';

import { LogRow, Props } from './LogRow';
import { createLogRow } from './__mocks__/logRow';
import { getLogRowStyles } from './getLogRowStyles';

const theme = createTheme();
const styles = getLogRowStyles(theme);
const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowModel>) => {
svennergr marked this conversation as resolved.
Show resolved Hide resolved
const props: Props = {
row: createLogRow({
entry: 'test123',
uid: 'log-row-id',
logLevel: LogLevel.error,
timeEpochMs: 1546297200000,
...rowOverrides,
}),
enableLogDetails: false,
getRows: () => [],
onOpenContext: () => {},
prettifyLogMessage: false,
app: CoreApp.Explore,
showDuplicates: false,
showLabels: false,
showTime: false,
wrapLogMessage: false,
timeZone: 'utc',
styles,
theme,
...(propOverrides || {}),
};

const { container } = render(
<table>
<tbody>
<LogRow {...props} />
</tbody>
</table>
);

return { props, container };
};

describe('LogRow', () => {
it('renders row entry', () => {
setup();
expect(screen.queryByText('test123')).toBeInTheDocument();
});

describe('with permalinking', () => {
it('highlights row with same permalink-id', () => {
const { container } = setup({ permalinkedRowId: 'log-row-id' });
const row = container.querySelector('tr');
expect(row).toHaveStyle(
`background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}`
);
});

it('does not highlight row details with same permalink-id', async () => {
svennergr marked this conversation as resolved.
Show resolved Hide resolved
const { container } = setup({ permalinkedRowId: 'log-row-id', enableLogDetails: true });
const row = container.querySelector('tr');
await userEvent.click(row!);
const allRows = container.querySelectorAll('tr');

expect(row).toHaveStyle(
`background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}`
);
expect(allRows[allRows.length - 1]).not.toHaveStyle(
`background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}`
);
});

it('not highlights row with different permalink-id', () => {
const { container } = setup({ permalinkedRowId: 'wrong-log-row-id' });
const row = container.querySelector('tr');
expect(row).not.toHaveStyle(
`background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}`
);
});

it('calls `scrollIntoView` if permalink matches', () => {
const scrollIntoView = jest.fn();
setup({ permalinkedRowId: 'log-row-id', scrollIntoView });
expect(scrollIntoView).toHaveBeenCalled();
});

it('not calls `scrollIntoView` if permalink does not match', () => {
const scrollIntoView = jest.fn();
setup({ permalinkedRowId: 'wrong-log-row-id', scrollIntoView });
expect(scrollIntoView).not.toHaveBeenCalled();
});
});
});