change(ui): remove old reducers, rewrite eventsblock component

This commit is contained in:
sylenien 2022-12-27 16:41:00 +01:00 committed by Shekar Siri
parent b4181fb9a9
commit c4cbeec841
26 changed files with 265 additions and 896 deletions

View file

@ -1,34 +1,27 @@
import React, { useEffect } from 'react';
import stl from './notifications.module.css';
import { connect } from 'react-redux';
import { Icon, Tooltip } from 'UI';
import { fetchList, setViewed, clearAll } from 'Duck/notifications';
import { setLastRead } from 'Duck/announcements';
import { useModal } from 'App/components/Modal';
import AlertTriggersModal from 'Shared/AlertTriggersModal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
interface Props {
notifications: any;
fetchList: any;
}
function Notifications(props: Props) {
function Notifications() {
const { showModal } = useModal();
const { notificationStore } = useStore();
const count = useObserver(() => notificationStore.notificationsCount);
const count = notificationStore.notificationsCount;
useEffect(() => {
const interval = setInterval(() => {
notificationStore.fetchNotificationsCount();
void notificationStore.fetchNotificationsCount();
}, AUTOREFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return useObserver(() => (
return (
<Tooltip title={`Alerts`}>
<div
className={stl.button}
@ -40,12 +33,7 @@ function Notifications(props: Props) {
<Icon name="bell" size="18" color="gray-dark" />
</div>
</Tooltip>
));
);
}
export default connect(
(state: any) => ({
notifications: state.getIn(['notifications', 'list']),
}),
{ fetchList, setLastRead, setViewed, clearAll }
)(Notifications);
export default observer(Notifications)

View file

@ -9,7 +9,6 @@ import {
init,
edit,
remove,
setAlertMetricId,
setActiveWidget,
updateActiveState,
} from 'Duck/customMetrics';
@ -181,7 +180,6 @@ export default connect(
{
remove,
setShowAlerts,
setAlertMetricId,
edit,
setActiveWidget,
updateActiveState,

View file

@ -1,14 +1,5 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {
edit,
updateSeries,
addSeriesFilterFilter,
removeSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
} from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { Button, Icon } from 'UI';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SeriesName from './SeriesName';
@ -18,21 +9,21 @@ import { observer } from 'mobx-react-lite';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
addSeriesFilterFilter: typeof addSeriesFilterFilter;
editSeriesFilterFilter: typeof editSeriesFilterFilter;
editSeriesFilter: typeof editSeriesFilter;
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
canDelete?: boolean;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
}
function FilterSeries(props: Props) {
const { observeChanges = () => {}, canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
const {
observeChanges = () => {
},
canDelete,
hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.'
} = props;
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
@ -46,7 +37,7 @@ function FilterSeries(props: Props) {
observeChanges()
}
const onChangeEventsOrder = (e, { name, value }: any) => {
const onChangeEventsOrder = (e: any, { name, value }: any) => {
series.filter.updateKey(name, value)
observeChanges()
}
@ -60,11 +51,11 @@ function FilterSeries(props: Props) {
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name) } />
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name)} />
</div>
<div className="flex items-center cursor-pointer">
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
<div onClick={props.onRemoveSeries} className={cn("ml-3", { 'disabled': !canDelete })}>
<Icon name="trash" size="16" />
</div>
@ -73,17 +64,17 @@ function FilterSeries(props: Props) {
</div>
</div>
</div>
{ expanded && (
{expanded && (
<>
<div className="p-5">
{ series.filter.filters.length > 0 ? (
{series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
) : (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
@ -103,11 +94,4 @@ function FilterSeries(props: Props) {
);
}
export default connect(null, {
edit,
updateSeries,
addSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
removeSeriesFilterFilter,
})(observer(FilterSeries));
export default observer(FilterSeries);

View file

@ -1,33 +1,30 @@
import React from 'react'
import EventsBlock from '../Session_/EventsBlock';
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import stl from './rightblock.module.css';
function RightBlock(props: any) {
const { activeTab } = props;
const { player, store } = React.useContext(PlayerContext)
const { eventListNow, playing } = store.get()
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const EventsBlockConnected = () => <EventsBlock playing={playing} player={player} setActiveTab={props.setActiveTab} currentTimeEventIndex={currentTimeEventIndex} />
const renderActiveTab = (tab: string) => {
switch(tab) {
case props.tabs.EVENTS:
return <EventsBlockConnected />
case props.tabs.HEATMAPS:
return <PageInsightsPanel setActiveTab={props.setActiveTab} />
}
if (activeTab === props.tabs.EVENTS) {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<EventsBlock
setActiveTab={props.setActiveTab}
/>
</div>
)
}
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
{renderActiveTab(activeTab)}
</div>
)
if (activeTab === props.tabs.HEATMAPS) {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<PageInsightsPanel setActiveTab={props.setActiveTab} />
</div>
)
}
return null
}
export default observer(RightBlock)
export default RightBlock

View file

@ -147,14 +147,6 @@ export default class Event extends React.PureComponent {
</button>
}
<div className={ cls.topBlock }>
{/* <div className={ cls.checkbox }>
<Checkbox
className="customCheckbox"
name={ event.key }
checked={ selected }
onClick={ onCheckboxClick }
/>
</div> */}
<div className={ cls.firstLine }>
{ this.renderBody() }
</div>

View file

@ -5,16 +5,10 @@ import { PlayerContext } from 'App/components/Session/playerContext';
function EventSearch(props) {
const { player } = React.useContext(PlayerContext)
const { onChange, clearSearch, value, header, setActiveTab } = props;
const { onChange, value, header, setActiveTab } = props;
const toggleEvents = () => player.toggleEvents()
useEffect(() => {
return () => {
clearSearch()
}
}, [])
return (
<div className="flex items-center w-full relative">
<div className="flex flex-1 flex-col">

View file

@ -1,249 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { Icon } from 'UI';
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized";
import { TYPES } from 'Types/session/event';
import { setSelected } from 'Duck/events';
import { setEventFilter, filterOutNote } from 'Duck/sessions';
import { show as showTargetDefiner } from 'Duck/components/targetDefiner';
import EventGroupWrapper from './EventGroupWrapper';
import styles from './eventsBlock.module.css';
import EventSearch from './EventSearch/EventSearch';
@connect(state => ({
session: state.getIn([ 'sessions', 'current' ]),
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
selectedEvents: state.getIn([ 'events', 'selected' ]),
targetDefinerDisplayed: state.getIn([ 'components', 'targetDefiner', 'isDisplayed' ]),
testsAvaliable: false,
}), {
showTargetDefiner,
setSelected,
setEventFilter,
filterOutNote
})
export default class EventsBlock extends React.Component {
state = {
editingEvent: null,
mouseOver: false,
query: ''
}
scroller = React.createRef();
cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 300
});
write = ({ target: { value, name } }) => {
const { filter } = this.state;
this.setState({ query: value })
this.props.setEventFilter({ query: value, filter })
setTimeout(() => {
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
clearSearch = () => {
const { filter } = this.state;
this.setState({ query: '' })
this.props.setEventFilter({ query: '', filter })
if (this.scroller.current) {
this.scroller.current.forceUpdateGrid();
}
setTimeout(() => {
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
onSetEventFilter = (e, { name, value }) => {
const { query } = this.state;
this.setState({ filter: value })
this.props.setEventFilter({ filter: value, query });
}
componentDidUpdate(prevProps) {
if (prevProps.targetDefinerDisplayed && !this.props.targetDefinerDisplayed) {
this.setState({ editingEvent: null });
}
if (prevProps.session !== this.props.session) { // Doesn't happen
this.cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 300
});
}
if (prevProps.currentTimeEventIndex !== this.props.currentTimeEventIndex &&
this.scroller.current !== null) {
this.scroller.current.forceUpdateGrid();
if (!this.state.mouseOver) {
this.scroller.current.scrollToRow(this.props.currentTimeEventIndex);
}
}
}
onCheckboxClick(e, event) {
e.stopPropagation();
const {
session: { events },
selectedEvents,
} = this.props;
this.props.player.pause();
let newSelectedSet;
const wasSelected = selectedEvents.contains(event);
if (wasSelected) {
newSelectedSet = selectedEvents.remove(event);
} else {
newSelectedSet = selectedEvents.add(event);
}
let selectNextLoad = false;
events.reverse().forEach((sessEvent) => {
if (sessEvent.type === TYPES.LOCATION) {
if (selectNextLoad) {
newSelectedSet = newSelectedSet.add(sessEvent);
}
selectNextLoad = false;
} else if (newSelectedSet.contains(sessEvent)) {
selectNextLoad = true;
}
});
this.props.setSelected(newSelectedSet);
}
onEventClick = (e, event) => this.props.player.jump(event.time)
onMouseOver = () => this.setState({ mouseOver: true })
onMouseLeave = () => this.setState({ mouseOver: false })
get eventsList() {
const { session: { notesWithEvents }, filteredEvents } = this.props
const usedEvents = filteredEvents || notesWithEvents
return usedEvents
}
renderGroup = ({ index, key, style, parent }) => {
const {
selectedEvents,
currentTimeEventIndex,
testsAvaliable,
playing,
eventsIndex,
filterOutNote,
} = this.props;
const { query } = this.state;
const _events = this.eventsList
const isLastEvent = index === _events.size - 1;
const isLastInGroup = isLastEvent || _events.get(index + 1).type === TYPES.LOCATION;
const event = _events.get(index);
const isNote = !!event.noteId
const isSelected = selectedEvents.includes(event);
const isCurrent = index === currentTimeEventIndex;
const isEditing = this.state.editingEvent === event;
const heightBug = index === 0 && event.type === TYPES.LOCATION && event.referrer ? { top: 2 } : {}
return (
<CellMeasurer
key={key}
cache={this.cache}
parent={parent}
rowIndex={index}
>
{({measure, registerChild}) => (
<div style={{ ...style, ...heightBug }} ref={registerChild}>
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}
onEventClick={ this.onEventClick }
onCheckboxClick={ this.onCheckboxClick }
event={ event }
isLastEvent={ isLastEvent }
isLastInGroup={ isLastInGroup }
isSelected={ isSelected }
isCurrent={ isCurrent }
isEditing={ isEditing }
showSelection={ testsAvaliable && !playing }
isNote={isNote}
filterOutNote={filterOutNote}
/>
</div>
)}
</CellMeasurer>
);
}
render() {
const { query } = this.state;
const {
session: {
events,
},
setActiveTab,
} = this.props;
const _events = this.eventsList
const isEmptySearch = query && (_events.size === 0 || !_events)
return (
<>
<div className={ cn(styles.header, 'p-4') }>
<div className={ cn(styles.hAndProgress, 'mt-3') }>
<EventSearch
onChange={this.write}
clearSearch={this.clearSearch}
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.size }</span></div>
}
/>
</div>
</div>
<div
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
id="eventList"
data-openreplay-masked
onMouseOver={ this.onMouseOver }
onMouseLeave={ this.onMouseLeave }
>
{isEmptySearch && (
<div className='flex items-center'>
<Icon name="binoculars" size={18} />
<span className='ml-2'>No Matching Results</span>
</div>
)}
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={this.scroller}
className={ styles.eventsList }
height={height + 10}
width={248}
overscanRowCount={6}
itemSize={230}
rowCount={_events.size}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
rowRenderer={this.renderGroup}
scrollToAlignment="start"
/>
)}
</AutoSizer>
</div>
</>
);
}
}

View file

@ -0,0 +1,187 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { Icon } from 'UI';
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
import { TYPES } from 'Types/session/event';
import { setEventFilter, filterOutNote } from 'Duck/sessions';
import { show as showTargetDefiner } from 'Duck/components/targetDefiner';
import EventGroupWrapper from './EventGroupWrapper';
import styles from './eventsBlock.module.css';
import EventSearch from './EventSearch/EventSearch';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { RootStore } from 'App/duck'
import { List as ImmList } from 'immutable'
import useCellMeasurerCache from 'Components/shared/DevTools/useCellMeasurerCache'
interface IProps {
setEventFilter: (filter: { query: string }) => void
filteredEvents: ImmList<Record<string, any>>
setActiveTab: (tab?: string) => void
query: string
session: Record<string, any>
filterOutNote: (id: string) => void
eventsIndex: number[]
}
function EventsBlock(props: IProps) {
const [mouseOver, setMouseOver] = React.useState(true)
const scroller = React.useRef<List>(null)
const cache = useCellMeasurerCache(undefined, {
fixedWidth: true,
defaultHeight: 300
});
const { store, player } = React.useContext(PlayerContext)
const { eventListNow, playing } = store.get()
const { session: { events, notesWithEvents }, filteredEvents,
eventsIndex,
filterOutNote,
query,
setActiveTab,
} = props
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const usedEvents = filteredEvents || notesWithEvents
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
props.setEventFilter({ query: value })
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
const clearSearch = () => {
props.setEventFilter({ query: '' })
if (scroller.current) {
scroller.current.forceUpdateGrid();
}
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
React.useEffect(() => {
return () => {
clearSearch()
}
}, [])
React.useEffect(() => {
if (scroller.current) {
scroller.current.forceUpdateGrid();
if (!mouseOver) {
scroller.current.scrollToRow(currentTimeEventIndex);
}
}
}, [currentTimeEventIndex])
const onEventClick = (_: React.MouseEvent, event: any) => player.jump(event.time)
const onMouseOver = () => setMouseOver(true)
const onMouseLeave = () => setMouseOver(false)
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: any; parent: any }) => {
const isLastEvent = index === usedEvents.size - 1;
const isLastInGroup = isLastEvent || usedEvents.get(index + 1)?.type === TYPES.LOCATION;
const event = usedEvents.get(index);
const isNote = !!event?.noteId
const isCurrent = index === currentTimeEventIndex;
const heightBug = index === 0 && event?.type === TYPES.LOCATION && event.referrer ? { top: 2 } : {}
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
rowIndex={index}
>
{({measure, registerChild}) => (
<div style={{ ...style, ...heightBug }} ref={registerChild}>
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}
onEventClick={ onEventClick }
event={ event }
isLastEvent={ isLastEvent }
isLastInGroup={ isLastInGroup }
isCurrent={ isCurrent }
showSelection={ !playing }
isNote={isNote}
filterOutNote={filterOutNote}
/>
</div>
)}
</CellMeasurer>
);
}
const isEmptySearch = query && (usedEvents.size === 0 || !usedEvents)
return (
<>
<div className={ cn(styles.header, 'p-4') }>
<div className={ cn(styles.hAndProgress, 'mt-3') }>
<EventSearch
onChange={write}
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.size }</span></div>
}
/>
</div>
</div>
<div
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
id="eventList"
data-openreplay-masked
onMouseOver={ onMouseOver }
onMouseLeave={ onMouseLeave }
>
{isEmptySearch && (
<div className='flex items-center'>
<Icon name="binoculars" size={18} />
<span className='ml-2'>No Matching Results</span>
</div>
)}
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={scroller}
className={ styles.eventsList }
height={height + 10}
width={248}
overscanRowCount={6}
itemSize={230}
rowCount={usedEvents.size}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowRenderer={renderGroup}
scrollToAlignment="start"
/>
)}
</AutoSizer>
</div>
</>
);
}
export default connect((state: RootStore) => ({
session: state.getIn([ 'sessions', 'current' ]),
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
query: state.getIn(['sessions', 'eventsQuery']),
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
targetDefinerDisplayed: state.getIn([ 'components', 'targetDefiner', 'isDisplayed' ]),
}), {
showTargetDefiner,
setEventFilter,
filterOutNote
})(observer(EventsBlock))

View file

@ -13,7 +13,6 @@ const JUMP_OFFSET = 1000;
interface Props {
filters: any;
fetchInsights: (filters: Record<string, any>) => void;
urls: [];
insights: any;
events: Array<any>;
urlOptions: Array<any>;

View file

@ -1,112 +0,0 @@
import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {
edit,
updateSeries,
addSeriesFilterFilter,
removeSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
} from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { IconButton, Icon } from 'UI';
import FilterSelection from '../../Filters/FilterSelection';
import SeriesName from './SeriesName';
import cn from 'classnames';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex) => void;
canDelete?: boolean;
addSeriesFilterFilter: typeof addSeriesFilterFilter;
editSeriesFilterFilter: typeof editSeriesFilterFilter;
editSeriesFilter: typeof editSeriesFilter;
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
hideHeader?: boolean;
emptyMessage?: any;
}
function FilterSeries(props: Props) {
const { canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
filter.value = [""]
if (filter.hasOwnProperty('filters') && Array.isArray(filter.filters)) {
filter.filters = filter.filters.map(i => ({ ...i, value: [""] }))
}
props.addSeriesFilterFilter(seriesIndex, filter);
}
const onUpdateFilter = (filterIndex, filter) => {
props.editSeriesFilterFilter(seriesIndex, filterIndex, filter);
}
const onChangeEventsOrder = (e, { name, value }) => {
props.editSeriesFilter(seriesIndex, { eventsOrder: value });
}
const onRemoveFilter = (filterIndex) => {
props.removeSeriesFilterFilter(seriesIndex, filterIndex);
}
return (
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
</div>
<div className="flex items-center cursor-pointer" >
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
<Icon name="trash" size="16" />
</div>
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
{ expanded && (
<>
<div className="p-5">
{ series.filter.filters.size > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
</div>
</>
)}
</div>
);
}
export default connect(null, {
edit,
updateSeries,
addSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
removeSeriesFilterFilter,
})(FilterSeries);

View file

@ -1,57 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { Icon } from 'UI';
interface Props {
name: string;
onUpdate: (name) => void;
seriesIndex?: number;
}
function SeriesName(props: Props) {
const { seriesIndex = 1 } = props;
const [editing, setEditing] = useState(false)
const [name, setName] = useState(props.name)
const ref = useRef<any>(null)
const write = ({ target: { value, name } }) => {
setName(value)
}
const onBlur = () => {
setEditing(false)
props.onUpdate(name)
}
useEffect(() => {
if (editing) {
ref.current.focus()
}
}, [editing])
useEffect(() => {
setName(props.name)
}, [props.name])
// const { name } = props;
return (
<div className="flex items-center">
{ editing ? (
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2 h-8"
value={name}
// readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
) : (
<div className="text-base h-8 flex items-center border-transparent">{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }</div>
)}
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
</div>
);
}
export default SeriesName;

View file

@ -1 +0,0 @@
export { default } from './SeriesName';

View file

@ -1 +0,0 @@
export { default } from './FilterSeries'

View file

@ -1,12 +1,12 @@
import { useMemo } from 'react'
import { CellMeasurerCache } from 'react-virtualized';
import { CellMeasurerCache, CellMeasurerCacheParams } from 'react-virtualized';
import useLatestRef from 'App/hooks/useLatestRef'
export default function useCellMeasurerCache(itemList: any[]) {
const filteredListRef = useLatestRef(itemList)
export default function useCellMeasurerCache(itemList?: any[], options?: CellMeasurerCacheParams) {
const filteredListRef = itemList ? useLatestRef(itemList) : undefined
return useMemo(() => new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index) => filteredListRef.current[index],
keyMapper: filteredListRef ? (index) => filteredListRef.current[index] : undefined,
...options
}), [])
}

View file

@ -1,55 +0,0 @@
redux -> other storage ::<< Entities + Lists + relations <|> methods:: crud. request declaration -> request realisation with middleware -< (uses) MODEL
!request declaration
action/request formatter => ReducerModule Fabrique =>
class ReducerModule {
_ns = "common"
_switch = {}
_n = 0
constructor(namespace) {
this._ns = namespace
}
/**
Action: state => newState | { reduce: state, action => newState, creator: () => {objects to action} }
*/
actions(actns): this {
Object.keys(actns).map(key => {
const type = `${this._namespace}/${key.toUpperCase()}`;
this._switch[ type ] = actns[ key ];
});
return this;
}
requests(reqsts): this {
Object.keys(reqsts).map(key => {
const type = `${this._namespace}/${key.toUpperCase()}`;
this._switch[ type ] = actns[ key ];
});
return this;
}
get actionTypes() {
}
get actionCreators() {
}
get reducer() {
return (state, action = {}) => {
const reduce = this._switch[ action.type ];
return reduce ? reduce(state, action) : state;
}
}
}

View file

@ -1,8 +1,8 @@
import { List, Map } from 'immutable';
import CustomMetric, { FilterSeries } from 'Types/customMetric'
import { createFetch, fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud';
import { fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud';
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
import { array, success, createListUpdater, mergeReducers } from './funcTools/tools';
import Filter from 'Types/filter';
import Session from 'Types/session';
@ -28,11 +28,6 @@ const INIT = `${name}/INIT`;
const SET_ACTIVE_WIDGET = `${name}/SET_ACTIVE_WIDGET`;
const REMOVE = removeType(name);
const UPDATE_SERIES = `${name}/UPDATE_SERIES`;
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
function chartWrapper(chart = []) {
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
}
const updateItemInList = createListUpdater(idKey);
const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
@ -117,12 +112,6 @@ export default mergeReducers(
export const edit = createEdit(name);
export const remove = createRemove(name);
export const updateSeries = (index, series) => ({
type: UPDATE_SERIES,
index,
series,
});
export function fetch(id) {
return {
id,
@ -147,34 +136,6 @@ export function fetchList() {
};
}
export function setAlertMetricId(id) {
return {
type: SET_ALERT_METRIC_ID,
id,
};
}
export const addSeries = (series = null) => (dispatch, getState) => {
const instance = getState().getIn([ 'customMetrics', 'instance' ]);
const seriesIndex = instance.series.size;
const newSeries = series || {
name: `Series ${seriesIndex + 1}`,
filter: new Filter({ filters: [], eventsOrder: 'then' }),
};
dispatch({
type: ADD_SERIES,
series: newSeries,
});
}
export const removeSeries = (index) => (dispatch, getState) => {
dispatch({
type: REMOVE_SERIES,
index,
});
}
export const init = (instance = null, forceNull = false) => (dispatch, getState) => {
dispatch({
type: INIT,

View file

@ -1,7 +0,0 @@
import Environment from 'Types/environment';
import crudDuckGenerator from './tools/crudDuck';
const crudDuck = crudDuckGenerator('environment', Environment);
export const { fetchList, fetch, init, edit, save, remove } = crudDuck.actions;
export default crudDuck.reducer;

View file

@ -1,81 +0,0 @@
import { List, Map, Set } from 'immutable';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
import Event from 'Types/filter/event';
import CustomFilter from 'Types/filter/customFilter';
import { KEYS } from 'Types/filter/customFilter';
import logger from 'App/logger';
import { countries } from 'App/constants';
import { getRE } from 'App/utils';
const FETCH_LIST = new RequestTypes('events/FETCH_LIST');
const TOGGLE_SELECT = 'events/TOGGLE_SELECT';
const SET_SELECTED = 'events/SET_SELECTED';
const countryOptions = Object.keys(countries).map(c => ({filterKey: KEYS.USER_COUNTRY, label: KEYS.USER_COUNTRY, type: KEYS.USER_COUNTRY, value: c, actualValue: countries[c], isFilter: true }));
const initialState = Map({
list: List(),
store: Set(),
// replace?
selected: Set(),
});
const filterKeys = ['METADATA', KEYS.USERID, KEYS.USER_COUNTRY, KEYS.USER_BROWSER, KEYS.USER_OS, KEYS.USER_DEVICE, KEYS.REFERRER]
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.SUCCESS: {
const regCountry = getRE(action.params.q, 'i');
const countryOptionsFiltered = List(countryOptions).filter(({ actualValue }) => regCountry.test(actualValue)).take(5);
const eventList = List(action.data).concat(countryOptionsFiltered).map(item => (
filterKeys.includes(item.type) ?
CustomFilter({...item, isFilter: true }) :
Event({...item, key: item.type, filterKey: item.type, label: item.type}) )
);
return state
.set('list', eventList)
.update('store', store => store.concat(eventList));
}
// TODO: use ids. or make a set-hoc?
case TOGGLE_SELECT: {
const { event, flag } = action;
const shouldBeInSet = typeof flag === 'boolean'
? flag
: !state.get('selected').contains(event);
return state.update('selected', set => (shouldBeInSet
? set.add(event)
: set.remove(event)));
}
case SET_SELECTED:
return state.set('selected', Set(action.events));
}
return state;
};
export default withRequestState(FETCH_LIST, reducer);
export function fetchList(params) {
return {
types: FETCH_LIST.toArray(),
call: client => client.get('/events/search', params),
params,
};
}
export function toggleSelect(event, flag) {
return {
type: TOGGLE_SELECT,
event,
flag,
};
}
export function setSelected(events) {
return {
type: SET_SELECTED,
events,
};
}

View file

@ -1,3 +1,4 @@
// @ts-ignore
import { combineReducers } from 'redux-immutable';
import user from './user';
@ -7,12 +8,8 @@ import target from './target';
import targetCustom from './targetCustom';
import filters from './filters';
import funnelFilters from './funnelFilters';
import events from './events';
import environments from './environments';
import variables from './variables';
import templates from './templates';
import alerts from './alerts';
import notifications from './notifications';
import dashboard from './dashboard';
import components from './components';
import sources from './sources';
@ -41,12 +38,8 @@ const rootReducer = combineReducers({
filters,
funnelFilters,
events,
environments,
variables,
templates,
alerts,
notifications,
dashboard,
components,
members,

View file

@ -1,63 +0,0 @@
import { List, Map } from 'immutable';
import Notification from 'Types/notification';
import { mergeReducers, success, array, request, createListUpdater } from './funcTools/tools';
import { createRequestReducer } from './funcTools/request';
import {
createCRUDReducer,
getCRUDRequestTypes,
createFetchList,
} from './funcTools/crud';
const name = 'notification';
const idKey = 'notificationId';
const SET_VIEWED = 'notifications/SET_VIEWED';
const CLEAR_ALL = 'notifications/CLEAR_ALL';
const SET_VIEWED_SUCCESS = success(SET_VIEWED);
const CLEAR_ALL_SUCCESS = success(CLEAR_ALL);
const listUpdater = createListUpdater(idKey);
const initialState = Map({
list: List(),
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case SET_VIEWED_SUCCESS:
if (!action.data) return state;
const item = state.get('list').find(item => item[ idKey ] === action.id)
return listUpdater(state, Notification({...item.toJS(), createdAt: item.createdAt.ts, viewed: true }));
case CLEAR_ALL_SUCCESS:
if (!action.data) return state;
return state.update('list', list => list.map(l => Notification({...l.toJS(), createdAt: l.createdAt.ts, viewed: true })))
}
return state;
};
export const fetchList = createFetchList(name);
export default mergeReducers(
reducer,
createCRUDReducer(name, Notification, idKey),
createRequestReducer({
setViewed: SET_VIEWED,
clearAll: CLEAR_ALL,
...getCRUDRequestTypes(name),
}),
);
export function setViewed(id) {
return {
types: array(SET_VIEWED),
call: client => client.get(`/notifications/${ id }/view`),
id,
};
}
export function clearAll(params) {
return {
types: array(CLEAR_ALL),
call: client => client.post('/notifications/view', params),
};
}

View file

@ -60,6 +60,7 @@ const initialState = Map({
eventsIndex: [],
sourcemapUploaded: true,
filteredEvents: null,
eventsQuery: '',
showChatWindow: false,
liveSessions: List(),
visitedEvents: List(),
@ -110,14 +111,17 @@ const reducer = (state = initialState, action = {}) => {
case SET_EVENT_QUERY: {
const events = state.get('current').events;
const query = action.filter.query;
// const filter = action.filter.filter;
const searchRe = getRE(query, 'i');
let filteredEvents = query ? events.filter((e) => searchRe.test(e.url) || searchRe.test(e.value) || searchRe.test(e.label)) : null;
// if (filter) {
// filteredEvents = filteredEvents ? filteredEvents.filter(e => e.type === filter) : events.filter(e => e.type === filter);
// }
return state.set('filteredEvents', filteredEvents);
const filteredEvents = query ? events.filter(
(e) => searchRe.test(e.url)
|| searchRe.test(e.value)
|| searchRe.test(e.label)
|| searchRe.test(e.type)
|| (e.type === 'LOCATION' && searchRe.test('visited'))
) : null;
return state.set('filteredEvents', filteredEvents).set('eventsQuery', query);
}
case FETCH.SUCCESS: {
// TODO: more common.. or TEMP

View file

@ -1,9 +0,0 @@
import Variable from 'Types/variable';
import crudDuckGenerator from './tools/crudDuck';
const crudDuck = crudDuckGenerator('variable', Variable);
export const {
fetchList, fetch, init, edit, save, remove,
} = crudDuck.actions;
export default crudDuck.reducer;

View file

@ -44,7 +44,7 @@ export default class UserStore {
}
initUser(user?: any ): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
if (user) {
this.instance = new User().fromJson(user.toJson());
} else {
@ -54,7 +54,7 @@ export default class UserStore {
})
}
updateKey(key: string, value: any) {
updateKey(key: keyof this, value: any) {
this[key] = value
if (key === 'searchQuery') {

View file

@ -1,20 +0,0 @@
import Record from 'Types/Record';
export default Record({
line1: '',
postal_code: '',
city: '',
state: '',
country: '',
}, {
methods: {
validate() {
return true;
},
toData() {
const js = this.toJS();
delete js.key;
return js;
},
},
});

View file

@ -1,87 +0,0 @@
import { Record, List, Set } from 'immutable';
import { validateName } from 'App/validate';
import { DateTime } from 'luxon';
import Run from './run';
import Step from './step';
class Test extends Record({
testId: undefined,
name: 'Unnamed Test',
steps: List(),
stepsCount: undefined,
framework: 'selenium',
sessionId: undefined,
generated: false,
tags: Set(),
runHistory: List(),
editedAt: undefined,
seqId: undefined,
seqChange: false,
uptime: 0,
}) {
// ???TODO
// idKey = "testId"
exists() {
return this.testId !== undefined;
}
validate() {
if (this.steps.size === 0) return false;
return validateName(this.name, {
empty: false,
admissibleChars: ':-',
});
}
isComplete() {
return this.stepsCount === this.steps.size;
}
// not the best code
toData() {
const js = this
.update('steps', steps => steps.map(step => step.toData()))
.toJS();
if (js.seqChange) {
const { testId, seqId } = js;
return { testId, seqId };
}
delete js.stepsCount;
delete js.seqChange;
return js;
}
// not the best code
}
const fromJS = (test = {}) => {
if (test instanceof Test) return test;
const stepsLength = test.steps && test.steps.length; //
const editedAt = test.editedAt ? DateTime.fromMillis(test.editedAt) : undefined;
const lastRun = Run(test.lastRun);
const runHistory = List(test.runHistory) // TODO: GOOD ENDPOINTS
.map(run => {
if (typeof run === 'string') {
return run === lastRun.runId
? lastRun
: Run({ runId: run })
}
return Run(run);
});
return new Test({ ...test, editedAt, uptime: parseInt(test.passed / test.count * 100) || 0 })
.set('stepsCount', typeof test.stepsCount === 'number'
? test.stepsCount
: stepsLength) //
.set('runHistory', runHistory)
.set('steps', List(test.steps).map(Step))
.set('tags', test.tags && Set(test.tags.map(t => t.toLowerCase())));
};
export default fromJS;

View file

@ -16,6 +16,20 @@ export interface IMember {
invitationLink: string
}
export interface IMemberApiRes {
userId: string
name: string
email: string
createdAt: string
admin: boolean
superAdmin: boolean
joined: boolean
expiredInvitation: boolean
roleId: string
roleName: string
invitationLink: string
}
export default Record({
id: undefined,
name: '',
@ -42,9 +56,9 @@ export default Record({
return js;
},
},
fromJS: ({ createdAt, ...rest }) => ({
fromJS: ({ createdAt, ...rest }: IMemberApiRes) => ({
...rest,
createdAt: createdAt && DateTime.fromISO(createdAt || 0),
createdAt: createdAt && DateTime.fromISO(createdAt || '0'),
id: rest.userId,
}),
});