+
+
+ {/*
*/}
+
+
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
+ >
+ {userDisplayName}
+
+ {/*
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
+ >
+ {userSessionsCount} Sessions
+
*/}
-
-
-
-
-
-
-
-
+
+
{formatTimeOrDate(startedAt, timezone) }
+
+ {!isAssist && (
+ <>
+
+ { eventsCount }
+ { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }
+
+
·
+ >
+ )}
+
{ live ? : formattedDuration }
+
-
-
-
- { live ?
: formattedDuration }
+
+
+
+
+
+
+
·
+
+
+
+
·
+
+
+
+
-
+ { !isAssist && (
+
+
+
+ )}
- {!live && (
-
-
{ eventsCount }
-
);
}
diff --git a/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx b/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx
new file mode 100644
index 000000000..96b082e96
--- /dev/null
+++ b/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Popup } from 'UI'
+import cn from 'classnames'
+import MetaItem from '../MetaItem';
+import MetaMoreButton from '../MetaMoreButton';
+
+interface Props {
+ className?: string,
+ metaList: []
+}
+const MAX_LENGTH = 3;
+export default function SessionMetaList(props: Props) {
+ const { className = '', metaList } = props
+ return (
+
+ {metaList.slice(0, MAX_LENGTH).map(({ label, value }, index) => (
+
+ ))}
+
+ {metaList.length > MAX_LENGTH && (
+
+ )}
+
+ )
+}
diff --git a/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts b/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts
new file mode 100644
index 000000000..18ad742da
--- /dev/null
+++ b/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionMetaList';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css
index cbf7bb2d1..f7fcde842 100644
--- a/frontend/app/components/shared/SessionItem/sessionItem.css
+++ b/frontend/app/components/shared/SessionItem/sessionItem.css
@@ -12,12 +12,12 @@
user-select: none;
@mixin defaultHover;
border-radius: 3px;
- padding: 10px 10px;
- padding-right: 15px;
- margin-bottom: 15px;
- background-color: white;
- display: flex;
- align-items: center;
+ /* padding: 10px 10px; */
+ /* padding-right: 15px; */
+ /* margin-bottom: 15px; */
+ /* background-color: white; */
+ /* display: flex; */
+ /* align-items: center; */
border: solid thin #EEEEEE;
& .favorite {
diff --git a/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx b/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx
new file mode 100644
index 000000000..6c730b4e8
--- /dev/null
+++ b/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx
@@ -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 (
+
+
onChange('asc')}
+ >
+
+
+ }
+ content={'Ascending'}
+ />
+
+
onChange('desc')}
+ >
+
+
+ }
+ content={'Descending'}
+ />
+
+ )
+})
diff --git a/frontend/app/components/shared/SortOrderButton/index.ts b/frontend/app/components/shared/SortOrderButton/index.ts
new file mode 100644
index 000000000..dedc48a43
--- /dev/null
+++ b/frontend/app/components/shared/SortOrderButton/index.ts
@@ -0,0 +1 @@
+export { default } from './SortOrderButton';
\ No newline at end of file
diff --git a/frontend/app/components/ui/Avatar/Avatar.js b/frontend/app/components/ui/Avatar/Avatar.js
index b369c5e30..fd1ca884b 100644
--- a/frontend/app/components/ui/Avatar/Avatar.js
+++ b/frontend/app/components/ui/Avatar/Avatar.js
@@ -11,14 +11,15 @@ const ICON_LIST = ['icn_chameleon', 'icn_fox', 'icn_gorilla', 'icn_hippo', 'icn_
'icn_wild1', 'icn_wild_bore']
-const Avatar = ({ className, width = "38px", height = "38px", iconSize = 26, seed }) => {
+const Avatar = ({ isAssist = false, className, width = "38px", height = "38px", iconSize = 26, seed }) => {
var iconName = avatarIconName(seed);
return (
);
};
diff --git a/frontend/app/components/ui/Confirmation/Confirmation.js b/frontend/app/components/ui/Confirmation/Confirmation.js
index d791b7fe2..563f383eb 100644
--- a/frontend/app/components/ui/Confirmation/Confirmation.js
+++ b/frontend/app/components/ui/Confirmation/Confirmation.js
@@ -22,7 +22,7 @@ const Confirmation = ({
content={confirmation}
header={header}
className="confirmCustom"
- confirmButton={
}
+ confirmButton={
}
cancelButton={
}
onCancel={() => proceed(false)}
onConfirm={() => proceed(true)}
diff --git a/frontend/app/components/ui/CountryFlag/CountryFlag.js b/frontend/app/components/ui/CountryFlag/CountryFlag.js
index 0ba14c967..db6a4491b 100644
--- a/frontend/app/components/ui/CountryFlag/CountryFlag.js
+++ b/frontend/app/components/ui/CountryFlag/CountryFlag.js
@@ -1,24 +1,34 @@
import cn from 'classnames';
import { countries } from 'App/constants';
-import { Popup } from 'UI';
+import { Popup, Icon } from 'UI';
import stl from './countryFlag.css';
-const CountryFlag = ({ country, className }) => {
+const CountryFlag = React.memo(({ country, className, style = {}, label = false }) => {
const knownCountry = !!country && country !== 'UN';
- const countryFlag = knownCountry ? country.toLowerCase() : '';
- const countryName = knownCountry ? countries[ country ] : 'Unknown Country';
+ const countryFlag = knownCountry ? country.toLowerCase() : '';
+ const countryName = knownCountry ? countries[ country ] : 'Unknown Country';
+
return (
-
- :
{ "N/A" }
- }
- content={ countryName }
- inverted
- size="tiny"
- />
+
+
+ : (
+
+ )
+ // :
{ "N/A" }
+ }
+ content={ countryName }
+ inverted
+ size="tiny"
+ />
+ { knownCountry && label &&
{ countryName }
}
+
);
-}
+})
CountryFlag.displayName = "CountryFlag";
diff --git a/frontend/app/components/ui/CountryFlag/countryFlag.css b/frontend/app/components/ui/CountryFlag/countryFlag.css
index 29a5d880b..4cbc1f39b 100644
--- a/frontend/app/components/ui/CountryFlag/countryFlag.css
+++ b/frontend/app/components/ui/CountryFlag/countryFlag.css
@@ -1,4 +1,8 @@
.default {
width: 22px !important;
height: 14px !important;
+}
+
+.label {
+ line-height: 0 !important;
}
\ No newline at end of file
diff --git a/frontend/app/components/ui/IconButton/IconButton.js b/frontend/app/components/ui/IconButton/IconButton.js
index eb708f21a..6aa9f3d5f 100644
--- a/frontend/app/components/ui/IconButton/IconButton.js
+++ b/frontend/app/components/ui/IconButton/IconButton.js
@@ -9,8 +9,10 @@ const IconButton = React.forwardRef(({
onClick,
plain = false,
shadow = false,
+ red = false,
primary = false,
primaryText = false,
+ redText = false,
outline = false,
loading = false,
roundedOutline = false,
@@ -40,7 +42,9 @@ const IconButton = React.forwardRef(({
[ stl.active ]: active,
[ stl.shadow ]: shadow,
[ stl.primary ]: primary,
+ [ stl.red ]: red,
[ stl.primaryText ]: primaryText,
+ [ stl.redText ]: redText,
[ stl.outline ]: outline,
[ stl.circle ]: circle,
[ stl.roundedOutline ]: roundedOutline,
diff --git a/frontend/app/components/ui/IconButton/iconButton.css b/frontend/app/components/ui/IconButton/iconButton.css
index 1685ca4d6..b34909039 100644
--- a/frontend/app/components/ui/IconButton/iconButton.css
+++ b/frontend/app/components/ui/IconButton/iconButton.css
@@ -67,17 +67,47 @@
&.primary {
background-color: $teal;
- box-shadow: 0 0 0 1px rgba(62, 170, 175, .8) inset !important;
+ box-shadow: 0 0 0 1px $teal inset !important;
& .icon {
fill: white;
}
+ & svg {
+ fill: white;
+ }
+
+ & .label {
+ color: white !important;
+ }
+
&:hover {
background-color: $teal-dark;
}
}
+ &.red {
+ background-color: $red;
+ box-shadow: 0 0 0 1px $red inset !important;
+
+ & .icon {
+ fill: white;
+ }
+
+ & svg {
+ fill: white;
+ }
+
+ & .label {
+ color: white !important;
+ }
+
+ &:hover {
+ background-color: $red;
+ filter: brightness(90%);
+ }
+ }
+
&.outline {
box-shadow: 0 0 0 1px $teal inset !important;
& .label {
@@ -116,4 +146,14 @@
.primaryText .label {
color: $teal !important;
+}
+
+.redText {
+ & .label {
+ color: $red !important;
+ }
+
+ & svg {
+ fill: $red;
+ }
}
\ No newline at end of file
diff --git a/frontend/app/components/ui/Loader/loader.css b/frontend/app/components/ui/Loader/loader.css
index 454e8f2db..76f31da26 100644
--- a/frontend/app/components/ui/Loader/loader.css
+++ b/frontend/app/components/ui/Loader/loader.css
@@ -1,7 +1,7 @@
.loader {
display: block;
margin: auto;
- background-image: svg-load(openreplay-preloader.svg, fill=#CCC);
+ background-image: svg-load(openreplay-preloader.svg, fill=#ffffff00);
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
diff --git a/frontend/app/components/ui/TextEllipsis/textEllipsis.css b/frontend/app/components/ui/TextEllipsis/textEllipsis.css
index 9baca35cc..9919f9160 100644
--- a/frontend/app/components/ui/TextEllipsis/textEllipsis.css
+++ b/frontend/app/components/ui/TextEllipsis/textEllipsis.css
@@ -1,7 +1,7 @@
.textEllipsis {
text-overflow: ellipsis;
overflow: hidden;
- display: inline-block;
+ /* display: inline-block; */
white-space: nowrap;
max-width: 100%;
}
\ No newline at end of file
diff --git a/frontend/app/duck/liveSearch.js b/frontend/app/duck/liveSearch.js
index bebdc9a35..5c9364e96 100644
--- a/frontend/app/duck/liveSearch.js
+++ b/frontend/app/duck/liveSearch.js
@@ -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,
+ };
}
\ No newline at end of file
diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js
index 8b4ab8e12..ad4ea944c 100644
--- a/frontend/app/duck/search.js
+++ b/frontend/app/duck/search.js
@@ -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));
}
diff --git a/frontend/app/duck/sessions.js b/frontend/app/duck/sessions.js
index 2ab1e5a5a..f3df333c7 100644
--- a/frontend/app/duck/sessions.js
+++ b/frontend/app/duck/sessions.js
@@ -270,12 +270,7 @@ function init(session) {
}
export const fetchList = (params = {}, clear = false, live = false) => (dispatch, getState) => {
- const activeTab = getState().getIn([ 'sessions', 'activeTab' ]);
-
- return dispatch((activeTab && activeTab.type === 'live' || live )? {
- types: FETCH_LIVE_LIST.toArray(),
- call: client => client.post('/assist/sessions', params),
- } : {
+ return dispatch({
types: FETCH_LIST.toArray(),
call: client => client.post('/sessions/search2', params),
clear,
@@ -283,13 +278,6 @@ export const fetchList = (params = {}, clear = false, live = false) => (dispatch
})
}
-// export const fetchLiveList = (id) => (dispatch, getState) => {
-// return dispatch({
-// types: FETCH_LIVE_LIST.toArray(),
-// call: client => client.get('/assist/sessions'),
-// })
-// }
-
export function fetchErrorStackList(sessionId, errorId) {
return {
types: FETCH_ERROR_STACK.toArray(),
diff --git a/frontend/app/routes.js b/frontend/app/routes.js
index cdccc6327..0f10950df 100644
--- a/frontend/app/routes.js
+++ b/frontend/app/routes.js
@@ -82,6 +82,7 @@ const routerOBTabString = `:activeTab(${ Object.values(OB_TABS).join('|') })`;
export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`;
export const sessions = params => queried('/sessions', params);
+export const assist = params => queried('/assist', params);
export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash);
export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash);
@@ -105,7 +106,7 @@ export const METRICS_QUERY_KEY = 'metrics';
export const SOURCE_QUERY_KEY = 'source';
export const WIDGET_QUERY_KEY = 'widget';
-const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ];
+const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), assist(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ];
const routeNeedsSiteId = path => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
const siteIdToUrl = (siteId = ':siteId') => {
if (Array.isArray(siteId)) {
@@ -128,7 +129,7 @@ export function isRoute(route, path){
routeParts.every((p, i) => p.startsWith(':') || p === pathParts[ i ]);
}
-const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), dashboard(), errors(), onboarding('')];
+const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), errors(), onboarding('')];
export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path));
diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css
index fc366bc39..81e5ab814 100644
--- a/frontend/app/styles/main.css
+++ b/frontend/app/styles/main.css
@@ -123,4 +123,22 @@
&:hover {
background-color: $active-blue;
}
+}
+
+.text-dotted-underline {
+ text-decoration: underline dotted !important;
+}
+
+.divider {
+ width: 1px;
+ margin: 0 15px;
+ background-color: $gray-light;
+}
+
+.divider-h {
+ height: 1px;
+ width: 100%;
+
+ margin: 25px 0;
+ background-color: $gray-light;
}
\ No newline at end of file
diff --git a/frontend/app/styles/semantic.css b/frontend/app/styles/semantic.css
index 7fe14933b..0bfa64bf4 100644
--- a/frontend/app/styles/semantic.css
+++ b/frontend/app/styles/semantic.css
@@ -336,4 +336,13 @@ a:hover {
overflow: hidden;
text-overflow: ellipsis;
margin-right: 15px;
+}
+
+.ui.mini.modal>.header:not(.ui) {
+ padding: 10px 17px !important;
+ font-size: 16px !important;
+}
+
+.ui.modal>.content {
+ padding: 10px 17px !important;
}
\ No newline at end of file
diff --git a/frontend/app/svg/icons/flag-na.svg b/frontend/app/svg/icons/flag-na.svg
new file mode 100644
index 000000000..ca42ac405
--- /dev/null
+++ b/frontend/app/svg/icons/flag-na.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app/svg/openreplay-preloader.svg b/frontend/app/svg/openreplay-preloader.svg
index 6bf6be13f..9a2cf1c33 100644
--- a/frontend/app/svg/openreplay-preloader.svg
+++ b/frontend/app/svg/openreplay-preloader.svg
@@ -1 +1,7 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js
index be186e4f9..6d3b177f9 100644
--- a/frontend/app/types/filter/filter.js
+++ b/frontend/app/types/filter/filter.js
@@ -34,6 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
+ groupByUser: true,
sort: 'startTs',
order: 'desc',
diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js
index d2125acdb..d4cb905a1 100644
--- a/frontend/app/types/filter/newFilter.js
+++ b/frontend/app/types/filter/newFilter.js
@@ -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
diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js
index 44dce3ab0..5eda0c987 100644
--- a/frontend/app/types/session/session.js
+++ b/frontend/app/types/session/session.js
@@ -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,