Skip to content

Commit

Permalink
[lens] Implement app-level filtering and time picker (#42031)
Browse files Browse the repository at this point in the history
* [lens] Implement app-level filtering and time picker

* More integration with filter bar

* Clean up test code and type errors

* Add frame level tests for syncing with app

* Add test coverage for app logic

* Simplify state management from app down

* Fix import errors

* Clarify whether properties are ids or titles for index pattern

* pass new saved object by ref

* add dirty state checking

* Fix tests
  • Loading branch information
Wylie Conlon committed Aug 7, 2019
1 parent 79ceaa1 commit b8eba00
Show file tree
Hide file tree
Showing 30 changed files with 1,317 additions and 965 deletions.
402 changes: 402 additions & 0 deletions x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx

Large diffs are not rendered by default.

204 changes: 192 additions & 12 deletions x-pack/legacy/plugins/lens/public/app_plugin/app.tsx
Expand Up @@ -4,25 +4,205 @@
* you may not use this file except in compliance with the Elastic License.
*/

import _ from 'lodash';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Storage } from 'ui/storage';
import { toastNotifications } from 'ui/notify';
import { Chrome } from 'ui/chrome';
import {
Query,
QueryBar as QueryBarType,
} from '../../../../../../src/legacy/core_plugins/data/public/query';
import { Document, SavedObjectStore } from '../persistence';
import { EditorFrameInstance } from '../types';
import { NativeRenderer } from '../native_renderer';

export function App({ editorFrame }: { editorFrame: EditorFrameInstance }) {
interface State {
isLoading: boolean;
isDirty: boolean;
dateRange: {
fromDate: string;
toDate: string;
};
query: Query;
indexPatternTitles: string[];
persistedDoc?: Document;
}

export function App({
editorFrame,
store,
chrome,
docId,
docStorage,
QueryBar,
redirectTo,
}: {
editorFrame: EditorFrameInstance;
chrome: Chrome;
store: Storage;
docId?: string;
docStorage: SavedObjectStore;
QueryBar: typeof QueryBarType;
redirectTo: (id?: string) => void;
}) {
const uiSettings = chrome.getUiSettingsClient();
const timeDefaults = uiSettings.get('timepicker:timeDefaults');
const language = store.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage');

const [state, setState] = useState<State>({
isLoading: !!docId,
isDirty: false,
query: { query: '', language },
dateRange: {
fromDate: timeDefaults.from,
toDate: timeDefaults.to,
},
indexPatternTitles: [],
});

const lastKnownDocRef = useRef<Document | undefined>(undefined);

useEffect(() => {
if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) {
setState({ ...state, isLoading: true });
docStorage
.load(docId)
.then(doc => {
setState({
...state,
isLoading: false,
persistedDoc: doc,
query: doc.state.query,
indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map(
({ title }) => title
),
});
})
.catch(() => {
setState({ ...state, isLoading: false });

toastNotifications.addDanger(
i18n.translate('xpack.lens.editorFrame.docLoadingError', {
defaultMessage: 'Error loading saved document',
})
);

redirectTo();
});
}
}, [docId]);

// Can save if the frame has told us what it has, and there is either:
// a) No saved doc
// b) A saved doc that differs from the frame state
const isSaveable = state.isDirty;

const onError = useCallback(
(e: { message: string }) =>
toastNotifications.addDanger({
title: e.message,
}),
[]
);

return (
<I18nProvider>
<NativeRenderer
className="lnsPage"
render={editorFrame.mount}
nativeProps={{
onError: (e: { message: string }) =>
toastNotifications.addDanger({
title: e.message,
}),
}}
/>
<div className="lnsApp">
<div className="lnsAppHeader">
<nav>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="lnsApp_saveButton"
onClick={() => {
if (isSaveable && lastKnownDocRef.current) {
docStorage
.save(lastKnownDocRef.current)
.then(({ id }) => {
// Prevents unnecessary network request and disables save button
const newDoc = { ...lastKnownDocRef.current!, id };
setState({
...state,
isDirty: false,
persistedDoc: newDoc,
});
if (docId !== id) {
redirectTo(id);
}
})
.catch(reason => {
toastNotifications.addDanger(
i18n.translate('xpack.lens.editorFrame.docSavingError', {
defaultMessage: 'Error saving document {reason}',
values: { reason },
})
);
});
}
}}
color={isSaveable ? 'primary' : 'subdued'}
disabled={!isSaveable}
>
{i18n.translate('xpack.lens.editorFrame.save', {
defaultMessage: 'Save',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</nav>
<QueryBar
data-test-subj="lnsApp_queryBar"
screenTitle={'lens'}
onSubmit={({ dateRange, query }) => {
setState({
...state,
dateRange: {
fromDate: dateRange.from,
toDate: dateRange.to,
},
query: query || state.query,
});
}}
appName={'lens'}
indexPatterns={state.indexPatternTitles}
store={store}
showDatePicker={true}
showQueryInput={true}
query={state.query}
dateRangeFrom={state.dateRange && state.dateRange.fromDate}
dateRangeTo={state.dateRange && state.dateRange.toDate}
/>
</div>

{(!state.isLoading || state.persistedDoc) && (
<NativeRenderer
className="lnsAppFrame"
render={editorFrame.mount}
nativeProps={{
dateRange: state.dateRange,
query: state.query,
doc: state.persistedDoc,
onError,
onChange: ({ indexPatternTitles, doc }) => {
const indexPatternChange = !_.isEqual(state.indexPatternTitles, indexPatternTitles);
const docChange = !_.isEqual(state.persistedDoc, doc);
if (indexPatternChange || docChange) {
setState({
...state,
indexPatternTitles,
isDirty: docChange,
});
}
lastKnownDocRef.current = doc;
},
}}
/>
)}
</div>
</I18nProvider>
);
}
23 changes: 23 additions & 0 deletions x-pack/legacy/plugins/lens/public/app_plugin/index.scss
@@ -0,0 +1,23 @@
.lnsApp {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}

.lnsAppHeader {
padding: $euiSize;
border-bottom: $euiBorderThin;
}

.lnsAppFrame {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
}
46 changes: 45 additions & 1 deletion x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
Expand Up @@ -5,8 +5,14 @@
*/

import React from 'react';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom';
import chrome, { Chrome } from 'ui/chrome';
import { localStorage } from 'ui/storage/storage_service';
import { QueryBar } from '../../../../../../src/legacy/core_plugins/data/public/query';
import { editorFrameSetup, editorFrameStop } from '../editor_frame_plugin';
import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin';
import { SavedObjectIndexStore } from '../persistence';
import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin';
import {
datatableVisualizationSetup,
Expand All @@ -17,6 +23,7 @@ import { EditorFrameInstance } from '../types';

export class AppPlugin {
private instance: EditorFrameInstance | null = null;
private chrome: Chrome | null = null;

constructor() {}

Expand All @@ -32,9 +39,46 @@ export class AppPlugin {
editorFrame.registerVisualization(xyVisualization);
editorFrame.registerVisualization(datatableVisualization);

this.chrome = chrome;
const store = new SavedObjectIndexStore(this.chrome!.getSavedObjectsClient());

this.instance = editorFrame.createInstance({});

return <App editorFrame={this.instance} />;
const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
return (
<App
editorFrame={this.instance!}
QueryBar={QueryBar}
chrome={chrome}
store={localStorage}
docId={routeProps.match.params.id}
docStorage={store}
redirectTo={id => {
if (!id) {
routeProps.history.push('/');
} else {
routeProps.history.push(`/edit/${id}`);
}
}}
/>
);
};

function NotFound() {
return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />;
}

return (
<I18nProvider>
<HashRouter>
<Switch>
<Route exact path="/edit/:id" render={renderEditor} />
<Route exact path="/" render={renderEditor} />
<Route component={NotFound} />
</Switch>
</HashRouter>
</I18nProvider>
);
}

stop() {
Expand Down
Expand Up @@ -22,6 +22,11 @@ function mockFrame(): FramePublicAPI {
addNewLayer: () => 'aaa',
removeLayers: () => {},
datasourceLayers: {},
query: { query: '', language: 'lucene' },
dateRange: {
fromDate: 'now-7d',
toDate: 'now',
},
};
}

Expand Down Expand Up @@ -71,15 +76,13 @@ describe('Datatable Visualization', () => {
const setState = jest.fn();
const datasource = createMockDatasource();
const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };

mount(
<DataTableLayer
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
frame={{
addNewLayer: jest.fn(),
removeLayers: jest.fn(),
datasourceLayers: { a: datasource.publicAPIMock },
}}
frame={frame}
layer={layer}
setState={setState}
state={{ layers: [layer] }}
Expand Down Expand Up @@ -110,14 +113,12 @@ describe('Datatable Visualization', () => {
const setState = jest.fn();
const datasource = createMockDatasource();
const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
const component = mount(
<DataTableLayer
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
frame={{
addNewLayer: jest.fn(),
removeLayers: jest.fn(),
datasourceLayers: { a: datasource.publicAPIMock },
}}
frame={frame}
layer={layer}
setState={setState}
state={{ layers: [layer] }}
Expand Down Expand Up @@ -146,14 +147,12 @@ describe('Datatable Visualization', () => {
const setState = jest.fn();
const datasource = createMockDatasource();
const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
const component = mount(
<DataTableLayer
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
frame={{
addNewLayer: jest.fn(),
removeLayers: jest.fn(),
datasourceLayers: { a: datasource.publicAPIMock },
}}
frame={frame}
layer={layer}
setState={setState}
state={{ layers: [layer] }}
Expand Down

0 comments on commit b8eba00

Please sign in to comment.