diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index a1a9966052c..e71bb6be176 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -55,6 +55,9 @@ export interface LogRowModel { // the same as rows final index when rendered. rowIndex: number; + // The value of the the dataframe's id field, if it exists + rowId?: string; + // Full DataFrame from which we parsed this log. // TODO: refactor this so we do not need to pass whole dataframes in addition to also parsed data. dataFrame: DataFrame; diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index dd38faa5daf..1f8519f0b22 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -97,9 +97,9 @@ describe('Logs', () => { const getComponent = (partialProps?: Partial>, logs?: LogRowModel[]) => { const rows = [ - makeLog({ uid: '1', timeEpochMs: 1 }), - makeLog({ uid: '2', timeEpochMs: 2 }), - makeLog({ uid: '3', timeEpochMs: 3 }), + makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }), + makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 2 }), + makeLog({ uid: '3', rowId: 'id3', timeEpochMs: 3 }), ]; const testDataFrame = { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index c825affc177..c7913eec258 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -374,6 +374,14 @@ class UnthemedLogs extends PureComponent { }; onPermalinkClick = async (row: LogRowModel) => { + // this is an extra check, to be sure that we are not + // creating permalinks for logs without an id-field. + // normally it should never happen, because we do not + // display the permalink button in such cases. + if (row.rowId === undefined) { + return; + } + // 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 } }; diff --git a/public/app/features/logs/components/LogRowMenuCell.tsx b/public/app/features/logs/components/LogRowMenuCell.tsx index e54ea208d21..c2aa8cd2b43 100644 --- a/public/app/features/logs/components/LogRowMenuCell.tsx +++ b/public/app/features/logs/components/LogRowMenuCell.tsx @@ -102,7 +102,7 @@ export const LogRowMenuCell = React.memo( aria-label="Pin line" /> )} - {onPermalinkClick && row.uid && ( + {onPermalinkClick && row.rowId !== undefined && row.uid && ( { }); describe('with permalinking', () => { - it('should show permalinking button when `onPermalinkClick` is defined', async () => { - setup({ onPermalinkClick: jest.fn() }); + it('should show permalinking button when `onPermalinkClick` is defined and rowId is defined', async () => { + setup({ onPermalinkClick: jest.fn() }, { rowId: 'id1' }); await userEvent.hover(screen.getByText('test123')); expect(screen.queryByLabelText('Copy shortlink')).toBeInTheDocument(); }); + it('should not show permalinking button when `onPermalinkClick` is defined and rowId is not defined', async () => { + setup({ onPermalinkClick: jest.fn() }); + await userEvent.hover(screen.getByText('test123')); + expect(screen.queryByLabelText('Copy shortlink')).not.toBeInTheDocument(); + }); + it('should not show permalinking button when `onPermalinkClick` is not defined', () => { setup(); expect(screen.queryByLabelText('Copy shortlink')).not.toBeInTheDocument(); @@ -79,7 +85,7 @@ describe('LogRowMessage', () => { it('should call `onPermalinkClick` with row on click', async () => { const permalinkClick = jest.fn(); - const props = setup({ onPermalinkClick: permalinkClick }); + const props = setup({ onPermalinkClick: permalinkClick }, { rowId: 'id1' }); await userEvent.hover(screen.getByText('test123')); const button = screen.getByLabelText('Copy shortlink'); diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index 6a0e069590c..a2b18caba86 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -424,7 +424,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[], queries: DataQuery[ const datasourceType = queries.find((query) => query.refId === series.refId)?.datasource?.type; - rows.push({ + const row: LogRowModel = { entryFieldIndex: stringField.index, rowIndex: j, dataFrame: series, @@ -444,7 +444,13 @@ export function logSeriesToLogsModel(logSeries: DataFrame[], queries: DataQuery[ // prepend refId to uid to make it unique across all series in a case when series contain duplicates uid: `${series.refId}_${idField ? idField.values[j] : j.toString()}`, datasourceType, - }); + }; + + if (idField !== null) { + row.rowId = idField.values[j]; + } + + rows.push(row); } } diff --git a/public/app/features/logs/logsModel_parse.test.ts b/public/app/features/logs/logsModel_parse.test.ts index 9461b6bf9b3..078b2591af0 100644 --- a/public/app/features/logs/logsModel_parse.test.ts +++ b/public/app/features/logs/logsModel_parse.test.ts @@ -137,6 +137,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( rows: [ { dataFrame: frames[0], + rowId: 'id1', datasourceType: undefined, entry: 'line1', entryFieldIndex: 1, @@ -164,6 +165,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( }, { dataFrame: frames[1], + rowId: 'id2', datasourceType: undefined, entry: 'line2', entryFieldIndex: 1, @@ -191,6 +193,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( }, { dataFrame: frames[2], + rowId: 'id3', datasourceType: undefined, entry: 'line3', entryFieldIndex: 1, @@ -290,6 +293,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( rows: [ { dataFrame: frames[0], + rowId: 'id1', datasourceType: undefined, entry: 'line1', entryFieldIndex: 2, @@ -318,6 +322,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( }, { dataFrame: frames[0], + rowId: 'id2', datasourceType: undefined, entry: 'line2', entryFieldIndex: 2, @@ -346,6 +351,7 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( }, { dataFrame: frames[0], + rowId: 'id3', datasourceType: undefined, entry: 'line3', entryFieldIndex: 2, @@ -925,4 +931,158 @@ describe('logSeriesToLogsModel should parse different logs-dataframe formats', ( expect(logSeriesToLogsModel(frames)).toStrictEqual(expected); }); + + it('should add rowId when id field exists)', () => { + const frames: DataFrame[] = [ + { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: [1686142519756, 1686142520411], + nanos: [641000, 0], + }, + { + name: 'Line', + type: FieldType.string, + config: {}, + values: ['line1', 'line2'], + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: ['id1', 'id2'], + }, + ], + length: 2, + }, + ]; + + const expected = { + hasUniqueLabels: false, + meta: [], + rows: [ + { + dataFrame: frames[0], + rowId: 'id1', + datasourceType: undefined, + entry: 'line1', + entryFieldIndex: 1, + hasAnsi: false, + hasUnescapedContent: false, + labels: {}, + logLevel: 'unknown', + raw: 'line1', + rowIndex: 0, + searchWords: [], + timeEpochMs: 1686142519756, + timeEpochNs: '1686142519756641000', + timeFromNow: 'mock:dateTimeFormatTimeAgo:2023-06-07T06:55:19-06:00', + timeLocal: '2023-06-07 06:55:19', + timeUtc: '2023-06-07 12:55:19', + uid: 'A_id1', + uniqueLabels: {}, + }, + { + dataFrame: frames[0], + rowId: 'id2', + datasourceType: undefined, + entry: 'line2', + entryFieldIndex: 1, + hasAnsi: false, + hasUnescapedContent: false, + labels: {}, + logLevel: 'unknown', + raw: 'line2', + rowIndex: 1, + searchWords: [], + timeEpochMs: 1686142520411, + timeEpochNs: '1686142520411000000', + timeFromNow: 'mock:dateTimeFormatTimeAgo:2023-06-07T06:55:20-06:00', + timeLocal: '2023-06-07 06:55:20', + timeUtc: '2023-06-07 12:55:20', + uid: 'A_id2', + uniqueLabels: {}, + }, + ], + }; + + expect(logSeriesToLogsModel(frames)).toStrictEqual(expected); + }); + + it('should not add rowId when id field does not exist)', () => { + const frames: DataFrame[] = [ + { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: [1686142519756, 1686142520411], + nanos: [641000, 0], + }, + { + name: 'Line', + type: FieldType.string, + config: {}, + values: ['line1', 'line2'], + }, + ], + length: 2, + }, + ]; + + const expected = { + hasUniqueLabels: false, + meta: [], + rows: [ + { + dataFrame: frames[0], + datasourceType: undefined, + entry: 'line1', + entryFieldIndex: 1, + hasAnsi: false, + hasUnescapedContent: false, + labels: {}, + logLevel: 'unknown', + raw: 'line1', + rowIndex: 0, + searchWords: [], + timeEpochMs: 1686142519756, + timeEpochNs: '1686142519756641000', + timeFromNow: 'mock:dateTimeFormatTimeAgo:2023-06-07T06:55:19-06:00', + timeLocal: '2023-06-07 06:55:19', + timeUtc: '2023-06-07 12:55:19', + uid: 'A_0', + uniqueLabels: {}, + }, + { + dataFrame: frames[0], + datasourceType: undefined, + entry: 'line2', + entryFieldIndex: 1, + hasAnsi: false, + hasUnescapedContent: false, + labels: {}, + logLevel: 'unknown', + raw: 'line2', + rowIndex: 1, + searchWords: [], + timeEpochMs: 1686142520411, + timeEpochNs: '1686142520411000000', + timeFromNow: 'mock:dateTimeFormatTimeAgo:2023-06-07T06:55:20-06:00', + timeLocal: '2023-06-07 06:55:20', + timeUtc: '2023-06-07 12:55:20', + uid: 'A_1', + uniqueLabels: {}, + }, + ], + }; + + expect(logSeriesToLogsModel(frames)).toStrictEqual(expected); + }); });