test(ui): component integration testing w/ mock redux store (#11577)

* wip: introduce connected async component test

* test(query-builder): mock findBuckets

* test(ui): add ability to pass arbitrary state to renderWithRedux

* test(ui/queryBuilder): click bucket and tag

* test(ui/queryBuilder): use regex matchers to query all

* test(ui/queryBuilder): it can click a tag value

* chore: change id to ID

* chore: move QueryBuilderFetcher

* test(ui/queryBuilder): try waiting for dropdown-items

* test(ui/queryBuilder): move initial state out of beforeEach

* test(ui/queryBuilder): get by bucket name

* wip

* test: put back to default

* test: remove await
pull/11623/head
Andrew Watkins 2019-01-25 14:08:57 -08:00 committed by GitHub
parent c72363c7ec
commit 854b2a43eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 394 additions and 155 deletions

6
ui/jestSetup.ts Normal file
View File

@ -0,0 +1,6 @@
import {cleanup} from 'react-testing-library'
// cleans up state between react-testing-library tests
afterEach(() => {
cleanup()
})

View File

@ -1,11 +1,5 @@
import {
Source,
SourceAuthenticationMethod,
Template,
SourceLinks,
TemplateType,
TemplateValueType,
} from 'src/types'
import {Template, SourceLinks, TemplateType, TemplateValueType} from 'src/types'
import {Source} from 'src/api'
import {Cell, Dashboard, Label} from 'src/types/v2'
import {Links} from 'src/types/v2/links'
import {Task} from 'src/types/v2/tasks'
@ -123,14 +117,12 @@ export const sourceLinks: SourceLinks = {
export const source: Source = {
id: '16',
name: 'ssl',
type: 'influx',
type: Source.TypeEnum.Self,
username: 'admin',
url: 'https://localhost:9086',
insecureSkipVerify: true,
default: false,
telegraf: 'telegraf',
links: sourceLinks,
authentication: SourceAuthenticationMethod.Basic,
}
export const timeRange = {
@ -583,7 +575,11 @@ export const setSetupParamsResponse = {
userID: '033bc62520fe3000',
user: 'iris',
permissions: [
{action: 'read', resource: 'authorizations', orgID: '033bc62534be3000'},
{
action: 'read',
resource: 'authorizations',
orgID: '033bc62534be3000',
},
{
action: 'write',
resource: 'authorizations',

33
ui/package-lock.json generated
View File

@ -902,6 +902,12 @@
"physical-cpu-count": "^2.0.0"
}
},
"@sheerun/mutationobserver-shim": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz",
"integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==",
"dev": true
},
"@types/abstract-leveldown": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz",
@ -3738,6 +3744,18 @@
}
}
},
"dom-testing-library": {
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.16.4.tgz",
"integrity": "sha512-D8tFLGe0xInL8F/KxZM7gc4r/vOCTgFGM93zXLB/AjFPz2O86y0UaruXl45K6xhqyclJFHHxUtgwaRddRyqxFw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.1.5",
"@sheerun/mutationobserver-shim": "^0.3.2",
"pretty-format": "^23.6.0",
"wait-for-expect": "^1.1.0"
}
},
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@ -10629,6 +10647,15 @@
"schedule": "^0.5.0"
}
},
"react-testing-library": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.4.4.tgz",
"integrity": "sha512-/TiERZ+URSNhZQfjrUXh0VLsiLSmhqP1WP+2e2wWqWqrRIWpcAxrfuBxzlT75LYMDNmicEikaXJqRDi/pqCEDg==",
"dev": true,
"requires": {
"dom-testing-library": "^3.13.1"
}
},
"react-tooltip": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.8.4.tgz",
@ -13000,6 +13027,12 @@
"browser-process-hrtime": "^0.1.2"
}
},
"wait-for-expect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.0.tgz",
"integrity": "sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==",
"dev": true
},
"walker": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",

View File

@ -30,6 +30,7 @@
"tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w"
},
"jest": {
"setupTestFrameworkScriptFile": "./jestSetup.ts",
"displayName": "test",
"testURL": "http://localhost",
"testPathIgnorePatterns": [
@ -104,6 +105,7 @@
"jsdom": "^9.0.0",
"parcel": "^1.11.0",
"prettier": "^1.14.3",
"react-testing-library": "^5.4.4",
"sass": "^1.15.3",
"ts-jest": "^23.10.3",
"tslib": "^1.9.0",

View File

@ -43,6 +43,8 @@ export interface Props {
mode?: DropdownMode
titleText?: string
menuHeader?: JSX.Element
testID: string
buttonTestID: string
}
interface State {
@ -122,6 +124,7 @@ class Dropdown extends Component<Props, State> {
icon,
children,
titleText,
buttonTestID,
} = this.props
const {expanded} = this.state
@ -147,6 +150,7 @@ class Dropdown extends Component<Props, State> {
onClick={this.toggleMenu}
status={status}
title={titleText}
testID={buttonTestID}
>
{dropdownLabel}
</DropdownButton>
@ -160,6 +164,7 @@ class Dropdown extends Component<Props, State> {
menuHeader,
menuColor,
children,
testID,
} = this.props
const {expanded} = this.state
@ -174,7 +179,10 @@ class Dropdown extends Component<Props, State> {
autoHeight={true}
maxHeight={maxMenuHeight}
>
<div className="dropdown--menu">
<div
className="dropdown--menu"
data-testid={`dropdown--menu ${testID}`}
>
{menuHeader && menuHeader}
{React.Children.map(children, (child: JSX.Element) => {
if (this.childTypeIsValid(child)) {

View File

@ -23,6 +23,7 @@ interface Props {
size?: ComponentSize
icon?: IconFont
title?: string
testID: string
}
@ErrorHandling
@ -35,7 +36,7 @@ class DropdownButton extends Component<Props> {
}
public render() {
const {onClick, children, title} = this.props
const {onClick, children, title, testID} = this.props
return (
<button
className={this.classname}
@ -43,6 +44,7 @@ class DropdownButton extends Component<Props> {
disabled={this.isDisabled}
title={title}
type={ButtonType.Button}
data-testid={testID}
>
{this.icon}
<span className="dropdown--selected">{children}</span>

View File

@ -14,6 +14,7 @@ interface Props {
selected?: boolean
checkbox?: boolean
onClick?: (value: any) => void
testid?: string
}
@ErrorHandling
@ -24,7 +25,7 @@ class DropdownItem extends Component<Props> {
}
public render(): JSX.Element {
const {selected, checkbox} = this.props
const {selected, checkbox, id} = this.props
return (
<div
@ -33,6 +34,7 @@ class DropdownItem extends Component<Props> {
active: selected && !checkbox,
'multi-select--item': checkbox,
})}
data-testid={`dropdown--item ${id}`}
onClick={this.handleClick}
>
{this.checkBox}

View File

@ -11,7 +11,7 @@ import {setActiveTimeMachine} from 'src/timeMachine/actions'
// Utils
import {DE_TIME_MACHINE_ID} from 'src/timeMachine/constants'
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
// Styles
import './DataExplorer.scss'

41
ui/src/mockState.tsx Normal file
View File

@ -0,0 +1,41 @@
import React from 'react'
import {Provider} from 'react-redux'
import {render} from 'react-testing-library'
import configureStore from 'src/store/configureStore'
import {createMemoryHistory} from 'history'
const localState = {
app: {
ephemeral: {
inPresentationMode: false,
},
persisted: {autoRefresh: 0, showTemplateControlBar: false},
},
VERSION: '2.0.0',
ranges: [
{
dashboardID: '0349ecda531ea000',
seconds: 900,
lower: 'now() - 15m',
upper: null,
label: 'Past 15m',
duration: '15m',
},
],
}
const history = createMemoryHistory({entries: ['/']})
export function renderWithRedux(ui, initialState = s => s) {
const seedStore = configureStore(localState, history)
const seedState = seedStore.getState()
const store = configureStore(initialState(seedState), history)
const provider = <Provider store={store}>{ui}</Provider>
return {
...render(provider),
store,
}
}

View File

@ -28,7 +28,7 @@ export default class SearchableDropdown extends Component<Props> {
}
public render() {
const {searchTerm, searchPlaceholder, buttonSize} = this.props
const {searchTerm, searchPlaceholder, buttonSize, testID} = this.props
const dropdownProps = omit(this.props, [
'searchTerm',
@ -50,6 +50,7 @@ export default class SearchableDropdown extends Component<Props> {
autoFocus={true}
/>
}
testID={testID}
/>
)
}

View File

@ -28,12 +28,9 @@ import protosReducer from 'src/protos/reducers'
import {LocalStorage} from 'src/types/localStorage'
import {AppState} from 'src/types/v2'
type ReducerState = Pick<
AppState,
Exclude<keyof AppState, 'VERSION' | 'timeRange'>
>
type ReducerState = Pick<AppState, Exclude<keyof AppState, 'timeRange'>>
const rootReducer = combineReducers<ReducerState>({
export const rootReducer = combineReducers<ReducerState>({
...sharedReducers,
ranges: rangesReducer,
dashboards: dashboardsReducer,
@ -49,6 +46,7 @@ const rootReducer = combineReducers<ReducerState>({
noteEditor: noteEditorReducer,
dataLoading: dataLoadingReducer,
protos: protosReducer,
VERSION: () => '',
})
const composeEnhancers =

View File

@ -1,5 +1,5 @@
// APIs
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
// Utils
import {
@ -47,7 +47,9 @@ interface SetBuilderBucketsAction {
payload: {buckets: string[]}
}
const setBuilderBuckets = (buckets: string[]): SetBuilderBucketsAction => ({
export const setBuilderBuckets = (
buckets: string[]
): SetBuilderBucketsAction => ({
type: 'SET_BUILDER_BUCKETS',
payload: {buckets},
})

View File

@ -0,0 +1,127 @@
// APIs
import {
findBuckets,
findKeys,
findValues,
} from 'src/timeMachine/apis/queryBuilder'
// Types
import {BuilderConfig} from 'src/types/v2'
import {WrappedCancelablePromise} from 'src/types/promises'
type CancelableQuery = WrappedCancelablePromise<string[]>
class QueryBuilderFetcher {
private findBucketsQuery: CancelableQuery
private findKeysQueries: CancelableQuery[] = []
private findValuesQueries: CancelableQuery[] = []
private findKeysCache: {[key: string]: string[]} = {}
private findValuesCache: {[key: string]: string[]} = {}
private findBucketsCache: {[key: string]: string[]} = {}
public async findBuckets(url: string): Promise<string[]> {
this.cancelFindBuckets()
const cacheKey = JSON.stringify([...arguments])
const cachedResult = this.findBucketsCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findBuckets(url)
pendingResult.promise.then(result => {
this.findBucketsCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindBuckets(): void {
if (this.findBucketsQuery) {
this.findBucketsQuery.cancel()
}
}
public async findKeys(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = ''
): Promise<string[]> {
this.cancelFindKeys(index)
const cacheKey = JSON.stringify([...arguments].slice(1))
const cachedResult = this.findKeysCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findKeys(url, bucket, tagsSelections, searchTerm)
this.findKeysQueries[index] = pendingResult
pendingResult.promise.then(result => {
this.findKeysCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindKeys(index: number): void {
if (this.findKeysQueries[index]) {
this.findKeysQueries[index].cancel()
}
}
public async findValues(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
key: string,
searchTerm: string = ''
): Promise<string[]> {
this.cancelFindValues(index)
const cacheKey = JSON.stringify([...arguments].slice(1))
const cachedResult = this.findValuesCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findValues(
url,
bucket,
tagsSelections,
key,
searchTerm
)
this.findValuesQueries[index] = pendingResult
pendingResult.promise.then(result => {
this.findValuesCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindValues(index: number): void {
if (this.findValuesQueries[index]) {
this.findValuesQueries[index].cancel()
}
}
public clearCache(): void {
this.findBucketsCache = {}
this.findKeysCache = {}
this.findValuesCache = {}
}
}
export const queryBuilderFetcher = new QueryBuilderFetcher()

View File

@ -0,0 +1,14 @@
export const findBuckets = (_: string) => ({
promise: Promise.resolve(['b1', 'b2']),
cancel: () => {},
})
export const findKeys = (_: string) => ({
promise: Promise.resolve(['tk1', 'tk2']),
cancel: () => {},
})
export const findValues = (_: string) => ({
promise: Promise.resolve(['tv1', 'tv2']),
cancel: () => {},
})

View File

@ -14,7 +14,7 @@ export const LIMIT = 200
type CancelableQuery = WrappedCancelablePromise<string[]>
function findBuckets(url: string): CancelableQuery {
export function findBuckets(url: string): CancelableQuery {
const query = `buckets()
|> sort(columns: ["name"])
|> limit(n: ${LIMIT})`
@ -27,7 +27,7 @@ function findBuckets(url: string): CancelableQuery {
}
}
function findKeys(
export function findKeys(
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
@ -57,7 +57,7 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT
}
}
function findValues(
export function findValues(
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
@ -81,7 +81,10 @@ v1.tagValues(bucket: "${bucket}", tag: "${key}", predicate: ${tagFilters}, start
}
}
function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
export function extractCol(
resp: ExecuteFluxQueryResult,
colName: string
): string[] {
const tables = parseResponse(resp.csv)
const data = get(tables, '0.data', [])
@ -104,7 +107,9 @@ function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
return colValues
}
function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
export function formatTagFilterPredicate(
tagsSelections: BuilderConfig['tags']
) {
const validSelections = tagsSelections.filter(
({key, values}) => key && values.length
)
@ -124,7 +129,7 @@ function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
return `(r) => ${calls}`
}
function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
export function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
const keys = tagsSelections.map(({key}) => key)
if (!keys.length) {
@ -136,125 +141,10 @@ function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
return `\n |> filter(fn: (r) => ${fnBody})`
}
function formatSearchFilterCall(searchTerm: string) {
export function formatSearchFilterCall(searchTerm: string) {
if (!searchTerm) {
return ''
}
return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)`
}
class QueryBuilderFetcher {
private findBucketsQuery: CancelableQuery
private findKeysQueries: CancelableQuery[] = []
private findValuesQueries: CancelableQuery[] = []
private findKeysCache: {[key: string]: string[]} = {}
private findValuesCache: {[key: string]: string[]} = {}
private findBucketsCache: {[key: string]: string[]} = {}
public async findBuckets(url: string): Promise<string[]> {
this.cancelFindBuckets()
const cacheKey = JSON.stringify([...arguments])
const cachedResult = this.findBucketsCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findBuckets(url)
pendingResult.promise.then(result => {
this.findBucketsCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindBuckets(): void {
if (this.findBucketsQuery) {
this.findBucketsQuery.cancel()
}
}
public async findKeys(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = ''
): Promise<string[]> {
this.cancelFindKeys(index)
const cacheKey = JSON.stringify([...arguments].slice(1))
const cachedResult = this.findKeysCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findKeys(url, bucket, tagsSelections, searchTerm)
this.findKeysQueries[index] = pendingResult
pendingResult.promise.then(result => {
this.findKeysCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindKeys(index: number): void {
if (this.findKeysQueries[index]) {
this.findKeysQueries[index].cancel()
}
}
public async findValues(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
key: string,
searchTerm: string = ''
): Promise<string[]> {
this.cancelFindValues(index)
const cacheKey = JSON.stringify([...arguments].slice(1))
const cachedResult = this.findValuesCache[cacheKey]
if (cachedResult) {
return Promise.resolve(cachedResult)
}
const pendingResult = findValues(
url,
bucket,
tagsSelections,
key,
searchTerm
)
this.findValuesQueries[index] = pendingResult
pendingResult.promise.then(result => {
this.findValuesCache[cacheKey] = result
})
return pendingResult.promise
}
public cancelFindValues(index: number): void {
if (this.findValuesQueries[index]) {
this.findValuesQueries[index].cancel()
}
}
public clearCache(): void {
this.findBucketsCache = {}
this.findKeysCache = {}
this.findValuesCache = {}
}
}
export const queryBuilderFetcher = new QueryBuilderFetcher()

View File

@ -143,6 +143,7 @@ const mstp = (state: AppState) => {
const {draftQueries, timeRange, isViewingRawData} = getActiveTimeMachine(
state
)
const activeQuery = getActiveQuery(state)
return {timeRange, activeQuery, draftQueries, isViewingRawData}

View File

@ -0,0 +1,94 @@
import React from 'react'
import {renderWithRedux} from 'src/mockState'
import {source} from 'mocks/dummyData'
import {waitForElement, fireEvent} from 'react-testing-library'
import QueryBuilder from 'src/timeMachine/components/QueryBuilder'
jest.mock('src/timeMachine/apis/queryBuilder')
const setInitialState = state => {
return {
...state,
sources: {
activeSourceID: source.id,
sources: {
[source.id]: source,
},
},
}
}
describe('QueryBuilder', () => {
it('can select a bucket', async () => {
const {
getByTestId,
getAllByTestId,
queryAllByTestId,
getByText,
} = renderWithRedux(<QueryBuilder />, setInitialState)
const bucketsDropdownClosed = await waitForElement(() => getByText('b1'))
fireEvent.click(bucketsDropdownClosed)
const bucketItems = getAllByTestId(/dropdown--item/)
expect(bucketItems.length).toBe(2)
const b2 = getByTestId('dropdown--item b2')
fireEvent.click(b2)
const closedDropdown = await waitForElement(() =>
getByTestId('buckets--button')
)
expect(closedDropdown.textContent).toBe('b2')
expect(queryAllByTestId(/dropdown--item/).length).toBe(0)
})
it('can select a tag', async () => {
const {
getByText,
getByTestId,
getAllByTestId,
queryAllByTestId,
} = renderWithRedux(<QueryBuilder />, setInitialState)
let keysButton = await waitForElement(() => getByText('tk1'))
fireEvent.click(keysButton)
const keyMenuItems = getAllByTestId(/dropdown--item/)
expect(keyMenuItems.length).toBe(2)
const tk2 = getByTestId('dropdown--item tk2')
fireEvent.click(tk2)
keysButton = await waitForElement(() =>
getByTestId('tag-selector--dropdown-button')
)
expect(keysButton.innerHTML.includes('tk2')).toBe(true)
expect(queryAllByTestId(/dropdown--item/).length).toBe(0)
})
it('can select a tag value', async () => {
const {getByText, getByTestId, queryAllByTestId} = renderWithRedux(
<QueryBuilder />,
setInitialState
)
const tagValue = await waitForElement(() => getByText('tv1'))
fireEvent.click(tagValue)
await waitForElement(() => getByTestId('tag-selector--container 1'))
expect(queryAllByTestId(/tag-selector--container/).length).toBe(2)
})
})

View File

@ -46,7 +46,7 @@ class TimeMachineQueryBuilder extends PureComponent<Props, State> {
const {tagFiltersLength} = this.props
return (
<div className="query-builder">
<div className="query-builder" data-testid="query-builder">
<div className="query-builder--buttons">
<Form.Element label="Bucket">
<QueryBuilderBucketDropdown />

View File

@ -39,6 +39,8 @@ const QueryBuilderBucketDropdown: SFC<Props> = props => {
onChange={bucket => onSelectBucket(bucket, true)}
buttonSize={ComponentSize.Small}
status={toComponentStatus(bucketsStatus)}
testID="buckets"
buttonTestID="buckets--button"
>
{buckets.map(bucket => (
<Dropdown.Item key={bucket} id={bucket} value={bucket}>

View File

@ -117,7 +117,10 @@ class TagSelector extends PureComponent<Props> {
return (
<>
<div className="tag-selector--top">
<div
className="tag-selector--top"
data-testid={`tag-selector--container ${index}`}
>
<SearchableDropdown
searchTerm={keysSearchTerm}
searchPlaceholder="Search keys..."
@ -126,6 +129,8 @@ class TagSelector extends PureComponent<Props> {
onChange={this.handleSelectTag}
status={toComponentStatus(keysStatus)}
titleText="No Tags Found"
testID="tag-selector--dropdown"
buttonTestID="tag-selector--dropdown-button"
>
{keys.map(key => (
<Dropdown.Item key={key} id={key} value={key}>

View File

@ -72,7 +72,16 @@ export const initialStateHelper = (): TimeMachineState => ({
queryBuilder: {
buckets: [],
bucketsStatus: RemoteDataState.NotStarted,
tags: [],
tags: [
{
valuesSearchTerm: '',
keysSearchTerm: '',
keys: [],
keysStatus: RemoteDataState.NotStarted,
values: [],
valuesStatus: RemoteDataState.NotStarted,
},
],
},
})
@ -128,13 +137,15 @@ export const timeMachinesReducer = (
const newActiveTimeMachine = timeMachineReducer(activeTimeMachine, action)
return {
const s = {
...state,
timeMachines: {
...timeMachines,
[activeTimeMachineID]: newActiveTimeMachine,
},
}
return s
}
export const timeMachineReducer = (
@ -667,7 +678,10 @@ const setViewProperties = (
const view: any = state.view
const properties = view.properties
return {...state, view: {...view, properties: {...properties, ...update}}}
return {
...state,
view: {...view, properties: {...properties, ...update}},
}
}
const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
@ -680,7 +694,10 @@ const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
...state,
view: {
...view,
properties: {...properties, axes: {...axes, y: {...yAxis, ...update}}},
properties: {
...properties,
axes: {...axes, y: {...yAxis, ...update}},
},
},
}
}

View File

@ -1,9 +1,7 @@
import {TimeRange} from 'src/types'
import {AppState} from 'src/shared/reducers/app'
export interface LocalStorage {
VERSION: string
app: AppState
ranges: []
timeRange: TimeRange
ranges: any[]
}