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

Drilldowns reload stored #60336

Merged
merged 6 commits into from
Mar 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,25 @@ export abstract class Embeddable<
// to update input when the parent changes.
private parentSubscription?: Rx.Subscription;

private storageSubscription?: Rx.Subscription;

// TODO: Rename to destroyed.
private destoyed: boolean = false;

private __dynamicActions?: UiActionsDynamicActionManager;
private storage = new EmbeddableActionStorage(this);

private cachedDynamicActions?: UiActionsDynamicActionManager;
public get dynamicActions(): UiActionsDynamicActionManager | undefined {
if (!this.params.uiActions) return undefined;
if (!this.__dynamicActions) {
this.__dynamicActions = new UiActionsDynamicActionManager({
if (!this.cachedDynamicActions) {
this.cachedDynamicActions = new UiActionsDynamicActionManager({
isCompatible: async ({ embeddable }: any) => embeddable.runtimeId === this.runtimeId,
storage: new EmbeddableActionStorage(this),
storage: this.storage,
uiActions: this.params.uiActions,
});
}

return this.__dynamicActions;
return this.cachedDynamicActions;
}

constructor(
Expand Down Expand Up @@ -112,6 +116,9 @@ export abstract class Embeddable<
console.error(error);
/* eslint-enable */
});
this.storageSubscription = this.input$.subscribe(() => {
this.storage.reload$.next();
});
}
}

Expand Down Expand Up @@ -201,6 +208,10 @@ export abstract class Embeddable<
});
}

if (this.storageSubscription) {
this.storageSubscription.unsubscribe();
}

if (this.parentSubscription) {
this.parentSubscription.unsubscribe();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@
* under the License.
*/

import { UiActionsActionStorage, UiActionsSerializedEvent } from '../../../../ui_actions/public';
import {
UiActionsAbstractActionStorage,
UiActionsSerializedEvent,
} from '../../../../ui_actions/public';
import { Embeddable } from '..';

export class EmbeddableActionStorage implements UiActionsActionStorage {
constructor(private readonly embbeddable: Embeddable<any, any>) {}
export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
constructor(private readonly embbeddable: Embeddable<any, any>) {
super();
}

async create(event: UiActionsSerializedEvent) {
const input = this.embbeddable.getInput();
Expand Down Expand Up @@ -96,10 +101,6 @@ export class EmbeddableActionStorage implements UiActionsActionStorage {
return (input.events || []) as UiActionsSerializedEvent[];
}

async count(): Promise<number> {
return this.__list().length;
}

async list(): Promise<UiActionsSerializedEvent[]> {
return this.__list();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface EmbeddableInput {
/**
* Reserved key for `ui_actions` events.
*/
events?: unknown;
events?: Array<{ eventId: string }>;

/**
* List of action IDs that this embeddable should not render.
Expand Down
51 changes: 50 additions & 1 deletion src/plugins/ui_actions/public/actions/dynamic_action_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,25 @@
*/

import { v4 as uuidv4 } from 'uuid';
import { Subscription } from 'rxjs';
import { ActionStorage, SerializedEvent } from './dynamic_action_storage';
import { UiActionsService } from '../service';
import { SerializedAction } from './types';
import { ActionDefinition } from './action';
import { defaultState, transitions, selectors, State } from './dynamic_action_manager_state';
import { StateContainer, createStateContainer } from '../../../kibana_utils';

const compareEvents = (
a: ReadonlyArray<{ eventId: string }>,
b: ReadonlyArray<{ eventId: string }>
) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i].eventId !== b[i].eventId) return false;
return true;
};

export type DynamicActionManagerState = State;

export interface DynamicActionManagerParams {
storage: ActionStorage;
uiActions: Pick<
Expand All @@ -38,8 +50,8 @@ export class DynamicActionManager {
static idPrefixCounter = 0;

private readonly idPrefix = `D_ACTION_${DynamicActionManager.idPrefixCounter++}_`;

private stopped: boolean = false;
private reloadSubscription?: Subscription;

/**
* UI State of the dynamic action manager.
Expand Down Expand Up @@ -85,6 +97,35 @@ export class DynamicActionManager {
uiActions.removeTriggerAction(triggerId as any, actionId);
}

private syncId = 0;

/**
* This function is called every time stored events might have changed not by
* us. For example, when in edit mode on dashboard user presses "back" button
* in the browser, then contents of storage changes.
*/
private onSync = () => {
if (this.stopped) return;

(async () => {
const syncId = ++this.syncId;
const events = await this.params.storage.list();

if (this.stopped) return;
if (syncId !== this.syncId) return;
if (compareEvents(events, this.ui.get().events)) return;

for (const event of this.ui.get().events) this.killAction(event);
for (const event of events) this.reviveAction(event);
this.ui.transitions.finishFetching(events);
})().catch(error => {
/* eslint-disable */
console.log('Dynamic action manager storage reload failed.');
console.error(error);
/* eslint-enable */
});
};

// Public API: ---------------------------------------------------------------

/**
Expand All @@ -108,6 +149,10 @@ export class DynamicActionManager {
const events = await this.params.storage.list();
for (const event of events) this.reviveAction(event);
this.ui.transitions.finishFetching(events);

if (this.params.storage.reload$) {
this.reloadSubscription = this.params.storage.reload$.subscribe(this.onSync);
}
}

/**
Expand All @@ -121,6 +166,10 @@ export class DynamicActionManager {
for (const event of events) {
this.killAction(event);
}

if (this.reloadSubscription) {
this.reloadSubscription.unsubscribe();
}
}

/**
Expand Down
22 changes: 21 additions & 1 deletion src/plugins/ui_actions/public/actions/dynamic_action_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { Observable, Subject } from 'rxjs';
import { SerializedAction } from './types';

/**
Expand All @@ -29,7 +30,7 @@ export interface SerializedEvent {
}

/**
* This interface needs to be implemented by dynamic action users if they
* This CRUD interface needs to be implemented by dynamic action users if they
* want to persist the dynamic actions. It has a default implementation in
* Embeddables, however one can use the dynamic actions without Embeddables,
* in that case they have to implement this interface.
Expand All @@ -41,4 +42,23 @@ export interface ActionStorage {
read(eventId: string): Promise<SerializedEvent>;
count(): Promise<number>;
list(): Promise<SerializedEvent[]>;

/**
* Triggered every time events changed in storage and should be re-loaded.
*/
readonly reload$?: Observable<void>;
}

export abstract class AbstractActionStorage implements ActionStorage {
public readonly reload$: Observable<void> & Pick<Subject<void>, 'next'> = new Subject<void>();

public async count(): Promise<number> {
return (await this.list()).length;
}

abstract create(event: SerializedEvent): Promise<void>;
abstract update(event: SerializedEvent): Promise<void>;
abstract remove(eventId: string): Promise<void>;
abstract read(eventId: string): Promise<SerializedEvent>;
abstract list(): Promise<SerializedEvent[]>;
}
2 changes: 2 additions & 0 deletions src/plugins/ui_actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export {
ActionFactoryDefinition as UiActionsActionFactoryDefinition,
ActionInternal as UiActionsActionInternal,
ActionStorage as UiActionsActionStorage,
AbstractActionStorage as UiActionsAbstractActionStorage,
createAction,
DynamicActionManager,
DynamicActionManagerState,
IncompatibleActionError,
SerializedAction as UiActionsSerializedAction,
SerializedEvent as UiActionsSerializedEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,7 @@ test('Create only mode', async () => {
expect(await mockDynamicActionManager.count()).toBe(1);
});

test("Error when can't fetch drilldown list", async () => {
const error = new Error('Oops');
jest.spyOn(mockDynamicActionManager, 'list').mockImplementationOnce(async () => {
throw error;
});
render(<FlyoutManageDrilldowns context={{}} dynamicActionManager={mockDynamicActionManager} />);
await wait(() =>
expect(notifications.toasts.addError).toBeCalledWith(error, {
title: toastDrilldownsFetchError,
})
);
});
test.todo("Error when can't fetch drilldown list");

test("Error when can't save drilldown changes", async () => {
const error = new Error('Oops');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import React, { useEffect, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import useMountedState from 'react-use/lib/useMountedState';
import {
AdvancedUiActionsActionFactory as ActionFactory,
Expand All @@ -20,14 +19,14 @@ import {
UiActionsSerializedEvent,
UiActionsSerializedAction,
} from '../../../../../../src/plugins/ui_actions/public';
import { useContainerState } from '../../../../../../src/plugins/kibana_utils/common';
import { DrilldownListItem } from '../list_manage_drilldowns';
import {
toastDrilldownCreated,
toastDrilldownDeleted,
toastDrilldownEdited,
toastDrilldownsCRUDError,
toastDrilldownsDeleted,
toastDrilldownsFetchError,
} from './i18n';

interface ConnectedFlyoutManageDrilldownsProps<Context extends object = object> {
Expand Down Expand Up @@ -243,8 +242,8 @@ function useDrilldownsStateManager(
actionManager: DynamicActionManager,
notifications: NotificationsStart
) {
const { events: drilldowns } = useContainerState(actionManager.state);
const [isLoading, setIsLoading] = useState(false);
const [drilldowns, setDrilldowns] = useState<readonly UiActionsSerializedEvent[]>();
const isMounted = useMountedState();

async function run(op: () => Promise<void>) {
Expand All @@ -259,36 +258,8 @@ function useDrilldownsStateManager(
setIsLoading(false);
return;
}

await reload();
}

async function reload() {
if (!isMounted) {
// don't do any side effects anymore because component is already unmounted
return;
}
if (!isLoading) {
setIsLoading(true);
}
try {
const drilldownsList = await actionManager.list();
if (!isMounted) {
return;
}
setDrilldowns(drilldownsList);
setIsLoading(false);
} catch (e) {
notifications.toasts.addError(e, {
title: toastDrilldownsFetchError,
});
}
}

useMount(() => {
reload();
});

async function createDrilldown(action: UiActionsSerializedAction<any>, triggerId?: string) {
await run(async () => {
await actionManager.createEvent(action, triggerId);
Expand Down
Loading