feat(ui) - assist ui - wip

This commit is contained in:
Shekar Siri 2022-02-22 18:08:21 +01:00
parent 54ebc487cc
commit 7c9df8c196
22 changed files with 266 additions and 241 deletions

View file

@ -19,6 +19,7 @@ import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal';
import FunnelDetails from 'Components/Funnels/FunnelDetails';
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import APIClient from './api_client';
import * as routes from './routes';
@ -77,7 +78,7 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
onboarding: state.getIn([ 'user', 'onboarding' ])
};
}, {
fetchUserInfo, fetchTenants, setSessionPath
fetchUserInfo, fetchTenants, setSessionPath, fetchIntegrationVariables
})
class Router extends React.Component {
state = {
@ -86,7 +87,11 @@ class Router extends React.Component {
constructor(props) {
super(props);
if (props.isLoggedIn) {
Promise.all([props.fetchUserInfo()])
Promise.all([
props.fetchUserInfo().then(() => {
props.fetchIntegrationVariables()
}),
])
// .then(() => this.onLoginLogout());
}
props.fetchTenants();

View file

@ -14,7 +14,7 @@ import stl from './bugFinder.css';
import { fetchList as fetchSiteList } from 'Duck/site';
import withLocationHandlers from "HOCs/withLocationHandlers";
import { fetch as fetchFilterVariables } from 'Duck/sources';
import { fetchList as fetchIntegrationVariables, fetchSources } from 'Duck/customField';
import { fetchSources } from 'Duck/customField';
import { RehydrateSlidePanel } from './WatchDogs/components';
import { setActiveTab, setFunnelPage } from 'Duck/sessions';
import SessionsMenu from './SessionsMenu/SessionsMenu';
@ -23,11 +23,8 @@ import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
import LiveSessionList from './LiveSessionList'
import SessionSearch from 'Shared/SessionSearch';
import MainSearchBar from 'Shared/MainSearchBar';
import LiveSearchBar from 'Shared/LiveSearchBar';
import LiveSessionSearch from 'Shared/LiveSessionSearch';
import { clearSearch, fetchSessions } from 'Duck/search';
const weakEqual = (val1, val2) => {
@ -54,7 +51,6 @@ const allowedQueryKeys = [
@withLocationHandlers()
@connect(state => ({
filter: state.getIn([ 'filters', 'appliedFilter' ]),
showLive: state.getIn([ 'user', 'account', 'appearance', 'sessionsLive' ]),
variables: state.getIn([ 'customFields', 'list' ]),
sources: state.getIn([ 'customFields', 'sources' ]),
filterValues: state.get('filterValues'),
@ -68,8 +64,7 @@ const allowedQueryKeys = [
fetchFavoriteSessionList,
applyFilter,
addAttribute,
fetchFilterVariables,
fetchIntegrationVariables,
fetchFilterVariables,
fetchSources,
clearEvents,
setActiveTab,
@ -101,15 +96,6 @@ export default class BugFinder extends React.PureComponent {
// keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
// };
// });
// // TODO should cache the response
// props.fetchIntegrationVariables().then(() => {
// defaultFilters[5] = {
// category: 'Metadata',
// type: 'custom',
// keys: this.props.variables.map(({ key }) => ({ type: 'METADATA', key, label: key, icon: 'filters/metadata', isFilter: true })).toJS()
// };
// });
props.fetchSessions();
props.resetFunnel();
props.resetFunnelFilters();
@ -172,28 +158,11 @@ export default class BugFinder extends React.PureComponent {
<div className={cn("side-menu-margined", stl.searchWrapper) }>
<TrackerUpdateMessage />
<NoSessionsMessage />
{/* Recorde Sessions */}
{ activeTab.type !== 'live' && (
<>
<div className="mb-5">
<MainSearchBar />
<SessionSearch />
</div>
{ activeTab.type !== 'live' && <SessionList onMenuItemClick={this.setActiveTab} /> }
</>
)}
{/* Live Sessions */}
{ activeTab.type === 'live' && (
<>
<div className="mb-5">
{/* <LiveSearchBar /> */}
<LiveSessionSearch />
</div>
{ activeTab.type === 'live' && <LiveSessionList /> }
</>
)}
<div className="mb-5">
<MainSearchBar />
<SessionSearch />
</div>
<SessionList onMenuItemClick={this.setActiveTab} />
</div>
</div>
<RehydrateSlidePanel

View file

@ -1,132 +0,0 @@
import React, { useEffect } from 'react';
import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux';
import { NoContent, Loader, LoadMoreButton } from 'UI';
import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch';
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
const PER_PAGE = 20;
interface Props {
loading: Boolean,
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,
}
function LiveSessionList(props: Props) {
const { loading, filters, list, currentPage } = props;
var timeoutId;
const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list);
const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
const addPage = () => props.updateCurrentPage(props.currentPage + 1)
useEffect(() => {
if (filters.size === 0) {
props.addFilterByKeyAndValue(FilterKey.USERID, '');
}
}, []);
useEffect(() => {
const filteredSessions = filters.size > 0 ? props.list.filter(session => {
let hasValidFilter = true;
filters.forEach(filter => {
if (!hasValidFilter) return;
const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase());
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;
}
})
return hasValidFilter;
}) : props.list;
setSessions(filteredSessions);
}, [filters, list]);
useEffect(() => {
props.fetchLiveList();
timeout();
return () => {
clearTimeout(timeoutId)
}
}, [])
const onUserClick = (userId, userAnonymousId) => {
if (userId) {
props.addFilterByKeyAndValue(FilterKey.USERID, userId);
} else {
props.addFilterByKeyAndValue(FilterKey.USERANONYMOUSID, userAnonymousId);
}
}
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchLiveList();
timeout();
}, AUTOREFRESH_INTERVAL);
}
return (
<div>
<NoContent
title={"No live sessions."}
subtext={
<span>
See how to <a target="_blank" className="link" href="https://docs.openreplay.com/plugins/assist">{'enable Assist'}</a> and ensure you're using tracker-assist <span className="font-medium">v3.5.0</span> or higher.
</span>
}
image={<img src="/img/live-sessions.png"
style={{ width: '70%', marginBottom: '30px' }}/>}
show={ !loading && sessions && sessions.size === 0}
>
<Loader loading={ loading }>
{sessions && sessions.take(displayedCount).map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
/>
))}
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={sessions.size}
onClick={addPage}
/>
</Loader>
</NoContent>
</div>
)
}
export default withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(connect(
(state) => ({
list: state.getIn(['sessions', 'liveSessions']),
loading: state.getIn([ 'sessions', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
}),
{ fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue, updateCurrentPage }
)(LiveSessionList));

View file

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

View file

@ -19,6 +19,7 @@ var timeoutId;
allList: state.getIn([ 'sessions', 'list' ]),
total: state.getIn([ 'sessions', 'total' ]),
filters: state.getIn([ 'search', 'instance', 'filters' ]),
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
}), {
applyFilter,
addAttribute,
@ -47,7 +48,7 @@ export default class SessionList extends React.PureComponent {
if (userId) {
this.props.addFilterByKeyAndValue(FilterKey.USERID, userId);
} else {
this.props.addFilterByKeyAndValue(FilterKey.USERANONYMOUSID, userAnonymousId);
this.props.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
}
}
@ -81,7 +82,8 @@ export default class SessionList extends React.PureComponent {
filters,
onMenuItemClick,
allList,
activeTab
activeTab,
metaList,
} = this.props;
const _filterKeys = filters.map(i => i.key);
const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
@ -118,6 +120,7 @@ export default class SessionList extends React.PureComponent {
session={ session }
hasUserFilter={hasUserFilter}
onUserClick={this.onUserClick}
metaList={metaList}
/>
))}
</Loader>

View file

@ -27,6 +27,10 @@ import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
export default class SiteDropdown extends React.PureComponent {
state = { showProductModal: false }
componentDidMount() {
this.props.fetchIntegrationVariables();
}
closeModal = (e, newSite) => {
this.setState({ showProductModal: false })
};

View file

@ -11,6 +11,7 @@ import HeaderInfo from './HeaderInfo';
import SharePopup from '../shared/SharePopup/SharePopup';
import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
import { countries } from 'App/constants';
import SessionMetaList from 'Shared/SessionItem/SessionMetaList';
import stl from './playerBlockHeader.css';
import Issues from './Issues/Issues';
@ -44,6 +45,7 @@ function capitalise(str) {
funnelRef: state.getIn(['funnels', 'navRef']),
siteId: state.getIn([ 'user', 'siteId' ]),
hasSessionsPath: hasSessioPath && !isAssist,
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
}
}, {
toggleFavorite, fetchListIntegration, setSessionPath
@ -94,6 +96,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
userBrowserVersion,
userDeviceType,
live,
metadata,
},
loading,
// live,
@ -102,8 +105,14 @@ export default class PlayerBlockHeader extends React.PureComponent {
fullscreen,
hasSessionsPath,
sessionPath,
metaList,
} = this.props;
const _live = live && !hasSessionsPath;
console.log('metaList', metaList);
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.header, "flex justify-between", { "hidden" : fullscreen}) }>
@ -127,10 +136,15 @@ export default class PlayerBlockHeader extends React.PureComponent {
<div className='ml-auto flex items-center'>
{ live && hasSessionsPath && (
<div className={stl.liveSwitchButton} onClick={() => this.props.setSessionPath('')}>
This Session is Now Continuing Live
</div>
<>
<div className={stl.liveSwitchButton} onClick={() => this.props.setSessionPath('')}>
This Session is Now Continuing Live
</div>
<div className={ stl.divider } />
</>
)}
<SessionMetaList className="" metaList={_metaList} />
<div className={ stl.divider } />
<Popup
trigger={(

View file

@ -0,0 +1,23 @@
.dropdown {
display: flex !important;
padding: 4px 6px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;
&:hover {
background-color: $gray-light;
}
}
.dropdownTrigger {
padding: 4px 8px;
border-radius: 3px;
&:hover {
background-color: $gray-light;
}
}
.dropdownIcon {
margin-top: 2px;
margin-left: 3px;
}

View file

@ -0,0 +1,29 @@
import React from 'react'
import stl from './DropdownPlain.css';
import { Dropdown, Icon } from 'UI';
interface Props {
options: any[];
onChange: (e, { name, value }) => void;
icon?: string;
direction?: string;
value: any;
}
export default function DropdownPlain(props: Props) {
const { value, options, icon = "chevron-down", direction = "left" } = props;
return (
<div>
<Dropdown
value={value}
name="sort"
className={ stl.dropdown }
direction={direction}
options={ options }
onChange={ props.onChange }
// defaultValue={ value }
icon={ icon ? <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> : null }
/>
</div>
)
}

View file

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

View file

@ -14,7 +14,7 @@ interface Props {
}
function FilterItem(props: Props) {
const { isFilter = false, filterIndex, filter } = props;
const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny");
const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny" || filter.operator === "isUndefined");
const replaceFilter = (filter) => {
props.onUpdate({ ...filter, value: [""]});

View file

@ -8,7 +8,11 @@ import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch';
import { addFilterByKeyAndValue, updateCurrentPage, toggleSortOrder } from 'Duck/liveSearch';
import DropdownPlain from 'Shared/DropdownPlain';
import SortOrderButton from 'Shared/SortOrderButton';
import { TimezoneDropdown } from 'UI';
import { capitalize } from 'App/utils';
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
const PER_PAGE = 20;
@ -23,14 +27,21 @@ interface Props {
addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
updateCurrentPage: (page: number) => void,
currentPage: number,
metaList: any,
sortOrder: string,
toggleSortOrder: (sortOrder: string) => void,
}
function LiveSessionList(props: Props) {
const { loading, filters, list, currentPage } = props;
const { loading, filters, list, currentPage, metaList = [], sortOrder } = props;
var timeoutId;
const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list);
const sortOptions = metaList.map(i => ({
text: capitalize(i), value: i
})).toJS();
const [sortBy, setSortBy] = React.useState('');
const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
const addPage = () => props.updateCurrentPage(props.currentPage + 1)
@ -41,6 +52,12 @@ function LiveSessionList(props: Props) {
}
}, []);
useEffect(() => {
if (metaList.size === 0 || !!sortBy) return;
setSortBy(sortOptions[0] && sortOptions[0].value)
}, [metaList]);
useEffect(() => {
const filteredSessions = filters.size > 0 ? props.list.filter(session => {
let hasValidFilter = true;
@ -78,6 +95,10 @@ function LiveSessionList(props: Props) {
}
}
const onSortChange = (e, { value }) => {
setSortBy(value);
}
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchLiveList();
@ -87,6 +108,24 @@ function LiveSessionList(props: Props) {
return (
<div>
<div className="flex mb-6 justify-between items-end">
<div></div>
<div className="flex items-center">
<div className="flex items-center">
<span className="mr-2 color-gray-medium">Timezone</span>
<TimezoneDropdown />
</div>
<div className="flex items-center ml-6 mr-4">
<span className="mr-2 color-gray-medium">Sort By</span>
<DropdownPlain
options={sortOptions}
onChange={onSortChange}
value={sortBy}
/>
</div>
<SortOrderButton onChange={props.toggleSortOrder} sortOrder={sortOrder} />
</div>
</div>
<NoContent
title={"No live sessions."}
subtext={
@ -99,22 +138,25 @@ function LiveSessionList(props: Props) {
show={ !loading && sessions && sessions.size === 0}
>
<Loader loading={ loading }>
{sessions && sessions.take(displayedCount).map(session => (
{sessions && sessions.sortBy(i => i.metadata[sortBy]).update(list => {
return sortOrder === 'desc' ? list.reverse() : list;
}).take(displayedCount).map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
metaList={metaList}
/>
))}
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={sessions.size}
onClick={addPage}
/>
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={sessions.size}
onClick={addPage}
/>
</Loader>
</NoContent>
</div>
@ -127,6 +169,15 @@ export default withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(connect(
loading: state.getIn([ 'sessions', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
sortOrder: state.getIn(['liveSearch', 'sortOrder']),
}),
{ fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue, updateCurrentPage }
{
fetchLiveList,
applyFilter,
addAttribute,
addFilterByKeyAndValue,
updateCurrentPage,
toggleSortOrder,
}
)(LiveSessionList));

View file

@ -3,8 +3,8 @@ import cn from 'classnames'
import stl from './ErrorBars.css'
const GOOD = 'Good'
const LESS_CRITICAL = 'Less Critical'
const CRITICAL = 'Critical'
const LESS_CRITICAL = 'Few Issues'
const CRITICAL = 'Many Issues'
const getErrorState = (count: number) => {
if (count === 0) { return GOOD }
if (count < 2) { return LESS_CRITICAL }
@ -18,20 +18,25 @@ interface Props {
export default function ErrorBars(props: Props) {
const { count = 2 } = props
const state = React.useMemo(() => getErrorState(count), [count])
const showSecondBar = (state === GOOD || state === LESS_CRITICAL || state === CRITICAL)
const showThirdBar = (state === GOOD || state === CRITICAL);
const bgColor = { 'bg-red' : state === CRITICAL, 'bg-green' : state === GOOD, 'bg-red2' : state === LESS_CRITICAL }
return (
<div className="relative" style={{ width: '60px' }}>
<div className="grid grid-cols-3 gap-1 absolute inset-0" style={{ opacity: '1'}}>
<div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div>
{ showSecondBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
{ showThirdBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
</div>
<div className="grid grid-cols-3 gap-1" style={{ opacity: '0.3'}}>
<div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div>
<div className={cn(bgColor, stl.bar)}></div>
<div className={cn("rounded-tr rounded-br", bgColor, stl.bar)}></div>
const isGood = state === GOOD
const showFirstBar = (state === LESS_CRITICAL || state === CRITICAL)
const showSecondBar = (state === CRITICAL)
// const showThirdBar = (state === GOOD || state === CRITICAL);
// const bgColor = { 'bg-red' : state === CRITICAL, 'bg-red2' : state === LESS_CRITICAL }
const bgColor = 'bg-red2'
return isGood ? <></> : (
<div>
<div className="relative" style={{ width: '100px' }}>
<div className="grid grid-cols-3 gap-1 absolute inset-0" style={{ opacity: '1'}}>
{ showFirstBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
{ showSecondBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
{/* { showThirdBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> } */}
</div>
<div className="grid grid-cols-3 gap-1" style={{ opacity: '0.3'}}>
<div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div>
<div className={cn(bgColor, stl.bar)}></div>
{/* <div className={cn("rounded-tr rounded-br", bgColor, stl.bar)}></div> */}
</div>
</div>
<div className="mt-1 color-gray-medium text-sm">{state}</div>
</div>

View file

@ -56,24 +56,23 @@ export default class SessionItem extends React.PureComponent {
userNumericHash,
live,
metadata,
userSessionsCount,
},
timezone,
onUserClick = () => null,
hasUserFilter = false,
disableUser = false
disableUser = false,
metaList = [],
} = this.props;
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isAssist = isRoute(ASSIST_ROUTE, this.props.location.pathname);
console.log('metadata', metadata);
const _metaList = Object.keys(metadata).map(key => {
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
const value = metadata[key];
return { label: key, value };
});
console.log('SessionItem', _metaList);
return (
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
<div className="flex items-start">
@ -84,15 +83,15 @@ export default class SessionItem extends React.PureComponent {
<div style={{ height: "38px" }} className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between">
<div
className={cn({'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
onClick={() => (!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
{userDisplayName}
</div>
<div
className="color-gray-medium text-dotted-underline cursor-pointer"
onClick={() => (!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
30 Sessions
{userSessionsCount} Sessions
</div>
</div>
</div>
@ -111,23 +110,21 @@ export default class SessionItem extends React.PureComponent {
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
</div>
</div>
<div style={{ width: "40%", height: "38px" }} className="px-2 flex flex-col justify-between">
{/* <div className="flex flex-col"> */}
<CountryFlag country={ userCountry } className="mr-2" label />
<div className="color-gray-medium flex items-center">
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ userBrowser } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ userOs } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ userDeviceType } popupProps={{ inverted: true, size: "tiny" }} />
</span>
</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={ userBrowser } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ userOs } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ userDeviceType } popupProps={{ inverted: true, size: "tiny" }} />
</span>
</div>
</div>
{ !isAssist && (
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
@ -139,14 +136,12 @@ export default class SessionItem extends React.PureComponent {
<div className="flex items-center">
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
<Link to={ sessionRoute(sessionId) }>
<Icon name={ viewed ? 'play-fill' : 'play-circle-light' } size="30" color={isAssist ? "tealx" : "teal"} />
<Icon name={ viewed ? 'play-fill' : 'play-circle-light' } size="36" color={isAssist ? "tealx" : "teal"} />
</Link>
</div>
</div>
</div>
{ isAssist && (
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
<SessionMetaList className="mt-4" metaList={_metaList} />
</div>
);
}

View file

@ -0,0 +1,44 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import cn from 'classnames'
interface Props {
sortOrder: string,
onChange?: (sortOrder: string) => void,
}
export default React.memo(function SortOrderButton(props: Props) {
const { sortOrder, onChange = () => null } = props
const isAscending = sortOrder === 'asc'
return (
<div className="flex items-center border">
<Popup
inverted
size="mini"
trigger={
<div
className={cn("p-1 hover:bg-active-blue", { 'cursor-pointer bg-white' : !isAscending, 'bg-active-blue' : isAscending })}
onClick={() => onChange('asc')}
>
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
</div>
}
content={'Ascending'}
/>
<Popup
inverted
size="mini"
trigger={
<div
className={cn("p-1 hover:bg-active-blue border-l", { 'cursor-pointer bg-white' : isAscending, 'bg-active-blue' : !isAscending })}
onClick={() => onChange('desc')}
>
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
</div>
}
content={'Descending'}
/>
</div>
)
})

View file

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

View file

@ -13,7 +13,7 @@ const CountryFlag = React.memo(({ country, className, style = {}, label = false
<Popup
trigger={ knownCountry
? <div className={ cn(`flag flag-${ countryFlag }`, className, stl.default) } />
: <Icon name="flag-na" size="22" className="-mx-1" />
: <Icon name="flag-na" size="22" className="" />
// : <div className={ cn('text-sm', className) }>{ "N/A" }</div>
}
content={ countryName }

View file

@ -15,21 +15,24 @@ const EDIT = editType(name);
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
const APPLY = `${name}/APPLY`;
const UPDATE_CURRENT_PAGE = `${name}/UPDATE_CURRENT_PAGE`;
const TOGGLE_SORT_ORDER = `${name}/TOGGLE_SORT_ORDER`;
const initialState = Map({
list: List(),
instance: new Filter({ filters: [] }),
filterSearchList: {},
currentPage: 1,
sortOrder: 'asc',
});
function reducer(state = initialState, action = {}) {
switch (action.type) {
case EDIT:
return state.mergeIn(['instance'], action.instance);
case UPDATE_CURRENT_PAGE:
return state.set('currentPage', action.page);
case TOGGLE_SORT_ORDER:
return state.set('sortOrder', action.order);
}
return state;
}
@ -98,4 +101,11 @@ export function updateCurrentPage(page) {
type: UPDATE_CURRENT_PAGE,
page,
};
}
export function toggleSortOrder (order) {
return {
type: TOGGLE_SORT_ORDER,
order,
};
}

View file

@ -243,9 +243,12 @@ export const addFilter = (filter) => (dispatch, getState) => {
}
}
export const addFilterByKeyAndValue = (key, value) => (dispatch, getState) => {
export const addFilterByKeyAndValue = (key, value, operator = undefined) => (dispatch, getState) => {
let defaultFilter = filtersMap[key];
defaultFilter.value = value;
if (operator) {
defaultFilter.operator = operator;
}
dispatch(addFilter(defaultFilter));
}

View file

@ -34,7 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
// groupByUser: true,
groupByUser: true,
sort: 'startTs',
order: 'desc',

View file

@ -44,7 +44,7 @@ export const filtersMap = {
[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.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, icon: 'filters/userid' },
[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' },
// PERFORMANCE

View file

@ -36,7 +36,7 @@ export default Record({
stackEvents: List(),
resources: List(),
missedResources: List(),
metadata: List(),
metadata: Map(),
favorite: false,
filterId: '',
messagesUrl: '',
@ -76,6 +76,7 @@ export default Record({
socket: null,
isIOS: false,
revId: '',
userSessionsCount: 0,
}, {
fromJS:({
startTs=0,