feat(ui) - assist ui - wip
This commit is contained in:
parent
54ebc487cc
commit
7c9df8c196
22 changed files with 266 additions and 241 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './LiveSessionList'
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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={(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
1
frontend/app/components/shared/DropdownPlain/index.ts
Normal file
1
frontend/app/components/shared/DropdownPlain/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DropdownPlain';
|
||||
|
|
@ -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: [""]});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
1
frontend/app/components/shared/SortOrderButton/index.ts
Normal file
1
frontend/app/components/shared/SortOrderButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SortOrderButton';
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default Record({
|
|||
rangeValue,
|
||||
startDate,
|
||||
endDate,
|
||||
// groupByUser: true,
|
||||
groupByUser: true,
|
||||
|
||||
sort: 'startTs',
|
||||
order: 'desc',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue