Skip to content

Commit

Permalink
[frontend] Integrate react-timeline-range-slider (#8816)
Browse files Browse the repository at this point in the history
Co-authored-by: Landry Trebon <[email protected]>
  • Loading branch information
richard-julien and lndrtrbn authored Nov 15, 2024
1 parent 79e6513 commit e91918a
Show file tree
Hide file tree
Showing 19 changed files with 595 additions and 898 deletions.
6 changes: 4 additions & 2 deletions opencti-platform/opencti-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"classnames": "2.5.1",
"convert": "5.5.1",
"d3-hierarchy": "3.1.2",
"d3-scale": "4.0.2",
"d3-timer": "3.0.1",
"dagre": "0.8.5",
"date-fns": "3.6.0",
Expand Down Expand Up @@ -59,6 +60,7 @@
"react": "18.3.1",
"react-apexcharts": "1.4.1",
"react-color": "2.19.3",
"react-compound-slider": "3.4.0",
"react-cookie": "7.2.0",
"react-csv": "2.2.2",
"react-dom": "18.3.1",
Expand All @@ -78,7 +80,6 @@
"react-relay-network-modern": "6.2.2",
"react-router-dom": "6.26.2",
"react-syntax-highlighter": "15.6.1",
"react-timeline-range-slider": "1.4.1",
"react-virtualized": "9.22.5",
"reactflow": "11.11.4",
"recharts": "2.12.7",
Expand All @@ -100,6 +101,7 @@
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/d3-scale": "4.0.8",
"@types/node": "20.17.0",
"@types/qrcode": "1.5.5",
"@types/ramda": "0.30.2",
Expand Down Expand Up @@ -185,14 +187,14 @@
},
"resolutions": {
"cookie": "0.7.0",
"cross-spawn": "7.0.5",
"eslint-plugin-custom-rules": "0.0.1",
"json5": "2.2.3",
"glob-parent": "6.0.2",
"webpack": "5.94.0",
"d3-color": "3.1.0",
"loader-utils": "3.3.1",
"scss-tokenizer": "0.4.3",
"node-sass": "9.0.0",
"react-virtualized": "patch:[email protected]#./patch/react-virtualized-9.22.5.patch"
},
"workspaces": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React from 'react';
import PropTypes from 'prop-types';
import { scaleTime } from 'd3-scale';
import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
import { addHours, addMinutes, differenceInMilliseconds, endOfToday, format, isAfter, isBefore, set, startOfToday } from 'date-fns';

import Track from './components/Track';
import Tick from './components/Tick';
import Handle from './components/Handle';
import RangeSliderRail from './components/SliderRail';

const getTimelineConfig = (timelineStart, timelineLength) => (date) => {
const percent = (differenceInMilliseconds(date, timelineStart) / timelineLength) * 100;
const value = Number(format(date, 'T'));
return { percent, value };
};

const getFormattedBlockedIntervals = (blockedDates, [startTime, endTime]) => {
if (!blockedDates || blockedDates.length === 0) return null;
const timelineLength = differenceInMilliseconds(endTime, startTime);
const getConfig = getTimelineConfig(startTime, timelineLength);
return blockedDates.map((interval, index) => {
let { start, end } = interval;

if (isBefore(start, startTime)) start = startTime;
if (isAfter(end, endTime)) end = endTime;

const source = getConfig(start);
const target = getConfig(end);

return { id: `blocked-track-${index}`, source, target };
});
};

const getNowConfig = ([startTime, endTime]) => {
const timelineLength = differenceInMilliseconds(endTime, startTime);
const getConfig = getTimelineConfig(startTime, timelineLength);

const source = getConfig(new Date());
const target = getConfig(addMinutes(new Date(), 1));

return { id: 'now-track', source, target };
};

class TimeRange extends React.Component {
get disabledIntervals() {
return getFormattedBlockedIntervals(this.props.disabledIntervals, this.props.timelineInterval);
}

get now() {
return getNowConfig(this.props.timelineInterval);
}

onChange = (newTime) => {
const formattedNewTime = newTime.map((t) => new Date(t));
this.props.onChangeCallback(formattedNewTime);
};

// eslint-disable-next-line class-methods-use-this
checkIsSelectedIntervalNotValid = ([start, end], source, target) => {
const { value: startInterval } = source;
const { value: endInterval } = target;

// eslint-disable-next-line no-mixed-operators
if (startInterval > start && endInterval <= end || startInterval >= start && endInterval < end) { return true; }
if (start >= startInterval && end <= endInterval) return true;

const isStartInBlockedInterval = start > startInterval && start < endInterval && end >= endInterval;
const isEndInBlockedInterval = end < endInterval && end > startInterval && start <= startInterval;

return isStartInBlockedInterval || isEndInBlockedInterval;
};

onUpdate = (newTime) => {
const { onUpdateCallback } = this.props;
const { disabledIntervals } = this;

if (disabledIntervals?.length) {
const isValuesNotValid = disabledIntervals.some(({ source, target }) => this.checkIsSelectedIntervalNotValid(newTime, source, target));
const formattedNewTime = newTime.map((t) => new Date(t));
onUpdateCallback({ error: isValuesNotValid, time: formattedNewTime });
return;
}

const formattedNewTime = newTime.map((t) => new Date(t));
onUpdateCallback({ error: false, time: formattedNewTime });
};

getDateTicks = () => {
const { timelineInterval, ticksNumber } = this.props;
return scaleTime().domain(timelineInterval).ticks(ticksNumber).map((t) => +t);
};

render() {
const {
sliderRailClassName,
timelineInterval,
selectedInterval,
containerClassName,
error,
step,
showNow,
formatTick,
mode,
} = this.props;

const domain = timelineInterval.map((t) => Number(t));

const { disabledIntervals } = this;

return (
<div className={containerClassName || 'react_time_range__time_range_container' }>
<Slider
mode={mode}
step={step}
domain={domain}
onUpdate={this.onUpdate}
onChange={this.onChange}
values={selectedInterval.map((t) => +t)}
rootStyle={{ position: 'relative', width: '100%' }}
>
<Rail>
{({ getRailProps }) => <RangeSliderRail className={sliderRailClassName} getRailProps={getRailProps} />}
</Rail>

<Handles>
{({ handles, getHandleProps }) => (
<>
{handles.map((handle) => (
<Handle
error={error}
key={handle.id}
handle={handle}
domain={domain}
getHandleProps={getHandleProps}
/>
))}
</>
)}
</Handles>

<Tracks left={false} right={false}>
{({ tracks, getTrackProps }) => (
<>
{tracks?.map(({ id, source, target }) => <Track
error={error}
key={id}
source={source}
target={target}
getTrackProps={getTrackProps}
/>)}
</>
)}
</Tracks>

{disabledIntervals?.length > 0 && (
<Tracks left={false} right={false}>
{({ getTrackProps }) => (
<>
{disabledIntervals.map(({ id, source, target }) => (
<Track
key={id}
source={source}
target={target}
getTrackProps={getTrackProps}
disabled
/>
))}
</>
)}
</Tracks>
)}

{showNow && (
<Tracks left={false} right={false}>
{({ getTrackProps }) => (
<Track
key={this.now?.id}
source={this.now?.source}
target={this.now?.target}
getTrackProps={getTrackProps}
/>
)}
</Tracks>
)}

<Ticks values={this.getDateTicks()}>
{({ ticks }) => (
<>
{ticks.map((tick) => (
<Tick
key={tick.id}
tick={tick}
count={ticks.length}
format={formatTick}
/>
))}
</>
)}
</Ticks>
</Slider>
</div>
);
}
}

TimeRange.propTypes = {
ticksNumber: PropTypes.number.isRequired,
selectedInterval: PropTypes.arrayOf(PropTypes.object),
timelineInterval: PropTypes.arrayOf(PropTypes.object),
disabledIntervals: PropTypes.arrayOf(PropTypes.object),
containerClassName: PropTypes.string,
sliderRailClassName: PropTypes.string,
step: PropTypes.number,
formatTick: PropTypes.func,
};

TimeRange.defaultProps = {
selectedInterval: [
set(new Date(), { minutes: 0, seconds: 0, milliseconds: 0 }),
set(addHours(new Date(), 1), { minutes: 0, seconds: 0, milliseconds: 0 }),
],
timelineInterval: [startOfToday(), endOfToday()],
formatTick: (ms) => format(new Date(ms), 'HH:mm'),
disabledIntervals: [],
step: 1000 * 60 * 30,
ticksNumber: 48,
error: false,
mode: 3,
};

export default TimeRange;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';

const Handle = ({
error,
domain: [min, max],
handle: { id, value, percent = 0 },
disabled,
getHandleProps,
}) => {
const leftPosition = `${percent}%`;

return (
<>
<div className='react_time_range__handle_wrapper' style={{ left: leftPosition }} {...getHandleProps(id)} />
<div
role='slider'
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
className={`react_time_range__handle_container${disabled ? '__disabled' : ''}`}
style={{ left: leftPosition }}
>
<div className={`react_time_range__handle_marker${error ? '__error' : ''}`} />
</div>
</>
);
};

Handle.propTypes = {
domain: PropTypes.array.isRequired,
handle: PropTypes.shape({
id: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
}).isRequired,
getHandleProps: PropTypes.func.isRequired,
disabled: PropTypes.bool,
style: PropTypes.object,
};

Handle.defaultProps = { disabled: false };

export default Handle;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import React from 'react';

const KeyboardHandle = ({ domain: [min, max], handle: { id, value, percent = 0 }, disabled, getHandleProps }) => (
<button
role='slider'
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
className='react_time_range__keyboard_handle'
style={{
left: `${percent}%`,
backgroundColor: disabled ? '#666' : '#ffc400',
}}
{...getHandleProps(id)}
/>
);

KeyboardHandle.propTypes = {
domain: PropTypes.array.isRequired,
handle: PropTypes.shape({
id: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
}).isRequired,
getHandleProps: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};

KeyboardHandle.defaultProps = { disabled: false };

export default KeyboardHandle;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';

export const SliderRail = ({ getRailProps }) => (
<>
<div className='react_time_range__rail__outer' {...getRailProps()} />
<div className='react_time_range__rail__inner' />
</>
);

SliderRail.propTypes = { getRailProps: PropTypes.func.isRequired };

export default SliderRail;
Loading

0 comments on commit e91918a

Please sign in to comment.