Merge branch 'dev' of github.com:openreplay/openreplay into funnels
This commit is contained in:
commit
3baa3ea9a5
27 changed files with 416 additions and 333 deletions
|
|
@ -14,7 +14,7 @@ const SOME_ERROR_MSG = "Some error occured.";
|
|||
const defaultValueToText = value => value;
|
||||
const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value }));
|
||||
|
||||
const hiddenStyle = {
|
||||
const hiddenStyle = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
opacity: 0, position: 'fixed', left: '-3000px'
|
||||
};
|
||||
|
|
@ -32,10 +32,10 @@ class AutoComplete extends React.PureComponent {
|
|||
values: [],
|
||||
noResultsMessage: TYPE_TO_SEARCH_MSG,
|
||||
ddOpen: false,
|
||||
query: this.props.value,
|
||||
query: this.props.value,
|
||||
loading: false,
|
||||
error: false
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (this.props.value !== newProps.value) {
|
||||
|
|
@ -49,8 +49,8 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
requestValues = (q) => {
|
||||
const { params, endpoint, method } = this.props;
|
||||
this.setState({
|
||||
loading: true,
|
||||
this.setState({
|
||||
loading: true,
|
||||
error: false,
|
||||
});
|
||||
return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q })
|
||||
|
|
@ -72,7 +72,7 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
debouncedRequestValues = debounce(this.requestValues, 1000)
|
||||
|
||||
setError = () => this.setState({
|
||||
setError = () => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
noResultsMessage: SOME_ERROR_MSG,
|
||||
|
|
@ -98,7 +98,7 @@ class AutoComplete extends React.PureComponent {
|
|||
const _value = value.trim();
|
||||
onSelect(null, {name, value: _value});
|
||||
}
|
||||
|
||||
|
||||
changed = false;
|
||||
pasted = false;
|
||||
}
|
||||
|
|
@ -126,10 +126,10 @@ class AutoComplete extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
const options = optionMapping(values, valueToText)
|
||||
|
||||
|
||||
return (
|
||||
<OutsideClickDetectingDiv
|
||||
className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
|
||||
<OutsideClickDetectingDiv
|
||||
className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
|
||||
onClickOutside={this.onClickOutside}
|
||||
>
|
||||
{/* <EventSearchInput /> */}
|
||||
|
|
@ -140,7 +140,6 @@ class AutoComplete extends React.PureComponent {
|
|||
onFocus={ () => this.setState({ddOpen: true})}
|
||||
onChange={ this.onInputChange }
|
||||
onBlur={ this.onBlur }
|
||||
onFocus={ () => this.setState({ddOpen: true})}
|
||||
value={ query }
|
||||
autoFocus={ true }
|
||||
type="text"
|
||||
|
|
@ -150,6 +149,7 @@ class AutoComplete extends React.PureComponent {
|
|||
this.hiddenInput.value = text;
|
||||
pasted = true; // to use only the hidden input
|
||||
} }
|
||||
autocomplete="do-not-autofill-bad-chrome"
|
||||
/>
|
||||
<div className={stl.right} onClick={showCloseButton ? onRemoveValue : onAddValue}>
|
||||
{ showCloseButton ? <Icon name="close" size="14" /> : <span className="px-1">or</span>}
|
||||
|
|
@ -182,11 +182,11 @@ class AutoComplete extends React.PureComponent {
|
|||
{ headerText && headerText }
|
||||
{
|
||||
options.map(item => (
|
||||
<FilterItem
|
||||
<FilterItem
|
||||
label={ item.value }
|
||||
icon={ item.icon }
|
||||
onClick={ (e) => this.onItemClick(e, item) }
|
||||
/>
|
||||
onClick={ (e) => this.onItemClick(e, item) }
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const sortOptionsMap = {
|
|||
'startTs-desc': 'Newest',
|
||||
'startTs-asc': 'Oldest',
|
||||
'eventsCount-asc': 'Events Ascending',
|
||||
'eventsCount-desc': 'Events Descending',
|
||||
'eventsCount-desc': 'Events Descending',
|
||||
};
|
||||
const sortOptions = Object.entries(sortOptionsMap)
|
||||
.map(([ value, text ]) => ({ value, text }));
|
||||
|
|
@ -50,10 +50,6 @@ function SessionListHeader({
|
|||
value='list'
|
||||
/>
|
||||
</div> */}
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Timezone</span>
|
||||
<TimezoneDropdown />
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Sort By</span>
|
||||
<SortDropdown options={ sortOptions }/>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const SOME_ERROR_MSG = "Some error occured.";
|
|||
const defaultValueToText = value => value;
|
||||
const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value }));
|
||||
|
||||
const hiddenStyle = {
|
||||
const hiddenStyle = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
opacity: 0, position: 'fixed', left: '-3000px'
|
||||
};
|
||||
|
|
@ -31,10 +31,10 @@ class AutoComplete extends React.PureComponent {
|
|||
values: [],
|
||||
noResultsMessage: TYPE_TO_SEARCH_MSG,
|
||||
ddOpen: false,
|
||||
query: this.props.value,
|
||||
query: this.props.value,
|
||||
loading: false,
|
||||
error: false
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (this.props.value !== newProps.value) {
|
||||
|
|
@ -48,8 +48,8 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
requestValues = (q) => {
|
||||
const { params, endpoint, method } = this.props;
|
||||
this.setState({
|
||||
loading: true,
|
||||
this.setState({
|
||||
loading: true,
|
||||
error: false,
|
||||
});
|
||||
return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q })
|
||||
|
|
@ -71,13 +71,13 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
debouncedRequestValues = debounce(this.requestValues, 1000)
|
||||
|
||||
setError = () => this.setState({
|
||||
setError = () => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
noResultsMessage: SOME_ERROR_MSG,
|
||||
})
|
||||
|
||||
onInputChange = ({ target: { value } }) => {
|
||||
onInputChange = ({ target: { value } }) => {
|
||||
changed = true;
|
||||
this.setState({ query: value, updated: true })
|
||||
const _value = value.trim();
|
||||
|
|
@ -96,7 +96,7 @@ class AutoComplete extends React.PureComponent {
|
|||
const _value = value.trim();
|
||||
onSelect(null, {name, value: _value});
|
||||
}
|
||||
|
||||
|
||||
changed = false;
|
||||
pasted = false;
|
||||
}
|
||||
|
|
@ -123,10 +123,10 @@ class AutoComplete extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
const options = optionMapping(values, valueToText)
|
||||
|
||||
|
||||
return (
|
||||
<OutsideClickDetectingDiv
|
||||
className={ cn("relative", { "flex-1" : fullWidth }) }
|
||||
<OutsideClickDetectingDiv
|
||||
className={ cn("relative", { "flex-1" : fullWidth }) }
|
||||
onClickOutside={this.onClickOutside}
|
||||
>
|
||||
{/* <Input
|
||||
|
|
@ -153,7 +153,6 @@ class AutoComplete extends React.PureComponent {
|
|||
onFocus={ () => this.setState({ddOpen: true})}
|
||||
onChange={ this.onInputChange }
|
||||
onBlur={ this.onBlur }
|
||||
onFocus={ () => this.setState({ddOpen: true})}
|
||||
value={ query }
|
||||
autoFocus={ true }
|
||||
type="text"
|
||||
|
|
@ -163,6 +162,7 @@ class AutoComplete extends React.PureComponent {
|
|||
this.hiddenInput.value = text;
|
||||
pasted = true; // to use only the hidden input
|
||||
} }
|
||||
autocomplete="do-not-autofill-bad-chrome"
|
||||
/>
|
||||
<div className={cn(stl.right, 'cursor-pointer')} onLick={onAddOrRemove}>
|
||||
{/* <Icon name="close" size="18" /> */}
|
||||
|
|
@ -175,10 +175,10 @@ class AutoComplete extends React.PureComponent {
|
|||
{ headerText && headerText }
|
||||
{
|
||||
options.map(item => (
|
||||
<FilterItem
|
||||
<FilterItem
|
||||
label={ item.value }
|
||||
icon={ item.icon }
|
||||
onClick={ (e) => this.onItemClick(e, item) }
|
||||
onClick={ (e) => this.onItemClick(e, item) }
|
||||
/>
|
||||
// <DropdownItem key={ item.value } value={ item.value } onSelect={ (e) => this.onItemClick(e, item) } />
|
||||
))
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export default class FilterModal extends React.PureComponent {
|
|||
|
||||
|
||||
return (!displayed ? null :
|
||||
<div className={ stl.modal }>
|
||||
<div className={ stl.modal }>
|
||||
<div className={ stl.filterListStatic }>
|
||||
{
|
||||
filteredList.map(category => (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { debounce } from 'App/utils';
|
|||
import stl from './FilterAutoComplete.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
const hiddenStyle = {
|
||||
const hiddenStyle = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
opacity: 0, position: 'fixed', left: '-3000px'
|
||||
};
|
||||
|
|
@ -43,8 +43,8 @@ function FilterAutoComplete(props: Props) {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [options, setOptions] = useState<any>([]);
|
||||
const [query, setQuery] = useState(value);
|
||||
|
||||
const requestValues = (q) => {
|
||||
|
||||
const requestValues = (q) => {
|
||||
setLoading(true);
|
||||
|
||||
return new APIClient()[method?.toLocaleLowerCase()](endpoint, { ...params, q })
|
||||
|
|
@ -90,7 +90,7 @@ function FilterAutoComplete(props: Props) {
|
|||
e.preventDefault();
|
||||
|
||||
if (query !== item.value) {
|
||||
setQuery(item.value);
|
||||
setQuery(item.value);
|
||||
}
|
||||
|
||||
props.onSelect(e, item);
|
||||
|
|
@ -107,6 +107,7 @@ function FilterAutoComplete(props: Props) {
|
|||
autoFocus={ true }
|
||||
type="text"
|
||||
placeholder={ placeholder }
|
||||
autoComplete="do-not-autofill-bad-chrome"
|
||||
// onPaste={(e) => {
|
||||
// const text = e.clipboardData.getData('Text');
|
||||
// // this.hiddenInput.value = text;
|
||||
|
|
@ -139,11 +140,11 @@ function FilterAutoComplete(props: Props) {
|
|||
>
|
||||
{ icon && <Icon name={ icon } size="16" marginRight="8" /> }
|
||||
<span className={ stl.label }>{ item.value }</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -151,4 +152,4 @@ function FilterAutoComplete(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default FilterAutoComplete;
|
||||
export default FilterAutoComplete;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface Props {
|
|||
searchQuery?: string,
|
||||
}
|
||||
function FilterModal(props: Props) {
|
||||
const {
|
||||
const {
|
||||
filters,
|
||||
metaOptions,
|
||||
onFilterClick = () => null,
|
||||
|
|
@ -32,13 +32,20 @@ function FilterModal(props: Props) {
|
|||
_filter.value = [filter.value];
|
||||
onFilterClick(_filter);
|
||||
}
|
||||
|
||||
|
||||
const isResultEmpty = !filterSearchList || Object.keys(filterSearchList).length === 0
|
||||
return (
|
||||
<div className={stl.wrapper} style={{ width: '480px', maxHeight: '380px', overflowY: 'auto'}}>
|
||||
|
||||
{ showSearchList && (
|
||||
<Loader size="small" loading={fetchingFilterSearchList}>
|
||||
<div className="-mx-6 px-6">
|
||||
{ filterSearchList && Object.keys(filterSearchList).map((key, index) => {
|
||||
{isResultEmpty && !fetchingFilterSearchList ? (
|
||||
<div className="flex items-center">
|
||||
<Icon className="color-gray-medium" name="binoculars" size="24" />
|
||||
<div className="color-gray-medium font-medium px-3"> No Suggestions Found </div>
|
||||
</div>
|
||||
) : Object.keys(filterSearchList).map((key, index) => {
|
||||
const filter = filterSearchList[key];
|
||||
const option = filtersMap[key];
|
||||
return option ? (
|
||||
|
|
@ -65,7 +72,7 @@ function FilterModal(props: Props) {
|
|||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
|
||||
|
||||
{ !hasSearchQuery && (
|
||||
<div className="" style={{ columns: "auto 200px" }}>
|
||||
{filters && Object.keys(filters).map((key) => (
|
||||
|
|
@ -92,4 +99,4 @@ export default connect(state => ({
|
|||
filterSearchList: state.getIn([ 'search', 'filterSearchList' ]),
|
||||
metaOptions: state.getIn([ 'customFields', 'list' ]),
|
||||
fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]),
|
||||
}))(FilterModal);
|
||||
}))(FilterModal);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface Props {
|
|||
searchQuery?: string,
|
||||
}
|
||||
function LiveFilterModal(props: Props) {
|
||||
const {
|
||||
const {
|
||||
filters,
|
||||
metaOptions,
|
||||
onFilterClick = () => null,
|
||||
|
|
@ -32,7 +32,9 @@ function LiveFilterModal(props: Props) {
|
|||
_filter.value = [filter.value];
|
||||
onFilterClick(_filter);
|
||||
}
|
||||
|
||||
|
||||
const isResultEmpty = !filterSearchList || Object.keys(filterSearchList).filter(i => filtersMap[i].isLive).length === 0
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper} style={{ width: '490px', maxHeight: '400px', overflowY: 'auto'}}>
|
||||
{ showSearchList && (
|
||||
|
|
@ -62,10 +64,39 @@ function LiveFilterModal(props: Props) {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{isResultEmpty && !fetchingFilterSearchList ? (
|
||||
<div className="flex items-center">
|
||||
<Icon className="color-gray-medium" name="binoculars" size="24" />
|
||||
<div className="color-gray-medium font-medium px-3"> No Suggestions Found </div>
|
||||
</div>
|
||||
) : Object.keys(filterSearchList).filter(i => filtersMap[i].isLive).map((key, index) => {
|
||||
const filter = filterSearchList[key];
|
||||
const option = filtersMap[key];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn('mb-3')}
|
||||
>
|
||||
<div className="font-medium uppercase color-gray-medium text-sm mb-2">{option.label}</div>
|
||||
<div>
|
||||
{filter.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(stl.filterSearchItem, "cursor-pointer px-3 py-1 text-sm flex items-center")}
|
||||
onClick={() => onFilterSearchClick({ type: key, value: f.value })}
|
||||
>
|
||||
<Icon className="mr-2" name={option.icon} size="16" />
|
||||
<div className="whitespace-nowrap text-ellipsis overflow-hidden">{f.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
|
||||
|
||||
{ !hasSearchQuery && (
|
||||
<div className="">
|
||||
{filters && Object.keys(filters).map((key) => (
|
||||
|
|
@ -92,4 +123,4 @@ export default connect(state => ({
|
|||
filterSearchList: state.getIn([ 'search', 'filterSearchList' ]),
|
||||
metaOptions: state.getIn([ 'customFields', 'list' ]),
|
||||
fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]),
|
||||
}))(LiveFilterModal);
|
||||
}))(LiveFilterModal);
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ const PER_PAGE = 10;
|
|||
|
||||
interface Props {
|
||||
loading: Boolean,
|
||||
list: List<any>,
|
||||
list: List<any>,
|
||||
fetchLiveList: () => Promise<void>,
|
||||
applyFilter: () => void,
|
||||
filters: any,
|
||||
addAttribute: (obj) => void,
|
||||
addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
|
||||
updateCurrentPage: (page: number) => void,
|
||||
currentPage: number,
|
||||
currentPage: number,
|
||||
metaList: any,
|
||||
updateSort: (sort: any) => void,
|
||||
sort: any,
|
||||
|
|
@ -41,7 +41,7 @@ function LiveSessionList(props: Props) {
|
|||
const sortOptions = metaList.map(i => ({
|
||||
text: capitalize(i), value: i
|
||||
})).toJS();
|
||||
|
||||
|
||||
// const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
|
||||
// const addPage = () => props.updateCurrentPage(props.currentPage + 1)
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ function LiveSessionList(props: Props) {
|
|||
if (filter.key === FilterKey.USERID) {
|
||||
const _userId = session.userId ? session.userId.toLowerCase() : '';
|
||||
hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter;
|
||||
}
|
||||
}
|
||||
if (filter.category === FilterCategory.METADATA) {
|
||||
const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : '';
|
||||
hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter;
|
||||
|
|
@ -80,7 +80,7 @@ function LiveSessionList(props: Props) {
|
|||
setSessions(filteredSessions);
|
||||
}, [filters, list]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
props.fetchLiveList();
|
||||
timeout();
|
||||
return () => {
|
||||
|
|
@ -88,7 +88,7 @@ function LiveSessionList(props: Props) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const onUserClick = (userId, userAnonymousId) => {
|
||||
const onUserClick = (userId: string, userAnonymousId: string) => {
|
||||
if (userId) {
|
||||
props.addFilterByKeyAndValue(FilterKey.USERID, userId);
|
||||
} else {
|
||||
|
|
@ -183,7 +183,7 @@ export default withPermissions(['ASSIST_LIVE'])(connect(
|
|||
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
|
||||
sort: state.getIn(['liveSearch', 'sort']),
|
||||
}),
|
||||
{
|
||||
{
|
||||
fetchLiveList,
|
||||
applyFilter,
|
||||
addAttribute,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Link,
|
||||
Icon,
|
||||
} from 'UI';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes';
|
||||
|
||||
const PLAY_ICON_NAMES = {
|
||||
notPlayed: 'play-fill',
|
||||
played: 'play-circle-light',
|
||||
hovered: 'play-hover'
|
||||
}
|
||||
|
||||
const getDefaultIconName = (isViewed) => !isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played
|
||||
|
||||
interface Props {
|
||||
isAssist: boolean;
|
||||
viewed: boolean;
|
||||
sessionId: string;
|
||||
}
|
||||
export default function PlayLink(props: Props) {
|
||||
const { isAssist, viewed, sessionId } = props
|
||||
const defaultIconName = getDefaultIconName(viewed)
|
||||
|
||||
const [isHovered, toggleHover] = useState(false)
|
||||
const [iconName, setIconName] = useState(defaultIconName)
|
||||
|
||||
useEffect(() => {
|
||||
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered)
|
||||
else setIconName(getDefaultIconName(viewed))
|
||||
}, [isHovered, viewed])
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }
|
||||
onMouseEnter={() => toggleHover(true)}
|
||||
onMouseLeave={() => toggleHover(false)}
|
||||
>
|
||||
<Icon name={iconName} size="42" color={isAssist ? "tealx" : "teal"} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './PlayLink'
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
Link,
|
||||
Icon,
|
||||
CountryFlag,
|
||||
Avatar,
|
||||
TextEllipsis,
|
||||
Label,
|
||||
} from 'UI';
|
||||
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import stl from './sessionItem.css';
|
||||
import Counter from './Counter'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
|
||||
import { capitalize } from 'App/utils';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession()
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
@connect(state => ({
|
||||
timezone: state.getIn(['sessions', 'timezone']),
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), { toggleFavorite, setSessionPath })
|
||||
@withRouter
|
||||
export default class SessionItem extends React.PureComponent {
|
||||
// eslint-disable-next-line complexity
|
||||
render() {
|
||||
const {
|
||||
session: {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
errorsCount,
|
||||
pagesCount,
|
||||
viewed,
|
||||
favorite,
|
||||
userDeviceType,
|
||||
userUuid,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
userSessionsCount,
|
||||
issueTypes,
|
||||
active,
|
||||
},
|
||||
timezone,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
showActive = false,
|
||||
lastPlayedSessionId,
|
||||
} = this.props;
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, this.props.location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, this.props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, this.props.location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
|
||||
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
|
||||
<div className="flex items-start">
|
||||
<div className={ cn('flex items-center w-full')}>
|
||||
<div className="flex items-center pr-2" style={{ width: "30%"}}>
|
||||
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
|
||||
<div
|
||||
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "20%", height: "38px" }} className="px-2 flex flex-col justify-between">
|
||||
<div>{formatTimeOrDate(startedAt, timezone) }</div>
|
||||
<div className="flex items-center color-gray-medium">
|
||||
{!isAssist && (
|
||||
<>
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{ eventsCount }</span>
|
||||
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
|
||||
</div>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
</>
|
||||
)}
|
||||
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "30%", height: "38px" }} className="px-2 flex flex-col justify-between">
|
||||
<CountryFlag country={ userCountry } className="mr-2" label />
|
||||
<div className="color-gray-medium flex items-center">
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userDeviceType) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{ isSessions && (
|
||||
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
|
||||
<ErrorBars count={issueTypes.length} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{ isAssist && showActive && (
|
||||
<Label success className={cn("bg-green color-white text-right mr-4", { 'opacity-0' : !active})}>
|
||||
<span className="color-white">ACTIVE</span>
|
||||
</Label>
|
||||
)}
|
||||
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
|
||||
{ isSessions && (
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{ isLastPlayed && (
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap'}}>LAST PLAYED</span>
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Link to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }>
|
||||
<Icon name={ !viewed && !isAssist ? 'play-fill' : 'play-circle-light' } size="42" color={isAssist ? "tealx" : "teal"} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ _metaList.length > 0 && (
|
||||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,117 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react'
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
Link,
|
||||
Icon,
|
||||
import {
|
||||
CountryFlag,
|
||||
Avatar,
|
||||
TextEllipsis,
|
||||
Label,
|
||||
} from 'UI';
|
||||
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import stl from './sessionItem.css';
|
||||
import Counter from './Counter'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import PlayLink from './PlayLink';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
|
||||
import { capitalize } from 'App/utils';
|
||||
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession()
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
// @connect(state => ({
|
||||
// timezone: state.getIn(['sessions', 'timezone']),
|
||||
// siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
// }), { toggleFavorite, setSessionPath })
|
||||
// @withRouter
|
||||
function SessionItem(props) {
|
||||
// render() {
|
||||
const {
|
||||
session: {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
errorsCount,
|
||||
pagesCount,
|
||||
viewed,
|
||||
favorite,
|
||||
userDeviceType,
|
||||
userUuid,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
userSessionsCount,
|
||||
issueTypes,
|
||||
active,
|
||||
},
|
||||
timezone,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
showActive = false,
|
||||
lastPlayedSessionId,
|
||||
} = props;
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, props.location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, props.location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
interface Props {
|
||||
session: {
|
||||
sessionId: string;
|
||||
userBrowser: string;
|
||||
userOs: string;
|
||||
userId: string;
|
||||
userAnonymousId: string;
|
||||
userDisplayName: string;
|
||||
userCountry: string;
|
||||
startedAt: number;
|
||||
duration: string;
|
||||
eventsCount: number;
|
||||
errorsCount: number;
|
||||
pagesCount: number;
|
||||
viewed: boolean;
|
||||
favorite: boolean;
|
||||
userDeviceType: string;
|
||||
userUuid: string;
|
||||
userNumericHash: number;
|
||||
live: boolean
|
||||
metadata: Record<string, any>;
|
||||
userSessionsCount: number
|
||||
issueTypes: [];
|
||||
active: boolean;
|
||||
},
|
||||
onUserClick?: (userId: string, userAnonymousId: string) => void;
|
||||
hasUserFilter?: boolean;
|
||||
disableUser?: boolean;
|
||||
metaList?: Array<any>;
|
||||
showActive?: boolean;
|
||||
lastPlayedSessionId?: string;
|
||||
live?: boolean;
|
||||
}
|
||||
|
||||
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
function SessionItem(props: RouteComponentProps<Props>) {
|
||||
const { settingsStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
|
||||
const {
|
||||
session,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
showActive = false,
|
||||
lastPlayedSessionId,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
viewed,
|
||||
userDeviceType,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
issueTypes,
|
||||
active,
|
||||
} = session;
|
||||
|
||||
const location = props.location;
|
||||
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, location.pathname) || isRoute(ASSIST_LIVE_SESSION, location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
|
||||
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col p-3 mb-3") } id="session-item" >
|
||||
<div className="flex items-start">
|
||||
<div className={ cn('flex items-center w-full')}>
|
||||
<div className="flex items-center pr-2" style={{ width: "30%"}}>
|
||||
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
|
||||
<div
|
||||
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, [stl.userName]: !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
|
|
@ -111,7 +138,7 @@ function SessionItem(props) {
|
|||
<div className="color-gray-medium flex items-center">
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
|
|
@ -145,9 +172,11 @@ function SessionItem(props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<Link to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }>
|
||||
<Icon name={ !viewed && !isAssist ? 'play-fill' : 'play-circle-light' } size="42" color={isAssist ? "tealx" : "teal"} />
|
||||
</Link>
|
||||
<PlayLink
|
||||
isAssist={isAssist}
|
||||
sessionId={sessionId}
|
||||
viewed={viewed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,10 +184,7 @@ function SessionItem(props) {
|
|||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
timezone: localStorage.getItem(TIMEZONE) || '',
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), { toggleFavorite, setSessionPath })(withRouter(SessionItem));
|
||||
export default withRouter<Props>(observer<Props>(SessionItem))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
.sessionItem {
|
||||
background-color: #fff;
|
||||
user-select: none;
|
||||
border-radius: 3px;
|
||||
border: solid thin #EEEEEE;
|
||||
transition: all 0.4s;
|
||||
|
||||
& .favorite {
|
||||
opacity: 0;
|
||||
|
|
@ -12,6 +13,10 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
border: solid thin $active-blue-border;
|
||||
transition: all 0.2s;
|
||||
|
||||
& .playLink {
|
||||
transition: all 0.4s;
|
||||
opacity: 1;
|
||||
|
|
@ -98,4 +103,13 @@
|
|||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.userName {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $teal;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,49 @@ import { useStore } from 'App/mstore';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const str = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)
|
||||
const d = str && str[1] || 'UTC';
|
||||
const timezoneOptions = [
|
||||
{ label: d, value: 'local' },
|
||||
{ label: 'UTC', value: 'UTC' },
|
||||
]
|
||||
|
||||
interface TimezonesDropdownValue {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
type TimezonesDropdown = TimezonesDropdownValue[]
|
||||
|
||||
const generateGMTZones = (): TimezonesDropdown => {
|
||||
const timezones: TimezonesDropdown = []
|
||||
|
||||
const positiveNumbers = [...Array(12).keys()];
|
||||
const negativeNumbers = [...Array(12).keys()].reverse();
|
||||
negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array
|
||||
|
||||
const combinedArray = [...negativeNumbers, ...positiveNumbers];
|
||||
|
||||
for (let i = 0; i < 23; i++) {
|
||||
let symbol = i < 11 ? '-' : '+';
|
||||
let isUTC = i === 11
|
||||
let prefix = isUTC ? 'UTC / GMT' : 'GMT';
|
||||
let value = String(combinedArray[i]).padStart(2, '0');
|
||||
|
||||
let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00`
|
||||
|
||||
let dropdownValue = `UTC${symbol}${value}`
|
||||
timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue })
|
||||
}
|
||||
|
||||
timezones.splice(17, 0, { label: 'GMT +05:30', value: 'GMT +05:30' })
|
||||
return timezones
|
||||
}
|
||||
|
||||
const timezoneOptions: TimezonesDropdown = [...generateGMTZones()]
|
||||
|
||||
function DefaultTimezone(props) {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
|
||||
|
||||
useEffect(() => {
|
||||
if (!timezone) setTimezone('local');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -38,9 +70,9 @@ function DefaultTimezone(props) {
|
|||
}}>Update</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm mt-3">This change will impact the timestamp on session card and player.</div>
|
||||
<div className="text-sm mt-3">This change will impact the timestamp on session card and player.</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultTimezone;
|
||||
export default DefaultTimezone;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function ListingVisibility(props) {
|
|||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const [durationSettings, setDurationSettings] = React.useState(sessionSettings.durationFilter);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-lg">Listing Visibility</h3>
|
||||
|
|
@ -27,7 +27,7 @@ function ListingVisibility(props) {
|
|||
<div className="col-span-4">
|
||||
<Select
|
||||
options={numberOptions}
|
||||
defaultValue={durationSettings.operator}
|
||||
defaultValue={numberOptions[0].value}
|
||||
onChange={({ value }) => {
|
||||
setDurationSettings({ ...durationSettings, operator: value });
|
||||
setChanged(true);
|
||||
|
|
@ -39,6 +39,7 @@ function ListingVisibility(props) {
|
|||
value={durationSettings.count}
|
||||
type="number"
|
||||
name="count"
|
||||
placeholder="E.g 10"
|
||||
style={{ height: '38px', width: '100%'}}
|
||||
onChange={(e, { value }) => {
|
||||
setDurationSettings({ ...durationSettings, count: value });
|
||||
|
|
@ -48,7 +49,7 @@ function ListingVisibility(props) {
|
|||
</div>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
defaultValue={durationSettings.countType}
|
||||
defaultValue={periodOptions[1].value}
|
||||
options={periodOptions}
|
||||
onChange={({ value }) => {
|
||||
setDurationSettings({ ...durationSettings, countType: value });
|
||||
|
|
@ -67,4 +68,4 @@ function ListingVisibility(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ListingVisibility;
|
||||
export default ListingVisibility;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const IconButton = React.forwardRef(({
|
|||
name,
|
||||
disabled = false,
|
||||
tooltip = false,
|
||||
tooltipPosition = 'top',
|
||||
tooltipPosition = 'top center',
|
||||
compact = false,
|
||||
...rest
|
||||
}, ref) => (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,42 @@
|
|||
import { useState, useRef, useEffect, forwardRef } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Popup } from 'UI';
|
||||
import styles from './textEllipsis.css';
|
||||
|
||||
/** calculates text width in pixels
|
||||
+ * by creating a hidden element with t
|
||||
+ * ext and counting its width
|
||||
+ * @param text String - text string
|
||||
+ * @param fontProp String - font properties
|
||||
+ * @returns width number
|
||||
+ */
|
||||
function findTextWidth(text, fontProp) {
|
||||
const tag = document.createElement('div')
|
||||
|
||||
tag.style.position = 'absolute'
|
||||
tag.style.left = '-99in'
|
||||
tag.style.whiteSpace = 'nowrap'
|
||||
tag.style.font = fontProp
|
||||
tag.innerHTML = text
|
||||
|
||||
document.body.appendChild(tag)
|
||||
const result = tag.clientWidth
|
||||
document.body.removeChild(tag)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const Trigger = forwardRef(({ textOrChildren, maxWidth, style, className, ...rest }, ref) => (
|
||||
<div
|
||||
className={ cn(styles.textEllipsis, className) }
|
||||
style={{ maxWidth, ...style }}
|
||||
ref={ref}
|
||||
{ ...rest }
|
||||
>
|
||||
{ textOrChildren }
|
||||
</div>
|
||||
))
|
||||
|
||||
const TextEllipsis = ({
|
||||
text,
|
||||
hintText = text,
|
||||
|
|
@ -14,23 +49,50 @@ const TextEllipsis = ({
|
|||
hintProps={},
|
||||
...props
|
||||
}) => {
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const textRef = useRef(null);
|
||||
|
||||
const textOrChildren = text || children;
|
||||
const trigger = (
|
||||
<div
|
||||
className={ cn(styles.textEllipsis, className) }
|
||||
style={{ maxWidth, ...style }}
|
||||
{ ...props }
|
||||
>
|
||||
{ textOrChildren }
|
||||
</div>
|
||||
);
|
||||
if (noHint) return trigger;
|
||||
|
||||
const popupId = (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
useEffect(() => {
|
||||
const element = textRef.current;
|
||||
|
||||
const fontSize = window.getComputedStyle(element, null).getPropertyValue('font-size');
|
||||
|
||||
const textWidth = findTextWidth(element.innerText, fontSize)
|
||||
if (textWidth > element.clientWidth) setShowPopup(true)
|
||||
else setShowPopup(false)
|
||||
}, [textRef.current])
|
||||
|
||||
if (noHint || !showPopup) return (
|
||||
<Trigger
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
style={style}
|
||||
textOrChildren={textOrChildren}
|
||||
ref={textRef}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popup
|
||||
trigger={ trigger }
|
||||
content={ <div className="customPopupText" { ...hintProps } >{ hintText || textOrChildren }</div> }
|
||||
{ ...popupProps }
|
||||
/>
|
||||
trigger={
|
||||
<Trigger
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
style={style}
|
||||
textOrChildren={textOrChildren}
|
||||
id={popupId}
|
||||
ref={textRef}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
content={ <div className="customPopupText" { ...hintProps } >{ hintText || textOrChildren }</div> }
|
||||
{ ...popupProps }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@ import stl from './timezoneDropdown.css';
|
|||
import { connect } from 'react-redux';
|
||||
import { setTimezone } from 'Duck/sessions';
|
||||
|
||||
const localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
|
||||
const middlePoint = localMachineFormat.length - 2
|
||||
const readableLocalTimezone =
|
||||
`${localMachineFormat.substring(0, 3)} ${localMachineFormat.substring(3, middlePoint)}:${localMachineFormat.substring(middlePoint)}`
|
||||
|
||||
const timezoneOptions = {
|
||||
'local': new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1],
|
||||
'local': readableLocalTimezone,
|
||||
'UTC': 'UTC'
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,10 @@
|
|||
width: 20px;
|
||||
left: 0;
|
||||
bottom: -2px;
|
||||
/* background-color: white; */
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
border: solid 1px rgba(0, 0, 0, 0.2);
|
||||
|
||||
background: #394EFF;
|
||||
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px rgba(0, 0, 0, 0.14), 0px 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +53,7 @@
|
|||
|
||||
.slider.checked:before {
|
||||
border: solid 1px $teal;
|
||||
background-color: $teal !important;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const durationFormatted = (duration: Duration):string => {
|
|||
|
||||
export function durationFromMsFormatted(ms: number): string {
|
||||
return durationFormatted(Duration.fromMillis(ms || 0));
|
||||
}
|
||||
}
|
||||
|
||||
export const durationFormattedFull = (duration: Duration): string => {
|
||||
if (duration.as('minutes') < 1) { // show in seconds
|
||||
|
|
@ -35,7 +35,7 @@ export const durationFormattedFull = (duration: Duration): string => {
|
|||
} else if (duration.as('months') < 1) { // show in days and hours
|
||||
let d = duration.toFormat('d');
|
||||
duration = d + (d > 1 ? ' days' : ' day');
|
||||
} else {
|
||||
} else {
|
||||
let d = Math.trunc(duration.as('months'));
|
||||
duration = d + (d > 1 ? ' months' : ' month');;
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export const msToSec = (ms:number): number => Math.round(ms / 1000);
|
|||
export const diffFromNowString = (ts:number): string =>
|
||||
durationFormattedFull(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts)));
|
||||
|
||||
export const diffFromNowShortString = (ts: number): string =>
|
||||
export const diffFromNowShortString = (ts: number): string =>
|
||||
durationFormatted(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts)));
|
||||
|
||||
export const getDateFromMill = date =>
|
||||
|
|
@ -69,11 +69,19 @@ export function formatDateTimeDefault(timestamp: number): string {
|
|||
return isToday(date) ? 'Today' : date.toFormat('LLL dd, yyyy') + ', ' + date.toFormat('hh:mm a')
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats timestamps into readable date
|
||||
* @param {Number} timestamp
|
||||
* @param {String} timezone fixed offset like UTC+6
|
||||
* @returns {String} formatted date (or time if its today)
|
||||
*/
|
||||
export function formatTimeOrDate(timestamp: number, timezone: string): string {
|
||||
var date = DateTime.fromMillis(timestamp)
|
||||
if (timezone === 'UTC')
|
||||
date = date.toUTC();
|
||||
|
||||
if (timezone) {
|
||||
if (timezone === 'UTC') date = date.toUTC();
|
||||
date = date.setZone(timezone)
|
||||
}
|
||||
|
||||
return isToday(date) ? date.toFormat('hh:mm a') : date.toFormat('LLL dd, yyyy, hh:mm a');
|
||||
}
|
||||
|
||||
|
|
|
|||
3
frontend/app/svg/icons/binoculars.svg
Normal file
3
frontend/app/svg/icons/binoculars.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-binoculars" viewBox="0 0 16 16">
|
||||
<path d="M3 2.5A1.5 1.5 0 0 1 4.5 1h1A1.5 1.5 0 0 1 7 2.5V5h2V2.5A1.5 1.5 0 0 1 10.5 1h1A1.5 1.5 0 0 1 13 2.5v2.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 15 7.118V14.5a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 14.5v-3a.5.5 0 0 1 .146-.354l.854-.853V9.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v.793l.854.853A.5.5 0 0 1 7 11.5v3A1.5 1.5 0 0 1 5.5 16h-3A1.5 1.5 0 0 1 1 14.5V7.118a1.5 1.5 0 0 1 .83-1.342l.894-.447A.5.5 0 0 0 3 4.882V2.5zM4.5 2a.5.5 0 0 0-.5.5V3h2v-.5a.5.5 0 0 0-.5-.5h-1zM6 4H4v.882a1.5 1.5 0 0 1-.83 1.342l-.894.447A.5.5 0 0 0 2 7.118V13h4v-1.293l-.854-.853A.5.5 0 0 1 5 10.5v-1A1.5 1.5 0 0 1 6.5 8h3A1.5 1.5 0 0 1 11 9.5v1a.5.5 0 0 1-.146.354l-.854.853V13h4V7.118a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 12 4.882V4h-2v1.5a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V4zm4-1h2v-.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5V3zm4 11h-4v.5a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5V14zm-8 0H2v.5a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5V14z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
3
frontend/app/svg/icons/filters/arrow-return-right.svg
Normal file
3
frontend/app/svg/icons/filters/arrow-return-right.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
12
frontend/app/svg/icons/play-hover.svg
Normal file
12
frontend/app/svg/icons/play-hover.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="42" height="42" viewBox="0 0 42 42" fill="inherit" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_47)">
|
||||
<path d="M21 0C25.1534 0 29.2135 1.23163 32.667 3.53914C36.1204 5.84665 38.812 9.1264 40.4015 12.9636C41.9909 16.8009 42.4068 21.0233 41.5965 25.0969C40.7862 29.1705 38.7861 32.9123 35.8492 35.8492C32.9123 38.7861 29.1705 40.7862 25.0969 41.5965C21.0233 42.4068 16.8009 41.9909 12.9636 40.4015C9.1264 38.812 5.84665 36.1204 3.53914 32.667C1.23163 29.2135 0 25.1534 0 21C0 15.4305 2.21249 10.089 6.15076 6.15076C10.089 2.21249 15.4305 0 21 0ZM21 1.27615C17.099 1.27615 13.2856 2.43294 10.042 4.60022C6.79845 6.76751 4.27039 9.84795 2.77754 13.452C1.28469 17.0561 0.894093 21.0219 1.65514 24.8479C2.41619 28.674 4.29471 32.1884 7.05313 34.9469C9.81156 37.7053 13.326 39.5838 17.1521 40.3449C20.9781 41.1059 24.9439 40.7153 28.548 39.2225C32.1521 37.7296 35.2325 35.2015 37.3998 31.958C39.5671 28.7144 40.7238 24.901 40.7238 21C40.7238 18.4098 40.2137 15.845 39.2225 13.452C38.2312 11.059 36.7784 8.88466 34.9469 7.05313C33.1153 5.22161 30.941 3.76876 28.548 2.77754C26.155 1.78633 23.5902 1.27615 21 1.27615Z" />
|
||||
<circle cx="21" cy="21" r="20" fill="inherit" />
|
||||
<path d="M29.3192 23.0838C31.1527 21.9531 31.1527 20.1438 29.3192 19.0454L18.6819 12.6565C16.8485 11.5258 15.3462 12.2608 15.3462 14.2719V27.7604C15.3462 29.7312 16.8485 30.4742 18.69 29.3758L29.3192 23.0838Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_47">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -121,9 +121,8 @@ export const defaultFilters = [
|
|||
{ label: 'Time Between Events', key: KEYS.TIME_BETWEEN_EVENTS, type: KEYS.TIME_BETWEEN_EVENTS, filterKey: KEYS.TIME_BETWEEN_EVENTS, icon: 'filters/click', isFilter: false },
|
||||
{ label: 'Avg CPU Load', key: KEYS.AVG_CPU_LOAD, type: KEYS.AVG_CPU_LOAD, filterKey: KEYS.AVG_CPU_LOAD, icon: 'filters/click', isFilter: false },
|
||||
{ label: 'Memory Usage', key: KEYS.AVG_MEMORY_USAGE, type: KEYS.AVG_MEMORY_USAGE, filterKey: KEYS.AVG_MEMORY_USAGE, icon: 'filters/click', isFilter: false },
|
||||
|
||||
{ label: 'Input', key: KEYS.INPUT, type: KEYS.INPUT, filterKey: KEYS.INPUT, icon: 'event/input', isFilter: false },
|
||||
{ label: 'Page', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false },
|
||||
{ label: 'Path', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false },
|
||||
// { label: 'View', key: KEYS.VIEW, type: KEYS.VIEW, filterKey: KEYS.VIEW, icon: 'event/view', isFilter: false }
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ export enum FilterCategory {
|
|||
GEAR = "Gear",
|
||||
RECORDING_ATTRIBUTES = "Recording Attributes",
|
||||
JAVASCRIPT = "Javascript",
|
||||
USER = "User",
|
||||
METADATA = "Metadata",
|
||||
USER = "User Identification",
|
||||
METADATA = "Session & User Metadata",
|
||||
PERFORMANCE = "Performance",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const filtersMap = {
|
|||
// EVENTS
|
||||
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
|
||||
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
|
||||
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
|
||||
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
|
||||
[FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
|
||||
// [FilterKey.REQUEST]: { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
|
||||
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
|
||||
|
|
@ -37,10 +37,10 @@ export const filtersMap = {
|
|||
[FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
|
||||
[FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
|
||||
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
|
||||
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/rev-id' },
|
||||
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/referrer' },
|
||||
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
|
||||
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
|
||||
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' },
|
||||
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
// [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
|
||||
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ text: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
|
||||
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue