@@ -127,10 +136,15 @@ export default class PlayerBlockHeader extends React.PureComponent {
{ live && hasSessionsPath && (
-
this.props.setSessionPath('')}>
- This Session is Now Continuing Live
-
+ <>
+
this.props.setSessionPath('')}>
+ This Session is Now Continuing Live
+
+
+ >
)}
+
+
void;
+ icon?: string;
+ direction?: string;
+ value: any;
+}
+
+export default function DropdownPlain(props: Props) {
+ const { value, options, icon = "chevron-down", direction = "left" } = props;
+ return (
+
+ : null }
+ />
+
+ )
+}
diff --git a/frontend/app/components/shared/DropdownPlain/index.ts b/frontend/app/components/shared/DropdownPlain/index.ts
new file mode 100644
index 000000000..3b2d43dcf
--- /dev/null
+++ b/frontend/app/components/shared/DropdownPlain/index.ts
@@ -0,0 +1 @@
+export { default } from './DropdownPlain';
\ No newline at end of file
diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
index a8760428b..db0bedf32 100644
--- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
+++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
@@ -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: [""]});
diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
index 9fd3f8e0e..3a4fcc275 100644
--- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
+++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
@@ -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 (
+
+
+
+
+ Timezone
+
+
+
+ Sort By
+
+
+
+
+
- {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 => (
))}
-
+
@@ -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));
diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx
index 31c233414..3b49db9c6 100644
--- a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx
+++ b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx
@@ -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 (
-
-
-
- { showSecondBar &&
}
- { showThirdBar &&
}
-
-
-
-
-
+ 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 ? <>> : (
+
+
+
+ { showFirstBar &&
}
+ { showSecondBar &&
}
+ {/* { showThirdBar &&
} */}
+
+
{state}
diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js
index 4956342f0..245dcb58b 100644
--- a/frontend/app/components/shared/SessionItem/SessionItem.js
+++ b/frontend/app/components/shared/SessionItem/SessionItem.js
@@ -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 (
@@ -84,15 +83,15 @@ export default class SessionItem extends React.PureComponent {
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
+ onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
{userDisplayName}
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
+ onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
- 30 Sessions
+ {userSessionsCount} Sessions
@@ -111,23 +110,21 @@ export default class SessionItem extends React.PureComponent {
{ live ? : formattedDuration }
-
- {/*
*/}
-
-
-
-
-
-
·
-
-
-
-
·
-
-
-
-
- {/*
*/}
+
+
+
+
+
+
+
·
+
+
+
+
·
+
+
+
+
{ !isAssist && (
@@ -139,14 +136,12 @@ export default class SessionItem extends React.PureComponent {
- { isAssist && (
-
- )}
+
);
}
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/CountryFlag/CountryFlag.js b/frontend/app/components/ui/CountryFlag/CountryFlag.js
index 6bcc6672b..f817cf65f 100644
--- a/frontend/app/components/ui/CountryFlag/CountryFlag.js
+++ b/frontend/app/components/ui/CountryFlag/CountryFlag.js
@@ -13,7 +13,7 @@ const CountryFlag = React.memo(({ country, className, style = {}, label = false
- :
+ :
// :
{ "N/A" }
}
content={ countryName }
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/types/filter/filter.js b/frontend/app/types/filter/filter.js
index df31f1d0e..6d3b177f9 100644
--- a/frontend/app/types/filter/filter.js
+++ b/frontend/app/types/filter/filter.js
@@ -34,7 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
- // groupByUser: true,
+ 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,