diff --git a/public/app/features/logs/UniqueKeyMaker.test.ts b/public/app/features/logs/UniqueKeyMaker.test.ts new file mode 100644 index 00000000000..2d5478c1ca3 --- /dev/null +++ b/public/app/features/logs/UniqueKeyMaker.test.ts @@ -0,0 +1,41 @@ +import { UniqueKeyMaker } from './UniqueKeyMaker'; + +describe('UniqueKeyMaker', () => { + const expectKeys = (testData: Array<[string, string]>) => { + const k = new UniqueKeyMaker(); + testData.forEach(([input, output]) => { + expect(k.getKey(input)).toBe(output); + }); + + // we also make a check that all the output-values are unique + const outputs = testData.map(([i, o]) => o); + const uniqueOutputLength = new Set(outputs).size; + expect(uniqueOutputLength).toBe(outputs.length); + }; + + it('should handle already unique keys', () => { + expectKeys([ + ['one', 'k_one'], + ['two', 'k_two'], + ['three', 'k_three'], + ]); + }); + + it('should handle duplicate keys', () => { + expectKeys([ + ['one', 'k_one'], + ['one', 'i_2'], + ['one', 'i_3'], + ]); + }); + + it('should handle a mix of unique and duplicate keys', () => { + expectKeys([ + ['one', 'k_one'], + ['two', 'k_two'], + ['one', 'i_3'], + ['two', 'i_4'], + ['three', 'k_three'], + ]); + }); +}); diff --git a/public/app/features/logs/UniqueKeyMaker.ts b/public/app/features/logs/UniqueKeyMaker.ts new file mode 100644 index 00000000000..0c274d847a3 --- /dev/null +++ b/public/app/features/logs/UniqueKeyMaker.ts @@ -0,0 +1,25 @@ +// this class generates react-keys that are guaranteed to be unique. +// it will try to use the provided `maybeId`, but if that's a duplicate, +// it will use an index-based key. +// NOTE: it will always add a prefix to the string, this is necessary +// to avoid the problem if your proposed key would conflict with +// the index-based names +export class UniqueKeyMaker { + seen: Set; + count: number; + constructor() { + this.seen = new Set(); + this.count = 0; + } + + getKey(proposedKey: string) { + this.count += 1; + const maybeKey = `k_${proposedKey}`; + if (this.seen.has(maybeKey)) { + return `i_${this.count}`; + } else { + this.seen.add(maybeKey); + return maybeKey; + } + } +} diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 064eb4e9821..c1794c95caa 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -13,6 +13,7 @@ import { } from '@grafana/data'; import { withTheme2, Themeable2 } from '@grafana/ui'; +import { UniqueKeyMaker } from '../UniqueKeyMaker'; import { sortLogRows } from '../utils'; //Components @@ -122,13 +123,15 @@ class UnThemedLogRows extends PureComponent { // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead const getRows = this.makeGetRows(orderedRows); + const keyMaker = new UniqueKeyMaker(); + return ( {hasData && firstRows.map((row) => ( { renderAll && lastRows.map((row) => (