-
Notifications
You must be signed in to change notification settings - Fork 621
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(webapp): Issue when comparison / diff timelines are out of range (…
…#1615) * feat: sync timeline
- Loading branch information
1 parent
6edb97b
commit 211ccca
Showing
14 changed files
with
456 additions
and
50 deletions.
There are no files selected for viewing
148 changes: 148 additions & 0 deletions
148
webapp/javascript/components/TimelineChart/SyncTimelines/SyncTimelines.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import React from 'react'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import SyncTimelines from './index'; | ||
import { getTitle, getSelectionBoundaries } from './useSync'; | ||
import { Selection } from '../markings'; | ||
|
||
const from = 1666790156; | ||
const to = 1666791905; | ||
|
||
const propsWhenActive = { | ||
timeline: { | ||
color: 'rgb(208, 102, 212)', | ||
data: { | ||
startTime: 1666790760, | ||
samples: [ | ||
16629, 50854, 14454, 3819, 40720, 23172, 22483, 7854, 33186, 81804, | ||
46942, 40631, 14135, 12824, 27514, 14366, 39691, 45412, 18631, 10371, | ||
31606, 53775, 42399, 40527, 20599, 27836, 23624, 80152, 9149, 45283, | ||
58361, 48738, 30363, 13834, 30849, 81892, | ||
], | ||
durationDelta: 10, | ||
}, | ||
}, | ||
leftSelection: { | ||
from: String(from), | ||
to: '1666790783', | ||
}, | ||
rightSelection: { | ||
from: '1666791459', | ||
to: String(to), | ||
}, | ||
}; | ||
|
||
const propsWhenHidden = { | ||
timeline: { | ||
data: { | ||
startTime: 1666779070, | ||
samples: [ | ||
1601, 30312, 22044, 53925, 44264, 26014, 15645, 14376, 21880, 8555, | ||
15995, 5849, 14138, 18929, 41842, 59101, 18931, 65541, 47674, 35886, | ||
55583, 19283, 19745, 9314, 1531, | ||
], | ||
durationDelta: 10, | ||
}, | ||
}, | ||
leftSelection: { | ||
from: '1666779093', | ||
to: '1666779239', | ||
}, | ||
rightSelection: { | ||
from: '1666779140', | ||
to: '1666779296', | ||
}, | ||
}; | ||
|
||
const { getByRole, queryByText } = screen; | ||
|
||
describe('SyncTimelines', () => { | ||
it('renders sync and ignore buttons when active', async () => { | ||
render(<SyncTimelines onSync={() => {}} {...propsWhenActive} />); | ||
|
||
expect(getByRole('button', { name: 'Ignore' })).toBeInTheDocument(); | ||
expect(getByRole('button', { name: 'Sync Timelines' })).toBeInTheDocument(); | ||
}); | ||
|
||
it('hidden when selections are in range', async () => { | ||
render(<SyncTimelines onSync={() => {}} {...propsWhenHidden} />); | ||
|
||
expect(queryByText('Sync')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('onSync returns correct from/to', async () => { | ||
let result = { from: '', to: '' }; | ||
render( | ||
<SyncTimelines | ||
{...propsWhenActive} | ||
onSync={(from, to) => { | ||
result = { from, to }; | ||
}} | ||
/> | ||
); | ||
|
||
fireEvent.click(getByRole('button', { name: 'Sync Timelines' })); | ||
|
||
// new main timeline FROM = from - 1ms, TO = to + 1ms | ||
expect(Number(result.from) - from * 1000).toEqual(-1); | ||
expect(Number(result.to) - to * 1000).toEqual(1); | ||
}); | ||
|
||
it('Hide button works', async () => { | ||
render(<SyncTimelines onSync={() => {}} {...propsWhenActive} />); | ||
|
||
fireEvent.click(getByRole('button', { name: 'Ignore' })); | ||
|
||
expect(queryByText('Sync')).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('getTitle', () => { | ||
it('both selections are out of range', () => { | ||
expect(getTitle(false, false)).toEqual( | ||
'Warning: Baseline and Comparison timeline selections are out of range' | ||
); | ||
}); | ||
it('baseline timeline selection is out of range', () => { | ||
expect(getTitle(false, true)).toEqual( | ||
'Warning: Baseline timeline selection is out of range' | ||
); | ||
}); | ||
it('comparison timeline selection is out of range', () => { | ||
expect(getTitle(true, false)).toEqual( | ||
'Warning: Comparison timeline selection is out of range' | ||
); | ||
}); | ||
}); | ||
|
||
describe('getSelectionBoundaries', () => { | ||
const boundariesFromRelativeTime = getSelectionBoundaries({ | ||
from: 'now-1h', | ||
to: 'now', | ||
} as Selection); | ||
const boundariesFromUnixTime = getSelectionBoundaries({ | ||
from: '1667204605', | ||
to: '1667204867', | ||
} as Selection); | ||
|
||
const res = [ | ||
boundariesFromRelativeTime.from, | ||
boundariesFromRelativeTime.to, | ||
boundariesFromUnixTime.from, | ||
boundariesFromUnixTime.to, | ||
]; | ||
|
||
it('returns correct data type', () => { | ||
expect(res.every((i) => typeof i === 'number')).toBe(true); | ||
}); | ||
|
||
it('returns ms format (13 digits)', () => { | ||
expect(res.every((i) => String(i).length === 13)).toBe(true); | ||
}); | ||
|
||
it('TO greater than FROM', () => { | ||
expect( | ||
boundariesFromRelativeTime.to > boundariesFromRelativeTime.from && | ||
boundariesFromUnixTime.to > boundariesFromUnixTime.from | ||
).toBe(true); | ||
}); | ||
}); |
64 changes: 64 additions & 0 deletions
64
webapp/javascript/components/TimelineChart/SyncTimelines/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react'; | ||
import Button from '@webapp/ui/Button'; | ||
import { TimelineData } from '@webapp/components/TimelineChart/TimelineChartWrapper'; | ||
import StatusMessage from '@webapp/ui/StatusMessage'; | ||
import { useSync } from './useSync'; | ||
import styles from './styles.module.scss'; | ||
|
||
interface SyncTimelinesProps { | ||
timeline: TimelineData; | ||
leftSelection: { | ||
from: string; | ||
to: string; | ||
}; | ||
rightSelection: { | ||
from: string; | ||
to: string; | ||
}; | ||
onSync: (from: string, until: string) => void; | ||
} | ||
|
||
function SyncTimelines({ | ||
timeline, | ||
leftSelection, | ||
rightSelection, | ||
onSync, | ||
}: SyncTimelinesProps) { | ||
const { isWarningHidden, onIgnore, title, onSyncClick } = useSync({ | ||
timeline, | ||
leftSelection, | ||
rightSelection, | ||
onSync, | ||
}); | ||
|
||
if (isWarningHidden) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<StatusMessage | ||
type="warning" | ||
message={title} | ||
action={ | ||
<div className={styles.buttons}> | ||
<Button | ||
kind="outline" | ||
onClick={onIgnore} | ||
className={styles.ignoreButton} | ||
> | ||
Ignore | ||
</Button> | ||
<Button | ||
kind="outline" | ||
onClick={onSyncClick} | ||
className={styles.syncButton} | ||
> | ||
Sync Timelines | ||
</Button> | ||
</div> | ||
} | ||
/> | ||
); | ||
} | ||
|
||
export default SyncTimelines; |
14 changes: 14 additions & 0 deletions
14
webapp/javascript/components/TimelineChart/SyncTimelines/styles.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.buttons { | ||
display: flex; | ||
align-items: center; | ||
flex-direction: row; | ||
} | ||
|
||
.syncButton { | ||
margin-left: 0.5em; | ||
font-weight: normal; | ||
} | ||
|
||
.ignoreButton { | ||
@extend .syncButton; | ||
} |
119 changes: 119 additions & 0 deletions
119
webapp/javascript/components/TimelineChart/SyncTimelines/useSync.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { centerTimelineData } from '@webapp/components/TimelineChart/centerTimelineData'; | ||
import { TimelineData } from '@webapp/components/TimelineChart/TimelineChartWrapper'; | ||
import { formatAsOBject } from '@webapp/util/formatDate'; | ||
import { Selection } from '../markings'; | ||
|
||
interface UseSyncParams { | ||
timeline: TimelineData; | ||
leftSelection: { | ||
from: string; | ||
to: string; | ||
}; | ||
rightSelection: { | ||
from: string; | ||
to: string; | ||
}; | ||
onSync: (from: string, until: string) => void; | ||
} | ||
|
||
const timeOffset = 5 * 60 * 1000; | ||
const selectionOffset = 5000; | ||
|
||
export const getTitle = (leftInRange: boolean, rightInRange: boolean) => { | ||
if (!leftInRange && !rightInRange) { | ||
return 'Warning: Baseline and Comparison timeline selections are out of range'; | ||
} | ||
if (!rightInRange) { | ||
return 'Warning: Comparison timeline selection is out of range'; | ||
} | ||
return 'Warning: Baseline timeline selection is out of range'; | ||
}; | ||
|
||
const isInRange = ( | ||
from: number, | ||
to: number, | ||
selectionFrom: number, | ||
selectionTo: number | ||
) => { | ||
return selectionFrom + timeOffset >= from && selectionTo - timeOffset <= to; | ||
}; | ||
|
||
export const getSelectionBoundaries = (selection: Selection) => { | ||
if (selection.from.startsWith('now') || selection.to.startsWith('now')) { | ||
return { | ||
from: new Date(formatAsOBject(selection.from)).getTime(), | ||
to: new Date(formatAsOBject(selection.to)).getTime(), | ||
}; | ||
} | ||
|
||
return { | ||
from: Number(selection.from) * 1000, | ||
to: Number(selection.to) * 1000, | ||
}; | ||
}; | ||
|
||
export function useSync({ | ||
timeline, | ||
leftSelection, | ||
rightSelection, | ||
onSync, | ||
}: UseSyncParams) { | ||
const [isIgnoring, setIgnoring] = useState(false); | ||
|
||
useEffect(() => { | ||
if (isIgnoring) { | ||
setIgnoring(false); | ||
} | ||
}, [leftSelection, rightSelection, timeline]); | ||
|
||
const { from: leftFrom, to: leftTo } = getSelectionBoundaries( | ||
leftSelection as Selection | ||
); | ||
|
||
const { from: rightFrom, to: rightTo } = getSelectionBoundaries( | ||
rightSelection as Selection | ||
); | ||
|
||
const centeredData = centerTimelineData(timeline); | ||
|
||
const timelineFrom = centeredData?.[0]?.[0]; | ||
const timelineTo = centeredData?.[centeredData?.length - 1]?.[0]; | ||
|
||
const isLeftInRange = isInRange(timelineFrom, timelineTo, leftFrom, leftTo); | ||
const isRightInRange = isInRange( | ||
timelineFrom, | ||
timelineTo, | ||
rightFrom, | ||
rightTo | ||
); | ||
|
||
const onSyncClick = () => { | ||
const selectionsLimits = [leftFrom, leftTo, rightFrom, rightTo]; | ||
const selectionMin = Math.min(...selectionsLimits); | ||
const selectionMax = Math.max(...selectionsLimits); | ||
// when some of selection is in relative time (now, now-1h etc.), we have to extend detecting time buffer | ||
// 1) to prevent falsy detections | ||
// 2) to workaraund pecularity that when we change selection we don't refetch main timeline | ||
const offset = [ | ||
leftSelection.from, | ||
rightSelection.from, | ||
leftSelection.to, | ||
rightSelection.to, | ||
].some((p) => String(p).startsWith('now')) | ||
? selectionOffset | ||
: 1; | ||
|
||
onSync(String(selectionMin - offset), String(selectionMax + offset)); | ||
}; | ||
|
||
return { | ||
isWarningHidden: | ||
!timeline.data?.samples.length || | ||
(isLeftInRange && isRightInRange) || | ||
isIgnoring, | ||
title: getTitle(isLeftInRange, isRightInRange), | ||
onIgnore: () => setIgnoring(true), | ||
onSyncClick, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.