Skip to content

Commit

Permalink
[Lens] Synchronize cursor position for X-axis across all Lens visuali…
Browse files Browse the repository at this point in the history
…zations in a dashboard (#106845)

* [Lens] Synchronize cursor position for X-axis across all Lens visualizations in a dashboard

Closes: #77530

* add mocks for active_cursor service

* fix jest tests

* fix jest tests

* apply PR comments

* fix cursor style

* update heatmap, jest

* add tests

* fix wrong import

* replace cursor for timelion

* update tsvb_dashboard baseline

* fix CI

* update baseline

* Update active_cursor_utils.ts

* add debounce

* remove cursor from heatmap and pie

* add tests for debounce

* return theme order back

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
alexwizp and kibanamachine committed Aug 4, 2021
1 parent 621a7cd commit 3c52800
Show file tree
Hide file tree
Showing 31 changed files with 657 additions and 93 deletions.
2 changes: 2 additions & 0 deletions src/plugins/charts/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export { ChartsPluginSetup, ChartsPluginStart } from './plugin';
export * from './static';
export * from './services/palettes/types';
export { lightenColor } from './services/palettes/lighten_color';
export { useActiveCursor } from './services/active_cursor';

export {
PaletteOutput,
CustomPaletteArguments,
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/charts/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { ChartsPlugin } from './plugin';
import { themeServiceMock } from './services/theme/mock';
import { activeCursorMock } from './services/active_cursor/mock';
import { colorsServiceMock } from './services/legacy_colors/mock';
import { getPaletteRegistry, paletteServiceMock } from './services/palettes/mock';

Expand All @@ -23,6 +24,7 @@ const createSetupContract = (): Setup => ({
const createStartContract = (): Start => ({
legacyColors: colorsServiceMock,
theme: themeServiceMock,
activeCursor: activeCursorMock,
palettes: paletteServiceMock.setup({} as any),
});

Expand Down
9 changes: 8 additions & 1 deletion src/plugins/charts/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { palette, systemPalette } from '../common';

import { ThemeService, LegacyColorsService } from './services';
import { PaletteService } from './services/palettes/service';
import { ActiveCursor } from './services/active_cursor';

export type Theme = Omit<ThemeService, 'init'>;
export type Color = Omit<LegacyColorsService, 'init'>;
Expand All @@ -28,13 +29,16 @@ export interface ChartsPluginSetup {
}

/** @public */
export type ChartsPluginStart = ChartsPluginSetup;
export type ChartsPluginStart = ChartsPluginSetup & {
activeCursor: ActiveCursor;
};

/** @public */
export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart> {
private readonly themeService = new ThemeService();
private readonly legacyColorsService = new LegacyColorsService();
private readonly paletteService = new PaletteService();
private readonly activeCursor = new ActiveCursor();

private palettes: undefined | ReturnType<PaletteService['setup']>;

Expand All @@ -45,6 +49,8 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
this.legacyColorsService.init(core.uiSettings);
this.palettes = this.paletteService.setup(this.legacyColorsService);

this.activeCursor.setup();

return {
legacyColors: this.legacyColorsService,
theme: this.themeService,
Expand All @@ -57,6 +63,7 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes!,
activeCursor: this.activeCursor,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ActiveCursor } from './active_cursor';

describe('ActiveCursor', () => {
let activeCursor: ActiveCursor;

beforeEach(() => {
activeCursor = new ActiveCursor();
});

test('should initialize activeCursor$ stream on setup hook', () => {
expect(activeCursor.activeCursor$).toBeUndefined();

activeCursor.setup();

expect(activeCursor.activeCursor$).toMatchInlineSnapshot(`
Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
}
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
*/

import { Subject } from 'rxjs';
import { PointerEvent } from '@elastic/charts';
import type { ActiveCursorPayload } from './types';

export const activeCursor$ = new Subject<PointerEvent>();
export class ActiveCursor {
public activeCursor$?: Subject<ActiveCursorPayload>;

setup() {
this.activeCursor$ = new Subject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { parseSyncOptions } from './active_cursor_utils';
import type { Datatable } from '../../../../expressions/public';

describe('active_cursor_utils', () => {
describe('parseSyncOptions', () => {
describe('dateHistogramSyncOption', () => {
test('should return isDateHistogram true in case if that mode is active', () => {
expect(parseSyncOptions({ isDateHistogram: true })).toMatchInlineSnapshot(`
Object {
"isDateHistogram": true,
}
`);
});

test('should return isDateHistogram false for other cases', () => {
expect(parseSyncOptions({ datatables: [] as Datatable[] })).toMatchInlineSnapshot(`
Object {
"accessors": Array [],
"isDateHistogram": false,
}
`);
});
});

describe('datatablesSyncOption', () => {
test('should extract accessors', () => {
expect(
parseSyncOptions({
datatables: ([
{
columns: [
{
meta: {
index: 'foo_index',
field: 'foo_field',
},
},
],
},
] as unknown) as Datatable[],
}).accessors
).toMatchInlineSnapshot(`
Array [
"foo_index:foo_field",
]
`);
});

test('should return isDateHistogram true in case all datatables is time based', () => {
expect(
parseSyncOptions({
datatables: ([
{
columns: [
{
meta: {
index: 'foo_index',
field: 'foo_field',
sourceParams: {
appliedTimeRange: {},
},
},
},
],
},
{
columns: [
{
meta: {
index: 'foo_index1',
field: 'foo_field1',
sourceParams: {
appliedTimeRange: {},
},
},
},
],
},
] as unknown) as Datatable[],
})
).toMatchInlineSnapshot(`
Object {
"accessors": Array [
"foo_index:foo_field",
"foo_index1:foo_field1",
],
"isDateHistogram": true,
}
`);
});

test('should return isDateHistogram false in case of not all datatables is time based', () => {
expect(
parseSyncOptions({
datatables: ([
{
columns: [
{
meta: {
index: 'foo_index',
field: 'foo_field',
sourceParams: {
appliedTimeRange: {},
},
},
},
],
},
{
columns: [
{
meta: {
index: 'foo_index1',
field: 'foo_field1',
},
},
],
},
] as unknown) as Datatable[],
})
).toMatchInlineSnapshot(`
Object {
"accessors": Array [
"foo_index:foo_field",
"foo_index1:foo_field1",
],
"isDateHistogram": false,
}
`);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { uniq } from 'lodash';

import type { Datatable } from '../../../../expressions/public';
import type { ActiveCursorSyncOption, DateHistogramSyncOption } from './types';
import type { ActiveCursorPayload } from './types';

function isDateHistogramSyncOption(
syncOption?: ActiveCursorSyncOption
): syncOption is DateHistogramSyncOption {
return Boolean(syncOption && 'isDateHistogram' in syncOption);
}

const parseDatatable = (dataTables: Datatable[]) => {
const isDateHistogram =
Boolean(dataTables.length) &&
dataTables.every((dataTable) =>
dataTable.columns.some((c) => Boolean(c.meta.sourceParams?.appliedTimeRange))
);

const accessors = uniq(
dataTables
.map((dataTable) => {
const column = dataTable.columns.find((c) => c.meta.index && c.meta.field);

if (column?.meta.index) {
return `${column.meta.index}:${column.meta.field}`;
}
})
.filter(Boolean) as string[]
);
return { isDateHistogram, accessors };
};

/** @internal **/
export const parseSyncOptions = (
syncOptions: ActiveCursorSyncOption
): Partial<ActiveCursorPayload> =>
isDateHistogramSyncOption(syncOptions)
? {
isDateHistogram: syncOptions.isDateHistogram,
}
: parseDatatable(syncOptions.datatables);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,5 @@
* Side Public License, v 1.
*/

import { Subject } from 'rxjs';
import { PointerEvent } from '@elastic/charts';

export const activeCursor$ = new Subject<PointerEvent>();
export { ActiveCursor } from './active_cursor';
export { useActiveCursor } from './use_active_cursor';
19 changes: 19 additions & 0 deletions src/plugins/charts/public/services/active_cursor/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { ActiveCursor } from './active_cursor';

export const activeCursorMock: ActiveCursor = {
activeCursor$: {
subscribe: jest.fn(),
pipe: jest.fn(() => ({
subscribe: jest.fn(),
})),
},
setup: jest.fn(),
} as any;
35 changes: 35 additions & 0 deletions src/plugins/charts/public/services/active_cursor/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { PointerEvent } from '@elastic/charts';
import type { Datatable } from '../../../../expressions/public';

/** @public **/
export type ActiveCursorSyncOption = DateHistogramSyncOption | DatatablesSyncOption;

/** @internal **/
export interface ActiveCursorPayload {
cursor: PointerEvent;
isDateHistogram?: boolean;
accessors?: string[];
}

/** @internal **/
interface BaseSyncOptions {
debounce?: number;
}

/** @internal **/
export interface DateHistogramSyncOption extends BaseSyncOptions {
isDateHistogram: boolean;
}

/** @internal **/
export interface DatatablesSyncOption extends BaseSyncOptions {
datatables: Datatable[];
}
Loading

0 comments on commit 3c52800

Please sign in to comment.