From 40b88446d190472ee206e2bf368b58f8079e8129 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 18 Feb 2022 15:37:14 +0100 Subject: [PATCH 1/8] feat(ui) - assist ui - wip --- frontend/app/Router.js | 4 + frontend/app/components/Assist/Assist.tsx | 17 ++- .../BugFinder/SessionsMenu/SessionsMenu.js | 4 +- frontend/app/components/Header/Header.js | 9 ++ frontend/app/components/Session/LivePlayer.js | 5 +- .../FilterSelection/FilterSelection.tsx | 6 +- .../LiveSessionList/LiveSessionList.tsx | 132 ++++++++++++++++++ .../shared/LiveSessionList/index.js | 1 + .../SessionItem/ErrorBars/ErrorBars.tsx | 36 +++++ .../shared/SessionItem/ErrorBars/index.ts | 1 + .../shared/SessionItem/SessionItem.js | 107 +++++++------- .../SessionMetaList/SessionMetaList.tsx | 48 +++++++ .../SessionItem/SessionMetaList/index.ts | 1 + .../shared/SessionItem/sessionItem.css | 12 +- frontend/app/routes.js | 5 +- frontend/app/types/filter/filter.js | 1 + 16 files changed, 318 insertions(+), 71 deletions(-) create mode 100644 frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx create mode 100644 frontend/app/components/shared/LiveSessionList/index.js create mode 100644 frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx create mode 100644 frontend/app/components/shared/SessionItem/ErrorBars/index.ts create mode 100644 frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx create mode 100644 frontend/app/components/shared/SessionItem/SessionMetaList/index.ts diff --git a/frontend/app/Router.js b/frontend/app/Router.js index c3a1721a6..d208c1baa 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'; @@ -29,6 +30,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 +41,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(); @@ -145,6 +148,7 @@ class Router extends React.Component { } + diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 74f2095f8..2180bfc8e 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -1,11 +1,20 @@ import React from 'react'; -import ChatWindow from './ChatWindow'; - +import LiveSessionList from 'Shared/LiveSessionList'; +import LiveSessionSearch from 'Shared/LiveSessionSearch'; +import cn from 'classnames' export default function Assist() { return ( -
- {/* */} +
+
+ {/*
+
*/} +
+ +
+ +
+
) } 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/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' } + - { showAssist && }
diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx index 33bbbe3ad..95b8eaa8d 100644 --- a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -4,9 +4,9 @@ import LiveFilterModal from '../LiveFilterModal'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import { Icon } from 'UI'; import { connect } from 'react-redux'; -import { dashboard as dashboardRoute, isRoute } from "App/routes"; +import { assist as assistRoute, isRoute } from "App/routes"; -const DASHBOARD_ROUTE = dashboardRoute(); +const ASSIST_ROUTE = assistRoute(); interface Props { filter?: any; // event/filter @@ -43,7 +43,7 @@ function FilterSelection(props: Props) { {showModal && (
- { (isLive && !isRoute(DASHBOARD_ROUTE, window.location.pathname)) ? : } + { isRoute(ASSIST_ROUTE, window.location.pathname) ? : }
)}
diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx new file mode 100644 index 000000000..9fd3f8e0e --- /dev/null +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -0,0 +1,132 @@ +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, + fetchLiveList: () => Promise, + 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 ( +
+ + See how to {'enable Assist'} and ensure you're using tracker-assist v3.5.0 or higher. + + } + image={} + show={ !loading && sessions && sessions.size === 0} + > + + {sessions && sessions.take(displayedCount).map(session => ( + + ))} + + + + +
+ ) +} + +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)); diff --git a/frontend/app/components/shared/LiveSessionList/index.js b/frontend/app/components/shared/LiveSessionList/index.js new file mode 100644 index 000000000..eb38fa3e7 --- /dev/null +++ b/frontend/app/components/shared/LiveSessionList/index.js @@ -0,0 +1 @@ +export { default } from './LiveSessionList' \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx new file mode 100644 index 000000000..64c7f91b4 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import cn from 'classnames' + +const GOOD = 'Good' +const LESS_CRITICAL = 'Less Critical' +const CRITICAL = 'Critical' +const getErrorState = (count: number) => { + if (count === 0) { return GOOD } + if (count < 2) { return LESS_CRITICAL } + return CRITICAL +} + + +interface Props { + count?: number +} +export default function ErrorBars(props: Props) { + const { count = 2 } = props + const state = React.useCallback(() => getErrorState(count), [count])() + const bgColor = { 'bg-red' : state === CRITICAL, 'bg-green' : state === GOOD, 'bg-red2' : state === LESS_CRITICAL } + return ( +
+
+
+ { (state === GOOD || state === LESS_CRITICAL || state === CRITICAL) &&
} + { (state === GOOD || state === CRITICAL) &&
} +
+
+
+
+
+
+
{state}
+
+ ) +} diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/index.ts b/frontend/app/components/shared/SessionItem/ErrorBars/index.ts new file mode 100644 index 000000000..b6291d438 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/ErrorBars/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorBars'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 3abf12ca8..7d39e8e4e 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -18,6 +18,8 @@ import LiveTag from 'Shared/LiveTag'; import Bookmark from 'Shared/Bookmark'; import Counter from './Counter' import { withRouter } from 'react-router-dom'; +import SessionMetaList from './SessionMetaList'; +import ErrorBars from './ErrorBars'; const Label = ({ label = '', color = 'color-gray-medium'}) => (
{label}
@@ -61,64 +63,69 @@ export default class SessionItem extends React.PureComponent { const hasUserId = userId || userAnonymousId; return ( -
-
-
- -
-
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} - > - +
+
+
+
+
+
+
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} + > + {userDisplayName} +
+
30 Sessions
-
+
+
{formatTimeOrDate(startedAt, timezone) }
+
+ {!live && ( +
+ { eventsCount } + { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } +
+ )} + - +
{ live ? : formattedDuration }
+
+
+
+
+ +
+ {userBrowser} - + {userOs} - + {userDeviceType} +
+
+
+
+
-
-
- - - - -
-
-
-
- { live ? : formattedDuration } -
-
- {!live && ( -
-
{ eventsCount }
-
+ ) +} 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/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/types/filter/filter.js b/frontend/app/types/filter/filter.js index be186e4f9..df31f1d0e 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', From 250eaf1eb67ebbd8a1efcbd65523d88d54a170bd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 21 Feb 2022 16:41:04 +0100 Subject: [PATCH 2/8] feat(ui) - assist ui - wip --- frontend/app/components/Assist/Assist.tsx | 6 +- .../AssistActions/AssistActions.tsx | 43 +++---- .../components/AssistTabs/AssistTabs.tsx | 11 +- .../app/components/BugFinder/BugFinder.js | 14 +-- .../app/components/Header/SiteDropdown.js | 3 + .../components/Session_/PlayerBlockHeader.js | 45 ++++++-- .../SessionInfoItem/SessionInfoItem.tsx | 24 ++++ .../Session_/SessionInfoItem/index.ts | 1 + .../components/Session_/playerBlockHeader.css | 4 +- .../SessionItem/ErrorBars/ErrorBars.css | 3 + .../SessionItem/ErrorBars/ErrorBars.tsx | 21 ++-- .../shared/SessionItem/MetaItem/MetaItem.tsx | 22 ++++ .../shared/SessionItem/MetaItem/index.ts | 1 + .../MetaMoreButton/MetaMoreButton.tsx | 32 ++++++ .../SessionItem/MetaMoreButton/index.ts | 1 + .../shared/SessionItem/SessionItem.js | 107 +++++++++++------- .../SessionMetaList/SessionMetaList.tsx | 31 +---- frontend/app/components/ui/Avatar/Avatar.js | 5 +- .../components/ui/CountryFlag/CountryFlag.js | 30 ++--- .../components/ui/CountryFlag/countryFlag.css | 4 + .../components/ui/IconButton/IconButton.js | 4 + .../components/ui/IconButton/iconButton.css | 40 +++++++ .../ui/TextEllipsis/textEllipsis.css | 2 +- frontend/app/styles/main.css | 11 ++ 24 files changed, 325 insertions(+), 140 deletions(-) create mode 100644 frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx create mode 100644 frontend/app/components/Session_/SessionInfoItem/index.ts create mode 100644 frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.css create mode 100644 frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx create mode 100644 frontend/app/components/shared/SessionItem/MetaItem/index.ts create mode 100644 frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx create mode 100644 frontend/app/components/shared/SessionItem/MetaMoreButton/index.ts diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 2180bfc8e..476b7ac5c 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -2,8 +2,10 @@ import React from 'react'; import LiveSessionList from 'Shared/LiveSessionList'; import LiveSessionSearch from 'Shared/LiveSessionSearch'; import cn from 'classnames' +import withPageTitle from 'HOCs/withPageTitle'; -export default function Assist() { +// @withPageTitle("Assist - OpenReplay") +function Assist() { return (
@@ -18,3 +20,5 @@ export default function Assist() {
) } + +export default withPageTitle("Assist - OpenReplay")(Assist); diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 8b2cf5245..d3390e47c 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,9 +77,28 @@ 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 +125,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..5c1641f78 100644 --- a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx +++ b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx @@ -15,16 +15,15 @@ const AssistTabs = (props: Props) => {
{props.userId && ( <> +
+ +
{props.userId}
+
setShowMenu(!showMenu)} > - More Live Sessions -
- by -
- -
{props.userId}
+ All Active Sessions
)} diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 80f819ecd..0ebfa80bb 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -102,13 +102,13 @@ export default class BugFinder extends React.PureComponent { // }; // }); // // 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.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(); diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index 37057866b..502b40085 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -10,6 +10,7 @@ import styles from './siteDropdown.css'; import cn from 'classnames'; import NewSiteForm from '../Client/Sites/NewSiteForm'; import { clearSearch } from 'Duck/search'; +import { fetchList as fetchIntegrationVariables } from 'Duck/customField'; @withRouter @connect(state => ({ @@ -21,6 +22,7 @@ import { clearSearch } from 'Duck/search'; pushNewSite, init, clearSearch, + fetchIntegrationVariables, }) export default class SiteDropdown extends React.PureComponent { state = { showProductModal: false } @@ -37,6 +39,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_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index d78e115c8..e32fd2bb6 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -3,19 +3,21 @@ import { withRouter } from 'react-router-dom'; import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames'; import { formatTimeOrDate } from 'App/date'; import { sessions as sessionsRoute, withSiteId } from 'App/routes'; -import { Icon, CountryFlag, IconButton, BackLink } from 'UI'; +import { Icon, CountryFlag, IconButton, BackLink, Popup } from 'UI'; import { toggleFavorite, setSessionPath } from 'Duck/sessions'; import cn from 'classnames'; import { connectPlayer } from 'Player'; import HeaderInfo from './HeaderInfo'; import SharePopup from '../shared/SharePopup/SharePopup'; import { fetchList as fetchListIntegration } from 'Duck/integrations/actions'; +import { countries } from 'App/constants'; import stl from './playerBlockHeader.css'; import Issues from './Issues/Issues'; import Autoplay from './Autoplay'; import AssistActions from '../Assist/components/AssistActions'; import AssistTabs from '../Assist/components/AssistTabs'; +import SessionInfoItem from './SessionInfoItem' const SESSIONS_ROUTE = sessionsRoute(); @@ -53,11 +55,13 @@ export default class PlayerBlockHeader extends React.PureComponent { this.props.fetchListIntegration('issues') } - getDimension = (width, height) => ( -
- { width || 'x' } { height || 'x' } -
- ); + getDimension = (width, height) => { + return width && height ? ( +
+ { width || 'x' } { height || 'x' } +
+ ) : Not Available; + } backHandler = () => { const { history, siteId, sessionPath } = this.props; @@ -85,6 +89,7 @@ export default class PlayerBlockHeader extends React.PureComponent { startedAt, userBrowser, userOs, + userOsVersion, userDevice, userBrowserVersion, userDeviceType, @@ -102,12 +107,13 @@ export default class PlayerBlockHeader extends React.PureComponent { return (
-
+
+ { _live && } -
+ {/*
{ formatTimeOrDate(startedAt) } { this.props.local === 'UTC' ? 'UTC' : ''} @@ -117,15 +123,33 @@ export default class PlayerBlockHeader extends React.PureComponent { - + */}
+
+ + )} + content={( +
+ } label={countries[userCountry]} value={ formatTimeOrDate(startedAt) } /> + + + +
+ )} + on="click" + position="top center" + hideOnScroll + /> +
+ { live && hasSessionsPath && (
this.props.setSessionPath('')}> This Session is Now Continuing Live
)} - { _live && } { _live && } { !_live && ( <> @@ -164,3 +188,4 @@ export default class PlayerBlockHeader extends React.PureComponent { ); } } + diff --git a/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx b/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx new file mode 100644 index 000000000..f89bfb9cd --- /dev/null +++ b/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Icon } from 'UI' +import cn from 'classnames' + +interface Props { + label: string, + icon?: string, + comp?: React.ReactNode, + value: string, + isLast?: boolean, +} +export default function SessionInfoItem(props: Props) { + const { label, icon, value, comp, isLast = false } = props + return ( +
+
+ { icon && } + { comp && comp } +
+
{label}
+
{value}
+
+ ) +} diff --git a/frontend/app/components/Session_/SessionInfoItem/index.ts b/frontend/app/components/Session_/SessionInfoItem/index.ts new file mode 100644 index 000000000..372f8dea8 --- /dev/null +++ b/frontend/app/components/Session_/SessionInfoItem/index.ts @@ -0,0 +1 @@ +export { default } from './SessionInfoItem'; \ No newline at end of file diff --git a/frontend/app/components/Session_/playerBlockHeader.css b/frontend/app/components/Session_/playerBlockHeader.css index d9934ef1e..764933a16 100644 --- a/frontend/app/components/Session_/playerBlockHeader.css +++ b/frontend/app/components/Session_/playerBlockHeader.css @@ -1,13 +1,13 @@ .header { height: 50px; border-bottom: solid thin $gray-light; - padding: 10px 15px; + padding: 0px 15px; background-color: white; } .divider { width: 1px; - height: 100%; + height: 49px; margin: 0 15px; background-color: $gray-light; } diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.css b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.css new file mode 100644 index 000000000..e88de9b3b --- /dev/null +++ b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.css @@ -0,0 +1,3 @@ +.bar { + height: 2px; +} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx index 64c7f91b4..31c233414 100644 --- a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx +++ b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx @@ -1,5 +1,6 @@ import React from 'react' import cn from 'classnames' +import stl from './ErrorBars.css' const GOOD = 'Good' const LESS_CRITICAL = 'Less Critical' @@ -16,21 +17,23 @@ interface Props { } export default function ErrorBars(props: Props) { const { count = 2 } = props - const state = React.useCallback(() => getErrorState(count), [count])() + 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 ( -
+
-
- { (state === GOOD || state === LESS_CRITICAL || state === CRITICAL) &&
} - { (state === GOOD || state === CRITICAL) &&
} +
+ { showSecondBar &&
} + { showThirdBar &&
}
-
-
-
+
+
+
-
{state}
+
{state}
) } diff --git a/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx b/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx new file mode 100644 index 000000000..1d2df4b04 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import cn from 'classnames' +import { TextEllipsis } from 'UI' + +interface Props { + className?: string, + label: string, + value?: string, +} +export default function MetaItem(props: Props) { + const { className = '', label, value } = props + return ( +
+ + + + + + +
+ ) +} diff --git a/frontend/app/components/shared/SessionItem/MetaItem/index.ts b/frontend/app/components/shared/SessionItem/MetaItem/index.ts new file mode 100644 index 000000000..f25b30c65 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/MetaItem/index.ts @@ -0,0 +1 @@ +export { default } from './MetaItem'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx b/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx new file mode 100644 index 000000000..21a5f38f1 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { Popup } from 'UI' +import MetaItem from '../MetaItem' + +interface Props { + list: any[], + maxLength: number, +} +export default function MetaMoreButton(props: Props) { + const { list, maxLength } = props + return ( + + + +{list.length - maxLength} More + +
+ ) } + content={ +
+ {list.slice(maxLength).map(({ label, value }, index) => ( + + ))} +
+ } + on="click" + position="top right" + hideOnScroll + /> + ) +} diff --git a/frontend/app/components/shared/SessionItem/MetaMoreButton/index.ts b/frontend/app/components/shared/SessionItem/MetaMoreButton/index.ts new file mode 100644 index 000000000..8a1f4733e --- /dev/null +++ b/frontend/app/components/shared/SessionItem/MetaMoreButton/index.ts @@ -0,0 +1 @@ +export { default } from './MetaMoreButton'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 7d39e8e4e..4956342f0 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -20,13 +20,15 @@ import Counter from './Counter' import { withRouter } from 'react-router-dom'; import SessionMetaList from './SessionMetaList'; import ErrorBars from './ErrorBars'; +import { assist as assistRoute, isRoute } from "App/routes"; + +const ASSIST_ROUTE = assistRoute(); const Label = ({ label = '', color = 'color-gray-medium'}) => (
{label}
) @connect(state => ({ timezone: state.getIn(['sessions', 'timezone']), - isAssist: state.getIn(['sessions', 'activeTab']).type === 'live', siteId: state.getIn([ 'user', 'siteId' ]), }), { toggleFavorite, setSessionPath }) @withRouter @@ -52,7 +54,8 @@ export default class SessionItem extends React.PureComponent { userDeviceType, userUuid, userNumericHash, - live + live, + metadata, }, timezone, onUserClick = () => null, @@ -61,71 +64,89 @@ export default class SessionItem extends React.PureComponent { } = 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 value = metadata[key]; + return { label: key, value }; + }); + + console.log('SessionItem', _metaList); return (
-
-
-
+
+
+ {/*
*/} +
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} > {userDisplayName}
-
30 Sessions
-
-
-
-
{formatTimeOrDate(startedAt, timezone) }
-
- {!live && ( -
- { eventsCount } - { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } -
- )} - - -
{ live ? : formattedDuration }
-
-
-
-
- -
- {userBrowser} - - {userOs} - - {userDeviceType} +
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} + > + 30 Sessions
-
- +
+
{formatTimeOrDate(startedAt, timezone) }
+
+ {!isAssist && ( + <> +
+ { eventsCount } + { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } +
+
·
+ + )} +
{ live ? : formattedDuration }
+
+
+ {/*
*/} + +
+ + + +
·
+ + + +
·
+ + + +
+ {/*
*/} +
+ { !isAssist && ( +
+ +
+ )}
- {/* { live && } */} -
- -
-
- +
- + { isAssist && ( + + )}
); } diff --git a/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx b/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx index 3abc72041..96b082e96 100644 --- a/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx +++ b/frontend/app/components/shared/SessionItem/SessionMetaList/SessionMetaList.tsx @@ -1,6 +1,8 @@ import React from 'react' import { Popup } from 'UI' import cn from 'classnames' +import MetaItem from '../MetaItem'; +import MetaMoreButton from '../MetaMoreButton'; interface Props { className?: string, @@ -12,36 +14,11 @@ export default function SessionMetaList(props: Props) { return (
{metaList.slice(0, MAX_LENGTH).map(({ label, value }, index) => ( -
- {label} - {value} -
+ ))} {metaList.length > MAX_LENGTH && ( - - - +{metaList.length - MAX_LENGTH} More - -
- ) } - content={ -
- {metaList.slice(MAX_LENGTH).map(({ label, value }, index) => ( -
- {label} - {value} -
- ))} -
- } - on="click" - position="top right" - // className={ styles.popup } - hideOnScroll - /> + )}
) 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 (
+ {isAssist &&
}
); }; diff --git a/frontend/app/components/ui/CountryFlag/CountryFlag.js b/frontend/app/components/ui/CountryFlag/CountryFlag.js index 0ba14c967..04cfaf38d 100644 --- a/frontend/app/components/ui/CountryFlag/CountryFlag.js +++ b/frontend/app/components/ui/CountryFlag/CountryFlag.js @@ -3,22 +3,26 @@ import { countries } from 'App/constants'; import { Popup } 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..11b3dd51f 100644 --- a/frontend/app/components/ui/IconButton/iconButton.css +++ b/frontend/app/components/ui/IconButton/iconButton.css @@ -73,11 +73,41 @@ 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/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/styles/main.css b/frontend/app/styles/main.css index fc366bc39..3339c4e48 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -123,4 +123,15 @@ &:hover { background-color: $active-blue; } +} + +.text-dotted-underline { + text-decoration: underline dotted !important; +} + +.divider { + width: 1px; + height: 49px; + margin: 0 15px; + background-color: $gray-light; } \ No newline at end of file From 54ebc487cc950926e05bc65c709b892e1ecc6551 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 21 Feb 2022 17:40:54 +0100 Subject: [PATCH 3/8] feat(ui) - assist ui - wip --- .../Assist/ChatControls/ChatControls.css | 2 +- .../Assist/ChatControls/ChatControls.tsx | 12 ++++++------ .../components/Assist/ChatWindow/ChatWindow.tsx | 7 +++---- .../components/Assist/ChatWindow/chatWindow.css | 5 +++-- .../components/AssistActions/AssistActions.tsx | 2 +- .../app/components/Session_/PlayerBlockHeader.js | 16 ++++++++-------- .../shared/SessionItem/MetaItem/MetaItem.tsx | 2 +- .../MetaMoreButton/MetaMoreButton.tsx | 2 +- .../components/ui/Confirmation/Confirmation.js | 2 +- .../app/components/ui/CountryFlag/CountryFlag.js | 5 +++-- frontend/app/styles/semantic.css | 9 +++++++++ frontend/app/svg/icons/flag-na.svg | 3 +++ 12 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 frontend/app/svg/icons/flag-na.svg 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 d3390e47c..b9ea38f47 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -117,7 +117,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus color={ onCall ? "red" : "gray-darkest" } /> { onCall ? 'End Call' : 'Call' } */} - +
} content={ cannotCall ? "You don’t have the permissions to perform this action." : `Call ${userId ? userId : 'User'}` } diff --git a/frontend/app/components/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index e32fd2bb6..631a3a3c8 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -126,6 +126,11 @@ export default class PlayerBlockHeader extends React.PureComponent { */}
+ { live && hasSessionsPath && ( +
this.props.setSessionPath('')}> + This Session is Now Continuing Live +
+ )}
- - { live && hasSessionsPath && ( -
this.props.setSessionPath('')}> - This Session is Now Continuing Live -
- )} { _live && } { !_live && ( <>
+
- + diff --git a/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx b/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx index 21a5f38f1..514192273 100644 --- a/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx +++ b/frontend/app/components/shared/SessionItem/MetaMoreButton/MetaMoreButton.tsx @@ -20,7 +20,7 @@ export default function MetaMoreButton(props: Props) { content={
{list.slice(maxLength).map(({ label, value }, index) => ( - + ))}
} 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 04cfaf38d..6bcc6672b 100644 --- a/frontend/app/components/ui/CountryFlag/CountryFlag.js +++ b/frontend/app/components/ui/CountryFlag/CountryFlag.js @@ -1,6 +1,6 @@ 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 = React.memo(({ country, className, style = {}, label = false }) => { @@ -13,7 +13,8 @@ const CountryFlag = React.memo(({ country, className, style = {}, label = false - :
{ "N/A" }
+ : + // :
{ "N/A" }
} content={ countryName } inverted 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 @@ + + + From 7c9df8c196d821b11f1b9ee1575903b794b3cbfd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 18:08:21 +0100 Subject: [PATCH 4/8] feat(ui) - assist ui - wip --- frontend/app/Router.js | 9 +- .../app/components/BugFinder/BugFinder.js | 45 +----- .../LiveSessionList/LiveSessionList.tsx | 132 ------------------ .../BugFinder/LiveSessionList/index.js | 1 - .../BugFinder/SessionList/SessionList.js | 7 +- .../app/components/Header/SiteDropdown.js | 4 + .../components/Session_/PlayerBlockHeader.js | 20 ++- .../shared/DropdownPlain/DropdownPlain.css | 23 +++ .../shared/DropdownPlain/DropdownPlain.tsx | 29 ++++ .../components/shared/DropdownPlain/index.ts | 1 + .../shared/Filters/FilterItem/FilterItem.tsx | 2 +- .../LiveSessionList/LiveSessionList.tsx | 73 ++++++++-- .../SessionItem/ErrorBars/ErrorBars.tsx | 37 ++--- .../shared/SessionItem/SessionItem.js | 53 ++++--- .../SortOrderButton/SortOrderButton.tsx | 44 ++++++ .../shared/SortOrderButton/index.ts | 1 + .../components/ui/CountryFlag/CountryFlag.js | 2 +- frontend/app/duck/liveSearch.js | 12 +- frontend/app/duck/search.js | 5 +- frontend/app/types/filter/filter.js | 2 +- frontend/app/types/filter/newFilter.js | 2 +- frontend/app/types/session/session.js | 3 +- 22 files changed, 266 insertions(+), 241 deletions(-) delete mode 100644 frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx delete mode 100644 frontend/app/components/BugFinder/LiveSessionList/index.js create mode 100644 frontend/app/components/shared/DropdownPlain/DropdownPlain.css create mode 100644 frontend/app/components/shared/DropdownPlain/DropdownPlain.tsx create mode 100644 frontend/app/components/shared/DropdownPlain/index.ts create mode 100644 frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx create mode 100644 frontend/app/components/shared/SortOrderButton/index.ts diff --git a/frontend/app/Router.js b/frontend/app/Router.js index d208c1baa..0c0e7433a 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -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(); diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 0ebfa80bb..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' && } - - )} +
+ + +
+
, - fetchLiveList: () => Promise, - 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 ( -
- - See how to {'enable Assist'} and ensure you're using tracker-assist v3.5.0 or higher. - - } - image={} - show={ !loading && sessions && sessions.size === 0} - > - - {sessions && sessions.take(displayedCount).map(session => ( - - ))} - - - - -
- ) -} - -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)); diff --git a/frontend/app/components/BugFinder/LiveSessionList/index.js b/frontend/app/components/BugFinder/LiveSessionList/index.js deleted file mode 100644 index eb38fa3e7..000000000 --- a/frontend/app/components/BugFinder/LiveSessionList/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './LiveSessionList' \ No newline at end of file diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index b2267e908..10db59c5b 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -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} /> ))} diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index 502b40085..55259c088 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -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 }) }; diff --git a/frontend/app/components/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index 631a3a3c8..93483d961 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -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 (
@@ -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, From 914f13f89c0792d85ac4df614e6dbe252f3803b8 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 19:20:39 +0100 Subject: [PATCH 5/8] feat(ui) - assist ui - wip --- .../BugFinder/SessionList/SessionListHeader.js | 12 ++++++++++++ .../app/components/Session_/PlayerBlockHeader.js | 9 +++++++-- .../shared/SessionItem/MetaItem/MetaItem.tsx | 8 ++++---- .../components/shared/SessionItem/SessionItem.js | 14 ++++++++------ frontend/app/duck/sessions.js | 14 +------------- 5 files changed, 32 insertions(+), 25 deletions(-) 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/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index 93483d961..697b2d8b3 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -144,8 +144,13 @@ export default class PlayerBlockHeader extends React.PureComponent { )} - -
+ { live && ( + <> + +
+ + )} + diff --git a/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx b/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx index 94089520c..8f8931e3b 100644 --- a/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx +++ b/frontend/app/components/shared/SessionItem/MetaItem/MetaItem.tsx @@ -11,11 +11,11 @@ export default function MetaItem(props: Props) { const { className = '', label, value } = props return (
- - + + - - + +
) diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 245dcb58b..328df74a9 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -80,19 +80,19 @@ export default class SessionItem extends React.PureComponent {
{/*
*/} -
+
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} > {userDisplayName}
-
(!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} > {userSessionsCount} Sessions -
+
*/}
@@ -136,12 +136,14 @@ export default class SessionItem extends React.PureComponent {
- +
- + { _metaList.length > 0 && ( + + )}
); } 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(), From 6800c19a904dbe4e56ab6990451cc1212a35bba0 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 20:06:06 +0100 Subject: [PATCH 6/8] feat(ui) - assist ui - wip --- frontend/app/components/Assist/Assist.tsx | 3 ++- .../Client/ProfileSettings/ProfileSettings.js | 10 +++++----- .../shared/LiveSessionList/LiveSessionList.tsx | 7 ++++++- .../app/components/shared/SessionItem/SessionItem.js | 2 +- frontend/app/styles/main.css | 9 ++++++++- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 476b7ac5c..77730f7b1 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -3,6 +3,7 @@ import LiveSessionList from 'Shared/LiveSessionList'; import LiveSessionSearch from 'Shared/LiveSessionSearch'; import cn from 'classnames' import withPageTitle from 'HOCs/withPageTitle'; +import withPermissions from 'HOCs/withPermissions' // @withPageTitle("Assist - OpenReplay") function Assist() { @@ -21,4 +22,4 @@ function Assist() { ) } -export default withPageTitle("Assist - OpenReplay")(Assist); +export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(Assist)); 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/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index 3a4fcc275..d498c53d9 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -109,7 +109,12 @@ function LiveSessionList(props: Props) { return (
-
+
+

+ Live Sessions + {sessions.size} +

+
Timezone diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 328df74a9..bb639a04e 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -136,7 +136,7 @@ export default class SessionItem extends React.PureComponent {
- +
diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index 3339c4e48..81e5ab814 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -131,7 +131,14 @@ .divider { width: 1px; - height: 49px; 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 From 2ce2ae092dae40dde78729c8baad0c22d6aa11b0 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 21:53:02 +0100 Subject: [PATCH 7/8] feat(ui) - assist ui - review --- .../Assist/components/AssistActions/AssistActions.tsx | 7 ++++--- .../Assist/components/AssistTabs/AssistTabs.tsx | 10 ++++++---- .../components/Session_/Player/Controls/Controls.js | 8 +++++--- frontend/app/components/Session_/PlayerBlockHeader.js | 7 ++++--- .../Session_/SessionInfoItem/SessionInfoItem.tsx | 4 ++-- frontend/app/components/Session_/playerBlockHeader.css | 9 +-------- .../components/shared/DropdownPlain/DropdownPlain.tsx | 1 + .../FilterAutoCompleteLocal.tsx | 2 +- .../SessionItem/MetaMoreButton/MetaMoreButton.tsx | 8 ++++---- .../app/components/shared/SessionItem/SessionItem.js | 7 ++++--- frontend/app/components/ui/CountryFlag/CountryFlag.js | 7 ++++++- frontend/app/components/ui/IconButton/iconButton.css | 2 +- frontend/app/components/ui/Loader/loader.css | 2 +- frontend/app/svg/openreplay-preloader.svg | 8 +++++++- 14 files changed, 47 insertions(+), 35 deletions(-) diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index b9ea38f47..2d7a7baf1 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -84,7 +84,8 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
{ 'Remote Control' } */}
-
+ { @@ -16,14 +17,15 @@ const AssistTabs = (props: Props) => { {props.userId && ( <>
- -
{props.userId}
+ {/* */} + +
{props.userId}'s
setShowMenu(!showMenu)} > - All Active Sessions + Active Sessions
)} 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' }
-
+