From c0f11b3cf4a3111105bc63bb7c6bd60670aa4641 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Fri, 18 Feb 2022 12:04:46 +0100 Subject: [PATCH 01/30] feat(api): handle jira unauthorized feat(api): fixed jira update --- api/chalicelib/core/integration_jira_cloud.py | 8 +++++--- api/chalicelib/utils/jira_client.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/api/chalicelib/core/integration_jira_cloud.py b/api/chalicelib/core/integration_jira_cloud.py index 65bad1e4a..8caf22832 100644 --- a/api/chalicelib/core/integration_jira_cloud.py +++ b/api/chalicelib/core/integration_jira_cloud.py @@ -1,6 +1,6 @@ -from chalicelib.utils import pg_client, helper -from chalicelib.core.integration_jira_cloud_issue import JIRACloudIntegrationIssue from chalicelib.core import integration_base +from chalicelib.core.integration_jira_cloud_issue import JIRACloudIntegrationIssue +from chalicelib.utils import pg_client, helper PROVIDER = "JIRA" @@ -89,7 +89,9 @@ class JIRAIntegration(integration_base.BaseIntegration): return self.update( changes={ "username": data["username"], - "token": data["token"], + "token": data["token"] \ + if data.get("token") and len(data["token"]) > 0 and data["token"].find("***") == -1 \ + else s["token"], "url": data["url"] } ) diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index a7ab92932..d3b637373 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -1,8 +1,9 @@ -from jira import JIRA -from jira.exceptions import JIRAError import time from datetime import datetime + import requests +from jira import JIRA +from jira.exceptions import JIRAError from requests.auth import HTTPBasicAuth fields = "id, summary, description, creator, reporter, created, assignee, status, updated, comment, issuetype, labels" @@ -15,7 +16,11 @@ class JiraManager: def __init__(self, url, username, password, project_id=None): self._config = {"JIRA_PROJECT_ID": project_id, "JIRA_URL": url, "JIRA_USERNAME": username, "JIRA_PASSWORD": password} - self._jira = JIRA({'server': url}, basic_auth=(username, password), logging=True) + try: + self._jira = JIRA({'server': url}, basic_auth=(username, password), logging=True, max_retries=1) + except Exception as e: + print("!!! JIRA AUTH ERROR") + print(e) def set_jira_project_id(self, project_id): self._config["JIRA_PROJECT_ID"] = project_id From 40b88446d190472ee206e2bf368b58f8079e8129 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 18 Feb 2022 15:37:14 +0100 Subject: [PATCH 02/30] 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 6930b71c5fc207cd2cefefc65742520e9c157ac2 Mon Sep 17 00:00:00 2001 From: ShiKhu Date: Fri, 18 Feb 2022 20:53:13 +0100 Subject: [PATCH 03/30] db (feat): pg batching --- backend/pkg/db/cache/messages_common.go | 3 +- backend/pkg/db/cache/messages_web.go | 2 +- backend/pkg/db/postgres/connector.go | 35 ++++++++++++-- backend/pkg/db/postgres/messages_common.go | 47 +++++++++---------- backend/pkg/db/postgres/messages_web.go | 9 ++-- backend/pkg/db/postgres/messages_web_stats.go | 25 +++++----- backend/services/db/main.go | 4 +- 7 files changed, 74 insertions(+), 51 deletions(-) diff --git a/backend/pkg/db/cache/messages_common.go b/backend/pkg/db/cache/messages_common.go index 65c8bf4e1..3983982fe 100644 --- a/backend/pkg/db/cache/messages_common.go +++ b/backend/pkg/db/cache/messages_common.go @@ -65,7 +65,8 @@ func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error { keyNo := project.GetMetadataNo(metadata.Key) if keyNo == 0 { - // insert project metadata + // TODO: insert project metadata + return nil } if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { return err diff --git a/backend/pkg/db/cache/messages_web.go b/backend/pkg/db/cache/messages_web.go index 21b3ac866..4aa4dfa7b 100644 --- a/backend/pkg/db/cache/messages_web.go +++ b/backend/pkg/db/cache/messages_web.go @@ -29,7 +29,7 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error UserDeviceType: s.UserDeviceType, UserDeviceMemorySize: s.UserDeviceMemorySize, UserDeviceHeapSize: s.UserDeviceHeapSize, - UserID: &s.UserID, + UserID: &s.UserID, } if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil { c.sessions[sessionID] = nil diff --git a/backend/pkg/db/postgres/connector.go b/backend/pkg/db/postgres/connector.go index cfa8f28f8..9e4e82633 100644 --- a/backend/pkg/db/postgres/connector.go +++ b/backend/pkg/db/postgres/connector.go @@ -15,7 +15,8 @@ func getTimeoutContext() context.Context { } type Conn struct { - c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?) + c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?) + batches map[uint64]*pgx.Batch } func NewConn(url string) *Conn { @@ -24,7 +25,8 @@ func NewConn(url string) *Conn { log.Println(err) log.Fatalln("pgxpool.Connect Error") } - return &Conn{c} + batches := make(map[uint64]*pgx.Batch) + return &Conn{c, batches} } func (conn *Conn) Close() error { @@ -32,6 +34,31 @@ func (conn *Conn) Close() error { return nil } +func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) error { + batch, ok := conn.batches[sessionID] + if !ok { + conn.batches[sessionID] = &pgx.Batch{} + batch = conn.batches[sessionID] + } + batch.Queue(sql, args...) + return nil +} + +func (conn *Conn) CommitBatches() { + for _, b := range conn.batches { + br := conn.c.SendBatch(getTimeoutContext(), b) + l := b.Len() + for i := 0; i < l; i++ { + if ct, err := br.Exec(); err != nil { + // TODO: ct info + log.Printf("Error in PG batch (command tag %v): %v \n", ct.String(), err) + } + } + br.Close() // returns err + } + conn.batches = make(map[uint64]*pgx.Batch) +} + func (conn *Conn) query(sql string, args ...interface{}) (pgx.Rows, error) { return conn.c.Query(getTimeoutContext(), sql, args...) } @@ -56,7 +83,7 @@ func (conn *Conn) begin() (_Tx, error) { func (tx _Tx) exec(sql string, args ...interface{}) error { _, err := tx.Exec(context.Background(), sql, args...) - return err; + return err } func (tx _Tx) rollback() error { @@ -66,5 +93,3 @@ func (tx _Tx) rollback() error { func (tx _Tx) commit() error { return tx.Commit(context.Background()) } - - diff --git a/backend/pkg/db/postgres/messages_common.go b/backend/pkg/db/postgres/messages_common.go index df539e05c..1f4f781ce 100644 --- a/backend/pkg/db/postgres/messages_common.go +++ b/backend/pkg/db/postgres/messages_common.go @@ -1,13 +1,13 @@ package postgres import ( + "fmt" "log" "strings" - "fmt" + "openreplay/backend/pkg/db/types" "openreplay/backend/pkg/hashid" "openreplay/backend/pkg/messages" - "openreplay/backend/pkg/db/types" ) func getAutocompleteType(baseType string, platform string) string { @@ -22,7 +22,7 @@ func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value str if len(value) == 0 { return } - if err := conn.exec(` + if err := conn.batchQueue(sessionID, ` INSERT INTO autocomplete ( value, type, @@ -31,7 +31,7 @@ func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value str $1, $2, project_id FROM sessions WHERE session_id = $3 - ) ON CONFLICT DO NOTHING`, + ) ON CONFLICT DO NOTHING`, value, tp, sessionID, ); err != nil { log.Printf("Insert autocomplete error: %v", err) @@ -59,16 +59,16 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { NULLIF($14, ''), NULLIF($15, ''), NULLIF($16, ''), NULLIF($17, 0), NULLIF($18, 0::bigint), NULLIF($19, '') )`, - sessionID, s.ProjectID, s.Timestamp, + sessionID, s.ProjectID, s.Timestamp, s.UserUUID, s.UserDevice, s.UserDeviceType, s.UserCountry, s.UserOS, s.UserOSVersion, - s.RevID, + s.RevID, s.TrackerVersion, s.Timestamp/1000, s.Platform, s.UserAgent, s.UserBrowser, s.UserBrowserVersion, s.UserDeviceMemorySize, s.UserDeviceHeapSize, s.UserID, ); err != nil { - return err; + return err } conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS) conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) @@ -81,7 +81,7 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) { // Search acceleration - if err := conn.exec(` + if err := conn.batchQueue(sessionID, ` UPDATE sessions SET issue_types=(SELECT CASE WHEN errors_count > 0 THEN @@ -96,7 +96,7 @@ func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, `, sessionID, ); err != nil { - log.Printf("Error while updating issue_types %v", sessionID) + log.Printf("Error while updating issue_types: %v. SessionID: %v", err, sessionID) } var dur uint64 @@ -113,33 +113,33 @@ func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, } func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error { - return conn.exec(` + return conn.batchQueue(sessionID, ` INSERT INTO events_common.requests ( session_id, timestamp, seq_index, url, duration, success ) VALUES ( $1, $2, $3, $4, $5, $6 )`, - sessionID, timestamp, + sessionID, timestamp, getSqIdx(index), url, duration, success, ) } func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index uint64, name string, payload string) error { - return conn.exec(` + return conn.batchQueue(sessionID, ` INSERT INTO events_common.customs ( session_id, timestamp, seq_index, name, payload ) VALUES ( $1, $2, $3, $4, $5 )`, - sessionID, timestamp, - getSqIdx(index), + sessionID, timestamp, + getSqIdx(index), name, payload, ) } func (conn *Conn) InsertUserID(sessionID uint64, userID string) error { - return conn.exec(` + return conn.batchQueue(sessionID, ` UPDATE sessions SET user_id = $1 WHERE session_id = $2`, userID, sessionID, @@ -147,16 +147,15 @@ func (conn *Conn) InsertUserID(sessionID uint64, userID string) error { } func (conn *Conn) InsertUserAnonymousID(sessionID uint64, userAnonymousID string) error { - return conn.exec(` + return conn.batchQueue(sessionID, ` UPDATE sessions SET user_anonymous_id = $1 WHERE session_id = $2`, userAnonymousID, sessionID, ) } - func (conn *Conn) InsertMetadata(sessionID uint64, keyNo uint, value string) error { - return conn.exec(fmt.Sprintf(` + return conn.batchQueue(sessionID, fmt.Sprintf(` UPDATE sessions SET metadata_%v = $1 WHERE session_id = $2`, keyNo), value, sessionID, @@ -173,11 +172,11 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag issueID := hashid.IssueID(projectID, e) // TEMP. TODO: nullable & json message field type - payload := &e.Payload; + payload := &e.Payload if *payload == "" || *payload == "{}" { payload = nil } - context := &e.Context; + context := &e.Context if *context == "" || *context == "{}" { context = nil } @@ -189,7 +188,7 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag project_id, $2, $3, $4, CAST($5 AS jsonb) FROM sessions WHERE session_id = $1 - )ON CONFLICT DO NOTHING`, + )ON CONFLICT DO NOTHING`, sessionID, issueID, e.Type, e.ContextString, context, ); err != nil { return err @@ -199,8 +198,8 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag session_id, issue_id, timestamp, seq_index, payload ) VALUES ( $1, $2, $3, $4, CAST($5 AS jsonb) - )`, - sessionID, issueID, e.Timestamp, + )`, + sessionID, issueID, e.Timestamp, getSqIdx(e.MessageID), payload, ); err != nil { @@ -228,5 +227,3 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag } return tx.commit() } - - diff --git a/backend/pkg/db/postgres/messages_web.go b/backend/pkg/db/postgres/messages_web.go index f7354252b..64d7ba561 100644 --- a/backend/pkg/db/postgres/messages_web.go +++ b/backend/pkg/db/postgres/messages_web.go @@ -68,16 +68,19 @@ func (conn *Conn) InsertWebPageEvent(sessionID uint64, e *PageEvent) error { if err := tx.exec(` INSERT INTO events.pages ( session_id, message_id, timestamp, referrer, base_referrer, host, path, base_path, - dom_content_loaded_time, load_time, response_end, first_paint_time, first_contentful_paint_time, speed_index, visually_complete, time_to_interactive, + dom_content_loaded_time, load_time, response_end, first_paint_time, first_contentful_paint_time, + speed_index, visually_complete, time_to_interactive, response_time, dom_building_time ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, - NULLIF($9, 0), NULLIF($10, 0), NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), NULLIF($14, 0), NULLIF($15, 0), NULLIF($16, 0), + NULLIF($9, 0), NULLIF($10, 0), NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), + NULLIF($14, 0), NULLIF($15, 0), NULLIF($16, 0), NULLIF($17, 0), NULLIF($18, 0) ) `, sessionID, e.MessageID, e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer), host, path, url.DiscardURLQuery(path), - e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, + e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, + e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e), ); err != nil { return err diff --git a/backend/pkg/db/postgres/messages_web_stats.go b/backend/pkg/db/postgres/messages_web_stats.go index 9e3b5bc77..933442b0b 100644 --- a/backend/pkg/db/postgres/messages_web_stats.go +++ b/backend/pkg/db/postgres/messages_web_stats.go @@ -1,21 +1,18 @@ -package postgres +package postgres import ( - "openreplay/backend/pkg/url" . "openreplay/backend/pkg/messages" + "openreplay/backend/pkg/url" ) - - func (conn *Conn) InsertWebStatsLongtask(sessionID uint64, l *LongTask) error { - return nil // Do we even use them? - // conn.exec(``); + return nil // Do we even use them? + // conn.exec(``); } - func (conn *Conn) InsertWebStatsPerformance(sessionID uint64, p *PerformanceTrackAggr) error { - timestamp := (p.TimestampEnd + p.TimestampStart) /2 - return conn.exec(` + timestamp := (p.TimestampEnd + p.TimestampStart) / 2 + return conn.batchQueue(sessionID, ` INSERT INTO events.performance ( session_id, timestamp, message_id, min_fps, avg_fps, max_fps, @@ -34,7 +31,7 @@ func (conn *Conn) InsertWebStatsPerformance(sessionID uint64, p *PerformanceTrac p.MinCPU, p.AvgCPU, p.MinCPU, p.MinTotalJSHeapSize, p.AvgTotalJSHeapSize, p.MaxTotalJSHeapSize, p.MinUsedJSHeapSize, p.AvgUsedJSHeapSize, p.MaxUsedJSHeapSize, - ); + ) } func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent) error { @@ -42,7 +39,7 @@ func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent if err != nil { return err } - return conn.exec(` + return conn.batchQueue(sessionID, ` INSERT INTO events.resources ( session_id, timestamp, message_id, type, @@ -58,11 +55,11 @@ func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent NULLIF($10, '')::events.resource_method, NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), NULLIF($14, 0), NULLIF($15, 0) )`, - sessionID, e.Timestamp, e.MessageID, + sessionID, e.Timestamp, e.MessageID, e.Type, e.URL, host, url.DiscardURLQuery(e.URL), - e.Success, e.Status, + e.Success, e.Status, url.EnsureMethod(e.Method), e.Duration, e.TTFB, e.HeaderSize, e.EncodedBodySize, e.DecodedBodySize, ) -} \ No newline at end of file +} diff --git a/backend/services/db/main.go b/backend/services/db/main.go index b60f7e7db..d6190a4f0 100644 --- a/backend/services/db/main.go +++ b/backend/services/db/main.go @@ -11,11 +11,11 @@ import ( "openreplay/backend/pkg/db/cache" "openreplay/backend/pkg/db/postgres" "openreplay/backend/pkg/env" + logger "openreplay/backend/pkg/log" "openreplay/backend/pkg/messages" "openreplay/backend/pkg/queue" "openreplay/backend/pkg/queue/types" "openreplay/backend/services/db/heuristics" - logger "openreplay/backend/pkg/log" ) var pg *cache.PGCache @@ -29,7 +29,6 @@ func main() { heurFinder := heuristics.NewHandler() - statsLogger := logger.NewQueueStats(env.Int("LOG_QUEUE_STATS_INTERVAL_SEC")) consumer := queue.NewMessageConsumer( @@ -91,6 +90,7 @@ func main() { consumer.Close() os.Exit(0) case <-tick: + pg.CommitBatches() if err := commitStats(); err != nil { log.Printf("Error on stats commit: %v", err) } From 245af3729144cff3ccbc7a7e44b41d11a8ecd090 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Sun, 20 Feb 2022 15:33:19 +0100 Subject: [PATCH 04/30] feat(api): changed jira integration feat(api): changed github integration --- api/chalicelib/core/integration_base.py | 3 ++- api/chalicelib/core/integration_github.py | 15 ++++++++------- api/chalicelib/core/integration_jira_cloud.py | 13 ++++++++++--- api/schemas.py | 8 ++++++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/api/chalicelib/core/integration_base.py b/api/chalicelib/core/integration_base.py index 45e1891a1..f8edaad62 100644 --- a/api/chalicelib/core/integration_base.py +++ b/api/chalicelib/core/integration_base.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod + from chalicelib.utils import pg_client, helper @@ -37,7 +38,7 @@ class BaseIntegration(ABC): pass @abstractmethod - def update(self, changes): + def update(self, changes, obfuscate=False): pass @abstractmethod diff --git a/api/chalicelib/core/integration_github.py b/api/chalicelib/core/integration_github.py index a13946e46..a05c946f4 100644 --- a/api/chalicelib/core/integration_github.py +++ b/api/chalicelib/core/integration_github.py @@ -1,6 +1,6 @@ -from chalicelib.utils import pg_client, helper -from chalicelib.core.integration_github_issue import GithubIntegrationIssue from chalicelib.core import integration_base +from chalicelib.core.integration_github_issue import GithubIntegrationIssue +from chalicelib.utils import pg_client, helper PROVIDER = "GITHUB" @@ -15,8 +15,6 @@ class GitHubIntegration(integration_base.BaseIntegration): def provider(self): return PROVIDER - - def get_obfuscated(self): integration = self.get() if integration is None: @@ -24,7 +22,7 @@ class GitHubIntegration(integration_base.BaseIntegration): token = "*" * (len(integration["token"]) - 4) + integration["token"][-4:] return {"token": token, "provider": self.provider.lower()} - def update(self, changes): + def update(self, changes, obfuscate=False): with pg_client.PostgresClient() as cur: sub_query = [f"{helper.key_to_snake_case(k)} = %({k})s" for k in changes.keys()] cur.execute( @@ -71,8 +69,11 @@ class GitHubIntegration(integration_base.BaseIntegration): if s is not None: return self.update( changes={ - "token": data["token"] - } + "token": data["token"] \ + if data.get("token") and len(data["token"]) > 0 and data["token"].find("***") == -1 \ + else s["token"] + }, + obfuscate=True ) else: return self.add(token=data["token"]) diff --git a/api/chalicelib/core/integration_jira_cloud.py b/api/chalicelib/core/integration_jira_cloud.py index 8caf22832..ea9c6c24e 100644 --- a/api/chalicelib/core/integration_jira_cloud.py +++ b/api/chalicelib/core/integration_jira_cloud.py @@ -5,6 +5,10 @@ from chalicelib.utils import pg_client, helper PROVIDER = "JIRA" +def obfuscate_string(string): + return "*" * (len(string) - 4) + string[-4:] + + class JIRAIntegration(integration_base.BaseIntegration): def __init__(self, tenant_id, user_id): self.__tenant_id = tenant_id @@ -36,11 +40,11 @@ class JIRAIntegration(integration_base.BaseIntegration): integration = self.get() if integration is None: return None - integration["token"] = "*" * (len(integration["token"]) - 4) + integration["token"][-4:] + integration["token"] = obfuscate_string(integration["token"]) integration["provider"] = self.provider.lower() return integration - def update(self, changes): + def update(self, changes, obfuscate=False): with pg_client.PostgresClient() as cur: sub_query = [f"{helper.key_to_snake_case(k)} = %({k})s" for k in changes.keys()] cur.execute( @@ -53,6 +57,8 @@ class JIRAIntegration(integration_base.BaseIntegration): **changes}) ) w = helper.dict_to_camel_case(cur.fetchone()) + if obfuscate: + w["token"] = obfuscate_string(w["token"]) return w # TODO: make this generic for all issue tracking integrations @@ -93,7 +99,8 @@ class JIRAIntegration(integration_base.BaseIntegration): if data.get("token") and len(data["token"]) > 0 and data["token"].find("***") == -1 \ else s["token"], "url": data["url"] - } + }, + obfuscate=True ) else: return self.add( diff --git a/api/schemas.py b/api/schemas.py index 56eb24cf7..4defa64df 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Optional, List, Union, Literal -from pydantic import BaseModel, Field, EmailStr, HttpUrl, root_validator +from pydantic import BaseModel, Field, EmailStr, HttpUrl, root_validator, validator from chalicelib.utils.TimeUTC import TimeUTC @@ -107,7 +107,11 @@ class JiraGithubSchema(BaseModel): provider: str = Field(...) username: str = Field(...) token: str = Field(...) - url: str = Field(...) + url: HttpUrl = Field(...) + + @validator('url') + def transform_url(cls, v: HttpUrl): + return HttpUrl.build(scheme=v.scheme, host=v.host) class CreateEditWebhookSchema(BaseModel): From 06866602e145334fabe80dc7edfaf9906b9ade1c Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Sun, 20 Feb 2022 17:52:07 +0100 Subject: [PATCH 05/30] feat(db): new release changes --- ee/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql | 8 ++++++++ scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 ee/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql create mode 100644 scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql diff --git a/ee/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql b/ee/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql new file mode 100644 index 000000000..48a326e9c --- /dev/null +++ b/ee/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql @@ -0,0 +1,8 @@ +BEGIN; +CREATE OR REPLACE FUNCTION openreplay_version() + RETURNS text AS +$$ +SELECT 'v1.5.1-ee' +$$ LANGUAGE sql IMMUTABLE; + +COMMIT; \ No newline at end of file diff --git a/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql b/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql new file mode 100644 index 000000000..babe0c705 --- /dev/null +++ b/scripts/helm/db/init_dbs/postgresql/1.5.1/1.5.1.sql @@ -0,0 +1,8 @@ +BEGIN; +CREATE OR REPLACE FUNCTION openreplay_version() + RETURNS text AS +$$ +SELECT 'v1.5.1' +$$ LANGUAGE sql IMMUTABLE; + +COMMIT; \ No newline at end of file From a65ac9c74f85f5cdc7ca28c170e3cdec1c552696 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Mon, 21 Feb 2022 13:31:55 +0100 Subject: [PATCH 06/30] feat(api): changed search-sessions-group-by-user_id --- api/chalicelib/core/sessions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 164cf366e..812041784 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -186,13 +186,15 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f COUNT(DISTINCT s.user_uuid) AS count_users {query_part};""", full_args) elif data.group_by_user: + meta_keys = metadata.get(project_id=project_id) main_query = cur.mogrify(f"""SELECT COUNT(*) AS count, jsonb_agg(users_sessions) FILTER ( WHERE rn <= 200 ) AS sessions FROM (SELECT user_id, count(full_sessions) AS user_sessions_count, jsonb_agg(full_sessions) FILTER (WHERE rn <= 1) AS last_session, ROW_NUMBER() OVER (ORDER BY count(full_sessions) DESC) AS rn FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY start_ts DESC) AS rn - FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS} + FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS}, + {",".join([f'metadata_{m["index"]} AS {m["key"]}' for m in meta_keys])} {query_part} ORDER BY s.session_id desc) AS filtred_sessions ORDER BY favorite DESC, issue_score DESC, {sort} {data.order}) AS full_sessions @@ -237,6 +239,10 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f if errors_only: return sessions + if data.group_by_user: + for i, s in enumerate(sessions): + sessions[i] = {**s.pop("last_session")[0], **s} + sessions[i].pop("rn") if not data.group_by_user and data.sort is not None and data.sort != "session_id": sessions = sorted(sessions, key=lambda s: s[helper.key_to_snake_case(data.sort)], reverse=data.order.upper() == "DESC") @@ -250,7 +256,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, density: int, view_type: schemas.MetricViewType): step_size = int(metrics_helper.__get_step_size(endTimestamp=data.endDate, startTimestamp=data.startDate, - density=density, factor=1, decimal=True)) + density=density, factor=1, decimal=True)) full_args, query_part, sort = search_query_parts(data=data, error_status=None, errors_only=False, favorite_only=False, issue=None, project_id=project_id, user_id=None) From 250eaf1eb67ebbd8a1efcbd65523d88d54a170bd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 21 Feb 2022 16:41:04 +0100 Subject: [PATCH 07/30] 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 08/30] 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 a16644f47e2fab746b5a2006e48d30870c3c4774 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Mon, 21 Feb 2022 18:57:50 +0100 Subject: [PATCH 09/30] feat(api): upgrade fastapi to v0.74.1 feat(api): upgrade uvicorn to v0.17.5 feat(api): upgrade python-decouple to v3.6 --- api/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/requirements.txt b/api/requirements.txt index 0a239790c..4af962f4f 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -8,8 +8,8 @@ jira==2.0.0 -fastapi==0.70.1 -uvicorn[standard]==0.16.0 -python-decouple==3.5 +fastapi==0.74.1 +uvicorn[standard]==0.17.5 +python-decouple==3.6 pydantic[email]==1.8.2 apscheduler==3.8.1 \ No newline at end of file From ce23ffc6d548130b025c2d9e9bf4c10766129736 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 13:29:03 +0100 Subject: [PATCH 10/30] feat(api): EE create new user wrong-role handling --- ee/api/chalicelib/core/users.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ee/api/chalicelib/core/users.py b/ee/api/chalicelib/core/users.py index 2d4effc0e..e831e066c 100644 --- a/ee/api/chalicelib/core/users.py +++ b/ee/api/chalicelib/core/users.py @@ -21,7 +21,10 @@ def create_new_member(tenant_id, email, invitation_token, admin, name, owner=Fal query = cur.mogrify(f"""\ WITH u AS ( INSERT INTO public.users (tenant_id, email, role, name, data, role_id) - VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, %(role_id)s) + VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, + (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name != 'Owner' LIMIT 1)))) RETURNING tenant_id,user_id,email,role,name,appearance, role_id ), au AS (INSERT INTO public.basic_authentication (user_id, generated_password, invitation_token, invited_at) @@ -42,7 +45,7 @@ def create_new_member(tenant_id, email, invitation_token, admin, name, owner=Fal roles.name AS role_name, roles.permissions, TRUE AS has_password - FROM au,u LEFT JOIN roles USING(tenant_id) WHERE roles.role_id IS NULL OR roles.role_id = %(role_id)s;""", + FROM au,u LEFT JOIN roles USING(tenant_id) WHERE roles.role_id IS NULL OR roles.role_id = (SELECT u.role_id FROM u);""", {"tenantId": tenant_id, "email": email, "role": "owner" if owner else "admin" if admin else "member", "name": name, "data": json.dumps({"lastAnnouncementView": TimeUTC.now()}), @@ -716,7 +719,10 @@ def create_sso_user(tenant_id, email, admin, name, origin, role_id, internal_id= query = cur.mogrify(f"""\ WITH u AS ( INSERT INTO public.users (tenant_id, email, role, name, data, origin, internal_id, role_id) - VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s, %(role_id)s) + VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s, + (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name != 'Owner' LIMIT 1)))) RETURNING * ), au AS ( From 66a029a1333df1f5d934fd33cf75efac1dcac334 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 13:33:56 +0100 Subject: [PATCH 11/30] feat(api): EE upgrade fastapi to v0.74.1 feat(api): EE upgrade uvicorn to v0.17.5 feat(api): EE upgrade python-decouple to v3.6 --- ee/api/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 82329a1f2..84a372567 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -8,9 +8,9 @@ jira==2.0.0 clickhouse-driver==0.2.2 python3-saml==1.12.0 -fastapi==0.70.1 +fastapi==0.74.1 python-multipart==0.0.5 -uvicorn[standard]==0.16.0 -python-decouple==3.5 +uvicorn[standard]==0.17.5 +python-decouple==3.6 pydantic[email]==1.8.2 apscheduler==3.8.1 \ No newline at end of file From 11b11ff69ab288675416110569f333a008600854 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 13:45:03 +0100 Subject: [PATCH 12/30] feat(api): EE restore and update user with wrong-role handling --- ee/api/chalicelib/core/users.py | 54 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/ee/api/chalicelib/core/users.py b/ee/api/chalicelib/core/users.py index e831e066c..c8c1c7669 100644 --- a/ee/api/chalicelib/core/users.py +++ b/ee/api/chalicelib/core/users.py @@ -21,10 +21,10 @@ def create_new_member(tenant_id, email, invitation_token, admin, name, owner=Fal query = cur.mogrify(f"""\ WITH u AS ( INSERT INTO public.users (tenant_id, email, role, name, data, role_id) - VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, - (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND role_id = %(role_id)s), - (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name = 'Member' LIMIT 1), - (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name != 'Owner' LIMIT 1)))) + VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s, + (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))) RETURNING tenant_id,user_id,email,role,name,appearance, role_id ), au AS (INSERT INTO public.basic_authentication (user_id, generated_password, invitation_token, invited_at) @@ -46,7 +46,7 @@ def create_new_member(tenant_id, email, invitation_token, admin, name, owner=Fal roles.permissions, TRUE AS has_password FROM au,u LEFT JOIN roles USING(tenant_id) WHERE roles.role_id IS NULL OR roles.role_id = (SELECT u.role_id FROM u);""", - {"tenantId": tenant_id, "email": email, + {"tenant_id": tenant_id, "email": email, "role": "owner" if owner else "admin" if admin else "member", "name": name, "data": json.dumps({"lastAnnouncementView": TimeUTC.now()}), "invitation_token": invitation_token, "role_id": role_id}) @@ -66,7 +66,9 @@ def restore_member(tenant_id, user_id, email, invitation_token, admin, name, own created_at = timezone('utc'::text, now()), tenant_id= %(tenant_id)s, api_key= generate_api_key(20), - role_id= %(role_id)s + role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))) WHERE user_id=%(user_id)s RETURNING user_id AS id, email, @@ -148,6 +150,10 @@ def update(tenant_id, user_id, changes): if key == "appearance": sub_query_users.append(f"appearance = %(appearance)s::jsonb") changes["appearance"] = json.dumps(changes[key]) + elif helper.key_to_snake_case(key) == "role_id": + sub_query_users.append("""role_id=(SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))""") else: sub_query_users.append(f"{helper.key_to_snake_case(key)} = %({key})s") @@ -283,11 +289,11 @@ def get(user_id, tenant_id): LEFT JOIN public.roles USING (role_id) WHERE users.user_id = %(userId)s - AND users.tenant_id = %(tenantId)s + AND users.tenant_id = %(tenant_id)s AND users.deleted_at IS NULL - AND (roles.role_id IS NULL OR roles.deleted_at IS NULL AND roles.tenant_id = %(tenantId)s) + AND (roles.role_id IS NULL OR roles.deleted_at IS NULL AND roles.tenant_id = %(tenant_id)s) LIMIT 1;""", - {"userId": user_id, "tenantId": tenant_id}) + {"userId": user_id, "tenant_id": tenant_id}) ) r = cur.fetchone() return helper.dict_to_camel_case(r, ignore_keys=["appearance"]) @@ -421,9 +427,9 @@ def get_members(tenant_id): FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id LEFT JOIN public.roles USING (role_id) - WHERE users.tenant_id = %(tenantId)s AND users.deleted_at IS NULL + WHERE users.tenant_id = %(tenant_id)s AND users.deleted_at IS NULL ORDER BY name, id""", - {"tenantId": tenant_id}) + {"tenant_id": tenant_id}) ) r = cur.fetchall() if len(r): @@ -537,8 +543,8 @@ def count_members(tenant_id): cur.mogrify( """SELECT COUNT(user_id) - FROM public.users WHERE tenant_id = %(tenantId)s AND deleted_at IS NULL;""", - {"tenantId": tenant_id}) + FROM public.users WHERE tenant_id = %(tenant_id)s AND deleted_at IS NULL;""", + {"tenant_id": tenant_id}) ) r = cur.fetchone() return r["count"] @@ -601,8 +607,8 @@ def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud): with pg_client.PostgresClient() as cur: cur.execute( cur.mogrify( - f"SELECT user_id AS id,jwt_iat, changed_at FROM public.users INNER JOIN public.basic_authentication USING(user_id) WHERE user_id = %(userId)s AND tenant_id = %(tenantId)s AND deleted_at IS NULL LIMIT 1;", - {"userId": user_id, "tenantId": tenant_id}) + f"SELECT user_id AS id,jwt_iat, changed_at FROM public.users INNER JOIN public.basic_authentication USING(user_id) WHERE user_id = %(userId)s AND tenant_id = %(tenant_id)s AND deleted_at IS NULL LIMIT 1;", + {"userId": user_id, "tenant_id": tenant_id}) ) r = cur.fetchone() return r is not None \ @@ -719,10 +725,10 @@ def create_sso_user(tenant_id, email, admin, name, origin, role_id, internal_id= query = cur.mogrify(f"""\ WITH u AS ( INSERT INTO public.users (tenant_id, email, role, name, data, origin, internal_id, role_id) - VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s, - (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND role_id = %(role_id)s), - (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name = 'Member' LIMIT 1), - (SELECT role_id FROM roles WHERE tenant_id = %(tenantId)s AND name != 'Owner' LIMIT 1)))) + VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s, + (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))) RETURNING * ), au AS ( @@ -740,7 +746,7 @@ def create_sso_user(tenant_id, email, admin, name, origin, role_id, internal_id= u.appearance, origin FROM u;""", - {"tenantId": tenant_id, "email": email, "internal_id": internal_id, + {"tenant_id": tenant_id, "email": email, "internal_id": internal_id, "role": "admin" if admin else "member", "name": name, "origin": origin, "role_id": role_id, "data": json.dumps({"lastAnnouncementView": TimeUTC.now()})}) cur.execute( @@ -754,13 +760,15 @@ def restore_sso_user(user_id, tenant_id, email, admin, name, origin, role_id, in query = cur.mogrify(f"""\ WITH u AS ( UPDATE public.users - SET tenant_id= %(tenantId)s, + SET tenant_id= %(tenant_id)s, role= %(role)s, name= %(name)s, data= %(data)s, origin= %(origin)s, internal_id= %(internal_id)s, - role_id= %(role_id)s, + role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1), + (SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))), deleted_at= NULL, created_at= default, api_key= default, @@ -793,7 +801,7 @@ def restore_sso_user(user_id, tenant_id, email, admin, name, origin, role_id, in u.appearance, origin FROM u;""", - {"tenantId": tenant_id, "email": email, "internal_id": internal_id, + {"tenant_id": tenant_id, "email": email, "internal_id": internal_id, "role": "admin" if admin else "member", "name": name, "origin": origin, "role_id": role_id, "data": json.dumps({"lastAnnouncementView": TimeUTC.now()}), "user_id": user_id}) From b0ad81e2a770ae4c284318dc1d4b54dc28fef2a1 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 15:30:08 +0100 Subject: [PATCH 13/30] feat(api): get session-replay changed metadata structure --- api/chalicelib/core/sessions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 812041784..e30f5b552 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -30,10 +30,10 @@ COALESCE((SELECT TRUE def __group_metadata(session, project_metadata): - meta = [] + meta = {} for m in project_metadata.keys(): if project_metadata[m] is not None and session.get(m) is not None: - meta.append({project_metadata[m]: session[m]}) + meta[project_metadata[m]] = session[m] session.pop(m) return meta From a4069cea9fe56b6e343f382a569b8b4754601297 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 15:43:23 +0100 Subject: [PATCH 14/30] feat(api): search-session group by userId changed metadata structure --- api/chalicelib/core/sessions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index e30f5b552..97ac86d29 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -167,7 +167,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f error_status="ALL", count_only=False, issue=None): full_args, query_part, sort = search_query_parts(data, error_status, errors_only, favorite_only, issue, project_id, user_id) - + meta_keys = [] with pg_client.PostgresClient() as cur: if errors_only: main_query = cur.mogrify(f"""SELECT DISTINCT er.error_id, ser.status, ser.parent_error_id, ser.payload, @@ -243,6 +243,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f for i, s in enumerate(sessions): sessions[i] = {**s.pop("last_session")[0], **s} sessions[i].pop("rn") + sessions[i]["metadata"] = {k["key"]: sessions[i][k["key"]] for k in meta_keys} if not data.group_by_user and data.sort is not None and data.sort != "session_id": sessions = sorted(sessions, key=lambda s: s[helper.key_to_snake_case(data.sort)], reverse=data.order.upper() == "DESC") From e4bb9158479f7b3704ce49fd5b55889e31a9507f Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 16:22:07 +0100 Subject: [PATCH 15/30] feat(api): search-sessions support isUndefined operator --- api/chalicelib/core/sessions.py | 28 +++++++++++++++++++++++++++- api/schemas.py | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 97ac86d29..7492558ae 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -162,6 +162,10 @@ def _isAny_opreator(op: schemas.SearchEventOperator): return op in [schemas.SearchEventOperator._on_any, schemas.SearchEventOperator._is_any] +def _isUndefined_operator(op: schemas.SearchEventOperator): + return op in [schemas.SearchEventOperator._is_undefined] + + @dev.timed def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, favorite_only=False, errors_only=False, error_status="ALL", count_only=False, issue=None): @@ -317,7 +321,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr op = __get_sql_operator(f.operator) \ if filter_type not in [schemas.FilterType.events_count] else f.operator is_any = _isAny_opreator(f.operator) - if not is_any and len(f.value) == 0: + is_undefined = _isUndefined_operator(f.operator) + if not is_any and not is_undefined and len(f.value) == 0: continue is_not = False if __is_negation_operator(f.operator): @@ -366,6 +371,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.utm_source IS NOT NULL') ss_constraints.append('ms.utm_source IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.utm_source IS NULL') + ss_constraints.append('ms.utm_source IS NULL') else: extra_constraints.append( _multiple_conditions(f's.utm_source {op} %({f_k})s::text', f.value, is_not=is_not, @@ -377,6 +385,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.utm_medium IS NOT NULL') ss_constraints.append('ms.utm_medium IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.utm_medium IS NULL') + ss_constraints.append('ms.utm_medium IS NULL') else: extra_constraints.append( _multiple_conditions(f's.utm_medium {op} %({f_k})s::text', f.value, is_not=is_not, @@ -388,6 +399,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.utm_campaign IS NOT NULL') ss_constraints.append('ms.utm_campaign IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.utm_campaign IS NULL') + ss_constraints.append('ms.utm_campaign IS NULL') else: extra_constraints.append( _multiple_conditions(f's.utm_campaign {op} %({f_k})s::text', f.value, is_not=is_not, @@ -421,6 +435,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append(f"s.{metadata.index_to_colname(meta_keys[f.source])} IS NOT NULL") ss_constraints.append(f"ms.{metadata.index_to_colname(meta_keys[f.source])} IS NOT NULL") + elif is_undefined: + extra_constraints.append(f"s.{metadata.index_to_colname(meta_keys[f.source])} IS NULL") + ss_constraints.append(f"ms.{metadata.index_to_colname(meta_keys[f.source])} IS NULL") else: extra_constraints.append( _multiple_conditions( @@ -434,6 +451,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.user_id IS NOT NULL') ss_constraints.append('ms.user_id IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.user_id IS NULL') + ss_constraints.append('ms.user_id IS NULL') else: extra_constraints.append( _multiple_conditions(f"s.user_id {op} %({f_k})s::text", f.value, is_not=is_not, value_key=f_k)) @@ -444,6 +464,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.user_anonymous_id IS NOT NULL') ss_constraints.append('ms.user_anonymous_id IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.user_anonymous_id IS NULL') + ss_constraints.append('ms.user_anonymous_id IS NULL') else: extra_constraints.append( _multiple_conditions(f"s.user_anonymous_id {op} %({f_k})s::text", f.value, is_not=is_not, @@ -455,6 +478,9 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr if is_any: extra_constraints.append('s.rev_id IS NOT NULL') ss_constraints.append('ms.rev_id IS NOT NULL') + elif is_undefined: + extra_constraints.append('s.rev_id IS NULL') + ss_constraints.append('ms.rev_id IS NULL') else: extra_constraints.append( _multiple_conditions(f"s.rev_id {op} %({f_k})s::text", f.value, is_not=is_not, value_key=f_k)) diff --git a/api/schemas.py b/api/schemas.py index 4defa64df..400dd94cc 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -439,6 +439,7 @@ class SearchEventOperator(str, Enum): _on = "on" _on_any = "onAny" _is_not = "isNot" + _is_undefined = "isUndefined" _not_on = "notOn" _contains = "contains" _not_contains = "notContains" From 7025f60f3d94ed95e7ef4154c930c61b2d1fdae7 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 16:27:59 +0100 Subject: [PATCH 16/30] feat(api): search-sessions group by userid: first-session-ts --- api/chalicelib/core/sessions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 7492558ae..8a5945261 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -195,6 +195,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f FROM (SELECT user_id, count(full_sessions) AS user_sessions_count, jsonb_agg(full_sessions) FILTER (WHERE rn <= 1) AS last_session, + MIN(full_sessions.start_ts) AS first_session_ts, ROW_NUMBER() OVER (ORDER BY count(full_sessions) DESC) AS rn FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY start_ts DESC) AS rn FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS}, From 67c5458bffcf8dedcefb269471fc36333bdc2fda Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 17:11:05 +0100 Subject: [PATCH 17/30] feat(api): search-sessions include metadata in response --- api/chalicelib/core/sessions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 8a5945261..a582531aa 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -207,9 +207,11 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f ORDER BY user_sessions_count DESC) AS users_sessions;""", full_args) else: + meta_keys = metadata.get(project_id=project_id) main_query = cur.mogrify(f"""SELECT COUNT(full_sessions) AS count, COALESCE(JSONB_AGG(full_sessions) FILTER (WHERE rn <= 200), '[]'::JSONB) AS sessions FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY favorite DESC, issue_score DESC, session_id desc, start_ts desc) AS rn - FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS} + FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS}, + {",".join([f'metadata_{m["index"]} AS {m["key"]}' for m in meta_keys])} {query_part} ORDER BY s.session_id desc) AS filtred_sessions ORDER BY favorite DESC, issue_score DESC, {sort} {data.order}) AS full_sessions;""", @@ -249,6 +251,9 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f sessions[i] = {**s.pop("last_session")[0], **s} sessions[i].pop("rn") sessions[i]["metadata"] = {k["key"]: sessions[i][k["key"]] for k in meta_keys} + else: + for i, s in enumerate(sessions): + sessions[i]["metadata"] = {k["key"]: sessions[i][k["key"]] for k in meta_keys} if not data.group_by_user and data.sort is not None and data.sort != "session_id": sessions = sorted(sessions, key=lambda s: s[helper.key_to_snake_case(data.sort)], reverse=data.order.upper() == "DESC") From 26aa417b7c7d9fd1e6ec8e53568cda41ea56d0bb Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 17:33:03 +0100 Subject: [PATCH 18/30] feat(api): search-sessions fixed metadata name confusion feat(api): search-sessions group by userId fixed metadata name confusion --- api/chalicelib/core/saved_search.py | 10 ---------- api/chalicelib/core/sessions.py | 11 ++++------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/api/chalicelib/core/saved_search.py b/api/chalicelib/core/saved_search.py index d1e8fe15f..1999c6758 100644 --- a/api/chalicelib/core/saved_search.py +++ b/api/chalicelib/core/saved_search.py @@ -49,16 +49,6 @@ def update(search_id, project_id, user_id, data: schemas.SavedSearchSchema): def get_all(project_id, user_id, details=False): with pg_client.PostgresClient() as cur: - print(cur.mogrify( - f"""\ - SELECT search_id, project_id, user_id, name, created_at, deleted_at, is_public - {",filter" if details else ""} - FROM public.searches - WHERE project_id = %(project_id)s - AND deleted_at IS NULL - AND (user_id = %(user_id)s OR is_public);""", - {"project_id": project_id, "user_id": user_id} - )) cur.execute( cur.mogrify( f"""\ diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index a582531aa..f1add5dfe 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -7,7 +7,6 @@ SESSION_PROJECTION_COLS = """s.project_id, s.session_id::text AS session_id, s.user_uuid, s.user_id, --- s.user_agent, s.user_os, s.user_browser, s.user_device, @@ -199,7 +198,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f ROW_NUMBER() OVER (ORDER BY count(full_sessions) DESC) AS rn FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY start_ts DESC) AS rn FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS}, - {",".join([f'metadata_{m["index"]} AS {m["key"]}' for m in meta_keys])} + {",".join([f'metadata_{m["index"]}' for m in meta_keys])} {query_part} ORDER BY s.session_id desc) AS filtred_sessions ORDER BY favorite DESC, issue_score DESC, {sort} {data.order}) AS full_sessions @@ -211,7 +210,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f main_query = cur.mogrify(f"""SELECT COUNT(full_sessions) AS count, COALESCE(JSONB_AGG(full_sessions) FILTER (WHERE rn <= 200), '[]'::JSONB) AS sessions FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY favorite DESC, issue_score DESC, session_id desc, start_ts desc) AS rn FROM (SELECT DISTINCT ON(s.session_id) {SESSION_PROJECTION_COLS}, - {",".join([f'metadata_{m["index"]} AS {m["key"]}' for m in meta_keys])} + {",".join([f'metadata_{m["index"]}' for m in meta_keys])} {query_part} ORDER BY s.session_id desc) AS filtred_sessions ORDER BY favorite DESC, issue_score DESC, {sort} {data.order}) AS full_sessions;""", @@ -250,10 +249,10 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f for i, s in enumerate(sessions): sessions[i] = {**s.pop("last_session")[0], **s} sessions[i].pop("rn") - sessions[i]["metadata"] = {k["key"]: sessions[i][k["key"]] for k in meta_keys} + sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys} else: for i, s in enumerate(sessions): - sessions[i]["metadata"] = {k["key"]: sessions[i][k["key"]] for k in meta_keys} + sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys} if not data.group_by_user and data.sort is not None and data.sort != "session_id": sessions = sorted(sessions, key=lambda s: s[helper.key_to_snake_case(data.sort)], reverse=data.order.upper() == "DESC") @@ -984,7 +983,6 @@ def get_favorite_sessions(project_id, user_id, include_viewed=False): s.session_id::text AS session_id, s.user_uuid, s.user_id, - -- s.user_agent, s.user_os, s.user_browser, s.user_device, @@ -1021,7 +1019,6 @@ def get_user_sessions(project_id, user_id, start_date, end_date): s.session_id::text AS session_id, s.user_uuid, s.user_id, - -- s.user_agent, s.user_os, s.user_browser, s.user_device, From 1457a6c5cff3b1de8e7ccf8f9cfd2c5d1768ec3e Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 17:48:33 +0100 Subject: [PATCH 19/30] feat(api): search-sessions ignore null metadata feat(api): search-sessions group by userId ignore metadata --- api/chalicelib/core/sessions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index f1add5dfe..36a5309cb 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -249,10 +249,12 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f for i, s in enumerate(sessions): sessions[i] = {**s.pop("last_session")[0], **s} sessions[i].pop("rn") - sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys} + sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys \ + if sessions[i][f'metadata_{k["index"]}'] is not None} else: for i, s in enumerate(sessions): - sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys} + sessions[i]["metadata"] = {k["key"]: sessions[i][f'metadata_{k["index"]}'] for k in meta_keys \ + if sessions[i][f'metadata_{k["index"]}'] is not None} if not data.group_by_user and data.sort is not None and data.sort != "session_id": sessions = sorted(sessions, key=lambda s: s[helper.key_to_snake_case(data.sort)], reverse=data.order.upper() == "DESC") From 7c9df8c196d821b11f1b9ee1575903b794b3cbfd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 18:08:21 +0100 Subject: [PATCH 20/30] 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 e54291028303c8cadcc8c61ce888eb5ce9d1ec51 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 22 Feb 2022 19:13:46 +0100 Subject: [PATCH 21/30] feat(api): search-sessions group by userId handle null sessions list --- api/chalicelib/core/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 36a5309cb..c095ce344 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -190,7 +190,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f {query_part};""", full_args) elif data.group_by_user: meta_keys = metadata.get(project_id=project_id) - main_query = cur.mogrify(f"""SELECT COUNT(*) AS count, jsonb_agg(users_sessions) FILTER ( WHERE rn <= 200 ) AS sessions + main_query = cur.mogrify(f"""SELECT COUNT(*) AS count, COALESCE(JSONB_AGG(users_sessions) FILTER ( WHERE rn <= 200 ), '[]'::JSONB) AS sessions FROM (SELECT user_id, count(full_sessions) AS user_sessions_count, jsonb_agg(full_sessions) FILTER (WHERE rn <= 1) AS last_session, From 914f13f89c0792d85ac4df614e6dbe252f3803b8 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Feb 2022 19:20:39 +0100 Subject: [PATCH 22/30] 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 23/30] 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 24/30] 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' }
-
+