diff --git a/frontend/app/Router.js b/frontend/app/Router.js index c3a1721a6..0c0e7433a 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -11,6 +11,7 @@ import UpdatePassword from 'Components/UpdatePassword/UpdatePassword'; import ClientPure from 'Components/Client/Client'; import OnboardingPure from 'Components/Onboarding/Onboarding'; import SessionPure from 'Components/Session/Session'; +import AssistPure from 'Components/Assist'; import BugFinderPure from 'Components/BugFinder/BugFinder'; import DashboardPure from 'Components/Dashboard/Dashboard'; import ErrorsPure from 'Components/Errors/Errors'; @@ -18,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'; @@ -29,6 +31,7 @@ import { setSessionPath } from 'Duck/sessions'; const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); const Session = withSiteIdUpdater(SessionPure); +const Assist = withSiteIdUpdater(AssistPure); const Client = withSiteIdUpdater(ClientPure); const Onboarding = withSiteIdUpdater(OnboardingPure); const Errors = withSiteIdUpdater(ErrorsPure); @@ -39,6 +42,7 @@ const withObTab = routes.withObTab; const DASHBOARD_PATH = routes.dashboard(); const SESSIONS_PATH = routes.sessions(); +const ASSIST_PATH = routes.assist(); const ERRORS_PATH = routes.errors(); const ERROR_PATH = routes.error(); const FUNNEL_PATH = routes.funnel(); @@ -74,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 = { @@ -83,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(); @@ -145,6 +153,7 @@ class Router extends React.Component { } + diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 74f2095f8..77730f7b1 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -1,11 +1,25 @@ import React from 'react'; -import ChatWindow from './ChatWindow'; +import LiveSessionList from 'Shared/LiveSessionList'; +import LiveSessionSearch from 'Shared/LiveSessionSearch'; +import cn from 'classnames' +import withPageTitle from 'HOCs/withPageTitle'; +import withPermissions from 'HOCs/withPermissions' - -export default function Assist() { +// @withPageTitle("Assist - OpenReplay") +function Assist() { return ( -
- {/* */} +
+
+ {/*
+
*/} +
+ +
+ +
+
) } + +export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(Assist)); diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.css b/frontend/app/components/Assist/ChatControls/ChatControls.css index 7ec77f758..b5a03ed10 100644 --- a/frontend/app/components/Assist/ChatControls/ChatControls.css +++ b/frontend/app/components/Assist/ChatControls/ChatControls.css @@ -15,7 +15,7 @@ &.disabled { /* background-color: red; */ & svg { - fill: red; + fill: $red; } } } diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.tsx b/frontend/app/components/Assist/ChatControls/ChatControls.tsx index 61803bc2f..fd0430282 100644 --- a/frontend/app/components/Assist/ChatControls/ChatControls.tsx +++ b/frontend/app/components/Assist/ChatControls/ChatControls.tsx @@ -28,17 +28,17 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props return (
-
-
-
-
diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx index 6da23c7e8..36bc0765b 100644 --- a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -20,7 +20,6 @@ const ChatWindow: FC = function ChatWindow({ userId, incomeStream, localS const [localVideoEnabled, setLocalVideoEnabled] = useState(false) const [remoteVideoEnabled, setRemoteVideoEnabled] = useState(false) - useEffect(() => { if (!incomeStream) { return } const iid = setInterval(() => { @@ -42,9 +41,9 @@ const ChatWindow: FC = function ChatWindow({ userId, incomeStream, localS className={cn(stl.wrapper, "fixed radius bg-white shadow-xl mt-16")} style={{ width: '280px' }} > -
-
Meeting {userId}
- +
+
Talking to {userId ? userId : 'Anonymous User'}
+
diff --git a/frontend/app/components/Assist/ChatWindow/chatWindow.css b/frontend/app/components/Assist/ChatWindow/chatWindow.css index 0f1f7694b..8bb359695 100644 --- a/frontend/app/components/Assist/ChatWindow/chatWindow.css +++ b/frontend/app/components/Assist/ChatWindow/chatWindow.css @@ -1,9 +1,10 @@ .wrapper { background-color: white; - border: solid thin #000; + border: solid thin $gray-light; border-radius: 3px; position: fixed; - width: 300px; + width: 300px; + box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); } .headerTitle { diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 8b2cf5245..2d7a7baf1 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { Popup, Icon } from 'UI' +import { Popup, Icon, IconButton } from 'UI' import { connect } from 'react-redux' import cn from 'classnames' import { toggleChatWindow } from 'Duck/sessions'; @@ -77,27 +77,48 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting const cannotCall = (peerConnectionStatus !== ConnectionStatus.Connected) || (isEnterprise && !hasPermission) + const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled return (
+
+ {/* + { 'Remote Control' } */} + +
+ - - { onCall ? 'End Call' : 'Call' } + { onCall ? 'End Call' : 'Call' } */} +
} content={ cannotCall ? "You don’t have the permissions to perform this action." : `Call ${userId ? userId : 'User'}` } @@ -105,22 +126,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus inverted position="top right" /> -
- - { 'Remote Control' } -
+
{ onCall && callObject && }
diff --git a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx index b7ecce906..07af99e24 100644 --- a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx +++ b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { SlideModal, Icon } from 'UI'; +import { SlideModal, Avatar, Icon } from 'UI'; import SessionList from '../SessionList'; import stl from './assistTabs.css' interface Props { userId: any, + userNumericHash: any, } const AssistTabs = (props: Props) => { @@ -15,16 +16,16 @@ const AssistTabs = (props: Props) => {
{props.userId && ( <> +
+ {/* */} + +
{props.userId}'s
+
setShowMenu(!showMenu)} > - More Live Sessions -
- by -
- -
{props.userId}
+ Active Sessions
)} diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 80f819ecd..f0024c20a 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -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 {
- - {/* Recorde Sessions */} - { activeTab.type !== 'live' && ( - <> -
- - -
- { activeTab.type !== 'live' && } - - )} - - {/* Live Sessions */} - { activeTab.type === 'live' && ( - <> -
- {/* */} - -
- { activeTab.type === 'live' && } - - )} +
+ + +
+
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} /> ))} diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index f0b82e367..67d1c4aaf 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -5,6 +5,7 @@ import SortDropdown from '../Filters/SortDropdown'; import DateRange from '../DateRange'; import { TimezoneDropdown } from 'UI'; import { numberWithCommas } from 'App/utils'; +import DropdownPlain from 'Shared/DropdownPlain'; const DEFAULT_SORT = 'startTs'; const DEFAULT_ORDER = 'desc'; @@ -38,6 +39,17 @@ function SessionListHeader({
+ {/*
+ Session View + {}} + value='list' + /> +
*/}
Timezone diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index 7275d9ac0..2436d4be6 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -73,7 +73,7 @@ function SessionsMenu(props) { /> ))} -
+ {/*
onMenuItemClick({ name: 'Assist', type: 'live' })} /> -
+
*/}
diff --git a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js index b12b2cf8c..48cf2ce23 100644 --- a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js +++ b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js @@ -26,7 +26,7 @@ export default class ProfileSettings extends React.PureComponent {
-
+
@@ -36,7 +36,7 @@ export default class ProfileSettings extends React.PureComponent {
-
+
@@ -46,7 +46,7 @@ export default class ProfileSettings extends React.PureComponent {
-
+
@@ -58,7 +58,7 @@ export default class ProfileSettings extends React.PureComponent { { !isEnterprise && ( <> -
+

{ 'Data Collection' }

@@ -71,7 +71,7 @@ export default class ProfileSettings extends React.PureComponent { { account.license && ( <> -
+
diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index a133c42d7..38d958c17 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -4,6 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom'; import cn from 'classnames'; import { sessions, + assist, client, errors, dashboard, @@ -27,6 +28,7 @@ import Alerts from '../Alerts/Alerts'; const DASHBOARD_PATH = dashboard(); const SESSIONS_PATH = sessions(); +const ASSIST_PATH = assist(); const ERRORS_PATH = errors(); const CLIENT_PATH = client(CLIENT_DEFAULT_TAB); const AUTOREFRESH_INTERVAL = 30 * 1000; @@ -86,6 +88,13 @@ const Header = (props) => { > { 'Sessions' } + + { 'Assist' } + ({ @@ -21,10 +22,15 @@ import { clearSearch } from 'Duck/search'; pushNewSite, init, clearSearch, + fetchIntegrationVariables, }) export default class SiteDropdown extends React.PureComponent { state = { showProductModal: false } + componentDidMount() { + this.props.fetchIntegrationVariables(); + } + closeModal = (e, newSite) => { this.setState({ showProductModal: false }) }; @@ -37,6 +43,7 @@ export default class SiteDropdown extends React.PureComponent { switchSite = (siteId) => { this.props.setSiteId(siteId); this.props.clearSearch(); + this.props.fetchIntegrationVariables(); } render() { diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index 878ddf10d..81b4df5e0 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -9,9 +9,7 @@ import { init as initPlayer, clean as cleanPlayer, } from 'Player'; -import withPermissions from 'HOCs/withPermissions' -import Assist from 'Components/Assist' - +import withPermissions from 'HOCs/withPermissions'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import EventsBlock from '../Session_/EventsBlock'; @@ -54,7 +52,6 @@ function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, l return ( - { showAssist && }
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index fdada40e7..b21d45f77 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -297,7 +297,7 @@ export default class Controls extends React.Component { >
{ speed + 'x' }
-
+ } + 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" - /> +
+ + : ( +
+ +
Unknown Country
+
+ ) + // :
{ "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,