mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
drag-n-drop part 1 (#25398)
* * redux wireup * * sorting and removing providers * * updating range state * * added updateData * * addProvider and updateProviders * * added getQuery to the provider interface * * fixed tests * * click to add a provider! * * passing timelineId to all Placeholders * * added redux-thunk * * added range to failing test * * pr feedback
This commit is contained in:
parent
022295f5a9
commit
34558a1e01
30 changed files with 716 additions and 143 deletions
|
@ -7,6 +7,7 @@
|
|||
import { createHashHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
// TODO use theme provided from parentApp when kibana supports it
|
||||
|
@ -15,17 +16,22 @@ import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
|
|||
|
||||
import { AppFrontendLibs } from '../lib/lib';
|
||||
import { PageRouter } from '../routes';
|
||||
import { createStore } from '../store';
|
||||
|
||||
export const startApp = async (libs: AppFrontendLibs) => {
|
||||
const history = createHashHistory();
|
||||
|
||||
const store = createStore();
|
||||
|
||||
libs.framework.render(
|
||||
<EuiErrorBoundary>
|
||||
<ApolloProvider client={libs.apolloClient}>
|
||||
<ThemeProvider theme={{ eui: euiVars }}>
|
||||
<PageRouter history={history} />
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
<ReduxStoreProvider store={store}>
|
||||
<ApolloProvider client={libs.apolloClient}>
|
||||
<ThemeProvider theme={{ eui: euiVars }}>
|
||||
<PageRouter history={history} />
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
</ReduxStoreProvider>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ describe('ColumnHeaders', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range="1 Day"
|
||||
rowRenderers={rowRenderers}
|
||||
width={1000}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('ColumnHeaders', () => {
|
|||
|
||||
test('it renders the other (data-driven) column headers', () => {
|
||||
const wrapper = mount(
|
||||
<ColumnHeaders columnHeaders={headers} sort={sort} onRangeSelected={noop} />
|
||||
<ColumnHeaders columnHeaders={headers} range="1 Day" sort={sort} onRangeSelected={noop} />
|
||||
);
|
||||
|
||||
headers.forEach(h => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { pure } from 'recompose';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { OnColumnSorted, OnFilterChange, OnRangeSelected } from '../../events';
|
||||
import { Range } from '../column_headers/range_picker/ranges';
|
||||
import { Sort } from '../sort';
|
||||
import { ColumnHeader } from './column_header';
|
||||
import { Header } from './header';
|
||||
|
@ -20,6 +21,7 @@ interface Props {
|
|||
onColumnSorted?: OnColumnSorted;
|
||||
onFilterChange?: OnFilterChange;
|
||||
onRangeSelected: OnRangeSelected;
|
||||
range: Range;
|
||||
sort: Sort;
|
||||
}
|
||||
|
||||
|
@ -31,15 +33,22 @@ const ColumnHeaderContainer = styled.div``;
|
|||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
margin-left: 3px;
|
||||
margin-left: 5px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
/** Renders the timeline header columns */
|
||||
export const ColumnHeaders = pure<Props>(
|
||||
({ columnHeaders, onColumnSorted = noop, onFilterChange = noop, onRangeSelected, sort }) => (
|
||||
({
|
||||
columnHeaders,
|
||||
onColumnSorted = noop,
|
||||
onFilterChange = noop,
|
||||
onRangeSelected,
|
||||
range,
|
||||
sort,
|
||||
}) => (
|
||||
<ColumnHeadersSpan data-test-subj="columnHeaders">
|
||||
<RangePicker selected={'1 Day'} onRangeSelected={onRangeSelected} />
|
||||
<RangePicker selected={range} onRangeSelected={onRangeSelected} />
|
||||
<Flex>
|
||||
{columnHeaders.map(header => (
|
||||
<ColumnHeaderContainer data-test-subj="columnHeaderContainer" key={header.id}>
|
||||
|
|
|
@ -17,12 +17,12 @@ interface Props {
|
|||
onRangeSelected: OnRangeSelected;
|
||||
}
|
||||
|
||||
export const RangePickerWidth = 110;
|
||||
export const rangePickerWidth = 120;
|
||||
|
||||
// TODO: Upgrade Eui library and use EuiSuperSelect
|
||||
const SelectContainer = styled.div`
|
||||
cursor: pointer;
|
||||
width: ${RangePickerWidth}px;
|
||||
width: ${rangePickerWidth}px;
|
||||
`;
|
||||
|
||||
/** Renders a time range picker for the MiniMap (e.g. 1 Day, 1 Week...) */
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as React from 'react';
|
|||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Range } from '../body/column_headers/range_picker/ranges';
|
||||
import { DataProvider } from '../data_providers/data_provider';
|
||||
import { ECS } from '../ecs';
|
||||
import { OnColumnSorted, OnDataProviderRemoved, OnFilterChange, OnRangeSelected } from '../events';
|
||||
|
@ -27,6 +28,7 @@ interface Props {
|
|||
onDataProviderRemoved: OnDataProviderRemoved;
|
||||
onFilterChange: OnFilterChange;
|
||||
onRangeSelected: OnRangeSelected;
|
||||
range: Range;
|
||||
rowRenderers: RowRenderer[];
|
||||
sort: Sort;
|
||||
width: number;
|
||||
|
@ -79,6 +81,7 @@ const Pin = styled(EuiIcon)`
|
|||
|
||||
const DataDrivenColumns = styled.div`
|
||||
display: flex;
|
||||
margin-left: 5px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
|
@ -99,6 +102,7 @@ export const Body = pure<Props>(
|
|||
onColumnSorted,
|
||||
onFilterChange,
|
||||
onRangeSelected,
|
||||
range,
|
||||
rowRenderers,
|
||||
sort,
|
||||
width,
|
||||
|
@ -109,6 +113,7 @@ export const Body = pure<Props>(
|
|||
onColumnSorted={onColumnSorted}
|
||||
onFilterChange={onFilterChange}
|
||||
onRangeSelected={onRangeSelected}
|
||||
range={range}
|
||||
sort={sort}
|
||||
/>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
|
|
|
@ -17,6 +17,10 @@ export interface DataProvider {
|
|||
* the timeline. default: `true`
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Returns a query that, when executed, returns the data for this provider
|
||||
*/
|
||||
getQuery: () => string;
|
||||
/**
|
||||
* When `true`, boolean logic is applied to the data provider to negate it.
|
||||
* default: `false`
|
||||
|
|
|
@ -27,6 +27,7 @@ const mockSourceNameToEventCount: NameToEventCount<number> = {
|
|||
'Provider 7': 533,
|
||||
'Provider 8': 429,
|
||||
'Provider 9': 706,
|
||||
'Provider 10': 863,
|
||||
};
|
||||
|
||||
/** Returns a collection of mock data provider names */
|
||||
|
@ -48,6 +49,7 @@ const Text = styled(EuiText)`
|
|||
export const mockDataProviders: DataProvider[] = Object.keys(mockSourceNameToEventCount).map(
|
||||
name => ({
|
||||
enabled: true,
|
||||
getQuery: () => `query-for-provider-id-${name}`,
|
||||
id: `id-${name}`,
|
||||
name,
|
||||
negated: false,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { noop, omit } from 'lodash/fp';
|
||||
import { noop, pick } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
getEventCount,
|
||||
|
@ -43,7 +43,10 @@ describe('Providers', () => {
|
|||
.first()
|
||||
.simulate('click');
|
||||
|
||||
const callbackParams = omit('render', mockOnDataProviderRemoved.mock.calls[0][0]);
|
||||
const callbackParams = pick(
|
||||
['enabled', 'id', 'name', 'negated'],
|
||||
mockOnDataProviderRemoved.mock.calls[0][0]
|
||||
);
|
||||
|
||||
expect(callbackParams).toEqual({
|
||||
enabled: true,
|
||||
|
|
|
@ -4,82 +4,135 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { defaultTo, noop } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
import { ECS } from './ecs';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
|
||||
import { Body } from './body';
|
||||
import { timelineActions } from '../../store';
|
||||
import { timelineDefaults } from '../../store/local/timeline/model';
|
||||
import { State } from '../../store/reducer';
|
||||
import { timelineByIdSelector } from '../../store/selectors';
|
||||
import { ColumnHeader } from './body/column_headers/column_header';
|
||||
import { ColumnRenderer } from './body/renderers';
|
||||
import { RowRenderer } from './body/renderers';
|
||||
import { Range } from './body/column_headers/range_picker/ranges';
|
||||
import { columnRenderers, rowRenderers } from './body/renderers';
|
||||
import { Sort } from './body/sort';
|
||||
import { DataProvider } from './data_providers/data_provider';
|
||||
import { OnColumnSorted, OnDataProviderRemoved, OnFilterChange, OnRangeSelected } from './events';
|
||||
import { TimelineHeader } from './header/timeline_header';
|
||||
import { ECS } from './ecs';
|
||||
import { OnColumnSorted, OnDataProviderRemoved, OnRangeSelected } from './events';
|
||||
import { Timeline } from './timeline';
|
||||
|
||||
interface Props {
|
||||
columnHeaders: ColumnHeader[];
|
||||
columnRenderers: ColumnRenderer[];
|
||||
data: ECS[];
|
||||
dataProviders: DataProvider[];
|
||||
height?: string;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
onDataProviderRemoved: OnDataProviderRemoved;
|
||||
onFilterChange: OnFilterChange;
|
||||
onRangeSelected: OnRangeSelected;
|
||||
rowRenderers: RowRenderer[];
|
||||
sort: Sort;
|
||||
export interface OwnProps {
|
||||
id: string;
|
||||
headers: ColumnHeader[];
|
||||
width: number;
|
||||
}
|
||||
|
||||
const TimelineDiv = styled.div<{ width: string; height: string }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 700px;
|
||||
overflow: none;
|
||||
user-select: none;
|
||||
width: ${props => props.width};
|
||||
height: ${props => props.height};
|
||||
`;
|
||||
interface StateProps {
|
||||
dataProviders: DataProvider[];
|
||||
data: ECS[];
|
||||
range: Range;
|
||||
sort: Sort;
|
||||
}
|
||||
|
||||
const defaultHeight = '100%';
|
||||
interface DispatchProps {
|
||||
createTimeline: ActionCreator<{ id: string }>;
|
||||
addProvider: ActionCreator<{
|
||||
id: string;
|
||||
provider: DataProvider;
|
||||
}>;
|
||||
updateData: ActionCreator<{
|
||||
id: string;
|
||||
data: ECS[];
|
||||
}>;
|
||||
updateProviders: ActionCreator<{
|
||||
id: string;
|
||||
providers: DataProvider[];
|
||||
}>;
|
||||
updateRange: ActionCreator<{
|
||||
id: string;
|
||||
range: Range;
|
||||
}>;
|
||||
updateSort: ActionCreator<{
|
||||
id: string;
|
||||
sort: Sort;
|
||||
}>;
|
||||
removeProvider: ActionCreator<{
|
||||
id: string;
|
||||
providerId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** The parent Timeline component */
|
||||
export const Timeline = pure<Props>(
|
||||
({
|
||||
columnHeaders,
|
||||
columnRenderers,
|
||||
dataProviders,
|
||||
data,
|
||||
height = defaultHeight,
|
||||
onColumnSorted,
|
||||
onDataProviderRemoved,
|
||||
onFilterChange,
|
||||
onRangeSelected,
|
||||
rowRenderers,
|
||||
sort,
|
||||
width,
|
||||
}) => (
|
||||
<TimelineDiv data-test-subj="timeline" width={`${width}px`} height={height}>
|
||||
<TimelineHeader
|
||||
dataProviders={dataProviders}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
width={width}
|
||||
/>
|
||||
<Body
|
||||
columnHeaders={columnHeaders}
|
||||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class StatefulTimelineComponent extends React.PureComponent<Props> {
|
||||
public componentDidMount() {
|
||||
const { createTimeline, id } = this.props;
|
||||
|
||||
createTimeline({ id });
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
data,
|
||||
dataProviders,
|
||||
headers,
|
||||
id,
|
||||
range,
|
||||
removeProvider,
|
||||
sort,
|
||||
updateRange,
|
||||
updateSort,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
const onColumnSorted: OnColumnSorted = sorted => {
|
||||
updateSort({ id, sort: sorted });
|
||||
};
|
||||
|
||||
const onDataProviderRemoved: OnDataProviderRemoved = dataProvider => {
|
||||
removeProvider({ id, providerId: dataProvider.id });
|
||||
};
|
||||
|
||||
const onRangeSelected: OnRangeSelected = selectedRange => {
|
||||
updateRange({ id, range: selectedRange });
|
||||
};
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={dataProviders}
|
||||
data={data}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={onFilterChange}
|
||||
onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery
|
||||
onRangeSelected={onRangeSelected}
|
||||
range={range}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={width}
|
||||
/>
|
||||
</TimelineDiv>
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, { id }: OwnProps) => {
|
||||
const timeline = timelineByIdSelector(state)[id];
|
||||
const { dataProviders, data, sort } = timeline || timelineDefaults;
|
||||
|
||||
return defaultTo({ id, dataProviders, data, sort }, timeline);
|
||||
};
|
||||
|
||||
export const StatefulTimeline = connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
addProvider: timelineActions.addProvider,
|
||||
createTimeline: timelineActions.createTimeline,
|
||||
updateData: timelineActions.updateData,
|
||||
updateProviders: timelineActions.updateProviders,
|
||||
updateRange: timelineActions.updateRange,
|
||||
updateSort: timelineActions.updateSort,
|
||||
removeProvider: timelineActions.removeProvider,
|
||||
}
|
||||
)(StatefulTimelineComponent);
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { noop, omit } from 'lodash/fp';
|
||||
import { noop, pick } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Timeline } from '.';
|
||||
import { mockECSData } from '../../pages/mock/mock_ecs';
|
||||
import { ColumnHeaderType } from './body/column_headers/column_header';
|
||||
import { headers } from './body/column_headers/headers';
|
||||
import { columnRenderers, rowRenderers } from './body/renderers';
|
||||
import { Sort } from './body/sort';
|
||||
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
|
||||
import { Timeline } from './timeline';
|
||||
|
||||
describe('Timeline', () => {
|
||||
const sort: Sort = {
|
||||
|
@ -34,6 +34,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
@ -54,6 +55,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
@ -79,6 +81,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
@ -111,6 +114,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={mockOnDataProviderRemoved}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
@ -122,7 +126,10 @@ describe('Timeline', () => {
|
|||
.first()
|
||||
.simulate('click');
|
||||
|
||||
const callbackParams = omit('render', mockOnDataProviderRemoved.mock.calls[0][0]);
|
||||
const callbackParams = pick(
|
||||
['enabled', 'id', 'name', 'negated'],
|
||||
mockOnDataProviderRemoved.mock.calls[0][0]
|
||||
);
|
||||
|
||||
expect(callbackParams).toEqual({
|
||||
enabled: true,
|
||||
|
@ -154,6 +161,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={mockOnFilterChange}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
@ -187,6 +195,7 @@ describe('Timeline', () => {
|
|||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={mockOnRangeSelected}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
import { ECS } from './ecs';
|
||||
|
||||
import { Body } from './body';
|
||||
import { ColumnHeader } from './body/column_headers/column_header';
|
||||
import { Range } from './body/column_headers/range_picker/ranges';
|
||||
import { ColumnRenderer } from './body/renderers';
|
||||
import { RowRenderer } from './body/renderers';
|
||||
import { Sort } from './body/sort';
|
||||
import { DataProvider } from './data_providers/data_provider';
|
||||
import { OnColumnSorted, OnDataProviderRemoved, OnFilterChange, OnRangeSelected } from './events';
|
||||
import { TimelineHeader } from './header/timeline_header';
|
||||
|
||||
interface Props {
|
||||
columnHeaders: ColumnHeader[];
|
||||
columnRenderers: ColumnRenderer[];
|
||||
data: ECS[];
|
||||
dataProviders: DataProvider[];
|
||||
height?: string;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
onDataProviderRemoved: OnDataProviderRemoved;
|
||||
onFilterChange: OnFilterChange;
|
||||
onRangeSelected: OnRangeSelected;
|
||||
range: Range;
|
||||
rowRenderers: RowRenderer[];
|
||||
sort: Sort;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const TimelineDiv = styled.div<{ width: string; height: string }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 700px;
|
||||
overflow: none;
|
||||
user-select: none;
|
||||
width: ${props => props.width};
|
||||
height: ${props => props.height};
|
||||
`;
|
||||
|
||||
const defaultHeight = '100%';
|
||||
|
||||
/** The parent Timeline component */
|
||||
export const Timeline = pure<Props>(
|
||||
({
|
||||
columnHeaders,
|
||||
columnRenderers,
|
||||
dataProviders,
|
||||
data,
|
||||
height = defaultHeight,
|
||||
onColumnSorted,
|
||||
onDataProviderRemoved,
|
||||
onFilterChange,
|
||||
onRangeSelected,
|
||||
range,
|
||||
rowRenderers,
|
||||
sort,
|
||||
width,
|
||||
}) => (
|
||||
<TimelineDiv data-test-subj="timeline" width={`${width}px`} height={height}>
|
||||
<TimelineHeader
|
||||
dataProviders={dataProviders}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
width={width}
|
||||
/>
|
||||
<Body
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={dataProviders}
|
||||
data={data}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={onFilterChange}
|
||||
onRangeSelected={onRangeSelected}
|
||||
range={range}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={width}
|
||||
/>
|
||||
</TimelineDiv>
|
||||
)
|
||||
);
|
|
@ -7,10 +7,13 @@
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import { range } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { WhoAmI } from '../containers/who_am_i';
|
||||
import { timelineActions } from '../store';
|
||||
import { mockDataProviders } from './timeline/data_providers/mock/mock_data_providers';
|
||||
|
||||
export const VisualizationPlaceholder = styled(EuiPanel)`
|
||||
&& {
|
||||
|
@ -26,27 +29,52 @@ export const VisualizationPlaceholder = styled(EuiPanel)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const ProviderContainer = styled.div`
|
||||
margin: 5px;
|
||||
user-select: none;
|
||||
cursor: grab;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
timelineId: string;
|
||||
count: number;
|
||||
myRoute: string;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
/** TODO: delete this stub */
|
||||
export const Placeholders = pure<Props>(({ count, myRoute }) => (
|
||||
<React.Fragment>
|
||||
{range(0, count).map(p => (
|
||||
<VisualizationPlaceholder
|
||||
data-test-subj="visualizationPlaceholder"
|
||||
key={`visualizationPlaceholder-${p}`}
|
||||
>
|
||||
<WhoAmI data-test-subj="whoAmI" sourceId="default">
|
||||
{({ appName }) => (
|
||||
<div>
|
||||
{appName} {myRoute}
|
||||
</div>
|
||||
)}
|
||||
</WhoAmI>
|
||||
</VisualizationPlaceholder>
|
||||
))}
|
||||
</React.Fragment>
|
||||
));
|
||||
class PlaceholdersComponent extends React.PureComponent<Props> {
|
||||
public render() {
|
||||
const { count, dispatch, myRoute, timelineId } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{range(0, count).map(i => (
|
||||
<VisualizationPlaceholder
|
||||
data-test-subj="visualizationPlaceholder"
|
||||
key={`visualizationPlaceholder-${i}`}
|
||||
>
|
||||
<WhoAmI data-test-subj="whoAmI" sourceId="default">
|
||||
{({ appName }) => (
|
||||
<div>
|
||||
{appName} {myRoute}
|
||||
</div>
|
||||
)}
|
||||
</WhoAmI>
|
||||
<ProviderContainer
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
timelineActions.addProvider({ id: timelineId, provider: mockDataProviders[i] })
|
||||
);
|
||||
}}
|
||||
>
|
||||
{mockDataProviders[i].render()}
|
||||
</ProviderContainer>
|
||||
</VisualizationPlaceholder>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Placeholders = connect()(PlaceholdersComponent);
|
||||
|
|
|
@ -35,46 +35,13 @@ import {
|
|||
import { DatePicker } from '../../components/page/date_picker';
|
||||
import { Footer } from '../../components/page/footer';
|
||||
import { Navigation } from '../../components/page/navigation';
|
||||
import { Timeline } from '../../components/timeline';
|
||||
import { StatefulTimeline } from '../../components/timeline';
|
||||
import { headers } from '../../components/timeline/body/column_headers/headers';
|
||||
import { Sort } from '../../components/timeline/body/sort';
|
||||
import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers';
|
||||
import {
|
||||
OnColumnSorted,
|
||||
OnDataProviderRemoved,
|
||||
OnFilterChange,
|
||||
OnRangeSelected,
|
||||
} from '../../components/timeline/events';
|
||||
|
||||
import { mockECSData } from '../mock/mock_ecs';
|
||||
|
||||
import { columnRenderers, rowRenderers } from '../../components/timeline/body/renderers';
|
||||
import { NotFoundPage } from '../404';
|
||||
import { Hosts } from '../hosts';
|
||||
import { Network } from '../network';
|
||||
import { Overview } from '../overview';
|
||||
|
||||
const onColumnSorted: OnColumnSorted = sorted => {
|
||||
alert(`column sorted: ${JSON.stringify(sorted)}`);
|
||||
};
|
||||
|
||||
const onDataProviderRemoved: OnDataProviderRemoved = dataProvider => {
|
||||
alert(`data provider removed: ${JSON.stringify(dataProvider)}`);
|
||||
};
|
||||
|
||||
const onRangeSelected: OnRangeSelected = range => {
|
||||
alert(`range selected: ${range}`);
|
||||
};
|
||||
|
||||
const onFilterChange: OnFilterChange = filter => {
|
||||
alert(`filter changed: ${JSON.stringify(filter)}`);
|
||||
};
|
||||
|
||||
const sort: Sort = {
|
||||
columnId: 'timestamp',
|
||||
sortDirection: 'descending',
|
||||
};
|
||||
|
||||
const maxTimelineWidth = 1125;
|
||||
|
||||
export const HomePage = pure(() => (
|
||||
|
@ -122,19 +89,7 @@ export const HomePage = pure(() => (
|
|||
|
||||
<Pane2 data-test-subj="pane2">
|
||||
<Pane2TimelineContainer data-test-subj="pane2TimelineContainer">
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
data={mockECSData}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={onFilterChange}
|
||||
onRangeSelected={onRangeSelected}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={maxTimelineWidth}
|
||||
/>
|
||||
<StatefulTimeline id="pane2-timeline" headers={headers} width={maxTimelineWidth} />
|
||||
</Pane2TimelineContainer>
|
||||
</Pane2>
|
||||
</SplitPane>
|
||||
|
|
|
@ -9,4 +9,6 @@ import { pure } from 'recompose';
|
|||
|
||||
import { Placeholders } from '../../components/visualization_placeholder';
|
||||
|
||||
export const Hosts = pure(() => <Placeholders count={10} myRoute="Hosts" />);
|
||||
export const Hosts = pure(() => (
|
||||
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Hosts" />
|
||||
));
|
||||
|
|
|
@ -9,4 +9,6 @@ import { pure } from 'recompose';
|
|||
|
||||
import { Placeholders } from '../../components/visualization_placeholder';
|
||||
|
||||
export const Network = pure(() => <Placeholders count={10} myRoute="Network" />);
|
||||
export const Network = pure(() => (
|
||||
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Network" />
|
||||
));
|
||||
|
|
|
@ -9,4 +9,6 @@ import { pure } from 'recompose';
|
|||
|
||||
import { Placeholders } from '../../components/visualization_placeholder';
|
||||
|
||||
export const Overview = pure(() => <Placeholders count={10} myRoute="Overview" />);
|
||||
export const Overview = pure(() => (
|
||||
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Overview" />
|
||||
));
|
||||
|
|
7
x-pack/plugins/secops/public/store/actions.ts
Normal file
7
x-pack/plugins/secops/public/store/actions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { timelineActions } from './local';
|
9
x-pack/plugins/secops/public/store/index.ts
Normal file
9
x-pack/plugins/secops/public/store/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
export * from './reducer';
|
||||
export { createStore } from './store';
|
7
x-pack/plugins/secops/public/store/local/actions.ts
Normal file
7
x-pack/plugins/secops/public/store/local/actions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { timelineActions } from './timeline';
|
8
x-pack/plugins/secops/public/store/local/index.ts
Normal file
8
x-pack/plugins/secops/public/store/local/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
export * from './reducer';
|
21
x-pack/plugins/secops/public/store/local/reducer.ts
Normal file
21
x-pack/plugins/secops/public/store/local/reducer.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { initialTimelineState, timelineReducer, TimelineState } from './timeline';
|
||||
|
||||
export interface LocalState {
|
||||
timeline: TimelineState;
|
||||
}
|
||||
|
||||
export const initialLocalState: LocalState = {
|
||||
timeline: initialTimelineState,
|
||||
};
|
||||
|
||||
export const localReducer = combineReducers<LocalState>({
|
||||
timeline: timelineReducer,
|
||||
});
|
29
x-pack/plugins/secops/public/store/local/timeline/actions.ts
Normal file
29
x-pack/plugins/secops/public/store/local/timeline/actions.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { Range } from '../../../components/timeline/body/column_headers/range_picker/ranges';
|
||||
import { Sort } from '../../../components/timeline/body/sort';
|
||||
import { DataProvider } from '../../../components/timeline/data_providers/data_provider';
|
||||
import { ECS } from '../../../components/timeline/ecs';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/secops/local/timeline');
|
||||
|
||||
export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER');
|
||||
|
||||
export const createTimeline = actionCreator<{ id: string }>('CREATE_TIMELINE');
|
||||
|
||||
export const updateData = actionCreator<{ id: string; data: ECS[] }>('UPDATE_DATA');
|
||||
|
||||
export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>(
|
||||
'UPDATE_PROVIDERS'
|
||||
);
|
||||
|
||||
export const updateRange = actionCreator<{ id: string; range: Range }>('UPDATE_RANGE');
|
||||
|
||||
export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT');
|
||||
|
||||
export const removeProvider = actionCreator<{ id: string; providerId: string }>('REMOVE_PROVIDER');
|
10
x-pack/plugins/secops/public/store/local/timeline/index.ts
Normal file
10
x-pack/plugins/secops/public/store/local/timeline/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as timelineActions from './actions';
|
||||
|
||||
export { timelineActions };
|
||||
export * from './reducer';
|
31
x-pack/plugins/secops/public/store/local/timeline/model.ts
Normal file
31
x-pack/plugins/secops/public/store/local/timeline/model.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Range } from '../../../components/timeline/body/column_headers/range_picker/ranges';
|
||||
import { Sort } from '../../../components/timeline/body/sort';
|
||||
import { DataProvider } from '../../../components/timeline/data_providers/data_provider';
|
||||
import { ECS } from '../../../components/timeline/ecs';
|
||||
import { mockECSData } from '../../../pages/mock/mock_ecs';
|
||||
|
||||
export interface TimelineModel {
|
||||
id: string;
|
||||
dataProviders: DataProvider[];
|
||||
data: ECS[];
|
||||
range: Range;
|
||||
sort: Sort;
|
||||
}
|
||||
|
||||
export const timelineDefaults: Readonly<
|
||||
Pick<TimelineModel, 'dataProviders' | 'data' | 'range' | 'sort'>
|
||||
> = {
|
||||
dataProviders: [],
|
||||
data: mockECSData,
|
||||
range: '1 Day',
|
||||
sort: {
|
||||
columnId: 'timestamp',
|
||||
sortDirection: 'descending',
|
||||
},
|
||||
};
|
214
x-pack/plugins/secops/public/store/local/timeline/reducer.ts
Normal file
214
x-pack/plugins/secops/public/store/local/timeline/reducer.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
|
||||
|
||||
import { filter } from 'lodash/fp';
|
||||
import { Range } from '../../../components/timeline/body/column_headers/range_picker/ranges';
|
||||
import { Sort } from '../../../components/timeline/body/sort';
|
||||
import { DataProvider } from '../../../components/timeline/data_providers/data_provider';
|
||||
import { ECS } from '../../../components/timeline/ecs';
|
||||
import {
|
||||
addProvider,
|
||||
createTimeline,
|
||||
removeProvider,
|
||||
updateData,
|
||||
updateProviders,
|
||||
updateRange,
|
||||
updateSort,
|
||||
} from './actions';
|
||||
import { timelineDefaults, TimelineModel } from './model';
|
||||
|
||||
/** A map of id to timeline */
|
||||
export interface TimelineById {
|
||||
[id: string]: TimelineModel;
|
||||
}
|
||||
|
||||
/** The state of all timelines is stored here */
|
||||
export interface TimelineState {
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference
|
||||
|
||||
export const initialTimelineState: TimelineState = {
|
||||
timelineById: EMPTY_TIMELINE_BY_ID,
|
||||
};
|
||||
|
||||
interface AddNewTimelineParams {
|
||||
id: string;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
/** Adds a new `Timeline` to the provided collection of `TimelineById` */
|
||||
const addNewTimeline = ({ id, timelineById }: AddNewTimelineParams): TimelineById => ({
|
||||
...timelineById,
|
||||
[id]: {
|
||||
id,
|
||||
...timelineDefaults,
|
||||
},
|
||||
});
|
||||
|
||||
interface AddTimelineProviderParams {
|
||||
id: string;
|
||||
provider: DataProvider;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const addTimelineProvider = ({
|
||||
id,
|
||||
provider,
|
||||
timelineById,
|
||||
}: AddTimelineProviderParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id);
|
||||
|
||||
const dataProviders =
|
||||
alreadyExistsAtIndex > -1
|
||||
? [
|
||||
...timeline.dataProviders.slice(0, alreadyExistsAtIndex),
|
||||
provider,
|
||||
...timeline.dataProviders.slice(alreadyExistsAtIndex + 1),
|
||||
]
|
||||
: [...timeline.dataProviders, provider];
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
dataProviders,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface UpdateTimelineDataParams {
|
||||
id: string;
|
||||
data: ECS[];
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const updateTimelineData = ({ id, data, timelineById }: UpdateTimelineDataParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
data,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface UpdateTimelineProvidersParams {
|
||||
id: string;
|
||||
providers: DataProvider[];
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const updateTimelineProviders = ({
|
||||
id,
|
||||
providers,
|
||||
timelineById,
|
||||
}: UpdateTimelineProvidersParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
dataProviders: providers,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface UpdateTimelineRangeParams {
|
||||
id: string;
|
||||
range: Range;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const updateTimelineRange = ({
|
||||
id,
|
||||
range,
|
||||
timelineById,
|
||||
}: UpdateTimelineRangeParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
range,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface UpdateTimelineSortParams {
|
||||
id: string;
|
||||
sort: Sort;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const updateTimelineSort = ({ id, sort, timelineById }: UpdateTimelineSortParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
sort,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface RemoveTimelineProviderParams {
|
||||
id: string;
|
||||
providerId: string;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
const removeTimelineProvider = ({
|
||||
id,
|
||||
providerId,
|
||||
timelineById,
|
||||
}: RemoveTimelineProviderParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
dataProviders: filter(p => p.id !== providerId, timeline.dataProviders),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** The reducer for all timeline actions */
|
||||
export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
||||
.case(createTimeline, (state, { id }) => ({
|
||||
...state,
|
||||
timelineById: addNewTimeline({ id, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(addProvider, (state, { id, provider }) => ({
|
||||
...state,
|
||||
timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(removeProvider, (state, { id, providerId }) => ({
|
||||
...state,
|
||||
timelineById: removeTimelineProvider({ id, providerId, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(updateData, (state, { id, data }) => ({
|
||||
...state,
|
||||
timelineById: updateTimelineData({ id, data, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(updateProviders, (state, { id, providers }) => ({
|
||||
...state,
|
||||
timelineById: updateTimelineProviders({ id, providers, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(updateRange, (state, { id, range }) => ({
|
||||
...state,
|
||||
timelineById: updateTimelineRange({ id, range, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(updateSort, (state, { id, sort }) => ({
|
||||
...state,
|
||||
timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }),
|
||||
}))
|
||||
.build();
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { State } from '../../reducer';
|
||||
import { TimelineById } from './reducer';
|
||||
|
||||
const selectTimelineById = (state: State): TimelineById => state.local.timeline.timelineById;
|
||||
|
||||
export const timelineByIdSelector = createSelector(
|
||||
selectTimelineById,
|
||||
timelineById => timelineById
|
||||
);
|
21
x-pack/plugins/secops/public/store/reducer.ts
Normal file
21
x-pack/plugins/secops/public/store/reducer.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { initialLocalState, localReducer, LocalState } from './local';
|
||||
|
||||
export interface State {
|
||||
local: LocalState;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
local: initialLocalState,
|
||||
};
|
||||
|
||||
export const reducer = combineReducers<State>({
|
||||
local: localReducer,
|
||||
});
|
7
x-pack/plugins/secops/public/store/selectors.ts
Normal file
7
x-pack/plugins/secops/public/store/selectors.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { timelineByIdSelector } from './local/timeline/selectors';
|
22
x-pack/plugins/secops/public/store/store.ts
Normal file
22
x-pack/plugins/secops/public/store/store.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnyAction, applyMiddleware, compose, createStore as createReduxStore, Store } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { initialState, reducer, State } from '.';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
|
||||
}
|
||||
}
|
||||
|
||||
export const createStore = (): Store<State, AnyAction> => {
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
return createReduxStore(reducer, initialState, composeEnhancers(applyMiddleware(thunk)));
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue