[WIP] Mobile replayer (#1452)
* fix(ui): fix up mobile recordings display * fix(ui): some messages * fix(ui): some messages * fix(player): fix msg generation for ios messages * feat(player): add generic mmanager interface for ios player impl * feat(player): mobile player and message manager; touch manager; videoplayer * feat(player): add iphone shells, add log panel, * feat(player): detect ios sessions and inject correct player * feat(player): move screen mapper to utils * feat(player): events panel for mobile, map shell sizes to device type data, * feat(player): added network tab to mobile player; unify network message (ios and web) * feat(player): resize canvas up to phone screen size, fix capitalize util * feat(player): some general changes to support mobile events and network entries * feat(player): remove swipes from timeline * feat(player): more stuff for list walker * fix(ui): performance tab, mobile project typings and form * fix(ui):some ui touches for ios replayer shell * fix(ui): more fixes for ui, new onboarding screen for mobile projects * feat(ui): mobile overview panel (xray) * feat(ui): fixes for phone shell and tap events * fix(tracker): change phone shells and sizes * fix(tracker): fix border on replay screen * feat(ui): use crashes from db to show in session * feat(ui): use event name for xray * feat(ui): some overall ui fixes * feat(ui): IOS -> iOS * feat(ui): change tags to ant d * fix(ui): fast fix * fix(ui): fix for capitalizer * fix(ui): fix for browser display * fix(ui): fix for note popup * fix(ui): change exceptions display * fix(ui): add click rage to ios xray * fix(ui): some icons and resizing * fix(ui): fix ios context menu overlay, fix console logs creation for ios * feat(ui): added icons * feat(ui): performance warnings * feat(ui): performance warnings * feat(ui): different styles * feat(ui): rm debug true * feat(ui): fix warnings display * feat(ui): some styles for animation * feat(ui): add some animations to warnings * feat(ui): move perf warnings to performance graph * feat(ui): hide/show warns dynamically * feat(ui): new mobile touch animation * fix(tracker): update msg for ios * fix(ui): taprage fixes * fix(ui): regenerate icons and messages * fix(ui): fix msgs * fix(backend): fix events gen * fix(backend): fix userid msg
This commit is contained in:
parent
3214e58ff5
commit
35461acaf3
109 changed files with 4567 additions and 1082 deletions
|
|
@ -63,7 +63,7 @@ func main() {
|
|||
messages.MsgUnbindNodes,
|
||||
// Mobile messages
|
||||
messages.MsgIOSSessionStart, messages.MsgIOSSessionEnd, messages.MsgIOSUserID, messages.MsgIOSUserAnonymousID,
|
||||
messages.MsgIOSMetadata, messages.MsgIOSCustomEvent, messages.MsgIOSNetworkCall,
|
||||
messages.MsgIOSMetadata, messages.MsgIOSEvent, messages.MsgIOSNetworkCall,
|
||||
messages.MsgIOSClickEvent, messages.MsgIOSSwipeEvent, messages.MsgIOSInputEvent,
|
||||
messages.MsgIOSCrash, messages.MsgIOSIssueEvent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,21 +72,21 @@ func (s *saverImpl) handleMobileMessage(msg Message) error {
|
|||
case *IOSSessionEnd:
|
||||
return s.pg.InsertIOSSessionEnd(m.SessionID(), m)
|
||||
case *IOSUserID:
|
||||
if err = s.sessions.UpdateUserID(session.SessionID, m.Value); err != nil {
|
||||
if err = s.sessions.UpdateUserID(session.SessionID, m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERID_IOS", m.Value)
|
||||
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERID_IOS", m.ID)
|
||||
return nil
|
||||
case *IOSUserAnonymousID:
|
||||
if err = s.sessions.UpdateAnonymousID(session.SessionID, m.Value); err != nil {
|
||||
if err = s.sessions.UpdateAnonymousID(session.SessionID, m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID_IOS", m.Value)
|
||||
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID_IOS", m.ID)
|
||||
return nil
|
||||
case *IOSMetadata:
|
||||
return s.sessions.UpdateMetadata(m.SessionID(), m.Key, m.Value)
|
||||
case *IOSCustomEvent:
|
||||
return s.pg.InsertIOSCustomEvent(session, m)
|
||||
case *IOSEvent:
|
||||
return s.pg.InsertIOSEvent(session, m)
|
||||
case *IOSClickEvent:
|
||||
if err := s.pg.InsertIOSClickEvent(session, m); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ func (conn *Conn) InsertIOSSessionEnd(sessionID uint64, e *messages.IOSSessionEn
|
|||
return nil
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertIOSCustomEvent(session *sessions.Session, e *messages.IOSCustomEvent) error {
|
||||
func (conn *Conn) InsertIOSEvent(session *sessions.Session, e *messages.IOSEvent) error {
|
||||
if err := conn.InsertCustomEvent(session.SessionID, e.Timestamp, truncSqIdx(e.Index), e.Name, e.Payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package messages
|
||||
|
||||
func IsReplayerType(id int) bool {
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 104 != id && 107 != id && 110 != id && 111 != id
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id
|
||||
}
|
||||
|
||||
func IsIOSType(id int) bool {
|
||||
|
|
@ -10,5 +10,5 @@ func IsIOSType(id int) bool {
|
|||
}
|
||||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 105 == id || 106 == id
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func GetTimestamp(message Message) uint64 {
|
|||
case *IOSMetadata:
|
||||
return msg.Timestamp
|
||||
|
||||
case *IOSCustomEvent:
|
||||
case *IOSEvent:
|
||||
return msg.Timestamp
|
||||
|
||||
case *IOSUserID:
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const (
|
|||
MsgIOSSessionStart = 90
|
||||
MsgIOSSessionEnd = 91
|
||||
MsgIOSMetadata = 92
|
||||
MsgIOSCustomEvent = 93
|
||||
MsgIOSEvent = 93
|
||||
MsgIOSUserID = 94
|
||||
MsgIOSUserAnonymousID = 95
|
||||
MsgIOSScreenChanges = 96
|
||||
|
|
@ -2411,7 +2411,7 @@ func (msg *IOSMetadata) TypeID() int {
|
|||
return 92
|
||||
}
|
||||
|
||||
type IOSCustomEvent struct {
|
||||
type IOSEvent struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
Length uint64
|
||||
|
|
@ -2419,7 +2419,7 @@ type IOSCustomEvent struct {
|
|||
Payload string
|
||||
}
|
||||
|
||||
func (msg *IOSCustomEvent) Encode() []byte {
|
||||
func (msg *IOSEvent) Encode() []byte {
|
||||
buf := make([]byte, 41+len(msg.Name)+len(msg.Payload))
|
||||
buf[0] = 93
|
||||
p := 1
|
||||
|
|
@ -2430,11 +2430,11 @@ func (msg *IOSCustomEvent) Encode() []byte {
|
|||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *IOSCustomEvent) Decode() Message {
|
||||
func (msg *IOSEvent) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *IOSCustomEvent) TypeID() int {
|
||||
func (msg *IOSEvent) TypeID() int {
|
||||
return 93
|
||||
}
|
||||
|
||||
|
|
@ -2442,16 +2442,16 @@ type IOSUserID struct {
|
|||
message
|
||||
Timestamp uint64
|
||||
Length uint64
|
||||
Value string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (msg *IOSUserID) Encode() []byte {
|
||||
buf := make([]byte, 31+len(msg.Value))
|
||||
buf := make([]byte, 31+len(msg.ID))
|
||||
buf[0] = 94
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
p = WriteUint(msg.Length, buf, p)
|
||||
p = WriteString(msg.Value, buf, p)
|
||||
p = WriteString(msg.ID, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
|
|
@ -2467,16 +2467,16 @@ type IOSUserAnonymousID struct {
|
|||
message
|
||||
Timestamp uint64
|
||||
Length uint64
|
||||
Value string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (msg *IOSUserAnonymousID) Encode() []byte {
|
||||
buf := make([]byte, 31+len(msg.Value))
|
||||
buf := make([]byte, 31+len(msg.ID))
|
||||
buf[0] = 95
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
p = WriteUint(msg.Length, buf, p)
|
||||
p = WriteString(msg.Value, buf, p)
|
||||
p = WriteString(msg.ID, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1479,9 +1479,9 @@ func DecodeIOSMetadata(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIOSCustomEvent(reader BytesReader) (Message, error) {
|
||||
func DecodeIOSEvent(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IOSCustomEvent{}
|
||||
msg := &IOSEvent{}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1506,7 +1506,7 @@ func DecodeIOSUserID(reader BytesReader) (Message, error) {
|
|||
if msg.Length, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Value, err = reader.ReadString(); err != nil {
|
||||
if msg.ID, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
|
|
@ -1521,7 +1521,7 @@ func DecodeIOSUserAnonymousID(reader BytesReader) (Message, error) {
|
|||
if msg.Length, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Value, err = reader.ReadString(); err != nil {
|
||||
if msg.ID, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
|
|
@ -2006,7 +2006,7 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
case 92:
|
||||
return DecodeIOSMetadata(reader)
|
||||
case 93:
|
||||
return DecodeIOSCustomEvent(reader)
|
||||
return DecodeIOSEvent(reader)
|
||||
case 94:
|
||||
return DecodeIOSUserID(reader)
|
||||
case 95:
|
||||
|
|
|
|||
|
|
@ -705,13 +705,13 @@ def handle_message(message: Message) -> Optional[DetailedEvent]:
|
|||
if isinstance(message, IOSUserID):
|
||||
n.iosuserid_timestamp = message.timestamp
|
||||
n.iosuserid_length = message.length
|
||||
n.iosuserid_value = message.value
|
||||
n.iosuserid_id = message.id
|
||||
return n
|
||||
|
||||
if isinstance(message, IOSUserAnonymousID):
|
||||
n.iosuseranonymousid_timestamp = message.timestamp
|
||||
n.iosuseranonymousid_length = message.length
|
||||
n.iosuseranonymousid_value = message.value
|
||||
n.iosuseranonymousid_id = message.id
|
||||
return n
|
||||
|
||||
if isinstance(message, IOSScreenEnter):
|
||||
|
|
@ -779,11 +779,11 @@ def handle_message(message: Message) -> Optional[DetailedEvent]:
|
|||
n.iosissueevent_payload = message.payload
|
||||
return n
|
||||
|
||||
if isinstance(message, IOSCustomEvent):
|
||||
n.ioscustomevent_timestamp = message.timestamp
|
||||
n.ioscustomevent_length = message.length
|
||||
n.ioscustomevent_name = message.name
|
||||
n.ioscustomevent_payload = message.payload
|
||||
if isinstance(message, IOSEvent):
|
||||
n.iosevent_timestamp = message.timestamp
|
||||
n.iosevent_length = message.length
|
||||
n.iosevent_name = message.name
|
||||
n.iosevent_payload = message.payload
|
||||
return n
|
||||
|
||||
if isinstance(message, IOSInternalError):
|
||||
|
|
|
|||
|
|
@ -850,7 +850,7 @@ class IOSMetadata(Message):
|
|||
self.value = value
|
||||
|
||||
|
||||
class IOSCustomEvent(Message):
|
||||
class IOSEvent(Message):
|
||||
__id__ = 93
|
||||
|
||||
def __init__(self, timestamp, length, name, payload):
|
||||
|
|
@ -863,19 +863,19 @@ class IOSCustomEvent(Message):
|
|||
class IOSUserID(Message):
|
||||
__id__ = 94
|
||||
|
||||
def __init__(self, timestamp, length, value):
|
||||
def __init__(self, timestamp, length, id):
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
class IOSUserAnonymousID(Message):
|
||||
__id__ = 95
|
||||
|
||||
def __init__(self, timestamp, length, value):
|
||||
def __init__(self, timestamp, length, id):
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
class IOSScreenChanges(Message):
|
||||
|
|
|
|||
|
|
@ -1258,7 +1258,7 @@ cdef class IOSMetadata(PyMessage):
|
|||
self.value = value
|
||||
|
||||
|
||||
cdef class IOSCustomEvent(PyMessage):
|
||||
cdef class IOSEvent(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
cdef public unsigned long length
|
||||
|
|
@ -1277,26 +1277,26 @@ cdef class IOSUserID(PyMessage):
|
|||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
cdef public unsigned long length
|
||||
cdef public str value
|
||||
cdef public str id
|
||||
|
||||
def __init__(self, unsigned long timestamp, unsigned long length, str value):
|
||||
def __init__(self, unsigned long timestamp, unsigned long length, str id):
|
||||
self.__id__ = 94
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
cdef class IOSUserAnonymousID(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
cdef public unsigned long length
|
||||
cdef public str value
|
||||
cdef public str id
|
||||
|
||||
def __init__(self, unsigned long timestamp, unsigned long length, str value):
|
||||
def __init__(self, unsigned long timestamp, unsigned long length, str id):
|
||||
self.__id__ = 95
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
cdef class IOSScreenChanges(PyMessage):
|
||||
|
|
|
|||
|
|
@ -762,7 +762,7 @@ class MessageCodec(Codec):
|
|||
)
|
||||
|
||||
if message_id == 93:
|
||||
return IOSCustomEvent(
|
||||
return IOSEvent(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
name=self.read_string(reader),
|
||||
|
|
@ -773,14 +773,14 @@ class MessageCodec(Codec):
|
|||
return IOSUserID(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 95:
|
||||
return IOSUserAnonymousID(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 96:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -646,7 +646,7 @@ class MessageCodec(Codec):
|
|||
)
|
||||
|
||||
if message_id == 93:
|
||||
return IOSCustomEvent(
|
||||
return IOSEvent(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
name=self.read_string(reader),
|
||||
|
|
@ -657,14 +657,14 @@ class MessageCodec(Codec):
|
|||
return IOSUserID(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 95:
|
||||
return IOSUserAnonymousID(
|
||||
timestamp=self.read_uint(reader),
|
||||
length=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 96:
|
||||
|
|
|
|||
|
|
@ -739,7 +739,7 @@ class IOSMetadata(Message):
|
|||
self.value = value
|
||||
|
||||
|
||||
class IOSCustomEvent(Message):
|
||||
class IOSEvent(Message):
|
||||
__id__ = 93
|
||||
|
||||
def __init__(self, timestamp, length, name: str, payload: str):
|
||||
|
|
@ -752,19 +752,19 @@ class IOSCustomEvent(Message):
|
|||
class IOSUserID(Message):
|
||||
__id__ = 94
|
||||
|
||||
def __init__(self, timestamp, length, value: str):
|
||||
def __init__(self, timestamp, length, id: str):
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
class IOSUserAnonymousID(Message):
|
||||
__id__ = 95
|
||||
|
||||
def __init__(self, timestamp, length, value: str):
|
||||
def __init__(self, timestamp, length, id: str):
|
||||
self.timestamp = timestamp
|
||||
self.length = length
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
|
||||
class IOSScreenChanges(Message):
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ function Integrations(props: Props) {
|
|||
<div className='mb-4' />
|
||||
|
||||
{filteredIntegrations.map((cat: any) => (
|
||||
<div className='grid grid-cols-3 mt-4 gap-3 auto-cols-max'>
|
||||
<div className={cn('grid grid-cols-3 gap-3 auto-cols-max', cat.integrations.length > 0 ? 'p-2' : '')}>
|
||||
{cat.integrations.map((integration: any) => (
|
||||
<IntegrationItem
|
||||
integrated={integratedList.includes(integration.slug)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Form, Input, Button, Icon } from 'UI';
|
||||
import { Form, Input, Button, Icon, SegmentSelection } from 'UI';
|
||||
import { save, edit, update, fetchList, remove } from 'Duck/site';
|
||||
import { pushNewSite } from 'Duck/user';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
|
|
@ -115,6 +115,20 @@ const NewSiteForm = ({
|
|||
className={styles.input}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Project Type</label>
|
||||
<SegmentSelection
|
||||
name={"platform"}
|
||||
value={{ name: site.platform, value: site.platform }}
|
||||
list={[
|
||||
{ name: 'Web', value: 'web' },
|
||||
{ name: 'iOS', value: 'ios' },
|
||||
]}
|
||||
onSelect={(_, { value }) => {
|
||||
edit({ platform: value });
|
||||
}}
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Drawer } from 'antd';
|
||||
import { Drawer, Tag } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
Loader,
|
||||
|
|
@ -8,7 +8,9 @@ import {
|
|||
TextLink,
|
||||
NoContent,
|
||||
Pagination,
|
||||
PageTitle, Divider
|
||||
PageTitle,
|
||||
Divider,
|
||||
Icon,
|
||||
} from 'UI';
|
||||
import {
|
||||
init,
|
||||
|
|
@ -31,6 +33,7 @@ import CaptureRate from 'Shared/SessionSettings/components/CaptureRate';
|
|||
type Project = {
|
||||
id: number;
|
||||
name: string;
|
||||
platform: 'web' | 'mobile';
|
||||
host: string;
|
||||
projectKey: string;
|
||||
sampleRate: number;
|
||||
|
|
@ -91,39 +94,32 @@ const Sites = ({
|
|||
const ProjectItem = ({ project }: { project: Project }) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className='grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center px-5 py-3'
|
||||
className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center px-5 py-3"
|
||||
>
|
||||
<div className='col-span-4'>
|
||||
<div className='flex items-center'>
|
||||
<div className='relative flex items-center justify-center w-10 h-10'>
|
||||
<div
|
||||
className='absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-30 bg-tealx' />
|
||||
<div className='text-lg uppercase color-tealx'>
|
||||
{getInitials(project.name)}
|
||||
<div className="col-span-4">
|
||||
<div className="flex items-center">
|
||||
<div className="relative flex items-center justify-center w-10 h-10 rounded-full bg-tealx-light">
|
||||
<Icon color={'tealx'} size={18} name={project.platform === 'web' ? 'browser/browser' : 'mobile'} />
|
||||
</div>
|
||||
<span className="ml-2">{project.host}</span>
|
||||
<div className={'ml-4 flex items-center gap-2'}>
|
||||
{project.platform === 'web' ? null : <Tag color="error">iOS BETA</Tag>}
|
||||
</div>
|
||||
</div>
|
||||
<span className='ml-2'>{project.host}</span>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<ProjectKey value={project.projectKey} tooltip="Project key copied to clipboard" />
|
||||
</div>
|
||||
<div className='col-span-3'>
|
||||
<ProjectKey
|
||||
value={project.projectKey}
|
||||
tooltip='Project key copied to clipboard'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<Button
|
||||
variant='text-primary'
|
||||
onClick={() => captureRateClickHandler(project)}
|
||||
>
|
||||
<div className="col-span-2">
|
||||
<Button variant="text-primary" onClick={() => captureRateClickHandler(project)}>
|
||||
{project.sampleRate}%
|
||||
</Button>
|
||||
</div>
|
||||
<div className='col-span-3 justify-self-end flex items-center'>
|
||||
<div className='mr-4'>
|
||||
<div className="col-span-3 justify-self-end flex items-center">
|
||||
<div className="mr-4">
|
||||
<InstallButton site={project} />
|
||||
</div>
|
||||
<div className='invisible group-hover:visible'>
|
||||
<div className="invisible group-hover:visible">
|
||||
<EditButton isAdmin={isAdmin} onClick={() => init(project)} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default class SiteDropdown extends React.PureComponent {
|
|||
<div className="border-b border-dashed my-1" />
|
||||
{sites.map((site) => (
|
||||
<li key={site.id} onClick={() => this.switchSite(site.id)}>
|
||||
<Icon name="folder2" size="16" />
|
||||
<Icon name={site.platform === 'web' ? 'browser/browser' : 'mobile'} size="16" />
|
||||
<span className="ml-3">{site.host}</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Segmented } from 'antd';
|
||||
import React from 'react';
|
||||
import CircleNumber from '../CircleNumber';
|
||||
import MetadataList from '../MetadataList/MetadataList';
|
||||
|
|
@ -10,6 +11,32 @@ import withPageTitle from 'App/components/hocs/withPageTitle';
|
|||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
function IdentifyUsersTab(props: Props) {
|
||||
const platforms = [
|
||||
{
|
||||
label: (
|
||||
<div className={'font-semibold flex gap-2 items-center'}>
|
||||
<Icon name="browser/browser" size={16} /> Web
|
||||
</div>
|
||||
),
|
||||
value: 'web',
|
||||
} as const,
|
||||
{
|
||||
label: (
|
||||
<div className={'font-semibold flex gap-2 items-center'}>
|
||||
<Icon name="mobile" size={16} /> Mobile
|
||||
</div>
|
||||
),
|
||||
value: 'mobile',
|
||||
} as const,
|
||||
];
|
||||
const [platform, setPlatform] = React.useState(platforms[0]);
|
||||
const { site } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (site.platform)
|
||||
setPlatform(platforms.find(({ value }) => value === site.platform) || platforms[0]);
|
||||
}, [site]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b justify-between">
|
||||
|
|
@ -18,12 +45,25 @@ function IdentifyUsersTab(props: Props) {
|
|||
<div className="ml-3">Identify Users</div>
|
||||
</div>
|
||||
|
||||
<a href="https://docs.openreplay.com/en/v1.10.0/installation/identify-user/" target="_blank">
|
||||
<a
|
||||
href="https://docs.openreplay.com/en/v1.10.0/installation/identify-user/"
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="text-primary" icon="question-circle" className="ml-2">
|
||||
See Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="p-4 flex gap-2 items-center">
|
||||
<span className="font-medium">Your platform</span>
|
||||
<Segmented
|
||||
options={platforms}
|
||||
value={platform.value}
|
||||
onChange={(value) =>
|
||||
setPlatform(platforms.find(({ value: v }) => v === value) || platforms[0])
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4 w-full p-4">
|
||||
<div className="col-span-4">
|
||||
<div>
|
||||
|
|
@ -34,12 +74,17 @@ function IdentifyUsersTab(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{platform.value === 'web' ? (
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
) : (
|
||||
<HighlightCode className="swift" text={`ORTracker.shared.setUserID('john@doe.com');`} />
|
||||
)}
|
||||
{platform.value === 'web' ? (
|
||||
<div className="flex items-center my-2">
|
||||
<Icon name="info-circle" color="gray-darkest" />
|
||||
<span className="ml-2">OpenReplay keeps the last communicated user ID.</span>
|
||||
</div>
|
||||
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DocCard
|
||||
|
|
@ -78,7 +123,9 @@ function IdentifyUsersTab(props: Props) {
|
|||
Use the <span className="highlight-blue">setMetadata</span> method in your code to
|
||||
inject custom user data in the form of a key/value pair (string).
|
||||
</div>
|
||||
{platform.value === 'web' ? (
|
||||
<HighlightCode className="js" text={`tracker.setMetadata('plan', 'premium');`} />
|
||||
) : <HighlightCode className="swift" text={`ORTracker.shared.setMetadata('plan', 'premium');`} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,4 +156,4 @@ function IdentifyUsersTab(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(withPageTitle("Identify Users - OpenReplay")(IdentifyUsersTab));
|
||||
export default withOnboarding(withPageTitle('Identify Users - OpenReplay')(IdentifyUsersTab));
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
import React from 'react';
|
||||
import OnboardingTabs from '../OnboardingTabs';
|
||||
import MobileOnboardingTabs from '../OnboardingTabs/OnboardingMobileTabs'
|
||||
import ProjectFormButton from '../ProjectFormButton';
|
||||
import { Button, Icon } from 'UI';
|
||||
import {Button, Icon } from 'UI';
|
||||
import withOnboarding from '../withOnboarding';
|
||||
import { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
import withPageTitle from 'App/components/hocs/withPageTitle';
|
||||
import { Segmented } from "antd";
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
function InstallOpenReplayTab(props: Props) {
|
||||
const platforms = [
|
||||
{ label: <div className={"font-semibold flex gap-2 items-center"}><Icon name="browser/browser" size={16} /> Web</div>, value: 'web' } as const,
|
||||
{ label: <div className={"font-semibold flex gap-2 items-center"}><Icon name="mobile" size={16} /> Mobile</div>, value: 'mobile' } as const,
|
||||
]
|
||||
const [platform, setPlatform] = React.useState(platforms[0]);
|
||||
const { site } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (site.platform) setPlatform(platforms.find(({ value }) => value === site.platform) || platforms[0])
|
||||
}, [site])
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b justify-between">
|
||||
|
|
@ -27,12 +38,18 @@ function InstallOpenReplayTab(props: Props) {
|
|||
</Button>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="p-4">
|
||||
<div className="mb-6 text-lg font-medium">
|
||||
Setup OpenReplay through NPM package <span className="text-sm">(recommended)</span> or
|
||||
script.
|
||||
<div className="p-4 flex gap-2 items-center">
|
||||
<span className="font-medium">Your platform</span>
|
||||
<Segmented
|
||||
options={platforms}
|
||||
value={platform.value}
|
||||
onChange={(value) => setPlatform(platforms.find(({ value: v }) => v === value) || platforms[0])}
|
||||
/>
|
||||
</div>
|
||||
<OnboardingTabs site={site} />
|
||||
<div className="p-4">
|
||||
{
|
||||
platform.value === 'web' ? <Snippet site={site} /> : <MobileOnboardingTabs site={site} />
|
||||
}
|
||||
</div>
|
||||
<div className="border-t px-4 py-3 flex justify-end">
|
||||
<Button
|
||||
|
|
@ -48,4 +65,16 @@ function InstallOpenReplayTab(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
function Snippet({ site }: { site: Record<string, any>}) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-lg font-medium">
|
||||
Setup OpenReplay through NPM package <span className="text-sm">(recommended)</span> or
|
||||
script.
|
||||
</div>
|
||||
<OnboardingTabs site={site} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withOnboarding(withPageTitle("Project Setup - OpenReplay")(InstallOpenReplayTab));
|
||||
|
|
|
|||
|
|
@ -104,6 +104,4 @@ function InstallDocs({ site }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
}))(InstallDocs);
|
||||
export default InstallDocs;
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import React from 'react';
|
||||
import stl from './installDocs.module.css';
|
||||
import cn from 'classnames';
|
||||
import Highlight from 'react-highlight';
|
||||
import CircleNumber from '../../CircleNumber';
|
||||
import { CopyButton } from 'UI';
|
||||
|
||||
const installationCommand = 'add command after publishing!';
|
||||
const usageCode = `// AppDelegate.swift
|
||||
import ORTracker
|
||||
|
||||
//...
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
ORTracker.shared.serverURL = "https://your.instance.com/ingest"
|
||||
ORTracker.shared.start(projectKey: "PROJECT_KEY", options: .defaults)
|
||||
|
||||
// ...
|
||||
return true
|
||||
}
|
||||
// ...`;
|
||||
const configuration = `let crashs: Bool
|
||||
let analytics: Bool
|
||||
let performances: Bool
|
||||
let logs: Bool
|
||||
let screen: Bool
|
||||
let wifiOnly: Bool`;
|
||||
|
||||
const touches = `// SceneDelegate.Swift
|
||||
import ORTracker
|
||||
|
||||
// ...
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
let contentView = ContentView()
|
||||
.environmentObject(TodoStore())
|
||||
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = TouchTrackingWindow(windowScene: windowScene) // <<<< here
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
// ...`
|
||||
|
||||
const sensitive = `import ORTracker
|
||||
|
||||
// swiftUI
|
||||
Text("Very important sensitive text")
|
||||
.sensitive()
|
||||
|
||||
// UIKit
|
||||
ORTracker.shared.addIgnoredView(view)`
|
||||
|
||||
const inputs = `// swiftUI
|
||||
TextField("Input", text: $text)
|
||||
.observeInput(text: $text, label: "tracker input #1", masked: Bool)
|
||||
|
||||
// UIKit will use placeholder as label and sender.isSecureTextEntry to mask the input
|
||||
Analytics.shared.addObservedInput(inputEl)`
|
||||
|
||||
function MobileInstallDocs({ site }: any) {
|
||||
const _usageCode = usageCode.replace('PROJECT_KEY', site.projectKey);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="1" />
|
||||
Install the Swift Package
|
||||
</div>
|
||||
<div className={cn(stl.snippetWrapper, 'ml-10')}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={installationCommand} />
|
||||
</div>
|
||||
<Highlight className="cli">{installationCommand}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="2" />
|
||||
Add to your app
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={_usageCode} />
|
||||
</div>
|
||||
<Highlight className="swift">{_usageCode}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-semibold mb-2 mt-4 flex items-center">
|
||||
<CircleNumber text="3" />
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<Highlight className="swift">{configuration}</Highlight>
|
||||
<div className={"mt-2"}>By default, all options equals <code className={'p-1 text-red rounded bg-gray-lightest'}>true</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-semibold mb-2 mt-4 flex items-center">
|
||||
<CircleNumber text="4" />
|
||||
Set up touch events listener
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={touches} />
|
||||
</div>
|
||||
<Highlight className="swift">{touches}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-semibold mb-2 mt-4 flex items-center">
|
||||
<CircleNumber text="5" />
|
||||
Hide sensitive views
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={sensitive} />
|
||||
</div>
|
||||
<Highlight className="swift">{sensitive}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-semibold mb-2 mt-4 flex items-center">
|
||||
<CircleNumber text="6" />
|
||||
Track inputs
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={inputs} />
|
||||
</div>
|
||||
<Highlight className="swift">{inputs}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileInstallDocs
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Tabs, CopyButton } from 'UI';
|
||||
import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
|
||||
import DocCard from 'Shared/DocCard/DocCard';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||
|
||||
const iOS = 'iOS';
|
||||
const TABS = [
|
||||
{ key: iOS, text: iOS },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
site: Record<string, any>;
|
||||
}
|
||||
|
||||
const MobileTrackingCodeModal = (props: Props) => {
|
||||
const { site } = props;
|
||||
const [activeTab, setActiveTab] = useState(iOS);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const showUserModal = () => {
|
||||
showModal(<UserForm />, { right: true });
|
||||
};
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case iOS:
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<MobileInstallDocs site={site} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<DocCard title="Need help from team member?">
|
||||
<a className="link" onClick={showUserModal}>
|
||||
Invite and Collaborate
|
||||
</a>
|
||||
</DocCard>
|
||||
|
||||
<DocCard title="Project Key">
|
||||
<div className={'p-2 rounded bg-white flex justify-between items-center'}>
|
||||
{site.projectKey}
|
||||
<CopyButton content={site.projectKey} />
|
||||
</div>
|
||||
</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={setActiveTab} />
|
||||
<div className="p-5 py-8">{renderActiveTab()}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileTrackingCodeModal;
|
||||
|
|
@ -31,7 +31,7 @@ const TrackingCodeModal = (props: Props) => {
|
|||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<ProjectCodeSnippet />
|
||||
<ProjectCodeSnippet site={site} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
|
|
@ -43,7 +43,7 @@ const TrackingCodeModal = (props: Props) => {
|
|||
<DocCard title="Project Key">
|
||||
<div className="rounded bg-white px-2 py-1 flex items-center justify-between">
|
||||
<span>{site.projectKey}</span>
|
||||
<CopyButton content={''} className="capitalize" />
|
||||
<CopyButton content={site.projectKey} className="capitalize" />
|
||||
</div>
|
||||
</DocCard>
|
||||
<DocCard title="Other ways to install">
|
||||
|
|
@ -63,7 +63,7 @@ const TrackingCodeModal = (props: Props) => {
|
|||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<InstallDocs />
|
||||
<InstallDocs site={site} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
|
|
@ -72,6 +72,13 @@ const TrackingCodeModal = (props: Props) => {
|
|||
Invite and Collaborate
|
||||
</a>
|
||||
</DocCard>
|
||||
|
||||
<DocCard title="Project Key">
|
||||
<div className={'p-2 rounded bg-white flex justify-between items-center'}>
|
||||
{site.projectKey}
|
||||
<CopyButton content={site.projectKey} />
|
||||
</div>
|
||||
</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { CountryFlag, IconButton, BackLink } from 'UI';
|
|||
import { toggleFavorite } from 'Duck/sessions';
|
||||
import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
||||
import SharePopup from 'Shared/SharePopup/SharePopup';
|
||||
import { capitalize } from "App/utils";
|
||||
|
||||
import Section from './Header/Section';
|
||||
import Resolution from './Header/Resolution';
|
||||
|
|
@ -18,10 +19,6 @@ import cls from './header.module.css';
|
|||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
function capitalise(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
function Header({
|
||||
player,
|
||||
|
|
|
|||
159
frontend/app/components/Session/MobilePlayer.tsx
Normal file
159
frontend/app/components/Session/MobilePlayer.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal, Loader } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import { createIOSPlayer } from 'Player';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||
import { useStore } from 'App/mstore';
|
||||
import MobilePlayerHeader from 'Components/Session/Player/MobilePlayer/MobilePlayerHeader';
|
||||
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
||||
import PlayerContent from './Player/MobilePlayer/PlayerContent';
|
||||
import { IOSPlayerContext, defaultContextValue, MobilePlayerContext } from './playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import PlayerErrorBoundary from 'Components/Session/Player/PlayerErrorBoundary';
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'User Events',
|
||||
};
|
||||
|
||||
let playerInst: IOSPlayerContext['player'] | undefined;
|
||||
|
||||
function MobilePlayer(props: any) {
|
||||
const { session, toggleFullscreen, closeBottomBlock, fullscreen, fetchList } = props;
|
||||
|
||||
const { notesStore, sessionStore } = useStore();
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [noteItem, setNoteItem] = useState<Note | undefined>(undefined);
|
||||
// @ts-ignore
|
||||
const [contextValue, setContextValue] = useState<IOSPlayerContext>(defaultContextValue);
|
||||
const params: { sessionId: string } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
playerInst = undefined;
|
||||
if (!session.sessionId || contextValue.player !== undefined) return;
|
||||
fetchList('issues');
|
||||
sessionStore.setUserTimezone(session.timezone);
|
||||
const [IOSPlayerInst, PlayerStore] = createIOSPlayer(
|
||||
session,
|
||||
(state) => makeAutoObservable(state),
|
||||
toast
|
||||
);
|
||||
setContextValue({ player: IOSPlayerInst, store: PlayerStore });
|
||||
playerInst = IOSPlayerInst;
|
||||
|
||||
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
|
||||
const note = props.query.get('note');
|
||||
if (note) {
|
||||
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
|
||||
IOSPlayerInst.pause();
|
||||
}
|
||||
});
|
||||
}, [session.sessionId]);
|
||||
|
||||
const { messagesProcessed } = contextValue.store?.get() || {};
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((messagesProcessed && session.events.length > 0) || session.errors.length > 0) {
|
||||
contextValue.player?.updateLists?.(session);
|
||||
}
|
||||
}, [session.events, session.errors, contextValue.player, messagesProcessed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (noteItem !== undefined) {
|
||||
contextValue.player.pause();
|
||||
}
|
||||
|
||||
if (activeTab === '' && !noteItem !== undefined && messagesProcessed && contextValue.player) {
|
||||
const jumpToTime = props.query.get('jumpto');
|
||||
|
||||
if (jumpToTime) {
|
||||
contextValue.player.jump(parseInt(jumpToTime));
|
||||
}
|
||||
|
||||
contextValue.player.play();
|
||||
}
|
||||
}, [activeTab, noteItem, messagesProcessed]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
console.debug('cleaning up player after', params.sessionId);
|
||||
toggleFullscreen(false);
|
||||
closeBottomBlock();
|
||||
playerInst?.clean();
|
||||
// @ts-ignore
|
||||
setContextValue(defaultContextValue);
|
||||
},
|
||||
[params.sessionId]
|
||||
);
|
||||
|
||||
const onNoteClose = () => {
|
||||
setNoteItem(undefined);
|
||||
contextValue.player.play();
|
||||
};
|
||||
|
||||
if (!session.sessionId)
|
||||
return (
|
||||
<Loader
|
||||
size={75}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
height: 75,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MobilePlayerContext.Provider value={contextValue}>
|
||||
<MobilePlayerHeader
|
||||
// @ts-ignore TODO?
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tabs={TABS}
|
||||
fullscreen={fullscreen}
|
||||
/>
|
||||
<PlayerErrorBoundary>
|
||||
{contextValue.player ? (
|
||||
<PlayerContent
|
||||
activeTab={activeTab}
|
||||
fullscreen={fullscreen}
|
||||
setActiveTab={setActiveTab}
|
||||
session={session}
|
||||
/>
|
||||
) : (
|
||||
<Loader
|
||||
style={{ position: 'fixed', top: '0%', left: '50%', transform: 'translateX(-50%)' }}
|
||||
/>
|
||||
)}
|
||||
<Modal open={noteItem !== undefined} onClose={onNoteClose}>
|
||||
{noteItem !== undefined ? (
|
||||
<ReadNote note={noteItem} onClose={onNoteClose} notFound={!noteItem} />
|
||||
) : null}
|
||||
</Modal>
|
||||
</PlayerErrorBoundary>
|
||||
</MobilePlayerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
|
||||
jwt: state.getIn(['user', 'jwt']),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
showEvents: state.get('showEvents'),
|
||||
members: state.getIn(['members', 'list']),
|
||||
}),
|
||||
{
|
||||
toggleFullscreen,
|
||||
closeBottomBlock,
|
||||
fetchList,
|
||||
}
|
||||
)(withLocationHandlers()(observer(MobilePlayer)));
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui';
|
||||
|
||||
import { Tooltip } from 'UI';
|
||||
import {
|
||||
fullscreenOff,
|
||||
fullscreenOn,
|
||||
OVERVIEW,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
CONSOLE, STACKEVENTS, NETWORK, PERFORMANCE, EXCEPTIONS,
|
||||
} from 'Duck/components/player';
|
||||
import { MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { fetchSessions } from 'Duck/liveSearch';
|
||||
|
||||
import Timeline from 'Components/Session_/Player/Controls/Timeline';
|
||||
import ControlButton from 'Components/Session_/Player/Controls/ControlButton';
|
||||
import PlayerControls from 'Components/Session_/Player/Controls/components/PlayerControls';
|
||||
|
||||
import styles from 'Components/Session_/Player/Controls/controls.module.css';
|
||||
import XRayButton from 'Shared/XRayButton';
|
||||
import CreateNote from 'Components/Session_/Player/Controls/components/CreateNote';
|
||||
|
||||
export const SKIP_INTERVALS = {
|
||||
2: 2e3,
|
||||
5: 5e3,
|
||||
10: 1e4,
|
||||
15: 15e3,
|
||||
20: 2e4,
|
||||
30: 3e4,
|
||||
60: 6e4,
|
||||
};
|
||||
|
||||
function Controls(props: any) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
|
||||
const { playing, completed, skip, speed, messagesLoading } = store.get();
|
||||
|
||||
const { bottomBlock, toggleBottomBlock, fullscreen, changeSkipInterval, skipInterval, session } =
|
||||
props;
|
||||
|
||||
const disabled = messagesLoading;
|
||||
const sessionTz = session?.timezone;
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
props.fullscreenOff();
|
||||
}
|
||||
if (e.key === 'ArrowRight') {
|
||||
forthTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
backTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
player.speedDown();
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
player.speedUp();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown.bind(this));
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown.bind(this));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const forthTenSeconds = () => {
|
||||
// @ts-ignore
|
||||
player.jumpInterval(SKIP_INTERVALS[skipInterval]);
|
||||
};
|
||||
|
||||
const backTenSeconds = () => {
|
||||
// @ts-ignore
|
||||
player.jumpInterval(-SKIP_INTERVALS[skipInterval]);
|
||||
};
|
||||
|
||||
const toggleBottomTools = (blockName: number) => {
|
||||
toggleBottomBlock(blockName);
|
||||
};
|
||||
|
||||
const state = completed
|
||||
? PlayingState.Completed
|
||||
: playing
|
||||
? PlayingState.Playing
|
||||
: PlayingState.Paused;
|
||||
|
||||
return (
|
||||
<div className={styles.controls}>
|
||||
<Timeline isMobile />
|
||||
<CreateNote />
|
||||
{!fullscreen && (
|
||||
<div className={cn(styles.buttons, '!px-2')}>
|
||||
<div className="flex items-center">
|
||||
<PlayerControls
|
||||
skip={skip}
|
||||
sessionTz={sessionTz}
|
||||
speed={speed}
|
||||
disabled={disabled}
|
||||
backTenSeconds={backTenSeconds}
|
||||
forthTenSeconds={forthTenSeconds}
|
||||
toggleSpeed={(speedIndex) => player.toggleSpeed(speedIndex)}
|
||||
toggleSkip={() => player.toggleSkip()}
|
||||
playButton={<PlayButton state={state} togglePlay={player.togglePlay} iconSize={36} />}
|
||||
skipIntervals={SKIP_INTERVALS}
|
||||
setSkipInterval={changeSkipInterval}
|
||||
currentInterval={skipInterval}
|
||||
startedAt={session.startedAt}
|
||||
/>
|
||||
<div className={cn('mx-2')} />
|
||||
<XRayButton
|
||||
isActive={bottomBlock === OVERVIEW}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full">
|
||||
<DevtoolsButtons toggleBottomTools={toggleBottomTools} bottomBlock={bottomBlock} />
|
||||
<Tooltip title="Fullscreen" delay={0} placement="top-start" className="mx-4">
|
||||
<FullScreenButton
|
||||
size={16}
|
||||
onClick={props.fullscreenOn}
|
||||
customClasses={'rounded hover:bg-gray-light-shade color-gray-medium'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DevtoolsButtonsProps {
|
||||
toggleBottomTools: (blockName: number) => void;
|
||||
bottomBlock: number;
|
||||
}
|
||||
|
||||
function DevtoolsButtons({ toggleBottomTools, bottomBlock }: DevtoolsButtonsProps) {
|
||||
const { store } = React.useContext(MobilePlayerContext);
|
||||
|
||||
const { exceptionsList, logMarkedCountNow, messagesLoading, stackMarkedCountNow, resourceMarkedCountNow } = store.get();
|
||||
|
||||
const showExceptions = exceptionsList.length > 0;
|
||||
return (
|
||||
<>
|
||||
<ControlButton
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(CONSOLE)}
|
||||
active={bottomBlock === CONSOLE}
|
||||
label="LOGS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
hasErrors={logMarkedCountNow > 0 || showExceptions}
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
<ControlButton
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(NETWORK)}
|
||||
active={bottomBlock === NETWORK}
|
||||
label="NETWORK"
|
||||
hasErrors={resourceMarkedCountNow > 0}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
{showExceptions ?
|
||||
<ControlButton
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(EXCEPTIONS)}
|
||||
active={bottomBlock === EXCEPTIONS}
|
||||
hasErrors={showExceptions}
|
||||
label="EXCEPTIONS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
: null}
|
||||
<ControlButton
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||
active={bottomBlock === STACKEVENTS}
|
||||
label="EVENTS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
<ControlButton
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||
active={bottomBlock === PERFORMANCE}
|
||||
label="PERFORMANCE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ControlPlayer = observer(Controls);
|
||||
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
|
||||
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
|
||||
return {
|
||||
disabledRedux: isEnterprise && !permissions.includes('DEV_TOOLS'),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
|
||||
showStorageRedux: !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
|
||||
showStackRedux: !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
totalAssistSessions: state.getIn(['liveSearch', 'total']),
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
};
|
||||
},
|
||||
{
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
fetchSessions,
|
||||
changeSkipInterval,
|
||||
}
|
||||
)(ControlPlayer);
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
PERFORMANCE,
|
||||
STACKEVENTS,
|
||||
STORAGE,
|
||||
toggleBottomBlock,
|
||||
} from 'Duck/components/player';
|
||||
import React from 'react';
|
||||
import AutoplayTimer from 'Components/Session_/Player/Overlay/AutoplayTimer';
|
||||
import PlayIconLayer from 'Components/Session_/Player/Overlay/PlayIconLayer';
|
||||
import Loader from 'Components/Session_/Player/Overlay/Loader';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import { setCreateNoteTooltip } from 'Duck/sessions';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
interface Props {
|
||||
nextId?: string;
|
||||
closedLive?: boolean;
|
||||
isClickmap?: boolean;
|
||||
toggleBottomBlock: (block: number) => void;
|
||||
setCreateNoteTooltip: (args: any) => void;
|
||||
}
|
||||
|
||||
enum ItemKey {
|
||||
Console = '1',
|
||||
Network = '2',
|
||||
Performance = '3',
|
||||
Events = '4',
|
||||
State = '5',
|
||||
AddNote = '6',
|
||||
}
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: ItemKey.Console,
|
||||
label: 'Console',
|
||||
icon: <Icon name={'terminal'} size={14} />,
|
||||
},
|
||||
{
|
||||
key: ItemKey.Network,
|
||||
label: 'Network',
|
||||
icon: <Icon name={'arrow-down-up'} size={14} />,
|
||||
},
|
||||
{
|
||||
key: ItemKey.Performance,
|
||||
label: 'Performance',
|
||||
icon: <Icon name={'speedometer2'} size={14} />,
|
||||
},
|
||||
{
|
||||
key: ItemKey.Events,
|
||||
label: 'Events',
|
||||
icon: <Icon name={'filetype-js'} size={14} />,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: ItemKey.AddNote,
|
||||
label: 'Add Note',
|
||||
icon: <Icon name={'quotes'} size={14} />,
|
||||
},
|
||||
];
|
||||
|
||||
function Overlay({ nextId, isClickmap, toggleBottomBlock, setCreateNoteTooltip }: Props) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const togglePlay = () => player.togglePlay();
|
||||
const { playing, messagesLoading, completed, autoplay } = store.get();
|
||||
const loading = messagesLoading
|
||||
|
||||
const showAutoplayTimer = completed && autoplay && nextId;
|
||||
const showPlayIconLayer =
|
||||
!isClickmap && !loading && !showAutoplayTimer;
|
||||
|
||||
const onClick = ({ key }: { key: string }) => {
|
||||
switch (key) {
|
||||
case ItemKey.Console:
|
||||
toggleBottomBlock(CONSOLE);
|
||||
break;
|
||||
case ItemKey.Network:
|
||||
toggleBottomBlock(NETWORK);
|
||||
break;
|
||||
case ItemKey.Performance:
|
||||
toggleBottomBlock(PERFORMANCE);
|
||||
break;
|
||||
case ItemKey.Events:
|
||||
toggleBottomBlock(STACKEVENTS);
|
||||
break;
|
||||
case ItemKey.State:
|
||||
toggleBottomBlock(STORAGE);
|
||||
break;
|
||||
case ItemKey.AddNote:
|
||||
setCreateNoteTooltip({ time: store.get().time, isVisible: true });
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{showAutoplayTimer && <AutoplayTimer />}
|
||||
{loading ? <Loader /> : null}
|
||||
<Dropdown menu={{ items: menuItems, onClick }} trigger={['contextMenu']}>
|
||||
<div>
|
||||
{showPlayIconLayer && <PlayIconLayer playing={playing} togglePlay={togglePlay} />}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
toggleBottomBlock,
|
||||
setCreateNoteTooltip,
|
||||
})(observer(Overlay));
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
sessions as sessionsRoute,
|
||||
liveSession as liveSessionRoute,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import { BackLink, Link } from 'UI';
|
||||
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
|
||||
import cn from 'classnames';
|
||||
import SessionMetaList from 'Shared/SessionItem/SessionMetaList';
|
||||
import UserCard from '../ReplayPlayer/EventsBlock/UserCard';
|
||||
import Tabs from 'Components/Session/Tabs';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import stl from '../ReplayPlayer/playerBlockHeader.module.css';
|
||||
import { fetchListActive as fetchMetadata } from 'Duck/customField';
|
||||
import { IFRAME } from 'App/constants/storageKeys';
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
// TODO props
|
||||
function PlayerBlockHeader(props: any) {
|
||||
const [hideBack, setHideBack] = React.useState(false);
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const playerState = store?.get?.() || { width: 0, height: 0, showEvents: false }
|
||||
const { width = 0, height = 0, showEvents = false } = playerState
|
||||
|
||||
const {
|
||||
session,
|
||||
fullscreen,
|
||||
metaList,
|
||||
siteId,
|
||||
setActiveTab,
|
||||
activeTab,
|
||||
history,
|
||||
sessionPath,
|
||||
fetchMetadata,
|
||||
} = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
const iframe = localStorage.getItem(IFRAME) || false;
|
||||
setHideBack(!!iframe && iframe === 'true');
|
||||
|
||||
if (metaList.size === 0) fetchMetadata();
|
||||
}, []);
|
||||
|
||||
const backHandler = () => {
|
||||
if (
|
||||
sessionPath.pathname === history.location.pathname ||
|
||||
sessionPath.pathname.includes('/session/')
|
||||
) {
|
||||
history.push(withSiteId(SESSIONS_ROUTE, siteId));
|
||||
} else {
|
||||
history.push(
|
||||
sessionPath ? sessionPath.pathname + sessionPath.search : withSiteId(SESSIONS_ROUTE, siteId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const { metadata } = session;
|
||||
let _metaList = Object.keys(metadata || {})
|
||||
.filter((i) => metaList.includes(i))
|
||||
.map((key) => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
const TABS = [props.tabs.EVENTS].map((tab) => ({
|
||||
text: tab,
|
||||
key: tab,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={cn(stl.header, 'flex justify-between', { hidden: fullscreen })}>
|
||||
<div className="flex w-full items-center">
|
||||
{!hideBack && (
|
||||
<div
|
||||
className="flex items-center h-full cursor-pointer group"
|
||||
onClick={backHandler}
|
||||
>
|
||||
{/* @ts-ignore TODO */}
|
||||
<BackLink label="Back" className="h-full ml-2" />
|
||||
<div className={stl.divider} />
|
||||
</div>
|
||||
)}
|
||||
<UserCard className="" width={width} height={height} />
|
||||
|
||||
<div className={cn('ml-auto flex items-center h-full')}>
|
||||
{_metaList.length > 0 && (
|
||||
<div className="border-l h-full flex items-center px-2">
|
||||
<SessionMetaList className="" metaList={_metaList} maxLength={2} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative border-l" style={{ minWidth: '270px' }}>
|
||||
<Tabs
|
||||
tabs={TABS}
|
||||
active={activeTab}
|
||||
onClick={(tab) => {
|
||||
if (activeTab === tab) {
|
||||
setActiveTab('');
|
||||
player.toggleEvents();
|
||||
} else {
|
||||
setActiveTab(tab);
|
||||
!showEvents && player.toggleEvents();
|
||||
}
|
||||
}}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PlayerHeaderCont = connect(
|
||||
(state: any) => {
|
||||
const session = state.getIn(['sessions', 'current']);
|
||||
|
||||
return {
|
||||
session,
|
||||
sessionPath: state.getIn(['sessions', 'sessionPath']),
|
||||
local: state.getIn(['sessions', 'timezone']),
|
||||
funnelRef: state.getIn(['funnels', 'navRef']),
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
|
||||
};
|
||||
},
|
||||
{
|
||||
toggleFavorite,
|
||||
setSessionPath,
|
||||
fetchMetadata,
|
||||
}
|
||||
)(observer(PlayerBlockHeader));
|
||||
|
||||
export default withRouter(PlayerHeaderCont);
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Button } from 'UI';
|
||||
import QueueControls from 'Components/Session_/QueueControls';
|
||||
import Bookmark from 'Shared/Bookmark';
|
||||
import SharePopup from 'Components/shared/SharePopup/SharePopup';
|
||||
import Issues from 'Components/Session_/Issues/Issues';
|
||||
import NotePopup from 'Components/Session_/components/NotePopup';
|
||||
import ItemMenu from 'Components/Session_/components/HeaderMenu';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AutoplayToggle from 'Shared/AutoplayToggle';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tag } from 'antd'
|
||||
|
||||
function SubHeader(props: any) {
|
||||
|
||||
const enabledIntegration = useMemo(() => {
|
||||
const { integrations } = props;
|
||||
if (!integrations || !integrations.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return integrations.some((i: Record<string, any>) => i.token);
|
||||
}, [props.integrations]);
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
const baseMenuItems = [
|
||||
{
|
||||
key: 1,
|
||||
component: <AutoplayToggle />,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
component: <Bookmark noMargin sessionId={props.sessionId} />,
|
||||
},
|
||||
]
|
||||
const menuItems = viewportWidth > 1400 ? baseMenuItems : baseMenuItems.concat({
|
||||
key: 3,
|
||||
component: <NotePopup />,
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<div className="w-full px-4 flex items-center border-b relative">
|
||||
<Tag color="error">iOS BETA</Tag>
|
||||
<div
|
||||
className="ml-auto text-sm flex items-center color-gray-medium gap-2"
|
||||
style={{ width: 'max-content' }}
|
||||
>
|
||||
{viewportWidth > 1400 ? <NotePopup /> : null}
|
||||
{enabledIntegration && <Issues sessionId={props.sessionId} />}
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={props.sessionId}
|
||||
showCopyLink={true}
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button icon="share-alt" variant="text" className="relative">
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ItemMenu
|
||||
items={menuItems}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<QueueControls />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
integrations: state.getIn(['issues', 'list']),
|
||||
modules: state.getIn(['user', 'account', 'modules']) || [],
|
||||
}))(observer(SubHeader));
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import {IosPerformanceEvent} from "Player/web/messages";
|
||||
import React from 'react';
|
||||
import { MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from 'UI';
|
||||
import { mapIphoneModel } from 'Player/mobile/utils';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { NONE } from 'Duck/components/player';
|
||||
|
||||
type warningsType =
|
||||
| 'thermalState'
|
||||
| 'memoryWarning'
|
||||
| 'lowDiskSpace'
|
||||
| 'isLowPowerModeEnabled'
|
||||
| 'batteryLevel';
|
||||
|
||||
const elements = {
|
||||
thermalState: {
|
||||
title: 'Overheating',
|
||||
icon: 'thermometer-sun',
|
||||
},
|
||||
memoryWarning: {
|
||||
title: 'High Memory Usage',
|
||||
icon: 'memory-ios',
|
||||
},
|
||||
lowDiskSpace: {
|
||||
title: 'Low Disk Space',
|
||||
icon: 'low-disc-space',
|
||||
},
|
||||
isLowPowerModeEnabled: {
|
||||
title: 'Low Power Mode',
|
||||
icon: 'battery-charging',
|
||||
},
|
||||
batteryLevel: {
|
||||
title: 'Low Battery',
|
||||
icon: 'battery',
|
||||
},
|
||||
} as const;
|
||||
|
||||
function PerfWarnings({ userDevice, bottomBlock }: { userDevice: string; bottomBlock: number }) {
|
||||
const { store } = React.useContext(MobilePlayerContext);
|
||||
const { scale, performanceListNow, performanceList } = store.get()
|
||||
|
||||
const allElements = Object.keys(elements) as warningsType[];
|
||||
const list = React.useMemo(() => allElements
|
||||
.filter(el => performanceList.findIndex((pw: IosPerformanceEvent & { techName: warningsType }) => pw.techName === el) !== -1)
|
||||
, [performanceList.length])
|
||||
|
||||
const contStyles = {
|
||||
left: '50%',
|
||||
display: 'flex',
|
||||
marginLeft: `${(mapIphoneModel(userDevice).styles.shell.width / 2 + 10) * scale}px`,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
position: 'absolute',
|
||||
width: '200px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 0,
|
||||
} as const;
|
||||
|
||||
const activeWarnings = React.useMemo(() => {
|
||||
const warnings: warningsType[] = []
|
||||
performanceListNow.forEach((warn: IosPerformanceEvent & { techName: warningsType }) => {
|
||||
switch (warn.techName) {
|
||||
case 'thermalState':
|
||||
if (warn.value > 1) warnings.push(warn.techName) // 2 = serious 3 = overheating
|
||||
break;
|
||||
case 'memoryWarning':
|
||||
warnings.push(warn.techName)
|
||||
break;
|
||||
case 'lowDiskSpace':
|
||||
warnings.push(warn.techName)
|
||||
break;
|
||||
case 'isLowPowerModeEnabled':
|
||||
if (warn.value === 1) warnings.push(warn.techName)
|
||||
break;
|
||||
case 'batteryLevel':
|
||||
if (warn.value < 25) warnings.push(warn.techName)
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
return warnings
|
||||
}, [performanceListNow.length]);
|
||||
if (bottomBlock !== NONE) return null;
|
||||
|
||||
return (
|
||||
<div style={contStyles}>
|
||||
{list.map((w) => (
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all flex items-center gap-1 bg-white border rounded px-2 py-1',
|
||||
activeWarnings.findIndex((a) => a === w) !== -1 ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<Icon name={elements[w].icon} size={16} />
|
||||
<span>{elements[w].title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
|
||||
}))(observer(PerfWarnings));
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import Player from './PlayerInst';
|
||||
import MobilePlayerSubheader from './MobilePlayerSubheader';
|
||||
|
||||
import styles from 'Components/Session_/playerBlock.module.css';
|
||||
|
||||
interface IProps {
|
||||
fullscreen: boolean;
|
||||
sessionId: string;
|
||||
disabled: boolean;
|
||||
activeTab: string;
|
||||
jiraConfig: Record<string, any>
|
||||
fullView?: boolean
|
||||
}
|
||||
|
||||
function PlayerBlock(props: IProps) {
|
||||
const {
|
||||
fullscreen,
|
||||
sessionId,
|
||||
disabled,
|
||||
activeTab,
|
||||
jiraConfig,
|
||||
fullView = false,
|
||||
} = props;
|
||||
|
||||
const shouldShowSubHeader = !fullscreen && !fullView
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}
|
||||
>
|
||||
{shouldShowSubHeader ? (
|
||||
<MobilePlayerSubheader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
) : null}
|
||||
<Player
|
||||
activeTab={activeTab}
|
||||
fullView={fullView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
|
||||
jiraConfig: state.getIn(['issues', 'list'])[0],
|
||||
}))(PlayerBlock)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
import styles from 'Components/Session_/session.module.css';
|
||||
import { countDaysFrom } from 'App/date';
|
||||
import RightBlock from 'Components/Session/RightBlock';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import Session from 'Types/session'
|
||||
import PlayerBlock from './PlayerBlock';
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'User Events',
|
||||
HEATMAPS: 'Click Map',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
fullscreen: boolean;
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
session: Session
|
||||
}
|
||||
|
||||
function PlayerContent({ session, fullscreen, activeTab, setActiveTab }: IProps) {
|
||||
const { store } = React.useContext(PlayerContext)
|
||||
|
||||
const {
|
||||
error,
|
||||
} = store.get()
|
||||
|
||||
const hasError = !!error
|
||||
|
||||
const sessionDays = countDaysFrom(session.startedAt);
|
||||
return (
|
||||
<div className="relative">
|
||||
{hasError ? (
|
||||
<div
|
||||
className="inset-0 flex items-center justify-center absolute"
|
||||
style={{
|
||||
height: 'calc(100vh - 50px)',
|
||||
zIndex: '999',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-lg -mt-8">
|
||||
{sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{sessionDays > 2
|
||||
? 'Please check your data retention policy.'
|
||||
: 'Please check it again in a few minutes.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn('flex', { 'pointer-events-none': hasError })}>
|
||||
<div
|
||||
className="w-full"
|
||||
style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined}
|
||||
>
|
||||
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
|
||||
<PlayerBlock activeTab={activeTab} />
|
||||
</div>
|
||||
</div>
|
||||
{activeTab !== '' && (
|
||||
<RightMenu
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fullscreen={fullscreen}
|
||||
tabs={TABS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RightMenu({ tabs, activeTab, setActiveTab, fullscreen }: any) {
|
||||
return (
|
||||
!fullscreen ? <RightBlock tabs={tabs} setActiveTab={setActiveTab} activeTab={activeTab} /> : null
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(PlayerContent);
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import { EscapeButton } from 'UI';
|
||||
import {
|
||||
NONE,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
STACKEVENTS,
|
||||
PERFORMANCE,
|
||||
EXCEPTIONS,
|
||||
OVERVIEW,
|
||||
fullscreenOff,
|
||||
} from 'Duck/components/player';
|
||||
import { MobileNetworkPanel } from 'Shared/DevTools/NetworkPanel';
|
||||
import { MobilePerformance } from 'Components/Session_/Performance';
|
||||
import { MobileExceptions } from 'Components/Session_/Exceptions/Exceptions';
|
||||
import MobileControls from './MobileControls';
|
||||
import Overlay from './MobileOverlay'
|
||||
import stl from 'Components/Session_/Player/player.module.css';
|
||||
import { updateLastPlayedSession } from 'Duck/sessions';
|
||||
import { MobileOverviewPanel } from 'Components/Session_/OverviewPanel';
|
||||
import MobileConsolePanel from 'Shared/DevTools/ConsolePanel/MobileConsolePanel';
|
||||
import { MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { MobileStackEventPanel } from 'Shared/DevTools/StackEventPanel';
|
||||
import ReplayWindow from "Components/Session/Player/MobilePlayer/ReplayWindow";
|
||||
import PerfWarnings from "Components/Session/Player/MobilePlayer/PerfWarnings";
|
||||
|
||||
interface IProps {
|
||||
fullView: boolean;
|
||||
isMultiview?: boolean;
|
||||
bottomBlock: number;
|
||||
fullscreen: boolean;
|
||||
fullscreenOff: () => any;
|
||||
nextId: string;
|
||||
sessionId: string;
|
||||
activeTab: string;
|
||||
updateLastPlayedSession: (id: string) => void
|
||||
videoURL: string;
|
||||
userDevice: string;
|
||||
}
|
||||
|
||||
function Player(props: IProps) {
|
||||
const {
|
||||
fullscreen,
|
||||
fullscreenOff,
|
||||
nextId,
|
||||
bottomBlock,
|
||||
activeTab,
|
||||
fullView,
|
||||
videoURL,
|
||||
userDevice,
|
||||
} = props;
|
||||
const playerContext = React.useContext(MobilePlayerContext);
|
||||
const isReady = playerContext.store.get().ready
|
||||
const screenWrapper = React.useRef<HTMLDivElement>(null);
|
||||
const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE;
|
||||
const [isAttached, setAttached] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
props.updateLastPlayedSession(props.sessionId);
|
||||
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
|
||||
if (parentElement && !isAttached) {
|
||||
playerContext.player.attach(parentElement);
|
||||
setAttached(true)
|
||||
}
|
||||
}, [isReady]);
|
||||
|
||||
React.useEffect(() => {
|
||||
playerContext.player.scale();
|
||||
}, [props.bottomBlock, props.fullscreen, playerContext.player, activeTab, fullView]);
|
||||
|
||||
React.useEffect(() => {
|
||||
playerContext.player.addFullscreenBoundary(props.fullscreen || fullView);
|
||||
}, [props.fullscreen, fullView])
|
||||
if (!playerContext.player) return null;
|
||||
|
||||
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw';
|
||||
return (
|
||||
<div
|
||||
className={cn(stl.playerBody, 'flex-1 flex flex-col relative', fullscreen && 'pb-2')}
|
||||
data-bottom-block={bottomBlockIsActive}
|
||||
>
|
||||
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
|
||||
<div className={"relative flex-1"}>
|
||||
<Overlay nextId={nextId} />
|
||||
|
||||
<div className={cn(stl.mobileScreenWrapper)} ref={screenWrapper}>
|
||||
<ReplayWindow videoURL={videoURL} userDevice={userDevice} />
|
||||
<PerfWarnings userDevice={userDevice} />
|
||||
</div>
|
||||
</div>
|
||||
{!fullscreen && !!bottomBlock && (
|
||||
<div style={{ maxWidth, width: '100%' }}>
|
||||
{bottomBlock === OVERVIEW && <MobileOverviewPanel />}
|
||||
{bottomBlock === CONSOLE && <MobileConsolePanel isLive={false} />}
|
||||
{bottomBlock === STACKEVENTS && <MobileStackEventPanel />}
|
||||
{bottomBlock === NETWORK && <MobileNetworkPanel />}
|
||||
{bottomBlock === PERFORMANCE && <MobilePerformance />}
|
||||
{bottomBlock === EXCEPTIONS && <MobileExceptions />}
|
||||
</div>
|
||||
)}
|
||||
{!fullView ? (
|
||||
<MobileControls
|
||||
speedDown={playerContext.player.speedDown}
|
||||
speedUp={playerContext.player.speedUp}
|
||||
jump={playerContext.player.jump}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
nextId: state.getIn(['sessions', 'nextId']),
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
userDevice: state.getIn(['sessions', 'current']).userDevice,
|
||||
videoURL: state.getIn(['sessions', 'current']).videoURL,
|
||||
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
|
||||
}),
|
||||
{
|
||||
fullscreenOff,
|
||||
updateLastPlayedSession,
|
||||
}
|
||||
)(Player);
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react'
|
||||
import { MobilePlayerContext, IOSPlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { mapIphoneModel } from "Player/mobile/utils";
|
||||
|
||||
interface Props {
|
||||
videoURL: string;
|
||||
userDevice: string;
|
||||
}
|
||||
|
||||
const appleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="white" viewBox="0 0 16 16">
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
|
||||
</svg>`
|
||||
|
||||
function ReplayWindow({ videoURL, userDevice }: Props) {
|
||||
const playerContext = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||
const videoRef = React.useRef<HTMLVideoElement>();
|
||||
|
||||
const time = playerContext.store.get().time
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
const timeSecs = time / 1000
|
||||
if (videoRef.current.duration >= timeSecs) {
|
||||
videoRef.current.currentTime = timeSecs
|
||||
}
|
||||
}
|
||||
}, [time])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (playerContext.player.screen.document && videoURL) {
|
||||
playerContext.player.pause()
|
||||
const { svg, styles } = mapIphoneModel(userDevice)
|
||||
|
||||
const host = document.createElement('div')
|
||||
const videoEl = document.createElement('video')
|
||||
const sourceEl = document.createElement('source')
|
||||
const shell = document.createElement('div')
|
||||
const icon = document.createElement('div')
|
||||
const videoContainer = document.createElement('div')
|
||||
|
||||
videoContainer.style.borderRadius = '10px'
|
||||
videoContainer.style.overflow = 'hidden'
|
||||
videoContainer.style.margin = styles.margin
|
||||
videoContainer.style.display = 'none'
|
||||
videoContainer.style.width = styles.screen.width + 'px'
|
||||
videoContainer.style.height = styles.screen.height + 'px'
|
||||
|
||||
videoContainer.appendChild(videoEl)
|
||||
|
||||
shell.innerHTML = svg
|
||||
|
||||
videoEl.width = styles.screen.width
|
||||
videoEl.height = styles.screen.height
|
||||
videoEl.style.backgroundColor = '#333'
|
||||
|
||||
Object.assign(icon.style, {
|
||||
backgroundColor: '#333',
|
||||
borderRadius: '10px',
|
||||
width: styles.screen.width + 'px',
|
||||
height: styles.screen.height + 'px',
|
||||
margin: styles.margin,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})
|
||||
const spacer = document.createElement('div')
|
||||
spacer.style.width = '60px'
|
||||
spacer.style.height = '60px'
|
||||
|
||||
const loadingBar = document.createElement('div')
|
||||
Object.assign(loadingBar.style, {
|
||||
width: styles.screen.width/2 + 'px',
|
||||
height: '6px',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: 'white',
|
||||
})
|
||||
icon.innerHTML = appleIcon
|
||||
icon.appendChild(spacer)
|
||||
icon.appendChild(loadingBar)
|
||||
|
||||
shell.style.position = 'absolute'
|
||||
shell.style.top = '0'
|
||||
|
||||
sourceEl.setAttribute('src', videoURL)
|
||||
sourceEl.setAttribute('type', 'video/mp4')
|
||||
|
||||
host.appendChild(videoContainer)
|
||||
host.appendChild(shell)
|
||||
host.appendChild(icon)
|
||||
videoEl.appendChild(sourceEl)
|
||||
|
||||
videoEl.addEventListener("loadeddata", () => {
|
||||
videoContainer.style.display = 'block'
|
||||
icon.style.display = 'none'
|
||||
host.removeChild(icon)
|
||||
console.log('loaded')
|
||||
playerContext.player.play()
|
||||
})
|
||||
|
||||
videoRef.current = videoEl
|
||||
playerContext.player.injectPlayer(host)
|
||||
playerContext.player.customScale(styles.shell.width, styles.shell.height)
|
||||
playerContext.player.updateDimensions({
|
||||
width: styles.screen.width,
|
||||
height: styles.screen.height,
|
||||
})
|
||||
playerContext.player.updateOverlayStyle({
|
||||
margin: styles.margin,
|
||||
width: styles.screen.width + 'px',
|
||||
height: styles.screen.height + 'px',
|
||||
})
|
||||
}
|
||||
}, [videoURL, playerContext.player.screen.document])
|
||||
return (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ReplayWindow);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React, { ErrorInfo } from 'react'
|
||||
import { Button } from 'UI'
|
||||
|
||||
class PlayerErrorBoundary extends React.Component<any> {
|
||||
state = { hasError: false, error: '' };
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
this.setState({
|
||||
hasError: true,
|
||||
error: error + info.componentStack
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
return (
|
||||
<div className={"flex flex-col p-4 gap-4"}>
|
||||
<h4>Something went wrong during player rendering.</h4>
|
||||
<p>{this.state.error}</p>
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
icon={"spinner"}
|
||||
variant={"primary"}
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerErrorBoundary
|
||||
|
|
@ -12,6 +12,7 @@ import SessionInfoItem from 'Components/Session_/SessionInfoItem';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import UserSessionsModal from 'Shared/UserSessionsModal';
|
||||
import { IFRAME } from 'App/constants/storageKeys';
|
||||
import { capitalize } from "App/utils";
|
||||
|
||||
function UserCard({ className, request, session, width, height, similarSessions, loading }) {
|
||||
const { settingsStore } = useStore();
|
||||
|
|
@ -48,6 +49,8 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
};
|
||||
|
||||
const avatarbgSize = '38px';
|
||||
|
||||
const safeOs = userOs === 'IOS' ? 'iOS' : userOs;
|
||||
return (
|
||||
<div className={cn('bg-white flex items-center w-full', className)}>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -69,16 +72,15 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
>
|
||||
{formatTimeOrDate(startedAt, timezone)}
|
||||
</Tooltip>
|
||||
|
||||
</span>
|
||||
<span className="mx-1 font-bold text-xl">·</span>
|
||||
{userCity && (
|
||||
<span className="mr-1">{userCity},</span>
|
||||
)}
|
||||
{userCity && <span className="mr-1">{userCity},</span>}
|
||||
<span>{countries[userCountry]}</span>
|
||||
<span className="mx-1 font-bold text-xl">·</span>
|
||||
<span className="capitalize">
|
||||
{userBrowser}, {userOs}, {userDevice}
|
||||
<span>
|
||||
{userBrowser ? `${capitalize(userBrowser)}, ` : ''}
|
||||
{`${/ios/i.test(userOs) ? 'iOS ' : capitalize(userOs) + ','} `}
|
||||
{capitalize(userDevice)}
|
||||
</span>
|
||||
<span className="mx-1 font-bold text-xl">·</span>
|
||||
<Popover
|
||||
|
|
@ -87,15 +89,25 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
<SessionInfoItem
|
||||
comp={<CountryFlag country={userCountry} height={11} />}
|
||||
label={countries[userCountry]}
|
||||
value={<span style={{ whiteSpace: 'nowrap' }}>{
|
||||
value={
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
{
|
||||
<>
|
||||
{userCity && <span className="mr-1">{userCity},</span>}
|
||||
{userState && <span className="mr-1">{userState}</span>}
|
||||
</>
|
||||
}</span>}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<SessionInfoItem icon={browserIcon(userBrowser)} label={userBrowser} value={`v${userBrowserVersion}`} />
|
||||
<SessionInfoItem icon={osIcon(userOs)} label={userOs} value={userOsVersion} />
|
||||
{userBrowser &&
|
||||
<SessionInfoItem
|
||||
icon={browserIcon(userBrowser)}
|
||||
label={userBrowser}
|
||||
value={`v${userBrowserVersion}`}
|
||||
/>
|
||||
}
|
||||
<SessionInfoItem icon={osIcon(userOs)} label={safeOs} value={userOsVersion} />
|
||||
<SessionInfoItem
|
||||
icon={deviceTypeIcon(userDeviceType)}
|
||||
label={userDeviceType}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import { WebStackEventPanel } from 'Shared/DevTools/StackEventPanel/StackEventPanel';
|
||||
import { EscapeButton } from 'UI';
|
||||
import {
|
||||
NONE,
|
||||
|
|
@ -17,22 +18,20 @@ import {
|
|||
OVERVIEW,
|
||||
fullscreenOff,
|
||||
} from 'Duck/components/player';
|
||||
import NetworkPanel from 'Shared/DevTools/NetworkPanel';
|
||||
import { WebNetworkPanel } from 'Shared/DevTools/NetworkPanel';
|
||||
import Storage from 'Components/Session_/Storage';
|
||||
import { ConnectedPerformance } from 'Components/Session_/Performance';
|
||||
import GraphQL from 'Components/Session_/GraphQL';
|
||||
import Exceptions from 'Components/Session_/Exceptions/Exceptions';
|
||||
import { Exceptions } from 'Components/Session_/Exceptions/Exceptions';
|
||||
import Inspector from 'Components/Session_/Inspector';
|
||||
import Controls from 'Components/Session_/Player/Controls';
|
||||
import Overlay from 'Components/Session_/Player/Overlay';
|
||||
import stl from 'Components/Session_/Player/player.module.css';
|
||||
import { updateLastPlayedSession } from 'Duck/sessions';
|
||||
import OverviewPanel from 'Components/Session_/OverviewPanel';
|
||||
import { OverviewPanel } from 'Components/Session_/OverviewPanel';
|
||||
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
|
||||
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import StackEventPanel from 'Shared/DevTools/StackEventPanel';
|
||||
|
||||
|
||||
interface IProps {
|
||||
fullView: boolean;
|
||||
|
|
@ -43,20 +42,13 @@ interface IProps {
|
|||
nextId: string;
|
||||
sessionId: string;
|
||||
activeTab: string;
|
||||
updateLastPlayedSession: (id: string) => void
|
||||
updateLastPlayedSession: (id: string) => void;
|
||||
}
|
||||
|
||||
function Player(props: IProps) {
|
||||
const {
|
||||
fullscreen,
|
||||
fullscreenOff,
|
||||
nextId,
|
||||
bottomBlock,
|
||||
activeTab,
|
||||
fullView,
|
||||
} = props;
|
||||
const { fullscreen, fullscreenOff, nextId, bottomBlock, activeTab, fullView } = props;
|
||||
const playerContext = React.useContext(PlayerContext);
|
||||
const isReady = playerContext.store.get().ready
|
||||
const isReady = playerContext.store.get().ready;
|
||||
const screenWrapper = React.useRef<HTMLDivElement>(null);
|
||||
const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE;
|
||||
const [isAttached, setAttached] = React.useState(false);
|
||||
|
|
@ -66,7 +58,7 @@ function Player(props: IProps) {
|
|||
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
|
||||
if (parentElement && !isAttached) {
|
||||
playerContext.player.attach(parentElement);
|
||||
setAttached(true)
|
||||
setAttached(true);
|
||||
}
|
||||
}, [isReady]);
|
||||
|
||||
|
|
@ -83,7 +75,7 @@ function Player(props: IProps) {
|
|||
data-bottom-block={bottomBlockIsActive}
|
||||
>
|
||||
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
|
||||
<div className={cn("relative flex-1",'overflow-hidden')}>
|
||||
<div className={cn('relative flex-1', 'overflow-hidden')}>
|
||||
<Overlay nextId={nextId} />
|
||||
<div className={cn(stl.screenWrapper)} ref={screenWrapper} />
|
||||
</div>
|
||||
|
|
@ -91,8 +83,8 @@ function Player(props: IProps) {
|
|||
<div style={{ maxWidth, width: '100%' }}>
|
||||
{bottomBlock === OVERVIEW && <OverviewPanel />}
|
||||
{bottomBlock === CONSOLE && <ConsolePanel />}
|
||||
{bottomBlock === NETWORK && <NetworkPanel />}
|
||||
{bottomBlock === STACKEVENTS && <StackEventPanel />}
|
||||
{bottomBlock === NETWORK && <WebNetworkPanel />}
|
||||
{bottomBlock === STACKEVENTS && <WebStackEventPanel />}
|
||||
{bottomBlock === STORAGE && <Storage />}
|
||||
{bottomBlock === PROFILER && <ProfilerPanel />}
|
||||
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,27 @@ import WebPlayer from './WebPlayer';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { clearLogs } from 'App/dev/console';
|
||||
|
||||
import MobilePlayer from "Components/Session/MobilePlayer";
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
interface Props {
|
||||
sessionId: string;
|
||||
loading: boolean;
|
||||
hasErrors: boolean;
|
||||
fetchV2: (sessionId: string) => void;
|
||||
clearCurrentSession: () => void;
|
||||
session: Record<string, any>;
|
||||
}
|
||||
|
||||
function Session({
|
||||
sessionId,
|
||||
loading,
|
||||
hasErrors,
|
||||
fetchV2,
|
||||
clearCurrentSession,
|
||||
}) {
|
||||
session,
|
||||
}: Props) {
|
||||
usePageTitle("OpenReplay Session Player");
|
||||
const [ initializing, setInitializing ] = useState(true)
|
||||
const { sessionStore } = useStore();
|
||||
|
|
@ -40,6 +52,7 @@ function Session({
|
|||
sessionStore.resetUserFilter();
|
||||
} ,[])
|
||||
|
||||
const player = session.platform === 'ios' ? <MobilePlayer /> : <WebPlayer />
|
||||
return (
|
||||
<NoContent
|
||||
show={ hasErrors }
|
||||
|
|
@ -52,13 +65,13 @@ function Session({
|
|||
}
|
||||
>
|
||||
<Loader className="flex-1" loading={ loading || initializing }>
|
||||
<WebPlayer />
|
||||
{player}
|
||||
</Loader>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, props) => {
|
||||
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state: any, props: any) => {
|
||||
const { match: { params: { sessionId } } } = props;
|
||||
return {
|
||||
sessionId,
|
||||
|
|
@ -12,9 +12,9 @@ import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
|||
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
|
||||
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Note } from "App/services/NotesService";
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'User Events',
|
||||
|
|
@ -24,30 +24,24 @@ const TABS = {
|
|||
let playerInst: IPlayerContext['player'] | undefined;
|
||||
|
||||
function WebPlayer(props: any) {
|
||||
const {
|
||||
session,
|
||||
toggleFullscreen,
|
||||
closeBottomBlock,
|
||||
fullscreen,
|
||||
fetchList,
|
||||
} = props;
|
||||
const { session, toggleFullscreen, closeBottomBlock, fullscreen, fetchList } = props;
|
||||
const { notesStore, sessionStore } = useStore();
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [noteItem, setNoteItem] = useState<Note | undefined>(undefined);
|
||||
const [visuallyAdjusted, setAdjusted] = useState(false);
|
||||
// @ts-ignore
|
||||
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
|
||||
const params: { sessionId: string } = useParams()
|
||||
const params: { sessionId: string } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
playerInst = undefined
|
||||
playerInst = undefined;
|
||||
if (!session.sessionId || contextValue.player !== undefined) return;
|
||||
fetchList('issues');
|
||||
sessionStore.setUserTimezone(session.timezone)
|
||||
sessionStore.setUserTimezone(session.timezone);
|
||||
const [WebPlayerInst, PlayerStore] = createWebPlayer(
|
||||
session,
|
||||
(state) => makeAutoObservable(state),
|
||||
toast,
|
||||
toast
|
||||
);
|
||||
setContextValue({ player: WebPlayerInst, store: PlayerStore });
|
||||
playerInst = WebPlayerInst;
|
||||
|
|
@ -58,55 +52,54 @@ function WebPlayer(props: any) {
|
|||
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
|
||||
WebPlayerInst.pause();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const freeze = props.query.get('freeze')
|
||||
const freeze = props.query.get('freeze');
|
||||
if (freeze) {
|
||||
void WebPlayerInst.freeze()
|
||||
void WebPlayerInst.freeze();
|
||||
}
|
||||
}, [session.sessionId]);
|
||||
|
||||
const { firstVisualEvent: visualOffset, messagesProcessed } = contextValue.store?.get() || {};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (messagesProcessed && session.events.length > 0 || session.errors.length > 0) {
|
||||
contextValue.player?.updateLists?.(session)
|
||||
if ((messagesProcessed && session.events.length > 0) || session.errors.length > 0) {
|
||||
contextValue.player?.updateLists?.(session);
|
||||
}
|
||||
}, [session.events, session.errors, contextValue.player, messagesProcessed])
|
||||
|
||||
}, [session.events, session.errors, contextValue.player, messagesProcessed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (noteItem !== undefined) {
|
||||
contextValue.player.pause()
|
||||
contextValue.player.pause();
|
||||
}
|
||||
|
||||
if (activeTab === '' && !noteItem !== undefined && messagesProcessed && contextValue.player) {
|
||||
const jumpToTime = props.query.get('jumpto');
|
||||
const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted
|
||||
const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted;
|
||||
|
||||
if (jumpToTime || shouldAdjustOffset) {
|
||||
if (jumpToTime > visualOffset) {
|
||||
contextValue.player.jump(parseInt(jumpToTime));
|
||||
} else {
|
||||
contextValue.player.jump(visualOffset)
|
||||
setAdjusted(true)
|
||||
contextValue.player.jump(visualOffset);
|
||||
setAdjusted(true);
|
||||
}
|
||||
}
|
||||
|
||||
contextValue.player.play()
|
||||
contextValue.player.play();
|
||||
}
|
||||
}, [activeTab, noteItem, visualOffset, messagesProcessed])
|
||||
}, [activeTab, noteItem, visualOffset, messagesProcessed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeTab === 'Click Map') {
|
||||
contextValue.player?.pause()
|
||||
contextValue.player?.pause();
|
||||
}
|
||||
}, [activeTab])
|
||||
}, [activeTab]);
|
||||
|
||||
// LAYOUT (TODO: local layout state - useContext or something..)
|
||||
useEffect(
|
||||
() => () => {
|
||||
console.debug('cleaning up player after', params.sessionId)
|
||||
console.debug('cleaning up player after', params.sessionId);
|
||||
toggleFullscreen(false);
|
||||
closeBottomBlock();
|
||||
playerInst?.clean();
|
||||
|
|
@ -117,11 +110,23 @@ function WebPlayer(props: any) {
|
|||
);
|
||||
|
||||
const onNoteClose = () => {
|
||||
setNoteItem(undefined)
|
||||
setNoteItem(undefined);
|
||||
contextValue.player.play();
|
||||
};
|
||||
|
||||
if (!session.sessionId) return <Loader size={75} style={{ position: 'fixed', top: '50%', left: '50%', transform: 'translateX(-50%)', height: 75 }} />;
|
||||
if (!session.sessionId)
|
||||
return (
|
||||
<Loader
|
||||
size={75}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
height: 75,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PlayerContext.Provider value={contextValue}>
|
||||
|
|
@ -133,19 +138,21 @@ function WebPlayer(props: any) {
|
|||
fullscreen={fullscreen}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{contextValue.player ? <PlayerContent
|
||||
{contextValue.player ? (
|
||||
<PlayerContent
|
||||
activeTab={activeTab}
|
||||
fullscreen={fullscreen}
|
||||
setActiveTab={setActiveTab}
|
||||
session={session}
|
||||
/> : <Loader style={{ position: 'fixed', top: '0%', left: '50%', transform: 'translateX(-50%)' }} />}
|
||||
/>
|
||||
) : (
|
||||
<Loader
|
||||
style={{ position: 'fixed', top: '0%', left: '50%', transform: 'translateX(-50%)' }}
|
||||
/>
|
||||
)}
|
||||
<Modal open={noteItem !== undefined} onClose={onNoteClose}>
|
||||
{noteItem !== undefined ? (
|
||||
<ReadNote
|
||||
note={noteItem}
|
||||
onClose={onNoteClose}
|
||||
notFound={!noteItem}
|
||||
/>
|
||||
<ReadNote note={noteItem} onClose={onNoteClose} notFound={!noteItem} />
|
||||
) : null}
|
||||
</Modal>
|
||||
</PlayerContext.Provider>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { createContext } from 'react';
|
||||
import { createContext, Context } from 'react';
|
||||
import {
|
||||
IWebPlayer,
|
||||
IIosPlayer,
|
||||
IIOSPlayerStore,
|
||||
IWebPlayerStore,
|
||||
IWebLivePlayer,
|
||||
IWebLivePlayerStore,
|
||||
} from 'Player'
|
||||
|
||||
export interface IOSPlayerContext {
|
||||
player: IIosPlayer
|
||||
store: IIOSPlayerStore
|
||||
}
|
||||
|
||||
export interface IPlayerContext {
|
||||
player: IWebPlayer
|
||||
store: IWebPlayerStore,
|
||||
|
|
@ -16,9 +23,15 @@ export interface ILivePlayerContext {
|
|||
store: IWebLivePlayerStore
|
||||
}
|
||||
|
||||
type ContextType =
|
||||
type WebContextType =
|
||||
| IPlayerContext
|
||||
| ILivePlayerContext
|
||||
export const defaultContextValue = { player: undefined, store: undefined}
|
||||
// @ts-ignore
|
||||
export const PlayerContext = createContext<ContextType>(defaultContextValue);
|
||||
|
||||
type MobileContextType = IOSPlayerContext
|
||||
|
||||
export const defaultContextValue = { player: undefined, store: undefined }
|
||||
|
||||
const ContextProvider = createContext<Partial<WebContextType | MobileContextType>>(defaultContextValue);
|
||||
|
||||
export const PlayerContext = ContextProvider as Context<WebContextType>
|
||||
export const MobilePlayerContext = ContextProvider as Context<MobileContextType>
|
||||
|
|
@ -23,7 +23,7 @@ type Props = {
|
|||
};
|
||||
|
||||
const isFrustrationEvent = (evt: any): boolean => {
|
||||
if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) {
|
||||
if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE || evt.type === TYPES.TAPRAGE) {
|
||||
return true;
|
||||
}
|
||||
if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) {
|
||||
|
|
@ -73,12 +73,22 @@ const Event: React.FC<Props> = ({
|
|||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
icon = 'location';
|
||||
icon = 'event/location';
|
||||
break;
|
||||
case TYPES.SWIPE:
|
||||
title = 'Swipe';
|
||||
body = event.direction;
|
||||
icon = `chevron-${event.direction}`
|
||||
break;
|
||||
case TYPES.TOUCH:
|
||||
title = 'Tapped';
|
||||
body = event.label;
|
||||
icon = 'event/click';
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
icon = isFrustration ? 'click_hesitation' : 'click';
|
||||
icon = isFrustration ? 'event/click_hesitation' : 'event/click';
|
||||
isFrustration
|
||||
? Object.assign(tooltip, {
|
||||
disabled: false,
|
||||
|
|
@ -89,7 +99,7 @@ const Event: React.FC<Props> = ({
|
|||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
icon = isFrustration ? 'input_hesitation' : 'input';
|
||||
icon = isFrustration ? 'event/input_hesitation' : 'event/input';
|
||||
isFrustration
|
||||
? Object.assign(tooltip, {
|
||||
disabled: false,
|
||||
|
|
@ -98,18 +108,19 @@ const Event: React.FC<Props> = ({
|
|||
: null;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${event.count} Clicks`;
|
||||
case TYPES.TAPRAGE:
|
||||
title = event.count ? `${event.count} Clicks` : 'Click Rage';
|
||||
body = event.label;
|
||||
icon = 'clickrage';
|
||||
icon = 'event/clickrage';
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
icon = 'ios_view';
|
||||
icon = 'event/ios_view';
|
||||
break;
|
||||
case 'mouse_thrashing':
|
||||
title = 'Mouse Thrashing';
|
||||
icon = 'mouse_thrashing';
|
||||
icon = 'event/mouse_thrashing';
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +134,7 @@ const Event: React.FC<Props> = ({
|
|||
>
|
||||
<div className={cn(cls.main, 'flex flex-col w-full')}>
|
||||
<div className={cn('flex items-center w-full', { 'px-4': isLocation })}>
|
||||
{event.type && <Icon name={`event/${icon}`} size='16' color={'gray-dark'} />}
|
||||
{event.type && <Icon name={icon} size='16' color={'gray-dark'} />}
|
||||
<div className='ml-3 w-full'>
|
||||
<div className='flex w-full items-first justify-between'>
|
||||
<div className='flex items-center w-full' style={{ minWidth: '0' }}>
|
||||
|
|
@ -160,6 +171,7 @@ const Event: React.FC<Props> = ({
|
|||
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
|
||||
const mobileTypes = [TYPES.TOUCH, TYPES.SWIPE, TYPES.TAPRAGE]
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
|
|
@ -172,12 +184,13 @@ const Event: React.FC<Props> = ({
|
|||
[cls.selected]: selected,
|
||||
[cls.showSelection]: showSelection,
|
||||
[cls.red]: isRed,
|
||||
[cls.clickType]: event.type === TYPES.CLICK,
|
||||
[cls.clickType]: event.type === TYPES.CLICK || event.type === TYPES.SWIPE,
|
||||
[cls.inputType]: event.type === TYPES.INPUT,
|
||||
[cls.frustration]: isFrustration,
|
||||
[cls.highlight]: presentInSearch,
|
||||
[cls.lastInGroup]: whiteBg,
|
||||
['pl-4 pr-6 ml-4 py-2 border-l']: event.type !== TYPES.LOCATION
|
||||
['pl-4 pr-6 ml-4 py-2 border-l']: event.type !== TYPES.LOCATION,
|
||||
['border-0 border-l-0 ml-0']: mobileTypes.includes(event.type),
|
||||
})}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function EventsBlock(props: IProps) {
|
|||
|
||||
const { store, player } = React.useContext(PlayerContext);
|
||||
|
||||
const { playing, tabStates, tabChangeEvents } = store.get();
|
||||
const { playing, tabStates, tabChangeEvents = [] } = store.get();
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
|
|
@ -53,10 +53,16 @@ function EventsBlock(props: IProps) {
|
|||
const filteredLength = filteredEvents?.length || 0;
|
||||
const notesWithEvtsLength = notesWithEvents?.length || 0;
|
||||
const notesLength = notes.length;
|
||||
const eventListNow = Object.values(tabStates)[0]?.eventListNow || [];
|
||||
const eventListNow: any[] = []
|
||||
if (tabStates !== undefined) {
|
||||
eventListNow.concat(Object.values(tabStates)[0]?.eventListNow || [])
|
||||
} else {
|
||||
eventListNow.concat(store.get().eventListNow)
|
||||
}
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0;
|
||||
const usedEvents = React.useMemo(() => {
|
||||
if (tabStates !== undefined) {
|
||||
tabChangeEvents.forEach(ev => {
|
||||
const urlsList = tabStates[ev.tabId].urlsList;
|
||||
let found = false;
|
||||
|
|
@ -70,6 +76,7 @@ function EventsBlock(props: IProps) {
|
|||
i--;
|
||||
}
|
||||
})
|
||||
}
|
||||
const eventsWithMobxNotes = [...notesWithEvents, ...notes].sort(sortEvents);
|
||||
return mergeEventLists(filteredLength > 0 ? filteredEvents : eventsWithMobxNotes, tabChangeEvents);
|
||||
}, [filteredLength, notesWithEvtsLength, notesLength])
|
||||
|
|
@ -133,7 +140,7 @@ function EventsBlock(props: IProps) {
|
|||
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
|
||||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event;
|
||||
const isTabChange = event.type === 'TABCHANGE';
|
||||
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { error as errorRoute } from 'App/routes';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -23,7 +23,52 @@ interface IProps {
|
|||
errorStack: Record<string, any>;
|
||||
}
|
||||
|
||||
function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) {
|
||||
function MobileExceptionsCont() {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const { exceptionsList: exceptions = [] } = store.get();
|
||||
const [filter, setFilter] = React.useState('');
|
||||
|
||||
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
|
||||
|
||||
const filterRE = getRE(filter, 'i');
|
||||
const filtered = exceptions.filter((e: any) => filterRE.test(e.name) || filterRE.test(e.message));
|
||||
|
||||
return (
|
||||
<>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Exceptions</span>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by name or message"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
height={28}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
|
||||
<Autoscroll>
|
||||
{filtered.map((e: any, index) => (
|
||||
<React.Fragment key={e.key}>
|
||||
<ErrorItem onJump={() => player.jump(e.time)} error={e} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExceptionsCont({ errorStack, sourcemapUploaded, loading }: IProps) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { tabStates, currentTab } = store.get();
|
||||
const { logListNow: logs = [], exceptionsList: exceptions = [] } = tabStates[currentTab]
|
||||
|
|
@ -119,8 +164,10 @@ function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
export const Exceptions = connect((state: any) => ({
|
||||
errorStack: state.getIn(['sessions', 'errorStack']),
|
||||
sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']),
|
||||
loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']),
|
||||
}))(observer(Exceptions));
|
||||
}))(observer(ExceptionsCont));
|
||||
|
||||
export const MobileExceptions = observer(MobileExceptionsCont)
|
||||
|
|
@ -11,9 +11,72 @@ import cn from 'classnames';
|
|||
import OverviewPanelContainer from './components/OverviewPanelContainer';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import {MobilePlayerContext, PlayerContext} from 'App/components/Session/playerContext';
|
||||
|
||||
function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
||||
function MobileOverviewPanelCont({ issuesList }: { issuesList: Record<string, any>[] }) {
|
||||
const { store, player } = React.useContext(MobilePlayerContext)
|
||||
const [dataLoaded, setDataLoaded] = React.useState(false);
|
||||
const [selectedFeatures, setSelectedFeatures] = React.useState([
|
||||
'PERFORMANCE',
|
||||
'FRUSTRATIONS',
|
||||
'ERRORS',
|
||||
'NETWORK',
|
||||
]);
|
||||
|
||||
const {
|
||||
endTime,
|
||||
eventList: eventsList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
fetchList,
|
||||
performanceChartData,
|
||||
performanceList,
|
||||
} = store.get()
|
||||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
|
||||
const resources = {
|
||||
NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow),
|
||||
ERRORS: exceptionsList,
|
||||
EVENTS: eventsList,
|
||||
PERFORMANCE: performanceChartData,
|
||||
FRUSTRATIONS: frustrationsList,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
exceptionsList.length > 0 ||
|
||||
eventsList.length > 0 ||
|
||||
issuesList.length > 0 ||
|
||||
performanceChartData.length > 0 ||
|
||||
frustrationsList.length > 0
|
||||
) {
|
||||
setDataLoaded(true);
|
||||
}
|
||||
}, [issuesList, exceptionsList, eventsList, performanceChartData, frustrationsList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
player.scale()
|
||||
}, [selectedFeatures])
|
||||
|
||||
return (
|
||||
<PanelComponent
|
||||
resources={resources}
|
||||
endTime={endTime}
|
||||
selectedFeatures={selectedFeatures}
|
||||
fetchPresented={fetchPresented}
|
||||
setSelectedFeatures={setSelectedFeatures}
|
||||
isMobile
|
||||
performanceList={performanceList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function WebOverviewPanelCont({ issuesList }: { issuesList: Record<string, any>[] }) {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const [dataLoaded, setDataLoaded] = React.useState(false);
|
||||
const [selectedFeatures, setSelectedFeatures] = React.useState([
|
||||
|
|
@ -28,7 +91,6 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
currentTab,
|
||||
tabStates,
|
||||
} = store.get();
|
||||
const states = Object.values(tabStates)
|
||||
|
||||
const stackEventList = tabStates[currentTab]?.stackList || []
|
||||
const eventsList = tabStates[currentTab]?.eventList || []
|
||||
|
|
@ -73,6 +135,10 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
}
|
||||
}, [resourceList, issuesList, exceptionsList, eventsList, stackEventList, performanceChartData, currentTab]);
|
||||
|
||||
return <PanelComponent resources={resources} endTime={endTime} selectedFeatures={selectedFeatures} fetchPresented={fetchPresented} setSelectedFeatures={setSelectedFeatures} />
|
||||
}
|
||||
|
||||
function PanelComponent({ selectedFeatures, endTime, resources, fetchPresented, setSelectedFeatures, isMobile, performanceList }: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BottomBlock style={{ height: '100%' }}>
|
||||
|
|
@ -104,7 +170,7 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
{selectedFeatures.map((feature: any, index: number) => (
|
||||
<div
|
||||
key={feature}
|
||||
className={cn('border-b last:border-none', { 'bg-white': index % 2 })}
|
||||
className={cn('border-b last:border-none relative', { 'bg-white': index % 2 })}
|
||||
>
|
||||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
|
|
@ -120,6 +186,25 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
endTime={endTime}
|
||||
message={HELP_MESSAGE[feature]}
|
||||
/>
|
||||
{isMobile && feature === 'PERFORMANCE' ? (
|
||||
<div className={"absolute top-0 left-0 py-2 flex items-center py-4 w-full"}>
|
||||
<EventRow
|
||||
isGraph={false}
|
||||
title={''}
|
||||
list={performanceList}
|
||||
renderElement={(pointer: any) => (
|
||||
<div className="rounded bg-white p-1 border">
|
||||
<TimelinePointer
|
||||
pointer={pointer}
|
||||
type={"FRUSTRATIONS"}
|
||||
fetchPresented={fetchPresented}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
endTime={endTime}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
|
|
@ -128,14 +213,23 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
export const OverviewPanel = connect(
|
||||
(state: any) => ({
|
||||
issuesList: state.getIn(['sessions', 'current']).issues,
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
}
|
||||
)(observer(OverviewPanel));
|
||||
)(observer(WebOverviewPanelCont));
|
||||
|
||||
export const MobileOverviewPanel = connect(
|
||||
(state: any) => ({
|
||||
issuesList: state.getIn(['sessions', 'current']).issues,
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
}
|
||||
)(observer(MobileOverviewPanelCont));
|
||||
|
|
@ -8,9 +8,9 @@ const FRUSTRATIONS = 'FRUSTRATIONS';
|
|||
const PERFORMANCE = 'PERFORMANCE';
|
||||
|
||||
export const HELP_MESSAGE: any = {
|
||||
NETWORK: 'Network requests made in this session',
|
||||
NETWORK: 'Network requests with issues in this session',
|
||||
EVENTS: 'Visualizes the events that takes place in the DOM',
|
||||
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
|
||||
ERRORS: 'Visualizes native errors like Type, URI, Syntax etc.',
|
||||
PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline',
|
||||
FRUSTRATIONS: 'Indicates user frustrations in the session',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,8 +76,9 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
const elData = { name: '', icon: ''}
|
||||
if (item.type === TYPES.CLICK) Object.assign(elData, { name: `User hesitated to click for ${Math.round(item.hesitation/1000)}s`, icon: 'click-hesitation' })
|
||||
if (item.type === TYPES.INPUT) Object.assign(elData, { name: `User hesitated to enter a value for ${Math.round(item.hesitation/1000)}s`, icon: 'input-hesitation' })
|
||||
if (item.type === TYPES.CLICKRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' })
|
||||
if (item.type === TYPES.CLICKRAGE || item.type === TYPES.TAPRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' })
|
||||
if (item.type === issueTypes.MOUSE_THRASHING) Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' })
|
||||
if (item.type === 'ios_perf_event') Object.assign(elData, { name: item.name, icon: item.icon })
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
@ -99,7 +100,7 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{'Stack Event'}</b>
|
||||
<b>{item.name || 'Stack Event'}</b>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './OverviewPanel';
|
||||
export { OverviewPanel, MobileOverviewPanel } from './OverviewPanel';
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import {Timed} from "Player";
|
||||
import {PerformanceChartPoint} from "Player/mobile/managers/IOSPerformanceTrackManager";
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import {MobilePlayerContext, PlayerContext} from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import {
|
||||
AreaChart,
|
||||
|
|
@ -96,6 +98,27 @@ const CPUTooltip = ({ active, payload }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const MobileCpuTooltip = ({ active, payload }) => {
|
||||
if (!payload) return null;
|
||||
if (!active || payload.length < 1) {
|
||||
return null;
|
||||
}
|
||||
if (payload[0].value === null) {
|
||||
return (
|
||||
<div className={stl.tooltipWrapper} style={{ color: HIDDEN_SCREEN_COLOR }}>
|
||||
{'App is in the background.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={stl.tooltipWrapper}>
|
||||
<span className="font-medium">{`${CPU}: `}</span>
|
||||
{payload[0].value}
|
||||
{'%'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const HeapTooltip = ({ active, payload }) => {
|
||||
if (!payload) return null;
|
||||
if (!active || payload.length < 2) return null;
|
||||
|
|
@ -113,6 +136,19 @@ const HeapTooltip = ({ active, payload }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const MobileMemoryTooltip = ({ active, payload }) => {
|
||||
if (!payload) return null;
|
||||
if (!active || payload.length < 1 || payload[1].value === null) return null;
|
||||
return (
|
||||
<div className={stl.tooltipWrapper}>
|
||||
<p>
|
||||
<span className="font-medium">Used Memory: </span>
|
||||
{formatBytes(payload[1].value)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NodesCountTooltip = ({ active, payload }) => {
|
||||
if (!payload) return null;
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
|
|
@ -178,6 +214,202 @@ function addFpsMetadata(data) {
|
|||
});
|
||||
}
|
||||
|
||||
function generateMobileChart(data: PerformanceChartPoint[], biggestMemSpike: number) {
|
||||
return data.map(p => ({
|
||||
...p,
|
||||
isBackground: p.isBackground ? 50 : 0,
|
||||
isMemBackground: p.isBackground ? biggestMemSpike : 0
|
||||
}))
|
||||
}
|
||||
|
||||
export const MobilePerformance = connect((state: any) => ({
|
||||
userDeviceMemorySize: state.getIn(['sessions', 'current']).userDeviceMemorySize || 0,
|
||||
}))(observer(({ userDeviceMemorySize }: { userDeviceMemorySize: number }) => {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const [_timeTicks, setTicks] = React.useState<number[]>([])
|
||||
const [_data, setData] = React.useState<any[]>([])
|
||||
|
||||
const {
|
||||
performanceChartTime = 0,
|
||||
performanceChartData = [],
|
||||
} = store.get();
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
// setTicks(generateTicks(performanceChartData));
|
||||
setTicks(performanceChartData.map(p => p.time));
|
||||
const biggestMemSpike = performanceChartData.reduce((acc, p) => {
|
||||
if (p.memory && p.memory > acc) return p.memory;
|
||||
return acc;
|
||||
}, 0);
|
||||
setData(generateMobileChart(performanceChartData, biggestMemSpike));
|
||||
}, [])
|
||||
|
||||
|
||||
const onDotClick = ({ index: pointer }: { index: number }) => {
|
||||
const point = _data[pointer];
|
||||
if (!!point) {
|
||||
player.jump(point.time);
|
||||
}
|
||||
};
|
||||
|
||||
const onChartClick = (e: any) => {
|
||||
if (e === null) return;
|
||||
const { activeTooltipIndex } = e;
|
||||
const point = _data[activeTooltipIndex];
|
||||
if (!!point) {
|
||||
player.jump(point.time);
|
||||
}
|
||||
};
|
||||
|
||||
const availableCount = 2
|
||||
const height = `${100 / availableCount}%`;
|
||||
|
||||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Memory Size"
|
||||
value={formatBytes(userDeviceMemorySize * 1024)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
||||
<ResponsiveContainer height={height}>
|
||||
<AreaChart
|
||||
onClick={onChartClick}
|
||||
data={_data}
|
||||
syncId="s"
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<Gradient id="cpuGradient" color={CPU_COLOR} />
|
||||
</defs>
|
||||
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
type="number"
|
||||
mirror
|
||||
orientation="top"
|
||||
tickLine={false}
|
||||
tickFormatter={() => ''}
|
||||
domain={[0, 'dataMax']}
|
||||
ticks={_timeTicks}
|
||||
>
|
||||
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
|
||||
</XAxis>
|
||||
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
|
||||
<Area
|
||||
dataKey="cpu"
|
||||
type="monotone"
|
||||
stroke={CPU_STROKE_COLOR}
|
||||
fill="url(#cpuGradient)"
|
||||
dot={false}
|
||||
activeDot={{
|
||||
onClick: onDotClick,
|
||||
style: { cursor: 'pointer' },
|
||||
}}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
dataKey="isBackground"
|
||||
type="stepBefore"
|
||||
stroke="none"
|
||||
fill={HIDDEN_SCREEN_COLOR}
|
||||
activeDot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||
<Tooltip content={MobileCpuTooltip} filterNull={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<ResponsiveContainer height={height}>
|
||||
<ComposedChart
|
||||
onClick={onChartClick}
|
||||
data={_data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
syncId="s"
|
||||
>
|
||||
<defs>
|
||||
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
type="number"
|
||||
mirror
|
||||
orientation="top"
|
||||
tickLine={false}
|
||||
tickFormatter={() => ''} // tick={false} + _timeTicks to cartesian array
|
||||
domain={[0, 'dataMax']}
|
||||
ticks={_timeTicks}
|
||||
>
|
||||
<Label value="Memory" position="insideTopRight" className="fill-gray-darkest" />
|
||||
</XAxis>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickFormatter={formatBytes}
|
||||
mirror
|
||||
// Hack to keep only end tick
|
||||
minTickGap={Number.MAX_SAFE_INTEGER}
|
||||
domain={[0, (max: number) => max * 1.2]}
|
||||
/>
|
||||
{/*<Line*/}
|
||||
{/* type="monotone"*/}
|
||||
{/* dataKey="totalHeap"*/}
|
||||
{/* stroke={TOTAL_HEAP_STROKE_COLOR}*/}
|
||||
{/* dot={false}*/}
|
||||
{/* activeDot={{*/}
|
||||
{/* onClick: onDotClick,*/}
|
||||
{/* style: { cursor: 'pointer' },*/}
|
||||
{/* }}*/}
|
||||
{/* isAnimationActive={false}*/}
|
||||
{/*/>*/}
|
||||
<Area
|
||||
dataKey="isMemBackground"
|
||||
type="stepBefore"
|
||||
stroke="none"
|
||||
fill={HIDDEN_SCREEN_COLOR}
|
||||
activeDot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
dataKey="memory"
|
||||
type="monotone"
|
||||
fill="url(#usedHeapGradient)"
|
||||
stroke={USED_HEAP_STROKE_COLOR}
|
||||
dot={false}
|
||||
activeDot={{
|
||||
onClick: onDotClick,
|
||||
style: { cursor: 'pointer' },
|
||||
}}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||
<Tooltip content={MobileMemoryTooltip} filterNull={false} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
}));
|
||||
|
||||
|
||||
function Performance({
|
||||
userDeviceHeapSize,
|
||||
}: {
|
||||
|
|
@ -226,6 +458,7 @@ function Performance({
|
|||
const availableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
|
||||
const height = availableCount === 0 ? '0' : `${100 / availableCount}%`;
|
||||
|
||||
console.log(_data)
|
||||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useContext } from 'react';
|
||||
import stl from './timeline.module.css';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { getTimelinePosition } from './getTimelinePosition'
|
||||
|
||||
|
|
@ -25,4 +25,24 @@ function EventsList({ scale }: { scale: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(EventsList);
|
||||
function MobileEventsList({ scale }: { scale: number }) {
|
||||
const { store } = useContext(MobilePlayerContext);
|
||||
const { eventList } = store.get();
|
||||
const events = eventList.filter(e => e.type !== 'SWIPE')
|
||||
|
||||
return (
|
||||
<>
|
||||
{events.map((e) => (
|
||||
<div
|
||||
/*@ts-ignore TODO */
|
||||
key={e.key}
|
||||
className={stl.event}
|
||||
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const WebEventsList = observer(EventsList);
|
||||
export const MobEventsList = observer(MobileEventsList);
|
||||
|
|
@ -12,7 +12,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import Issue from "Types/session/issue";
|
||||
import EventsList from './EventsList';
|
||||
import { WebEventsList, MobEventsList } from './EventsList';
|
||||
import NotesList from './NotesList';
|
||||
import SkipIntervalsList from './SkipIntervalsList'
|
||||
import TimelineTracker from "Components/Session_/Player/Controls/TimelineTracker";
|
||||
|
|
@ -23,6 +23,7 @@ interface IProps {
|
|||
startedAt: number
|
||||
tooltipVisible: boolean
|
||||
timezone?: string
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
|
|
@ -135,7 +136,7 @@ function Timeline(props: IProps) {
|
|||
>
|
||||
<div
|
||||
className={stl.progress}
|
||||
onClick={ready ? jumpToTime : undefined }
|
||||
onClick={ready ? jumpToTime : undefined}
|
||||
ref={progressRef}
|
||||
role="button"
|
||||
onMouseMoveCapture={showTimeTooltip}
|
||||
|
|
@ -150,12 +151,11 @@ function Timeline(props: IProps) {
|
|||
maxX={progressRef.current ? progressRef.current.offsetWidth : 0}
|
||||
/>
|
||||
|
||||
|
||||
<div className={stl.timeline} ref={timelineRef}>
|
||||
{devtoolsLoading || domLoading || !ready ? <div className={stl.stripes} /> : null}
|
||||
</div>
|
||||
|
||||
<EventsList scale={scale} />
|
||||
{props.isMobile ? <MobEventsList scale={scale} /> : <WebEventsList scale={scale} />}
|
||||
<NotesList scale={scale} />
|
||||
<SkipIntervalsList scale={scale} />
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ function Timeline(props: IProps) {
|
|||
{/*))}*/}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@
|
|||
50% / 10px 10px;
|
||||
}
|
||||
|
||||
.mobileScreenWrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background: #F6F6F6;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
font-size: 40px;
|
||||
font-weight: 200;
|
||||
|
|
|
|||
|
|
@ -12,13 +12,17 @@ interface Props {
|
|||
export default function SessionInfoItem(props: Props) {
|
||||
const { label, icon, value, comp, isLast = false } = props
|
||||
return (
|
||||
<div className={cn("flex items-center w-full py-2 color-gray-dark", {'border-b' : !isLast})}>
|
||||
<div className={cn('flex items-center w-full py-2 color-gray-dark', { 'border-b': !isLast })}>
|
||||
<div className="px-2 capitalize" style={{ width: '30px' }}>
|
||||
{ icon && <Icon name={icon} size="16" /> }
|
||||
{ comp && comp }
|
||||
{icon && <Icon name={icon} size="16" />}
|
||||
{comp && comp}
|
||||
</div>
|
||||
<div className="px-2 capitalize" style={{ minWidth: '160px' }}>{label}</div>
|
||||
<div className="color-gray-medium px-2" style={{ minWidth: '160px' }}>{value}</div>
|
||||
<div className={cn('px-2', /ios/i.test(label) ? '' : 'capitalize')} style={{ minWidth: '160px' }}>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
<div className="color-gray-medium px-2" style={{ minWidth: '160px' }}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function SubHeader(props) {
|
|||
}
|
||||
|
||||
return integrations.some((i) => i.token);
|
||||
});
|
||||
}, [props.integrations]);
|
||||
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { useModal } from 'App/components/Modal';
|
|||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { toJS } from 'mobx'
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -60,7 +59,7 @@ const getIconProps = (level: any) => {
|
|||
|
||||
const INDEX_KEY = 'console';
|
||||
|
||||
function ConsolePanel({ isLive }: { isLive: boolean }) {
|
||||
function ConsolePanel({ isLive }: { isLive?: boolean }) {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { LogLevel, ILog } from 'Player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { Tabs, Input, Icon, NoContent } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import ConsoleRow from '../ConsoleRow';
|
||||
import { IOSPlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { List, CellMeasurer, AutoSizer } from 'react-virtualized';
|
||||
import { useStore } from 'App/mstore';
|
||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
const WARNINGS = 'WARNINGS';
|
||||
const ERRORS = 'ERRORS';
|
||||
|
||||
const LEVEL_TAB = {
|
||||
[LogLevel.INFO]: INFO,
|
||||
[LogLevel.LOG]: INFO,
|
||||
[LogLevel.WARN]: WARNINGS,
|
||||
[LogLevel.ERROR]: ERRORS,
|
||||
[LogLevel.EXCEPTION]: ERRORS,
|
||||
} as const;
|
||||
|
||||
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
function renderWithNL(s: string | null = '') {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.split('\n').map((line, i) => (
|
||||
<div key={i + line.slice(0, 6)} className={cn({ 'ml-20': i !== 0 })}>
|
||||
{line}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
const getIconProps = (level: any) => {
|
||||
switch (level) {
|
||||
case LogLevel.INFO:
|
||||
case LogLevel.LOG:
|
||||
return {
|
||||
name: 'console/info',
|
||||
color: 'blue2',
|
||||
};
|
||||
case LogLevel.WARN:
|
||||
return {
|
||||
name: 'console/warning',
|
||||
color: 'red2',
|
||||
};
|
||||
case LogLevel.ERROR:
|
||||
return {
|
||||
name: 'console/error',
|
||||
color: 'red',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const INDEX_KEY = 'console';
|
||||
|
||||
function MobileConsolePanel() {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore();
|
||||
|
||||
const filter = devTools[INDEX_KEY].filter;
|
||||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
// Why do we need to keep index in the store? if we could get read of it it would simplify the code
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const { player, store } = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
|
||||
const {
|
||||
logList,
|
||||
// exceptionsList,
|
||||
logListNow,
|
||||
exceptionsListNow,
|
||||
} = store.get();
|
||||
|
||||
const list = logList as ILog[];
|
||||
// useMemo(() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
|
||||
// [ logList.length, exceptionsList.length ],
|
||||
// ) as ILog[]
|
||||
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
cache.clearAll();
|
||||
_list.current?.recomputeRowHeights();
|
||||
}, 0);
|
||||
}, [activeTab, filter]);
|
||||
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: any) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
|
||||
// AutoScroll
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(logListNow, exceptionsListNow),
|
||||
activeIndex,
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
if (isDetailsModalActive) {
|
||||
return;
|
||||
}
|
||||
timeoutStartAutoscroll();
|
||||
};
|
||||
|
||||
const _list = useRef<List>(null); // TODO: fix react-virtualized types & encapsulate scrollToRow logic
|
||||
useEffect(() => {
|
||||
if (_list.current) {
|
||||
// @ts-ignore
|
||||
_list.current.scrollToRow(activeIndex);
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
const cache = useCellMeasurerCache();
|
||||
|
||||
const showDetails = (log: any) => {
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(<ErrorDetailsModal errorId={log.errorId} />, {
|
||||
right: true,
|
||||
width: 1200,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
},
|
||||
});
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) });
|
||||
stopAutoscroll();
|
||||
};
|
||||
const _rowRenderer = ({ index, key, parent, style }: any) => {
|
||||
const item = filteredList[index];
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
<ConsoleRow
|
||||
log={item}
|
||||
jump={jump}
|
||||
iconProps={getIconProps(item.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(item)}
|
||||
recalcHeight={measure}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '300px' }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
</BottomBlock.Header>
|
||||
{/* @ts-ignore */}
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<AutoSizer>
|
||||
{({ height, width }: any) => (
|
||||
// @ts-ignore
|
||||
<List
|
||||
ref={_list}
|
||||
deferredMeasurementCache={cache}
|
||||
overscanRowCount={5}
|
||||
estimatedRowSize={36}
|
||||
rowCount={Math.ceil(filteredList.length || 1)}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={_rowRenderer}
|
||||
width={width}
|
||||
height={height}
|
||||
// scrollToIndex={activeIndex}
|
||||
scrollToAlignment="center"
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</NoContent>
|
||||
{/* @ts-ignore */}
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(MobileConsolePanel);
|
||||
|
|
@ -29,6 +29,7 @@ function ConsoleRow(props: Props) {
|
|||
const toggleExpand = () => {
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
|
|
@ -52,14 +53,14 @@ function ConsoleRow(props: Props) {
|
|||
{canExpand && (
|
||||
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
|
||||
)}
|
||||
<span>{renderWithNL(lines.pop())}</span>
|
||||
<span style={{ fontFamily: 'Menlo, Monaco, Consolas' }}>{renderWithNL(lines.pop())}</span>
|
||||
</div>
|
||||
{log.errorId && <TextEllipsis className="ml-2 overflow-hidden" text={log.message }></TextEllipsis>}
|
||||
{log.errorId && <TextEllipsis className="ml-2 overflow-hidden" text={log.message}></TextEllipsis>}
|
||||
</div>
|
||||
{canExpand &&
|
||||
expanded &&
|
||||
lines.map((l: string, i: number) => (
|
||||
<div key={l.slice(0, 4) + i} className="ml-4 mb-1">
|
||||
<div key={l.slice(0, 4) + i} className="ml-4 mb-1" style={{ fontFamily: 'Menlo, Monaco, Consolas' }}>
|
||||
{l}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import WebPlayer from 'Player/web/WebPlayer';
|
||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
|
||||
import { ResourceType } from 'Player';
|
||||
import { ResourceType, Timed } from 'Player';
|
||||
import { formatBytes } from 'App/utils';
|
||||
import { formatMs } from 'App/date';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { connect } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import TimeTable from '../TimeTable';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
|
||||
import { toJS } from 'mobx';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
|
||||
const INDEX_KEY = 'network';
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ const TYPE_TO_TAB = {
|
|||
[ResourceType.IMG]: IMG,
|
||||
[ResourceType.MEDIA]: MEDIA,
|
||||
[ResourceType.OTHER]: OTHER,
|
||||
}
|
||||
};
|
||||
|
||||
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER] as const;
|
||||
const TABS = TAP_KEYS.map((tab) => ({
|
||||
|
|
@ -129,38 +130,109 @@ export function renderDuration(r: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function renderStatus({ status, cached }: { status: string, cached: boolean }) {
|
||||
function renderStatus({ status, cached }: { status: string; cached: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{cached ? (
|
||||
<Tooltip title={"Served from cache"}>
|
||||
<Tooltip title={'Served from cache'}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">{status}</span>
|
||||
<Icon name="wifi" size={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : status}
|
||||
) : (
|
||||
status
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkPanel({ startedAt }: { startedAt: number }) {
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
function NetworkPanelCont({ startedAt }: { startedAt: number }) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const {
|
||||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
tabStates,
|
||||
currentTab
|
||||
} = store.get()
|
||||
const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get();
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
fetchListNow = [],
|
||||
resourceListNow = []
|
||||
} = tabStates[currentTab]
|
||||
resourceListNow = [],
|
||||
} = tabStates[currentTab];
|
||||
|
||||
return (
|
||||
<NetworkPanelComp
|
||||
loadTime={loadTime}
|
||||
domBuildingTime={domBuildingTime}
|
||||
domContentLoadedTime={domContentLoadedTime}
|
||||
fetchList={fetchList}
|
||||
resourceList={resourceList}
|
||||
fetchListNow={fetchListNow}
|
||||
resourceListNow={resourceListNow}
|
||||
player={player}
|
||||
startedAt={startedAt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNetworkPanelCont({ startedAt }: { startedAt: number }) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
|
||||
const domContentLoadedTime = undefined;
|
||||
const loadTime = undefined;
|
||||
const domBuildingTime = undefined;
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
fetchListNow = [],
|
||||
resourceListNow = [],
|
||||
} = store.get();
|
||||
|
||||
return (
|
||||
<NetworkPanelComp
|
||||
isMobile
|
||||
loadTime={loadTime}
|
||||
domBuildingTime={domBuildingTime}
|
||||
domContentLoadedTime={domContentLoadedTime}
|
||||
fetchList={fetchList}
|
||||
resourceList={resourceList}
|
||||
fetchListNow={fetchListNow}
|
||||
resourceListNow={resourceListNow}
|
||||
player={player}
|
||||
startedAt={startedAt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
domContentLoadedTime?: {
|
||||
time: number;
|
||||
value: number;
|
||||
};
|
||||
loadTime?: {
|
||||
time: number;
|
||||
value: number;
|
||||
};
|
||||
domBuildingTime?: number;
|
||||
fetchList: Timed[];
|
||||
resourceList: Timed[];
|
||||
fetchListNow: Timed[];
|
||||
resourceListNow: Timed[];
|
||||
player: WebPlayer | MobilePlayer;
|
||||
startedAt: number;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
const NetworkPanelComp = observer(({
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
domContentLoadedTime,
|
||||
fetchList,
|
||||
resourceList,
|
||||
fetchListNow,
|
||||
resourceListNow,
|
||||
player,
|
||||
startedAt,
|
||||
isMobile
|
||||
}: Props) => {
|
||||
const { showModal } = useModal();
|
||||
const [sortBy, setSortBy] = useState('time');
|
||||
const [sortAscending, setSortAscending] = useState(true);
|
||||
|
|
@ -174,70 +246,87 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
|
||||
const list = useMemo(() =>
|
||||
const list = useMemo(
|
||||
() =>
|
||||
// TODO: better merge (with body size info) - do it in player
|
||||
resourceList.filter(res => !fetchList.some(ft => {
|
||||
resourceList
|
||||
.filter(
|
||||
(res) =>
|
||||
!fetchList.some((ft) => {
|
||||
// res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player)
|
||||
if (res.name === ft.name) {
|
||||
if (res.time === ft.time) return true;
|
||||
if (res.url.includes(ft.url)) {
|
||||
return Math.abs(res.time - ft.time) < 350
|
||||
|| Math.abs(res.timestamp - ft.timestamp) < 350;
|
||||
return (
|
||||
Math.abs(res.time - ft.time) < 350 ||
|
||||
Math.abs(res.timestamp - ft.timestamp) < 350
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.name !== ft.name) { return false }
|
||||
if (Math.abs(res.time - ft.time) > 250) { return false } // TODO: find good epsilons
|
||||
if (Math.abs(res.duration - ft.duration) > 200) { return false }
|
||||
if (res.name !== ft.name) {
|
||||
return false;
|
||||
}
|
||||
if (Math.abs(res.time - ft.time) > 250) {
|
||||
return false;
|
||||
} // TODO: find good epsilons
|
||||
if (Math.abs(res.duration - ft.duration) > 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}))
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.concat(fetchList)
|
||||
.sort((a, b) => a.time - b.time)
|
||||
, [ resourceList.length, fetchList.length ])
|
||||
.sort((a, b) => a.time - b.time),
|
||||
[resourceList.length, fetchList.length]
|
||||
);
|
||||
|
||||
let filteredList = useMemo(() => {
|
||||
if (!showOnlyErrors) { return list }
|
||||
return list.filter(it => parseInt(it.status) >= 400 || !it.success)
|
||||
}, [ showOnlyErrors, list ])
|
||||
if (!showOnlyErrors) {
|
||||
return list;
|
||||
}
|
||||
return list.filter((it) => parseInt(it.status) >= 400 || !it.success);
|
||||
}, [showOnlyErrors, list]);
|
||||
filteredList = useRegExListFilterMemo(
|
||||
filteredList,
|
||||
it => [ it.status, it.name, it.type ],
|
||||
filter,
|
||||
)
|
||||
filteredList = useTabListFilterMemo(filteredList, it => TYPE_TO_TAB[it.type], ALL, activeTab)
|
||||
(it) => [it.status, it.name, it.type],
|
||||
filter
|
||||
);
|
||||
filteredList = useTabListFilterMemo(filteredList, (it) => TYPE_TO_TAB[it.type], ALL, activeTab);
|
||||
|
||||
const onTabClick = (activeTab: typeof TAP_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab })
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value })
|
||||
const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
|
||||
// AutoScroll
|
||||
const [
|
||||
timeoutStartAutoscroll,
|
||||
stopAutoscroll,
|
||||
] = useAutoscroll(
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(fetchListNow, resourceListNow),
|
||||
activeIndex,
|
||||
index => devTools.update(INDEX_KEY, { index })
|
||||
)
|
||||
const onMouseEnter = stopAutoscroll
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
if (isDetailsModalActive) { return }
|
||||
timeoutStartAutoscroll()
|
||||
if (isDetailsModalActive) {
|
||||
return;
|
||||
}
|
||||
timeoutStartAutoscroll();
|
||||
};
|
||||
|
||||
const resourcesSize = useMemo(() =>
|
||||
const resourcesSize = useMemo(
|
||||
() => resourceList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0),
|
||||
[resourceList.length]
|
||||
);
|
||||
const transferredSize = useMemo(
|
||||
() =>
|
||||
resourceList.reduce(
|
||||
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
|
||||
0,
|
||||
), [ resourceList.length ])
|
||||
const transferredSize = useMemo(() =>
|
||||
resourceList.reduce(
|
||||
(sum, { headerSize, encodedBodySize }) =>
|
||||
sum + (headerSize || 0) + (encodedBodySize || 0),
|
||||
0,
|
||||
), [ resourceList.length ])
|
||||
|
||||
(sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0),
|
||||
0
|
||||
),
|
||||
[resourceList.length]
|
||||
);
|
||||
|
||||
const referenceLines = useMemo(() => {
|
||||
const arr = [];
|
||||
|
|
@ -256,24 +345,29 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
}
|
||||
|
||||
return arr;
|
||||
}, [ domContentLoadedTime, loadTime ])
|
||||
}, [domContentLoadedTime, loadTime]);
|
||||
|
||||
const showDetailsModal = (item: any) => {
|
||||
setIsDetailsModalActive(true)
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
<FetchDetailsModal time={item.time + startedAt} resource={item} rows={filteredList} fetchPresented={fetchList.length > 0} />,
|
||||
<FetchDetailsModal
|
||||
time={item.time + startedAt}
|
||||
resource={item}
|
||||
rows={filteredList}
|
||||
fetchPresented={fetchList.length > 0}
|
||||
/>,
|
||||
{
|
||||
right: true,
|
||||
width: 500,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false)
|
||||
timeoutStartAutoscroll()
|
||||
}
|
||||
}
|
||||
)
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) })
|
||||
stopAutoscroll()
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
},
|
||||
}
|
||||
);
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
||||
stopAutoscroll();
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -286,6 +380,7 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Network</span>
|
||||
{isMobile ? null :
|
||||
<Tabs
|
||||
className="uppercase"
|
||||
tabs={TABS}
|
||||
|
|
@ -293,6 +388,7 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
onClick={onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<Input
|
||||
className="input-small"
|
||||
|
|
@ -413,8 +509,17 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
export default connect((state: any) => ({
|
||||
const WebNetworkPanel = connect((state: any) => ({
|
||||
startedAt: state.getIn(['sessions', 'current']).startedAt,
|
||||
}))(observer(NetworkPanel));
|
||||
}))(observer(NetworkPanelCont));
|
||||
|
||||
const MobileNetworkPanel = connect((state: any) => ({
|
||||
startedAt: state.getIn(['sessions', 'current']).startedAt,
|
||||
}))(observer(MobileNetworkPanelCont));
|
||||
|
||||
export {
|
||||
WebNetworkPanel,
|
||||
MobileNetworkPanel
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
export { default } from './NetworkPanel'
|
||||
export { WebNetworkPanel, MobileNetworkPanel } from './NetworkPanel';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Timed } from 'Player';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Tabs, Input, NoContent, Icon } from 'UI';
|
||||
import { List, CellMeasurer, AutoSizer } from 'react-virtualized';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { PlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -14,20 +15,47 @@ import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
|||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
|
||||
const mapNames = (type: string) => {
|
||||
if (type === 'openreplay') return 'OpenReplay';
|
||||
return type
|
||||
}
|
||||
|
||||
const INDEX_KEY = 'stackEvent';
|
||||
const ALL = 'ALL';
|
||||
const TAB_KEYS = [ALL, ...typeList] as const;
|
||||
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
function StackEventPanel() {
|
||||
type EventsList = Array<Timed & { name: string; source: string, key: string }>
|
||||
|
||||
export const WebStackEventPanel = observer(() => {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { currentTab, tabStates } = store.get();
|
||||
|
||||
const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab];
|
||||
|
||||
return <EventsPanel list={list as EventsList} listNow={listNow as EventsList} jump={jump} />;
|
||||
});
|
||||
|
||||
export const MobileStackEventPanel = observer(() => {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
|
||||
|
||||
return <EventsPanel list={list as EventsList} listNow={listNow as EventsList} jump={jump} />;
|
||||
});
|
||||
|
||||
function EventsPanel({
|
||||
list,
|
||||
listNow,
|
||||
jump,
|
||||
}: {
|
||||
list: EventsList;
|
||||
listNow: EventsList;
|
||||
jump: (t: number) => void;
|
||||
}) {
|
||||
const {
|
||||
sessionStore: { devTools }
|
||||
sessionStore: { devTools },
|
||||
} = useStore();
|
||||
const { showModal } = useModal();
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal
|
||||
|
|
@ -35,24 +63,23 @@ function StackEventPanel() {
|
|||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
|
||||
let filteredList = useRegExListFilterMemo(list, it => it.name, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, it => it.source, ALL, activeTab);
|
||||
let filteredList = useRegExListFilterMemo(list, (it) => it.name, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab);
|
||||
|
||||
const onTabClick = (activeTab: typeof TAB_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(() =>
|
||||
TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)),
|
||||
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(
|
||||
() => TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)),
|
||||
[list.length]
|
||||
);
|
||||
|
||||
const [
|
||||
timeoutStartAutoscroll,
|
||||
stopAutoscroll
|
||||
] = useAutoscroll(
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(listNow),
|
||||
activeIndex,
|
||||
index => devTools.update(INDEX_KEY, { index })
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
|
|
@ -66,17 +93,14 @@ function StackEventPanel() {
|
|||
|
||||
const showDetails = (item: any) => {
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
<StackEventModal event={item} />,
|
||||
{
|
||||
showModal(<StackEventModal event={item} />, {
|
||||
right: true,
|
||||
width: 500,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
||||
stopAutoscroll();
|
||||
};
|
||||
|
|
@ -89,7 +113,6 @@ function StackEventPanel() {
|
|||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
|
||||
const _rowRenderer = ({ index, key, parent, style }: any) => {
|
||||
const item = filteredList[index];
|
||||
|
||||
|
|
@ -121,29 +144,29 @@ function StackEventPanel() {
|
|||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<BottomBlock.Header>
|
||||
<div className='flex items-center'>
|
||||
<span className='font-semibold color-gray-medium mr-4'>Stack Events</span>
|
||||
<Tabs tabs={tabs} active={activeTab} onClick={onTabClick} border={false} />
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Stack Events</span>
|
||||
<Tabs renameTab={mapNames} tabs={tabs} active={activeTab} onClick={onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className='input-small h-8'
|
||||
placeholder='Filter by keyword'
|
||||
icon='search'
|
||||
name='filter'
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className='overflow-y-auto'>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className='capitalize flex items-center mt-16'>
|
||||
<Icon name='info-circle' className='mr-2' size='18' />
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
size='small'
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
<AutoSizer>
|
||||
|
|
@ -157,7 +180,7 @@ function StackEventPanel() {
|
|||
rowRenderer={_rowRenderer}
|
||||
width={width}
|
||||
height={height}
|
||||
scrollToAlignment='center'
|
||||
scrollToAlignment="center"
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
|
@ -166,5 +189,3 @@ function StackEventPanel() {
|
|||
</BottomBlock>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(StackEventPanel);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './StackEventPanel';
|
||||
export { WebStackEventPanel, MobileStackEventPanel } from './StackEventPanel';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const NoSessionsMessage = (props) => {
|
|||
const activeSite = sites.find((s) => s.id === siteId);
|
||||
const showNoSessions = !!activeSite && !activeSite.recorded;
|
||||
const onboardingPath = withSiteId(onboardingRoute('installing'), siteId);
|
||||
console.log('onboardingPath', onboardingPath, siteId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNoSessions && (
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ import { useModal } from 'Components/Modal';
|
|||
import { init as initProject } from 'Duck/site';
|
||||
import NewSiteForm from 'Components/Client/Sites/NewSiteForm';
|
||||
import { withStore } from 'App/mstore';
|
||||
import { Icon } from 'UI'
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface Site {
|
||||
id: string;
|
||||
host: string;
|
||||
platform: 'web' | 'mobile';
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
|
|
@ -66,7 +68,7 @@ function ProjectDropdown(props: Props) {
|
|||
|
||||
{sites.map((site) => (
|
||||
<Menu.Item
|
||||
icon={<FolderOutlined />}
|
||||
icon={<Icon name={site.platform === 'web' ? 'browser/browser' : 'mobile'} />}
|
||||
key={site.id}
|
||||
onClick={() => handleSiteChange(site.id)}
|
||||
className='!py-2'
|
||||
|
|
|
|||
|
|
@ -260,22 +260,21 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
showLabel={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='color-gray-medium flex items-center py-1'>
|
||||
{userBrowser && (
|
||||
<>
|
||||
<span className='capitalize' style={{ maxWidth: '70px' }}>
|
||||
<div className="color-gray-medium flex items-center py-1">
|
||||
{userBrowser ? (
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis
|
||||
text={capitalize(userBrowser)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
<Icon name='circle-fill' size={3} className='mx-4' />
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className='capitalize' style={{ maxWidth: '70px' }}>
|
||||
) : null}
|
||||
{userOs && userBrowser ? (
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
) : null}
|
||||
<span className={/ios/i.test(userOs) ? '' : 'capitalize'} style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis
|
||||
text={capitalize(userOs)}
|
||||
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,10 +1,29 @@
|
|||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import {IconNames} from "UI/SVG";
|
||||
import styles from './segmentSelection.module.css';
|
||||
|
||||
class SegmentSelection extends React.Component {
|
||||
setActiveItem = (item) => {
|
||||
type Entry = { value: string, name: string, disabled?: boolean, icon?: IconNames };
|
||||
|
||||
interface Props<T extends Entry> {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: T;
|
||||
list: T[];
|
||||
onSelect: (_: null, data: { name: string, value: T['value']}) => void;
|
||||
small?: boolean;
|
||||
extraSmall?: boolean;
|
||||
primary?: boolean;
|
||||
size?: 'normal' | 'small' | 'extraSmall';
|
||||
icons?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledMessage?: string;
|
||||
outline?: boolean;
|
||||
}
|
||||
|
||||
class SegmentSelection<T extends Entry> extends React.Component<Props<T>> {
|
||||
setActiveItem = (item: T) => {
|
||||
this.props.onSelect(null, { name: this.props.name, value: item.value });
|
||||
};
|
||||
|
||||
|
|
@ -49,7 +68,7 @@ class SegmentSelection extends React.Component {
|
|||
<Icon
|
||||
name={item.icon}
|
||||
size={size === 'extraSmall' || size === 'small' || icons ? 14 : 20}
|
||||
marginRight={item.name ? '6' : ''}
|
||||
marginRight={item.name ? 6 : undefined}
|
||||
/>
|
||||
)}
|
||||
<div className="leading-none">{item.name}</div>
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import stl from './tabs.module.css';
|
||||
|
||||
const Tabs = ({ tabs, active, onClick, border = true, className = '' }) => (
|
||||
const Tabs = ({ tabs, active, onClick, border = true, className = '', renameTab = (tab) => tab }) => (
|
||||
<div className={ cn(stl.tabs, className, { [ stl.bordered ]: border }) } role="tablist" >
|
||||
{ tabs.map(({ key, text, hidden = false, disabled = false }) => (
|
||||
<div
|
||||
|
|
@ -11,9 +11,9 @@ const Tabs = ({ tabs, active, onClick, border = true, className = '' }) => (
|
|||
data-hidden={ hidden }
|
||||
onClick={ onClick && (() => onClick(key)) }
|
||||
role="tab"
|
||||
data-openreplay-label={text}
|
||||
data-openreplay-label={renameTab(text)}
|
||||
>
|
||||
{ text }
|
||||
{ renameTab(text) }
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -191,10 +191,11 @@ const reducer = (state = initialState, action: IAction) => {
|
|||
errors,
|
||||
events,
|
||||
issues,
|
||||
crashes,
|
||||
resources,
|
||||
stackEvents,
|
||||
userEvents
|
||||
} = action.data as { errors: any[], events: any[], issues: any[], resources: any[], stackEvents: any[], userEvents: EventData[] };
|
||||
} = action.data as { errors: any[], crashes: any[], events: any[], issues: any[], resources: any[], stackEvents: any[], userEvents: EventData[] };
|
||||
const filterEvents = action.filter.events as Record<string, any>[];
|
||||
const session = state.get('current') as Session;
|
||||
const matching: number[] = [];
|
||||
|
|
@ -228,6 +229,7 @@ const reducer = (state = initialState, action: IAction) => {
|
|||
|
||||
const newSession = session.addEvents(
|
||||
events,
|
||||
crashes,
|
||||
errors,
|
||||
issues,
|
||||
resources,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Timed } from './types';
|
||||
import type { Timed } from 'Player';
|
||||
|
||||
export default class ListWalker<T extends Timed> {
|
||||
private p = 0 /* Pointer to the "current" item */
|
||||
/* Pointer to the "current" item */
|
||||
private p = 0
|
||||
constructor(private _list: Array<T> = []) {}
|
||||
|
||||
append(m: T): void {
|
||||
|
|
@ -123,10 +124,12 @@ export default class ListWalker<T extends Timed> {
|
|||
}
|
||||
|
||||
let changed = false;
|
||||
// @ts-ignore
|
||||
while (this.p < this.length && this.list[this.p][key] <= val) {
|
||||
this.moveNext()
|
||||
changed = true;
|
||||
}
|
||||
// @ts-ignore
|
||||
while (this.p > 0 && this.list[ this.p - 1 ][key] > val) {
|
||||
this.movePrev()
|
||||
changed = true;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export interface Timed {
|
||||
time: number
|
||||
/** present in mobile events and in db events */
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface Indexed {
|
||||
|
|
|
|||
|
|
@ -1,50 +1,72 @@
|
|||
import SimpleStore from './common/SimpleStore'
|
||||
import type { Store, SessionFilesInfo } from './common/types'
|
||||
import SimpleStore from './common/SimpleStore';
|
||||
import type { Store, SessionFilesInfo } from './common/types';
|
||||
|
||||
import WebPlayer from './web/WebPlayer'
|
||||
import WebLivePlayer from './web/WebLivePlayer'
|
||||
import WebPlayer from './web/WebPlayer';
|
||||
import WebLivePlayer from './web/WebLivePlayer';
|
||||
|
||||
type WebState = typeof WebPlayer.INITIAL_STATE
|
||||
type WebPlayerStore = Store<WebState>
|
||||
export type IWebPlayer = WebPlayer
|
||||
export type IWebPlayerStore = WebPlayerStore
|
||||
import IOSPlayer from 'Player/mobile/IOSPlayer';
|
||||
|
||||
type WebLiveState = typeof WebLivePlayer.INITIAL_STATE
|
||||
type WebLivePlayerStore = Store<WebLiveState>
|
||||
export type IWebLivePlayer = WebLivePlayer
|
||||
export type IWebLivePlayerStore = WebLivePlayerStore
|
||||
type IosState = typeof IOSPlayer.INITIAL_STATE;
|
||||
type IOSPlayerStore = Store<IosState>;
|
||||
export type IIosPlayer = IOSPlayer;
|
||||
export type IIOSPlayerStore = IOSPlayerStore;
|
||||
|
||||
type WebState = typeof WebPlayer.INITIAL_STATE;
|
||||
type WebPlayerStore = Store<WebState>;
|
||||
export type IWebPlayer = WebPlayer;
|
||||
export type IWebPlayerStore = WebPlayerStore;
|
||||
|
||||
type WebLiveState = typeof WebLivePlayer.INITIAL_STATE;
|
||||
type WebLivePlayerStore = Store<WebLiveState>;
|
||||
export type IWebLivePlayer = WebLivePlayer;
|
||||
export type IWebLivePlayerStore = WebLivePlayerStore;
|
||||
|
||||
export function createIOSPlayer(
|
||||
session: SessionFilesInfo,
|
||||
wrapStore?: (s: IOSPlayerStore) => IOSPlayerStore,
|
||||
uiErrorHandler?: { error: (msg: string) => void }
|
||||
): [IIosPlayer, IOSPlayerStore] {
|
||||
let store: IOSPlayerStore = new SimpleStore<IosState>({
|
||||
...IOSPlayer.INITIAL_STATE,
|
||||
});
|
||||
if (wrapStore) {
|
||||
store = wrapStore(store);
|
||||
}
|
||||
|
||||
const player = new IOSPlayer(store, session, uiErrorHandler);
|
||||
return [player, store];
|
||||
}
|
||||
|
||||
export function createWebPlayer(
|
||||
session: SessionFilesInfo,
|
||||
wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore,
|
||||
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
|
||||
uiErrorHandler?: { error: (msg: string) => void }
|
||||
): [IWebPlayer, IWebPlayerStore] {
|
||||
let store: WebPlayerStore = new SimpleStore<WebState>({
|
||||
...WebPlayer.INITIAL_STATE,
|
||||
})
|
||||
});
|
||||
if (wrapStore) {
|
||||
store = wrapStore(store)
|
||||
store = wrapStore(store);
|
||||
}
|
||||
|
||||
const player = new WebPlayer(store, session, false, false, uiErrorHandler)
|
||||
return [player, store]
|
||||
const player = new WebPlayer(store, session, false, false, uiErrorHandler);
|
||||
return [player, store];
|
||||
}
|
||||
|
||||
|
||||
export function createClickMapPlayer(
|
||||
session: SessionFilesInfo,
|
||||
wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore,
|
||||
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
|
||||
uiErrorHandler?: { error: (msg: string) => void }
|
||||
): [IWebPlayer, IWebPlayerStore] {
|
||||
let store: WebPlayerStore = new SimpleStore<WebState>({
|
||||
...WebPlayer.INITIAL_STATE,
|
||||
})
|
||||
});
|
||||
if (wrapStore) {
|
||||
store = wrapStore(store)
|
||||
store = wrapStore(store);
|
||||
}
|
||||
|
||||
const player = new WebPlayer(store, session, false, true, uiErrorHandler)
|
||||
return [player, store]
|
||||
const player = new WebPlayer(store, session, false, true, uiErrorHandler);
|
||||
return [player, store];
|
||||
}
|
||||
|
||||
export function createLiveWebPlayer(
|
||||
|
|
|
|||
|
|
@ -8,3 +8,7 @@ export * from './web/WebPlayer';
|
|||
export * from './web/storageSelectors';
|
||||
export * from './web/types';
|
||||
export type { MarkedTarget } from './web/addons/TargetMarker'
|
||||
|
||||
export * from './mobile/IOSPlayer'
|
||||
export * from './mobile/IOSMessageManager'
|
||||
export * from './mobile/managers/TouchManager'
|
||||
|
|
|
|||
92
frontend/app/player/mobile/IOSLists.tsx
Normal file
92
frontend/app/player/mobile/IOSLists.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import ListWalker from '../common/ListWalker';
|
||||
import ListWalkerWithMarks from '../common/ListWalkerWithMarks';
|
||||
import type { Timed } from '../common/types';
|
||||
|
||||
|
||||
const SIMPLE_LIST_NAMES = [
|
||||
"event",
|
||||
"exceptions",
|
||||
"profiles",
|
||||
"frustrations",
|
||||
"performance"
|
||||
] as const
|
||||
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const
|
||||
|
||||
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ] as const
|
||||
|
||||
type KeysList = `${typeof LIST_NAMES[number]}List`
|
||||
type KeysListNow = `${typeof LIST_NAMES[number]}ListNow`
|
||||
type KeysMarkedCountNow = `${typeof MARKED_LIST_NAMES[number]}MarkedCountNow`
|
||||
type StateList = {
|
||||
[key in KeysList]: Timed[]
|
||||
}
|
||||
type StateListNow = {
|
||||
[key in KeysListNow]: Timed[]
|
||||
}
|
||||
type StateMarkedCountNow = {
|
||||
[key in KeysMarkedCountNow]: number
|
||||
}
|
||||
type StateNow = StateListNow & StateMarkedCountNow
|
||||
export type State = StateList & StateNow
|
||||
|
||||
// maybe use list object itself inside the store
|
||||
|
||||
export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}List`] = []
|
||||
state[`${name}ListNow`] = []
|
||||
return state
|
||||
}, MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = 0
|
||||
return state
|
||||
}, {} as Partial<StateMarkedCountNow>) as Partial<State>
|
||||
) as State
|
||||
|
||||
|
||||
type SimpleListsObject = {
|
||||
[key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker<Timed>
|
||||
}
|
||||
type MarkedListsObject = {
|
||||
[key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks<Timed>
|
||||
}
|
||||
type ListsObject = SimpleListsObject & MarkedListsObject
|
||||
|
||||
export type InitialLists = {
|
||||
[key in typeof LIST_NAMES[number]]: any[] // .isRed()?
|
||||
}
|
||||
|
||||
export default class Lists {
|
||||
lists: ListsObject
|
||||
constructor(initialLists: Partial<InitialLists> = {}) {
|
||||
const lists: Partial<ListsObject> = {}
|
||||
for (const name of SIMPLE_LIST_NAMES) {
|
||||
lists[name] = new ListWalker(initialLists[name])
|
||||
}
|
||||
for (const name of MARKED_LIST_NAMES) {
|
||||
// TODO: provide types
|
||||
lists[name] = new ListWalkerWithMarks((el) => el.isRed, initialLists[name])
|
||||
}
|
||||
this.lists = lists as ListsObject
|
||||
}
|
||||
|
||||
getFullListsState(): StateList {
|
||||
return LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}List`] = this.lists[name].list
|
||||
return state
|
||||
}, {} as Partial<StateList>) as StateList
|
||||
}
|
||||
|
||||
moveGetState(t: number): StateNow {
|
||||
return LIST_NAMES.reduce((state, name) => {
|
||||
const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index);
|
||||
if (lastMsg != null) {
|
||||
state[`${name}ListNow`] = this.lists[name].listNow
|
||||
}
|
||||
return state
|
||||
}, MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = this.lists[name].markedCountNow // Red --> Marked
|
||||
return state
|
||||
}, {} as Partial<StateMarkedCountNow>) as Partial<State>
|
||||
) as State
|
||||
}
|
||||
|
||||
}
|
||||
264
frontend/app/player/mobile/IOSMessageManager.ts
Normal file
264
frontend/app/player/mobile/IOSMessageManager.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
import logger from 'App/logger';
|
||||
import { getResourceFromNetworkRequest } from "Player";
|
||||
|
||||
import type { Store } from 'Player';
|
||||
import { IMessageManager } from 'Player/player/Animator';
|
||||
|
||||
import TouchManager from 'Player/mobile/managers/TouchManager';
|
||||
import ActivityManager from '../web/managers/ActivityManager';
|
||||
import Lists, {
|
||||
InitialLists,
|
||||
INITIAL_STATE as LISTS_INITIAL_STATE,
|
||||
State as ListsState,
|
||||
} from './IOSLists';
|
||||
import IOSPerformanceTrackManager, { PerformanceChartPoint } from "Player/mobile/managers/IOSPerformanceTrackManager";
|
||||
import { MType } from '../web/messages';
|
||||
import type { Message } from '../web/messages';
|
||||
|
||||
import Screen, {
|
||||
INITIAL_STATE as SCREEN_INITIAL_STATE,
|
||||
State as ScreenState,
|
||||
} from '../web/Screen/Screen';
|
||||
|
||||
import { Log } from './types/log'
|
||||
|
||||
import type { SkipInterval } from '../web/managers/ActivityManager';
|
||||
|
||||
export const performanceWarnings = ['thermalState', 'memoryWarning', 'lowDiskSpace', 'isLowPowerModeEnabled', 'batteryLevel']
|
||||
|
||||
const perfWarningFrustrations = {
|
||||
thermalState: {
|
||||
title: "Overheating",
|
||||
icon: "thermometer-sun",
|
||||
},
|
||||
memoryWarning: {
|
||||
title: "High Memory Usage",
|
||||
icon: "memory-ios"
|
||||
},
|
||||
lowDiskSpace: {
|
||||
title: "Low Disk Space",
|
||||
icon: "low-disc-space"
|
||||
},
|
||||
isLowPowerModeEnabled: {
|
||||
title: "Low Power Mode",
|
||||
icon: "battery-charging"
|
||||
},
|
||||
batteryLevel: {
|
||||
title: "Low Battery",
|
||||
icon: "battery"
|
||||
}
|
||||
}
|
||||
|
||||
export interface State extends ScreenState, ListsState {
|
||||
skipIntervals: SkipInterval[];
|
||||
performanceChartData: PerformanceChartPoint[];
|
||||
performanceChartTime: number;
|
||||
location?: string;
|
||||
|
||||
error: boolean;
|
||||
messagesLoading: boolean;
|
||||
|
||||
cssLoading: boolean;
|
||||
ready: boolean;
|
||||
lastMessageTime: number;
|
||||
messagesProcessed: boolean;
|
||||
eventCount: number;
|
||||
updateWarnings: number;
|
||||
}
|
||||
|
||||
const userEvents = [MType.IosSwipeEvent, MType.IosClickEvent, MType.IosInputEvent, MType.IosScreenChanges];
|
||||
|
||||
export default class IOSMessageManager implements IMessageManager {
|
||||
static INITIAL_STATE: State = {
|
||||
...SCREEN_INITIAL_STATE,
|
||||
...LISTS_INITIAL_STATE,
|
||||
updateWarnings: 0,
|
||||
eventCount: 0,
|
||||
performanceChartData: [],
|
||||
performanceChartTime: 0,
|
||||
skipIntervals: [],
|
||||
error: false,
|
||||
ready: false,
|
||||
cssLoading: false,
|
||||
lastMessageTime: 0,
|
||||
messagesProcessed: false,
|
||||
messagesLoading: false,
|
||||
};
|
||||
|
||||
private activityManager: ActivityManager | null = null;
|
||||
private performanceManager = new IOSPerformanceTrackManager();
|
||||
|
||||
private readonly sessionStart: number;
|
||||
private lastMessageTime: number = 0;
|
||||
private touchManager: TouchManager;
|
||||
private lists: Lists;
|
||||
|
||||
constructor(
|
||||
private readonly session: Record<string, any>,
|
||||
private readonly state: Store<State & { time: number }>,
|
||||
private readonly screen: Screen,
|
||||
private readonly uiErrorHandler?: { error: (error: string) => void },
|
||||
initialLists?: Partial<InitialLists>
|
||||
) {
|
||||
this.sessionStart = this.session.startedAt;
|
||||
this.lists = new Lists(initialLists);
|
||||
this.touchManager = new TouchManager(screen);
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
|
||||
}
|
||||
|
||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||
this.touchManager.updateDimensions(dimensions);
|
||||
}
|
||||
|
||||
public updateLists(lists: Partial<InitialLists>) {
|
||||
const exceptions = lists.exceptions
|
||||
exceptions?.forEach(e => {
|
||||
this.lists.lists.exceptions.insert(e);
|
||||
this.lists.lists.log.insert(e)
|
||||
})
|
||||
lists.frustrations?.forEach(f => {
|
||||
this.lists.lists.frustrations.insert(f);
|
||||
})
|
||||
|
||||
const eventCount = this.lists.lists.event.count //lists?.event?.length || 0;
|
||||
const currentState = this.state.get();
|
||||
this.state.update({
|
||||
eventCount: currentState.eventCount + eventCount,
|
||||
...this.lists.getFullListsState(),
|
||||
});
|
||||
}
|
||||
|
||||
_sortMessagesHack() {
|
||||
return;
|
||||
}
|
||||
|
||||
private waitingForFiles: boolean = false;
|
||||
public onFileReadSuccess = () => {
|
||||
let newState: Partial<State> = {
|
||||
...this.state.get(),
|
||||
eventCount: this.lists?.lists.event?.length || 0,
|
||||
performanceChartData: this.performanceManager.chartData,
|
||||
...this.lists.getFullListsState(),
|
||||
}
|
||||
|
||||
if (this.activityManager) {
|
||||
this.activityManager.end();
|
||||
newState['skipIntervals'] = this.activityManager.list
|
||||
}
|
||||
this.state.update(newState);
|
||||
};
|
||||
|
||||
public onFileReadFailed = (e: any) => {
|
||||
logger.error(e);
|
||||
this.state.update({error: true});
|
||||
this.uiErrorHandler?.error('Error requesting a session file');
|
||||
};
|
||||
|
||||
public onFileReadFinally = () => {
|
||||
this.waitingForFiles = false;
|
||||
this.state.update({messagesProcessed: true});
|
||||
};
|
||||
|
||||
public startLoading = () => {
|
||||
this.waitingForFiles = true;
|
||||
this.state.update({messagesProcessed: false});
|
||||
this.setMessagesLoading(true);
|
||||
};
|
||||
|
||||
resetMessageManagers() {
|
||||
this.touchManager = new TouchManager(this.screen);
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds);
|
||||
}
|
||||
|
||||
move(t: number): any {
|
||||
const stateToUpdate: Record<string, any> = {};
|
||||
|
||||
const lastPerformanceTrackMessage = this.performanceManager.moveGetLast(t);
|
||||
if (lastPerformanceTrackMessage) {
|
||||
Object.assign(stateToUpdate, {
|
||||
performanceChartTime: lastPerformanceTrackMessage.time,
|
||||
})
|
||||
}
|
||||
|
||||
this.touchManager.move(t);
|
||||
if (
|
||||
this.waitingForFiles &&
|
||||
this.lastMessageTime <= t &&
|
||||
t !== this.session.duration.milliseconds
|
||||
) {
|
||||
this.setMessagesLoading(true);
|
||||
}
|
||||
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
||||
Object.assign(stateToUpdate, { performanceListNow: this.lists.lists.performance.listNow })
|
||||
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
|
||||
}
|
||||
|
||||
distributeMessage = (msg: Message & { tabId: string }): void => {
|
||||
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
this.state.update({lastMessageTime});
|
||||
if (userEvents.includes(msg.tp)) {
|
||||
this.activityManager?.updateAcctivity(msg.time);
|
||||
}
|
||||
|
||||
switch (msg.tp) {
|
||||
case MType.IosPerformanceEvent:
|
||||
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU']
|
||||
if (performanceStats.includes(msg.name)) {
|
||||
this.performanceManager.append(msg);
|
||||
}
|
||||
if (performanceWarnings.includes(msg.name)) {
|
||||
// @ts-ignore
|
||||
const item = perfWarningFrustrations[msg.name]
|
||||
this.lists.lists.performance.append({
|
||||
...msg,
|
||||
name: item.title,
|
||||
techName: msg.name,
|
||||
icon: item.icon,
|
||||
type: 'ios_perf_event'
|
||||
} as any)
|
||||
}
|
||||
break;
|
||||
// case MType.IosInputEvent:
|
||||
// console.log('input', msg)
|
||||
// break;
|
||||
case MType.IosNetworkCall:
|
||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
|
||||
break;
|
||||
case MType.IosEvent:
|
||||
// @ts-ignore
|
||||
this.lists.lists.event.insert({...msg, source: 'openreplay'});
|
||||
break;
|
||||
case MType.IosSwipeEvent:
|
||||
case MType.IosClickEvent:
|
||||
this.touchManager.append(msg);
|
||||
break;
|
||||
case MType.IosLog:
|
||||
const log = {...msg, level: msg.severity}
|
||||
// @ts-ignore
|
||||
this.lists.lists.log.append(Log(log));
|
||||
break;
|
||||
default:
|
||||
console.log(msg)
|
||||
// stuff
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
setMessagesLoading = (messagesLoading: boolean) => {
|
||||
this.screen.display(!messagesLoading);
|
||||
// @ts-ignore idk
|
||||
this.state.update({messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading});
|
||||
};
|
||||
|
||||
private setSize({height, width}: { height: number; width: number }) {
|
||||
this.screen.scale({height, width});
|
||||
this.state.update({width, height});
|
||||
}
|
||||
|
||||
// TODO: clean managers?
|
||||
clean() {
|
||||
this.state.update(IOSMessageManager.INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
131
frontend/app/player/mobile/IOSPlayer.ts
Normal file
131
frontend/app/player/mobile/IOSPlayer.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { Log, LogLevel, SessionFilesInfo } from 'Player'
|
||||
|
||||
import type { Store } from 'Player'
|
||||
import MessageLoader from "Player/web/MessageLoader";
|
||||
import Player from '../player/Player'
|
||||
import Screen, { ScaleMode } from '../web/Screen/Screen'
|
||||
import IOSMessageManager from "Player/mobile/IOSMessageManager";
|
||||
|
||||
export default class IOSPlayer extends Player {
|
||||
static readonly INITIAL_STATE = {
|
||||
...Player.INITIAL_STATE,
|
||||
...MessageLoader.INITIAL_STATE,
|
||||
...IOSMessageManager.INITIAL_STATE,
|
||||
scale: 1,
|
||||
}
|
||||
public screen: Screen
|
||||
protected messageManager: IOSMessageManager
|
||||
protected readonly messageLoader: MessageLoader
|
||||
constructor(
|
||||
protected wpState: Store<any>,
|
||||
session: SessionFilesInfo,
|
||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {
|
||||
const screen = new Screen(true, ScaleMode.Embed)
|
||||
const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler)
|
||||
const messageLoader = new MessageLoader(
|
||||
session,
|
||||
wpState,
|
||||
messageManager,
|
||||
false,
|
||||
uiErrorHandler
|
||||
)
|
||||
super(wpState, messageManager);
|
||||
this.screen = screen
|
||||
this.messageManager = messageManager
|
||||
this.messageLoader = messageLoader
|
||||
|
||||
void messageLoader.loadFiles()
|
||||
const endTime = session.duration?.valueOf() || 0
|
||||
|
||||
wpState.update({
|
||||
session,
|
||||
endTime,
|
||||
})
|
||||
}
|
||||
|
||||
attach = (parent: HTMLElement) => {
|
||||
this.screen.attach(parent)
|
||||
}
|
||||
|
||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||
return this.messageManager.updateDimensions(dimensions)
|
||||
}
|
||||
|
||||
public updateLists(session: any) {
|
||||
const exceptions = session.crashes.concat(session.errors || [])
|
||||
const lists = {
|
||||
event: session.events.map((e: Record<string, any>) => {
|
||||
if (e.name === 'Click') e.name = 'Touch'
|
||||
return e
|
||||
}) || [],
|
||||
frustrations: session.frustrations || [],
|
||||
stack: session.stackEvents || [],
|
||||
exceptions: exceptions.map(({ name, ...rest }: any) =>
|
||||
Log({
|
||||
level: LogLevel.ERROR,
|
||||
value: name,
|
||||
name,
|
||||
message: rest.reason,
|
||||
errorId: rest.crashId || rest.errorId,
|
||||
...rest,
|
||||
})
|
||||
) || [],
|
||||
}
|
||||
|
||||
return this.messageManager.updateLists(lists)
|
||||
}
|
||||
|
||||
public updateOverlayStyle(style: Partial<CSSStyleDeclaration>) {
|
||||
this.screen.updateOverlayStyle(style)
|
||||
}
|
||||
|
||||
injectPlayer = (player: HTMLElement) => {
|
||||
this.screen.addToBody(player)
|
||||
this.screen.addMobileStyles()
|
||||
window.addEventListener('resize', () =>
|
||||
this.customScale(this.customConstrains.width, this.customConstrains.height)
|
||||
)
|
||||
}
|
||||
|
||||
scale = () => {
|
||||
// const { width, height } = this.wpState.get()
|
||||
if (!this.screen) return;
|
||||
console.debug("using customConstrains to scale player")
|
||||
// sometimes happens in live assist sessions for some reason
|
||||
this.screen?.scale?.(this.customConstrains)
|
||||
}
|
||||
|
||||
customConstrains = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
customScale = (width: number, height: number) => {
|
||||
if (!this.screen) return;
|
||||
this.screen?.scale?.({ width, height })
|
||||
this.customConstrains = { width, height }
|
||||
this.wpState.update({ scale: this.screen.getScale() })
|
||||
}
|
||||
|
||||
addFullscreenBoundary = (isFullscreen?: boolean) => {
|
||||
if (isFullscreen) {
|
||||
this.screen?.addFullscreenBoundary()
|
||||
} else {
|
||||
this.screen?.addMobileStyles()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clean = () => {
|
||||
super.clean()
|
||||
this.screen.clean()
|
||||
// @ts-ignore
|
||||
this.screen = undefined;
|
||||
this.messageLoader.clean()
|
||||
// @ts-ignore
|
||||
this.messageManager = undefined;
|
||||
window.removeEventListener('resize', this.scale)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import ListWalker from "Player/common/ListWalker";
|
||||
import type { IosPerformanceEvent } from "Player/web/messages";
|
||||
|
||||
const performanceEvTypes = {
|
||||
MemoryUsage: 'memoryUsage',
|
||||
MainThreadCPU: 'mainThreadCPU',
|
||||
Background: 'background',
|
||||
}
|
||||
|
||||
export type PerformanceChartPoint = {
|
||||
time: number,
|
||||
cpu: number | null,
|
||||
memory: number | null,
|
||||
isBackground: boolean,
|
||||
}
|
||||
|
||||
export default class IOSPerformanceTrackManager extends ListWalker<IosPerformanceEvent> {
|
||||
private chart: Array<PerformanceChartPoint> = [];
|
||||
private isInBg = false;
|
||||
|
||||
lastData: { cpu: number | null, memory: number | null } = { cpu: null, memory: null };
|
||||
append(msg: IosPerformanceEvent): void {
|
||||
if (!Object.values(performanceEvTypes).includes(msg.name)) {
|
||||
return console.log('Unsupported performance event type', msg.name)
|
||||
}
|
||||
|
||||
let cpu: number | null = null;
|
||||
let memory: number | null = null;
|
||||
if (msg.time < 0) msg.time = 1;
|
||||
if (msg.name === performanceEvTypes.Background) {
|
||||
// @ts-ignore
|
||||
const isBackground = msg.value === 1;
|
||||
if (isBackground === this.isInBg) return;
|
||||
this.isInBg = isBackground;
|
||||
this.chart.push({
|
||||
time: msg.time,
|
||||
cpu: null,
|
||||
memory: null,
|
||||
isBackground,
|
||||
})
|
||||
return super.append(msg);
|
||||
}
|
||||
if (msg.name === performanceEvTypes.MemoryUsage) {
|
||||
memory = msg.value;
|
||||
cpu = this.lastData.cpu;
|
||||
this.lastData.memory = memory;
|
||||
}
|
||||
if (msg.name === performanceEvTypes.MainThreadCPU) {
|
||||
cpu = msg.value;
|
||||
memory = this.lastData.memory;
|
||||
this.lastData.cpu = cpu;
|
||||
}
|
||||
|
||||
this.chart.push({
|
||||
time: msg.time,
|
||||
cpu,
|
||||
memory,
|
||||
isBackground: false,
|
||||
});
|
||||
super.append(msg);
|
||||
}
|
||||
|
||||
get chartData(): Array<PerformanceChartPoint> {
|
||||
return this.chart;
|
||||
}
|
||||
}
|
||||
49
frontend/app/player/mobile/managers/TouchManager.ts
Normal file
49
frontend/app/player/mobile/managers/TouchManager.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import {MOUSE_TRAIL} from "App/constants/storageKeys";
|
||||
import ListWalker from 'Player/common/ListWalker';
|
||||
import MouseTrail, { SwipeEvent } from 'Player/web/addons/MouseTrail';
|
||||
import type {IosClickEvent, IosSwipeEvent} from 'Player/web/messages';
|
||||
import {MType} from "Player/web/messages";
|
||||
import type Screen from 'Player/web/Screen/Screen';
|
||||
|
||||
export default class TouchManager extends ListWalker<IosClickEvent | IosSwipeEvent> {
|
||||
private touchTrail: MouseTrail | undefined;
|
||||
private readonly removeTouchTrail: boolean = false;
|
||||
|
||||
constructor(private screen: Screen) {
|
||||
super();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'openreplay-touch-trail';
|
||||
// canvas.className = styles.canvas;
|
||||
|
||||
this.removeTouchTrail = localStorage.getItem(MOUSE_TRAIL) === 'false'
|
||||
if (!this.removeTouchTrail) {
|
||||
this.touchTrail = new MouseTrail(canvas, true)
|
||||
}
|
||||
|
||||
this.screen.overlay.appendChild(canvas);
|
||||
this.touchTrail?.createContext();
|
||||
}
|
||||
|
||||
public updateDimensions({ width, height }: { width: number; height: number; }) {
|
||||
return this.touchTrail?.resizeCanvas(width, height);
|
||||
}
|
||||
|
||||
public move(t: number) {
|
||||
const lastTouch = this.moveGetLast(t)
|
||||
if (!!lastTouch) {
|
||||
if (lastTouch.tp === MType.IosSwipeEvent) {
|
||||
return
|
||||
// not using swipe rn
|
||||
// this.touchTrail?.createSwipeTrail({
|
||||
// x: lastTouch.x,
|
||||
// y: lastTouch.y,
|
||||
// direction: lastTouch.direction
|
||||
// } as SwipeEvent)
|
||||
} else {
|
||||
this.screen.cursor.move(lastTouch)
|
||||
this.screen.cursor.mobileClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
49
frontend/app/player/mobile/types/log.ts
Normal file
49
frontend/app/player/mobile/types/log.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export const enum LogLevel {
|
||||
INFO = 'info',
|
||||
LOG = 'log',
|
||||
//ASSERT = 'assert', //?
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
EXCEPTION = 'exception',
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
content: string;
|
||||
severity: "info" | "log" | "warn" | "error" | "exception";
|
||||
time: number;
|
||||
timestamp: number;
|
||||
tp: number;
|
||||
_index: number;
|
||||
}
|
||||
|
||||
export const Log = (log: ILog) => ({
|
||||
isRed: log.severity === LogLevel.EXCEPTION || log.severity === LogLevel.ERROR,
|
||||
isYellow: log.severity === LogLevel.WARN,
|
||||
value: log.content,
|
||||
...log
|
||||
})
|
||||
|
||||
// content
|
||||
// :
|
||||
// ">>>POST:https://foss.openreplay.com/ingest/v1/mobile/i\n<<<\n"
|
||||
// length
|
||||
// :
|
||||
// 65
|
||||
// severity
|
||||
// :
|
||||
// "info"
|
||||
// tabId
|
||||
// :
|
||||
// "back-compatability"
|
||||
// time
|
||||
// :
|
||||
// 10048
|
||||
// timestamp
|
||||
// :
|
||||
// 1692966743780
|
||||
// tp
|
||||
// :
|
||||
// 103
|
||||
// _index
|
||||
// :
|
||||
// 50
|
||||
103
frontend/app/player/mobile/utils.ts
Normal file
103
frontend/app/player/mobile/utils.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
|
||||
const iPhone12ProSvg2 = `<svg xmlns="http://www.w3.org/2000/svg" width="432" height="881" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" xmlns:v="https://vecta.io/nano"><rect x="12.188" y="9.53" width="408" height="862" rx="59" stroke="#000" stroke-width="18"/><rect x="12.688" y="10.03" width="407" height="861" rx="58.5" stroke="#b6b6b6" stroke-width="17"/><rect x="13.688" y="11.03" width="405" height="859" rx="57.5" stroke="#000" stroke-width="15"/><path d="M427.284 265.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V265.897z" fill="url(#A)"/><path d="M.784 330.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z" fill="url(#B)"/><path d="M.784 245.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z" fill="url(#C)"/><path d="M.784 182.332a2 2 0 0 1 2-2h2.5v32h-2.5a2 2 0 0 1-2-2v-28z" fill="url(#D)"/><path d="M124.735 17.949V5.174h189.803v12.775h-8.775a4 4 0 0 0-4 4v7.215c0 12.15-9.85 22-22 22H161.335c-12.15 0-22-9.85-22-22v-7.215a4 4 0 0 0-4-4h-10.6z" fill="#000"/><circle opacity=".34" cx="162.528" cy="29.219" r="5.62" fill="url(#E)"/><circle opacity=".34" cx="164.865" cy="31.417" r="1.283" fill="url(#F)"/><path opacity=".2" fill-rule="evenodd" d="M167.16 29.219a4.49 4.49 0 0 0 .02-.436c0-2.569-2.083-4.652-4.652-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.216 4.632-4.216s4.412 1.851 4.632 4.216z" fill="#fff"/><defs><linearGradient id="A" x1="429.534" y1="265.897" x2="429.534" y2="368.522" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="B" x1="3.034" y1="328.332" x2="3.034" y2="392.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="C" x1="3.034" y1="243.332" x2="3.034" y2="307.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="D" x1="3.034" y1="180.332" x2="3.034" y2="212.332" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="E" x1="165.111" y1="28.357" x2="156.908" y2="28.493" xlink:href="#G"><stop stop-color="#2983ab"/><stop offset="1" stop-color="#1948ab"/></linearGradient><linearGradient id="F" x1="163.143" y1="30.133" x2="166.148" y2="32.7" xlink:href="#G"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="G" gradientUnits="userSpaceOnUse"/></defs></svg>`
|
||||
const iPhone12ProMaxSvg2 = `<svg xmlns="http://www.w3.org/2000/svg" width="470" height="963" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" xmlns:v="https://vecta.io/nano"><rect x="12.188" y="9.53" width="446" height="944" rx="59" stroke="#000" stroke-width="18"/><rect x="12.688" y="10.03" width="445" height="943" rx="58.5" stroke="#b6b6b6" stroke-width="17"/><rect x="13.688" y="11.03" width="443" height="941" rx="57.5" stroke="#000" stroke-width="15"/><path d="M465.142 283.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V283.897z" fill="url(#A)"/><path d="M.642 348.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z" fill="url(#B)"/><path d="M.642 263.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z" fill="url(#C)"/><path d="M.642 200.332a2 2 0 0 1 2-2h2.5v32h-2.5a2 2 0 0 1-2-2v-28z" fill="url(#D)"/><path d="M141.741 18.949V6.174h189.802v12.775h-8.775a4 4 0 0 0-4 4v7.215c0 12.15-9.85 22-22 22H178.341c-12.15 0-22-9.85-22-22v-7.215a4 4 0 0 0-4-4h-10.6z" fill="#000"/><circle opacity=".34" cx="179.533" cy="30.219" r="5.62" fill="url(#E)"/><circle opacity=".34" cx="181.87" cy="32.417" r="1.283" fill="url(#F)"/><path opacity=".2" fill-rule="evenodd" d="M184.166 30.219l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.216 4.632-4.216s4.413 1.851 4.633 4.216z" fill="#fff"/><defs><linearGradient id="A" x1="467.392" y1="283.897" x2="467.392" y2="386.522" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="B" x1="2.892" y1="346.332" x2="2.892" y2="410.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="C" x1="2.892" y1="261.332" x2="2.892" y2="325.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="D" x1="2.892" y1="198.332" x2="2.892" y2="230.332" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="E" x1="182.117" y1="29.357" x2="173.914" y2="29.493" xlink:href="#G"><stop stop-color="#2983ab"/><stop offset="1" stop-color="#1948ab"/></linearGradient><linearGradient id="F" x1="180.148" y1="31.133" x2="183.153" y2="33.7" xlink:href="#G"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="G" gradientUnits="userSpaceOnUse"/></defs></svg>`
|
||||
const iphone14ProSvg2 = `<svg xmlns="http://www.w3.org/2000/svg" width="440" height="895" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" xmlns:v="https://vecta.io/nano"><rect x="12.904" y="11.472" width="414" height="873" rx="62.5" stroke="#000" stroke-width="21"/><rect x="13.404" y="11.972" width="413" height="872" rx="62" stroke="#b6b6b6" stroke-width="20"/><path d="M364.404 13.472h-289c-33.413 0-60.5 27.087-60.5 60.5v748c0 33.413 27.087 60.5 60.5 60.5h289c33.413 0 60.5-27.087 60.5-60.5v-748c0-33.413-27.087-60.5-60.5-60.5z" stroke="#000" stroke-width="17"/><path d="M435.284 265.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V265.897z" fill="url(#A)"/><path d="M0 330.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z" fill="url(#B)"/><path d="M0 245.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z" fill="url(#C)"/><path d="M0 182.332a2 2 0 0 1 2-2h2.5v32H2a2 2 0 0 1-2-2v-28z" fill="url(#D)"/><rect x="156.5" y="33.483" width="123" height="36" rx="18" fill="#000"/><circle opacity=".34" cx="176.033" cy="51.483" r="5.62" fill="url(#E)"/><circle opacity=".34" cx="178.37" cy="53.681" r="1.283" fill="url(#F)"/><path opacity=".2" fill-rule="evenodd" d="M180.666 51.483l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.217 4.632-4.217s4.413 1.851 4.633 4.216z" fill="#fff"/><defs><linearGradient id="A" x1="437.534" y1="265.897" x2="437.534" y2="368.522" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="B" x1="2.25" y1="328.332" x2="2.25" y2="392.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="C" x1="2.25" y1="243.332" x2="2.25" y2="307.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="D" x1="2.25" y1="180.332" x2="2.25" y2="212.332" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="E" x1="178.617" y1="50.621" x2="170.414" y2="50.757" xlink:href="#G"><stop stop-color="#2983ab"/><stop offset="1" stop-color="#1948ab"/></linearGradient><linearGradient id="F" x1="176.648" y1="52.397" x2="179.653" y2="54.964" xlink:href="#G"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="G" gradientUnits="userSpaceOnUse"/></defs></svg>`
|
||||
const iphone14ProMaxSvg2 = `<svg xmlns="http://www.w3.org/2000/svg" width="477" height="975" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" xmlns:v="https://vecta.io/nano"><rect x="12.904" y="11.472" width="451" height="953" rx="62.5" stroke="#000" stroke-width="21"/><rect x="13.404" y="11.972" width="450" height="952" rx="62" stroke="#b6b6b6" stroke-width="20"/><rect x="14.904" y="13.472" width="447" height="949" rx="60.5" stroke="#000" stroke-width="17"/><rect x="174.5" y="33.483" width="128" height="36" rx="18" fill="#000"/><circle opacity=".34" cx="194.033" cy="51.483" r="5.62" fill="url(#A)"/><circle opacity=".34" cx="196.37" cy="53.681" r="1.283" fill="url(#B)"/><path opacity=".2" fill-rule="evenodd" d="M198.666 51.483l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.217 4.632-4.217s4.413 1.851 4.633 4.216z" fill="#fff"/><path d="M472.284 265.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V265.897z" fill="url(#C)"/><path d="M0 330.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z" fill="url(#D)"/><path d="M0 245.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z" fill="url(#E)"/><path d="M0 182.332a2 2 0 0 1 2-2h2.5v32H2a2 2 0 0 1-2-2v-28z" fill="url(#F)"/><defs><linearGradient id="A" x1="196.617" y1="50.621" x2="188.414" y2="50.757" xlink:href="#G"><stop stop-color="#2983ab"/><stop offset="1" stop-color="#1948ab"/></linearGradient><linearGradient id="B" x1="194.648" y1="52.397" x2="197.653" y2="54.964" xlink:href="#G"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="C" x1="474.534" y1="265.897" x2="474.534" y2="368.522" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="D" x1="2.25" y1="328.332" x2="2.25" y2="392.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="E" x1="2.25" y1="243.332" x2="2.25" y2="307.328" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="F" x1="2.25" y1="180.332" x2="2.25" y2="212.332" xlink:href="#G"><stop stop-color="#575757"/><stop offset="1" stop-color="#222"/></linearGradient><linearGradient id="G" gradientUnits="userSpaceOnUse"/></defs></svg>`
|
||||
// old svg frames
|
||||
// const iPhone12ProSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"438\" height=\"883\" fill=\"none\" xmlns:v=\"https://vecta.io/nano\"><path fill-rule=\"evenodd\" d=\"M70.284.332h298c35.898 0 65 29.102 65 65v752c0 35.898-29.102 65-65 65h-298c-35.899 0-65-29.102-65-65v-752c0-35.898 29.102-65 65-65zm0 19c-25.405 0-46 20.595-46 46v752c0 25.405 20.595 46 46 46h298c25.405 0 46-20.595 46-46v-752c0-25.405-20.595-46-46-46h-298z\" fill=\"#000\"/><path d=\"M433.284 262.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V262.897zM.784 327.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996zm0-85a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z\" fill=\"#222\"/><path d=\"M.784 179.332a2 2 0 0 1 2-2h2.5v32h-2.5a2 2 0 0 1-2-2v-28z\" fill=\"#444\"/><path d=\"M123.735 18.949V6.174h189.803v12.775h-8.775a4 4 0 0 0-4 4v7.215c0 12.15-9.85 22-22 22H160.335c-12.15 0-22-9.85-22-22v-7.215a4 4 0 0 0-4-4h-10.6z\" fill=\"#000\"/><circle opacity=\".34\" cx=\"161.528\" cy=\"30.219\" r=\"5.62\" fill=\"url(#A)\"/><circle opacity=\".34\" cx=\"163.865\" cy=\"32.417\" r=\"1.283\" fill=\"url(#B)\"/><path opacity=\".2\" fill-rule=\"evenodd\" d=\"M166.16 30.219a4.49 4.49 0 0 0 .02-.436c0-2.569-2.083-4.652-4.652-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.216 4.632-4.216s4.412 1.851 4.632 4.216z\" fill=\"#fff\"/><defs><linearGradient id=\"A\" x1=\"164.111\" y1=\"29.357\" x2=\"155.908\" y2=\"29.493\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#2983ab\"/><stop offset=\"1\" stop-color=\"#1948ab\"/></linearGradient><linearGradient id=\"B\" x1=\"162.143\" y1=\"31.133\" x2=\"165.148\" y2=\"33.7\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#fff\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#fff\"/></linearGradient></defs></svg>"
|
||||
// const iPhone12ProMaxSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"476\" height=\"965\" fill=\"none\" xmlns:v=\"https://vecta.io/nano\"><path fill-rule=\"evenodd\" d=\"M69.495.332h336.001c35.898 0 65 29.102 65 65v834c0 35.898-29.102 65-65 65h-336c-35.899 0-65-29.102-65-65v-834c0-35.898 29.101-65 65-65zm0 19c-25.405 0-46 20.595-46 46v834c0 25.405 20.595 46 46 46h336.001c25.405 0 46-20.595 46-46v-834c0-25.405-20.595-46-46-46h-336z\" fill=\"#000\"/><path d=\"M470.142 281.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V281.897zM.642 346.332a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996zm0-85a2 2 0 0 1 2-2h2.5v63.996h-2.5a2 2 0 0 1-2-2v-59.996z\" fill=\"#222\"/><path d=\"M.642 198.332a2 2 0 0 1 2-2h2.5v32h-2.5a2 2 0 0 1-2-2v-28z\" fill=\"#444\"/><path d=\"M142.741 18.949V6.174h189.802v12.775h-8.775a4 4 0 0 0-4 4v7.215c0 12.15-9.85 22-22 22H179.341c-12.15 0-22-9.85-22-22v-7.215a4 4 0 0 0-4-4h-10.6z\" fill=\"#000\"/><circle opacity=\".34\" cx=\"180.533\" cy=\"30.219\" r=\"5.62\" fill=\"url(#A)\"/><circle opacity=\".34\" cx=\"182.87\" cy=\"32.417\" r=\"1.283\" fill=\"url(#B)\"/><path opacity=\".2\" fill-rule=\"evenodd\" d=\"M185.166 30.219l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.216 4.632-4.216s4.413 1.851 4.633 4.216z\" fill=\"#fff\"/><defs><linearGradient id=\"A\" x1=\"183.117\" y1=\"29.357\" x2=\"174.914\" y2=\"29.493\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#2983ab\"/><stop offset=\"1\" stop-color=\"#1948ab\"/></linearGradient><linearGradient id=\"B\" x1=\"181.148\" y1=\"31.133\" x2=\"184.153\" y2=\"33.7\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#fff\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#fff\"/></linearGradient></defs></svg>"
|
||||
// const iphone14ProSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"436\" height=\"891\" fill=\"none\" xmlns:v=\"https://vecta.io/nano\"><rect x=\"11.904\" y=\"10.472\" width=\"412\" height=\"871\" rx=\"61.5\" stroke=\"#b6b6b6\" stroke-width=\"19\"/><rect x=\"12.904\" y=\"11.472\" width=\"410\" height=\"869\" rx=\"60.5\" stroke=\"#000\" stroke-width=\"17\"/><rect x=\"154.5\" y=\"31.483\" width=\"123\" height=\"36\" rx=\"18\" fill=\"#000\"/><circle opacity=\".34\" cx=\"174.033\" cy=\"49.483\" r=\"5.62\" fill=\"url(#A)\"/><circle opacity=\".34\" cx=\"176.37\" cy=\"51.681\" r=\"1.283\" fill=\"url(#B)\"/><path opacity=\".2\" fill-rule=\"evenodd\" d=\"M178.666 49.483l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.217 4.632-4.217s4.413 1.851 4.633 4.216z\" fill=\"#fff\"/><path d=\"M431.284 263.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V263.897zM0 328.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996zm0-85a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z\" fill=\"#222\"/><path d=\"M0 180.332a2 2 0 0 1 2-2h2.5v32H2a2 2 0 0 1-2-2v-28z\" fill=\"#444\"/><defs><linearGradient id=\"A\" x1=\"176.617\" y1=\"48.621\" x2=\"168.414\" y2=\"48.757\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#2983ab\"/><stop offset=\"1\" stop-color=\"#1948ab\"/></linearGradient><linearGradient id=\"B\" x1=\"174.648\" y1=\"50.397\" x2=\"177.653\" y2=\"52.964\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#fff\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#fff\"/></linearGradient></defs></svg>"
|
||||
// const iphone14ProMaxSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"473\" height=\"971\" fill=\"none\" xmlns:v=\"https://vecta.io/nano\"><rect x=\"11.904\" y=\"10.472\" width=\"449\" height=\"951\" rx=\"61.5\" stroke=\"#b6b6b6\" stroke-width=\"19\"/><rect x=\"12.904\" y=\"11.472\" width=\"447\" height=\"949\" rx=\"60.5\" stroke=\"#000\" stroke-width=\"17\"/><rect x=\"172.5\" y=\"31.483\" width=\"128\" height=\"36\" rx=\"18\" fill=\"#000\"/><circle opacity=\".34\" cx=\"192.033\" cy=\"49.483\" r=\"5.62\" fill=\"url(#A)\"/><circle opacity=\".34\" cx=\"194.37\" cy=\"51.681\" r=\"1.283\" fill=\"url(#B)\"/><path opacity=\".2\" fill-rule=\"evenodd\" d=\"M196.666 49.483l.02-.436c0-2.569-2.083-4.652-4.653-4.652s-4.652 2.083-4.652 4.652l.02.436c.22-2.365 2.21-4.217 4.632-4.217s4.413 1.851 4.633 4.216z\" fill=\"#fff\"/><path d=\"M468.284 263.897h2.5a2 2 0 0 1 2 2v98.625a2 2 0 0 1-2 2h-2.5V263.897zM0 328.332a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996zm0-85a2 2 0 0 1 2-2h2.5v63.996H2a2 2 0 0 1-2-2v-59.996z\" fill=\"#222\"/><path d=\"M0 180.332a2 2 0 0 1 2-2h2.5v32H2a2 2 0 0 1-2-2v-28z\" fill=\"#444\"/><defs><linearGradient id=\"A\" x1=\"194.617\" y1=\"48.621\" x2=\"186.414\" y2=\"48.757\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#2983ab\"/><stop offset=\"1\" stop-color=\"#1948ab\"/></linearGradient><linearGradient id=\"B\" x1=\"192.648\" y1=\"50.397\" x2=\"195.653\" y2=\"52.964\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#fff\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#fff\"/></linearGradient></defs></svg>"
|
||||
|
||||
const screenResolutions = {
|
||||
iPhone12Pro: {
|
||||
margin: '18px 0 0 21px',
|
||||
screen: {
|
||||
width: 391,
|
||||
height: 845,
|
||||
},
|
||||
shell: {
|
||||
width: 438,
|
||||
height: 883,
|
||||
},
|
||||
},
|
||||
iPhone12ProMax: {
|
||||
margin: '18px 0 0 21px',
|
||||
screen: {
|
||||
width: 429,
|
||||
height: 927,
|
||||
},
|
||||
shell: {
|
||||
width: 476,
|
||||
height: 965,
|
||||
},
|
||||
},
|
||||
iPhone14Pro: {
|
||||
margin: '21px 0 0 23px',
|
||||
screen: {
|
||||
width: 394,
|
||||
height: 853,
|
||||
},
|
||||
shell: {
|
||||
width: 436,
|
||||
height: 891,
|
||||
},
|
||||
},
|
||||
iPhone14ProMax: {
|
||||
margin: '21px 0 0 23px',
|
||||
screen: {
|
||||
width: 431,
|
||||
height: 933,
|
||||
},
|
||||
shell: {
|
||||
width: 473,
|
||||
height: 971,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param modelName - device.safeDescription from DeviceKit
|
||||
* @returns - returns a phone shell svg and styles for inner screen dimensions
|
||||
* plus margins from shell
|
||||
* _______
|
||||
* currently mapped: 12, 12pro, map, 13, 13pro, max, 14, 14 pro, 14 max/plus
|
||||
*
|
||||
* everything else is considered as 12 pro
|
||||
* */
|
||||
export function mapIphoneModel(modelName: string) {
|
||||
|
||||
const iPhone12Pro = [
|
||||
"iPhone 12",
|
||||
"iPhone 12 Pro",
|
||||
"iPhone 13",
|
||||
"iPhone 13 Pro",
|
||||
"iPhone 14"
|
||||
];
|
||||
|
||||
const iPhone12ProMax = [
|
||||
"iPhone 12 Pro Max",
|
||||
"iPhone 13 Pro Max",
|
||||
"iPhone 14 Plus"
|
||||
];
|
||||
|
||||
const iPhone14Pro = [
|
||||
"iPhone 14 Pro"
|
||||
];
|
||||
|
||||
const iPhone14ProMax = [
|
||||
"iPhone 14 Pro Max"
|
||||
];
|
||||
|
||||
if (iPhone12Pro.includes(modelName)) {
|
||||
return { svg: iPhone12ProSvg2, styles: screenResolutions.iPhone12Pro } as const;
|
||||
} else if (iPhone12ProMax.includes(modelName)) {
|
||||
return { svg: iPhone12ProMaxSvg2, styles: screenResolutions.iPhone12ProMax } as const;
|
||||
} else if (iPhone14Pro.includes(modelName)) {
|
||||
return { svg: iphone14ProSvg2, styles: screenResolutions.iPhone14Pro } as const;
|
||||
} else if (iPhone14ProMax.includes(modelName)) {
|
||||
return { svg: iphone14ProMaxSvg2, styles: screenResolutions.iPhone14ProMax } as const;
|
||||
} else {
|
||||
return { svg: iPhone12ProSvg2, styles: screenResolutions.iPhone12Pro} as const; // Default fallback
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Store, Interval } from '../common/types';
|
||||
import MessageManager from 'App/player/web/MessageManager'
|
||||
import {Message} from "Player/web/messages";
|
||||
import type { Store, Interval } from 'Player';
|
||||
|
||||
|
||||
const fps = 60
|
||||
|
|
@ -21,6 +21,18 @@ const cancelAnimationFrame =
|
|||
window.mozCancelAnimationFrame ||
|
||||
window.clearTimeout
|
||||
|
||||
export interface IMessageManager {
|
||||
onFileReadSuccess(): void;
|
||||
onFileReadFailed(e: any): void;
|
||||
onFileReadFinally(): void;
|
||||
startLoading(): void;
|
||||
resetMessageManagers(): void;
|
||||
move(t: number): any;
|
||||
distributeMessage(msg: Message): void;
|
||||
setMessagesLoading(messagesLoading: boolean): void;
|
||||
clean(): void;
|
||||
_sortMessagesHack: (msgs: Message[]) => void;
|
||||
}
|
||||
|
||||
export interface SetState {
|
||||
time: number
|
||||
|
|
@ -56,7 +68,7 @@ export default class Animator {
|
|||
|
||||
private animationFrameRequestId: number = 0
|
||||
|
||||
constructor(private store: Store<GetState>, private mm: MessageManager) {
|
||||
constructor(private store: Store<GetState>, private mm: IMessageManager) {
|
||||
|
||||
// @ts-ignore
|
||||
window.playerJump = this.jump.bind(this)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import * as typedLocalStorage from './localStorage';
|
||||
|
||||
import type { Store } from '../common/types';
|
||||
import Animator from './Animator';
|
||||
import Animator, { IMessageManager } from './Animator';
|
||||
import type { GetState as AnimatorGetState } from './Animator';
|
||||
import MessageManager from "Player/web/MessageManager";
|
||||
export const SPEED_OPTIONS = [0.5, 1, 2, 4, 8, 16]
|
||||
|
||||
import type { Message } from "Player/web/messages";
|
||||
|
||||
/* == separate this == */
|
||||
const HIGHEST_SPEED = 16
|
||||
|
|
@ -35,7 +34,7 @@ export default class Player extends Animator {
|
|||
speed: initialSpeed,
|
||||
} as const
|
||||
|
||||
constructor(private pState: Store<State & AnimatorGetState>, private manager: MessageManager) {
|
||||
constructor(private pState: Store<State & AnimatorGetState>, private manager: IMessageManager) {
|
||||
super(pState, manager)
|
||||
|
||||
// Autoplay
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Store, SessionFilesInfo } from 'Player';
|
||||
import {IMessageManager} from "Player/player/Animator";
|
||||
import { decryptSessionBytes } from './network/crypto';
|
||||
import MFileReader from './messages/MFileReader';
|
||||
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
|
||||
|
|
@ -6,7 +7,6 @@ import type {
|
|||
Message,
|
||||
} from './messages';
|
||||
import logger from 'App/logger';
|
||||
import MessageManager from "Player/web/MessageManager";
|
||||
|
||||
|
||||
interface State {
|
||||
|
|
@ -27,7 +27,7 @@ export default class MessageLoader {
|
|||
constructor(
|
||||
private readonly session: SessionFilesInfo,
|
||||
private store: Store<State>,
|
||||
private messageManager: MessageManager,
|
||||
private messageManager: IMessageManager,
|
||||
private isClickmap: boolean,
|
||||
private uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default class Cursor {
|
|||
}
|
||||
|
||||
click() {
|
||||
const styleList = this.isMobile ? styles.clickedMobile : styles.clicked
|
||||
const styleList = styles.clicked
|
||||
this.cursor.classList.add(styleList)
|
||||
this.onClick?.()
|
||||
setTimeout(() => {
|
||||
|
|
@ -82,6 +82,22 @@ export default class Cursor {
|
|||
}, 600)
|
||||
}
|
||||
|
||||
clickTimeout?: NodeJS.Timeout
|
||||
mobileClick() {
|
||||
const styleList = styles.mobileTouch
|
||||
if (this.clickTimeout) {
|
||||
clearTimeout(this.clickTimeout)
|
||||
this.cursor.classList.remove(styleList)
|
||||
this.clickTimeout = undefined
|
||||
}
|
||||
this.cursor.classList.add(styleList)
|
||||
this.onClick?.()
|
||||
this.clickTimeout = setTimeout(() => {
|
||||
this.cursor.classList.remove(styleList)
|
||||
this.clickTimeout = undefined
|
||||
}, 600)
|
||||
}
|
||||
|
||||
setOnClickHook(callback: () => void) {
|
||||
this.onClick = callback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,15 @@ export default class Screen {
|
|||
this.cursor = new Cursor(this.overlay, isMobile) // TODO: move outside
|
||||
}
|
||||
|
||||
addMobileStyles() {
|
||||
this.iframe.className = styles.mobileIframe
|
||||
this.screen.className = styles.mobileScreen
|
||||
}
|
||||
|
||||
addFullscreenBoundary() {
|
||||
this.screen.className = styles.mobileScreenFullview
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.iframe?.remove?.();
|
||||
this.overlay?.remove?.();
|
||||
|
|
@ -100,6 +109,13 @@ export default class Screen {
|
|||
this.parentElement = parentElement;
|
||||
}
|
||||
|
||||
addToBody(el: HTMLElement) {
|
||||
if (this.document) {
|
||||
this.document.body.style.margin = '0';
|
||||
this.document.body.appendChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
getParentElement(): HTMLElement | null {
|
||||
return this.parentElement
|
||||
}
|
||||
|
|
@ -236,7 +252,7 @@ export default class Screen {
|
|||
})
|
||||
|
||||
this.boundingRect = this.screen.getBoundingClientRect();
|
||||
this.onUpdateHook(width, height)
|
||||
this.onUpdateHook?.(width, height)
|
||||
}
|
||||
|
||||
setOnUpdate(cb: any) {
|
||||
|
|
@ -255,6 +271,10 @@ export default class Screen {
|
|||
}, 750)
|
||||
}
|
||||
|
||||
public updateOverlayStyle(style: Partial<CSSStyleDeclaration>) {
|
||||
Object.assign(this.overlay.style, style)
|
||||
}
|
||||
|
||||
public clearSelection() {
|
||||
if (this.selectionTargets.start && this.selectionTargets.end) {
|
||||
this.overlay.removeChild(this.selectionTargets.start);
|
||||
|
|
|
|||
|
|
@ -68,9 +68,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cursor.clickedMobile::after {
|
||||
-webkit-animation: anim-effect-sanja 1s ease-out forwards;
|
||||
animation: anim-effect-sanja 1s ease-out forwards;
|
||||
.mobileTouch {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 42 42'%3E%3Cstyle%3E @keyframes x2menhl6cupbu94auppeu01e_t %7B 0%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); %7D 62.5%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 75%25 %7B transform: translate(23px, 23px) scale(0.9, 0.9) translate(-2px, -2px); animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 100%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); %7D %7D @keyframes x2menhl6cupbu94auppeu01e_sw %7B 0%25 %7B stroke-width: 2px; %7D 62.5%25 %7B stroke-width: 2px; animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 75%25 %7B stroke-width: 5px; animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 100%25 %7B stroke-width: 2px; %7D %7D %3C/style%3E%3Cellipse rx='20' ry='20' fill='rgba(0,0,0,0.50)' stroke='%23fff' stroke-width='2' transform='translate(23,23) translate(-2,-2)' style='animation: 0.6s infinite linear both x2menhl6cupbu94auppeu01e_t, 0.6s infinite linear both x2menhl6cupbu94auppeu01e_sw;' /%3E%3C/svg%3E")!important;
|
||||
}
|
||||
|
||||
@keyframes touch-animation {
|
||||
0% {
|
||||
transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px);
|
||||
}
|
||||
31% {
|
||||
transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px);
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.6, 1);
|
||||
}
|
||||
47.5% {
|
||||
transform: translate(23px, 23px) scale(0.9, 0.9) translate(-2px, -2px);
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.6, 1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px);
|
||||
}
|
||||
51% {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
62.5% {
|
||||
stroke-width: 2px;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.6, 1);
|
||||
}
|
||||
75% {
|
||||
stroke-width: 5px;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.6, 1);
|
||||
}
|
||||
100% {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes anim-effect-sanja {
|
||||
|
|
|
|||
|
|
@ -25,3 +25,32 @@
|
|||
opacity: 0;
|
||||
transition: all 0.25s cubic-bezier(0, 0, 0.4, 1.0);
|
||||
}
|
||||
|
||||
.mobileScreen {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
transform-origin: left top;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: rgb(223, 223, 223);
|
||||
border-radius: 70px;
|
||||
box-shadow: 0 0 30px 0 rgba(0,0,0,0.2); /* 0 0 70px 30px rgba(0,0,0,0.1); */
|
||||
margin-top: -20px;
|
||||
}
|
||||
.mobileScreenFullview {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
transform-origin: left top;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: rgb(223, 223, 223);
|
||||
border-radius: 70px;
|
||||
box-shadow: 0 0 30px 0 rgba(0,0,0,0.2);
|
||||
margin-top: 0!important;
|
||||
}
|
||||
|
||||
.mobileIframe {
|
||||
position: absolute;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
const LINE_DURATION = 3.5;
|
||||
const LINE_WIDTH_START = 5;
|
||||
|
||||
export type SwipeEvent = { x: number; y: number; direction: 'up' | 'down' | 'left' | 'right' }
|
||||
|
||||
export default class MouseTrail {
|
||||
public isActive = true;
|
||||
public context: CanvasRenderingContext2D;
|
||||
|
|
@ -13,13 +15,15 @@ export default class MouseTrail {
|
|||
* 1 - every frame,
|
||||
* 2 - every 2nd frame
|
||||
* and so on, doesn't always work properly
|
||||
* but 1 doesnt affect performance so we fine
|
||||
* but 1 doesnt affect performance so we're fine
|
||||
* */
|
||||
private drawOnFrame = 1;
|
||||
private currentFrame = 0;
|
||||
private lineDuration = LINE_DURATION;
|
||||
private points: Point[] = [];
|
||||
private swipePoints: Point[] = []
|
||||
|
||||
constructor(private readonly canvas: HTMLCanvasElement) {
|
||||
constructor(private readonly canvas: HTMLCanvasElement, isNativeMobile: boolean = false) {
|
||||
// @ts-ignore patching window
|
||||
window.requestAnimFrame =
|
||||
window.requestAnimationFrame ||
|
||||
|
|
@ -34,6 +38,10 @@ export default class MouseTrail {
|
|||
function (callback: any) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
};
|
||||
|
||||
if (isNativeMobile) {
|
||||
this.lineDuration = 5
|
||||
}
|
||||
}
|
||||
|
||||
resizeCanvas = (w: number, h: number) => {
|
||||
|
|
@ -73,10 +81,31 @@ export default class MouseTrail {
|
|||
}
|
||||
};
|
||||
|
||||
createSwipeTrail = ({ x, y, direction }: SwipeEvent) => {
|
||||
const startCoords = this.calculateTrail({ x, y, direction });
|
||||
this.addPoint(startCoords.x, startCoords.y);
|
||||
this.addPoint(x, y);
|
||||
}
|
||||
|
||||
calculateTrail = ({ x, y, direction }: SwipeEvent) => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return { x, y: y - 20 };
|
||||
case 'down':
|
||||
return { x, y: y + 20 };
|
||||
case 'left':
|
||||
return { x: x - 20, y };
|
||||
case 'right':
|
||||
return { x: x + 20, y };
|
||||
default:
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
animatePoints = () => {
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
|
||||
const duration = (LINE_DURATION * 1000) / 60;
|
||||
const duration = (this.lineDuration * 1000) / 60;
|
||||
let point, lastPoint;
|
||||
|
||||
for (let i = 0; i < this.points.length; i++) {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,9 @@ class SkipIntervalCls {
|
|||
|
||||
export type SkipInterval = InstanceType<typeof SkipIntervalCls>;
|
||||
|
||||
|
||||
export default class ActivityManager extends ListWalker<SkipInterval> {
|
||||
private endTime: number = 0;
|
||||
private minInterval: number = 0;
|
||||
private readonly endTime: number = 0;
|
||||
private readonly minInterval: number = 0;
|
||||
private lastActivity: number = 0;
|
||||
constructor(duration: number) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { MOUSE_TRAIL } from "App/constants/storageKeys";
|
|||
export default class MouseMoveManager extends ListWalker<MouseMove> {
|
||||
private hoverElements: Array<Element> = []
|
||||
private mouseTrail: MouseTrail | undefined
|
||||
private removeMouseTrail = false
|
||||
private readonly removeMouseTrail: boolean = false
|
||||
|
||||
constructor(private screen: Screen) {
|
||||
super()
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
|
||||
const index = this.noIndexes ? 0 : this.getLastMessageID()
|
||||
const msg = Object.assign(rewriteMessage(rMsg), {
|
||||
time: this.currentTime,
|
||||
time: this.currentTime || rMsg.timestamp - this.startTime!,
|
||||
tabId: this.currentTab,
|
||||
}, !this.noIndexes ? { _index: index } : {})
|
||||
|
||||
|
|
|
|||
|
|
@ -719,7 +719,7 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
const name = this.readString(); if (name === null) { return resetPointer() }
|
||||
const payload = this.readString(); if (payload === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.IosCustomEvent,
|
||||
tp: MType.IosEvent,
|
||||
timestamp,
|
||||
length,
|
||||
name,
|
||||
|
|
@ -805,6 +805,18 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 104: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const length = this.readUint(); if (length === null) { return resetPointer() }
|
||||
const content = this.readString(); if (content === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.IosInternalError,
|
||||
timestamp,
|
||||
length,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
case 105: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const length = this.readUint(); if (length === null) { return resetPointer() }
|
||||
|
|
@ -847,6 +859,22 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 111: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const type = this.readString(); if (type === null) { return resetPointer() }
|
||||
const contextString = this.readString(); if (contextString === null) { return resetPointer() }
|
||||
const context = this.readString(); if (context === null) { return resetPointer() }
|
||||
const payload = this.readString(); if (payload === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.IosIssueEvent,
|
||||
timestamp,
|
||||
type,
|
||||
contextString,
|
||||
context,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unrecognizable message type: ${ tp }; Pointer at the position ${this.p} of ${this.buf.length}`)
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import { MType } from './raw.gen'
|
||||
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118,93,96,100,101,102,103,105,106]
|
||||
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118]
|
||||
export function isDOMType(t: MType) {
|
||||
return DOM_TYPES.includes(t)
|
||||
}
|
||||
|
|
@ -61,14 +61,16 @@ import type {
|
|||
RawResourceTiming,
|
||||
RawTabChange,
|
||||
RawTabData,
|
||||
RawIosCustomEvent,
|
||||
RawIosEvent,
|
||||
RawIosScreenChanges,
|
||||
RawIosClickEvent,
|
||||
RawIosInputEvent,
|
||||
RawIosPerformanceEvent,
|
||||
RawIosLog,
|
||||
RawIosInternalError,
|
||||
RawIosNetworkCall,
|
||||
RawIosSwipeEvent,
|
||||
RawIosIssueEvent,
|
||||
} from './raw.gen'
|
||||
|
||||
export type Message = RawMessage & Timed
|
||||
|
|
@ -188,7 +190,7 @@ export type TabChange = RawTabChange & Timed
|
|||
|
||||
export type TabData = RawTabData & Timed
|
||||
|
||||
export type IosCustomEvent = RawIosCustomEvent & Timed
|
||||
export type IosEvent = RawIosEvent & Timed
|
||||
|
||||
export type IosScreenChanges = RawIosScreenChanges & Timed
|
||||
|
||||
|
|
@ -200,7 +202,11 @@ export type IosPerformanceEvent = RawIosPerformanceEvent & Timed
|
|||
|
||||
export type IosLog = RawIosLog & Timed
|
||||
|
||||
export type IosInternalError = RawIosInternalError & Timed
|
||||
|
||||
export type IosNetworkCall = RawIosNetworkCall & Timed
|
||||
|
||||
export type IosSwipeEvent = RawIosSwipeEvent & Timed
|
||||
|
||||
export type IosIssueEvent = RawIosIssueEvent & Timed
|
||||
|
||||
|
|
|
|||
|
|
@ -59,14 +59,16 @@ export const enum MType {
|
|||
ResourceTiming = 116,
|
||||
TabChange = 117,
|
||||
TabData = 118,
|
||||
IosCustomEvent = 93,
|
||||
IosEvent = 93,
|
||||
IosScreenChanges = 96,
|
||||
IosClickEvent = 100,
|
||||
IosInputEvent = 101,
|
||||
IosPerformanceEvent = 102,
|
||||
IosLog = 103,
|
||||
IosInternalError = 104,
|
||||
IosNetworkCall = 105,
|
||||
IosSwipeEvent = 106,
|
||||
IosIssueEvent = 111,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -474,8 +476,8 @@ export interface RawTabData {
|
|||
tabId: string,
|
||||
}
|
||||
|
||||
export interface RawIosCustomEvent {
|
||||
tp: MType.IosCustomEvent,
|
||||
export interface RawIosEvent {
|
||||
tp: MType.IosEvent,
|
||||
timestamp: number,
|
||||
length: number,
|
||||
name: string,
|
||||
|
|
@ -526,6 +528,13 @@ export interface RawIosLog {
|
|||
content: string,
|
||||
}
|
||||
|
||||
export interface RawIosInternalError {
|
||||
tp: MType.IosInternalError,
|
||||
timestamp: number,
|
||||
length: number,
|
||||
content: string,
|
||||
}
|
||||
|
||||
export interface RawIosNetworkCall {
|
||||
tp: MType.IosNetworkCall,
|
||||
timestamp: number,
|
||||
|
|
@ -549,5 +558,14 @@ export interface RawIosSwipeEvent {
|
|||
direction: string,
|
||||
}
|
||||
|
||||
export interface RawIosIssueEvent {
|
||||
tp: MType.IosIssueEvent,
|
||||
timestamp: number,
|
||||
type: string,
|
||||
contextString: string,
|
||||
context: string,
|
||||
payload: string,
|
||||
}
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall | RawIosSwipeEvent;
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
|
||||
|
|
|
|||
|
|
@ -60,12 +60,14 @@ export const TP_MAP = {
|
|||
116: MType.ResourceTiming,
|
||||
117: MType.TabChange,
|
||||
118: MType.TabData,
|
||||
93: MType.IosCustomEvent,
|
||||
93: MType.IosEvent,
|
||||
96: MType.IosScreenChanges,
|
||||
100: MType.IosClickEvent,
|
||||
101: MType.IosInputEvent,
|
||||
102: MType.IosPerformanceEvent,
|
||||
103: MType.IosLog,
|
||||
104: MType.IosInternalError,
|
||||
105: MType.IosNetworkCall,
|
||||
106: MType.IosSwipeEvent,
|
||||
111: MType.IosIssueEvent,
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ResourceTiming, NetworkRequest, Fetch } from '../messages'
|
||||
import type {ResourceTiming, NetworkRequest, Fetch, IosNetworkCall} from '../messages'
|
||||
|
||||
export const enum ResourceType {
|
||||
XHR = 'xhr',
|
||||
|
|
@ -103,7 +103,7 @@ export function getResourceFromResourceTiming(msg: ResourceTiming, sessStart: nu
|
|||
})
|
||||
}
|
||||
|
||||
export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch, sessStart: number) {
|
||||
export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch | IosNetworkCall, sessStart: number) {
|
||||
return Resource({
|
||||
...msg,
|
||||
// @ts-ignore
|
||||
|
|
|
|||
6
frontend/app/svg/icons/battery-charging.svg
Normal file
6
frontend/app/svg/icons/battery-charging.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-battery-charging" viewBox="0 0 16 16">
|
||||
<path d="M9.585 2.568a.5.5 0 0 1 .226.58L8.677 6.832h1.99a.5.5 0 0 1 .364.843l-5.334 5.667a.5.5 0 0 1-.842-.49L5.99 9.167H4a.5.5 0 0 1-.364-.843l5.333-5.667a.5.5 0 0 1 .616-.09z"/>
|
||||
<path d="M2 4h4.332l-.94 1H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h2.38l-.308 1H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/>
|
||||
<path d="M2 6h2.45L2.908 7.639A1.5 1.5 0 0 0 3.313 10H2V6zm8.595-2-.308 1H12a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9.276l-.942 1H12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.405z"/>
|
||||
<path d="M12 10h-1.783l1.542-1.639c.097-.103.178-.218.241-.34V10zm0-3.354V6h-.646a1.5 1.5 0 0 1 .646.646zM16 8a1.5 1.5 0 0 1-1.5 1.5v-3A1.5 1.5 0 0 1 16 8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 719 B |
3
frontend/app/svg/icons/battery.svg
Normal file
3
frontend/app/svg/icons/battery.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-battery" viewBox="0 0 16 16">
|
||||
<path d="M0 6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6zm2-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H2zm14 3a1.5 1.5 0 0 1-1.5 1.5v-3A1.5 1.5 0 0 1 16 8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
10
frontend/app/svg/icons/low-disc-space.svg
Normal file
10
frontend/app/svg/icons/low-disc-space.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_82_1859)">
|
||||
<path d="M10.7141 6.08031H5.91944V1.28567C5.91944 1.22674 5.87123 1.17852 5.8123 1.17852H5.46408C4.76056 1.17737 4.06375 1.31534 3.41374 1.58449C2.76373 1.85364 2.17336 2.24866 1.67658 2.74683C1.18736 3.23454 0.797544 3.81267 0.528817 4.44906C0.249181 5.10911 0.105689 5.81883 0.106942 6.53567C0.10579 7.23919 0.243758 7.93601 0.512909 8.58601C0.78206 9.23602 1.17708 9.82639 1.67525 10.3232C2.16676 10.8147 2.73998 11.2017 3.37748 11.4709C4.03753 11.7506 4.74724 11.8941 5.46408 11.8928C6.16761 11.894 6.86442 11.756 7.51443 11.4868C8.16444 11.2177 8.75481 10.8227 9.25158 10.3245C9.7431 9.83299 10.1302 9.25978 10.3994 8.62228C10.679 7.96222 10.8225 7.25251 10.8212 6.53567V6.18745C10.8212 6.12852 10.773 6.08031 10.7141 6.08031ZM8.594 9.6937C8.17928 10.1052 7.68745 10.4308 7.14669 10.6519C6.60593 10.873 6.02686 10.9852 5.44266 10.9821C4.26275 10.9767 3.15382 10.5147 2.31944 9.68031C1.47971 8.84058 1.01766 7.72361 1.01766 6.53567C1.01766 5.34772 1.47971 4.23076 2.31944 3.39103C3.05069 2.65978 3.99221 2.21379 5.00873 2.11201V6.99103H9.88775C9.78462 8.0129 9.33462 8.95978 8.594 9.6937ZM11.8927 5.33567L11.8578 4.95799C11.744 3.72451 11.1962 2.56067 10.315 1.6821C9.43312 0.801913 8.27166 0.257184 7.03105 0.141918L6.65203 0.107096C6.58908 0.101739 6.53551 0.149953 6.53551 0.2129V5.3571C6.53551 5.41602 6.58373 5.46424 6.64266 5.46424L11.7855 5.45085C11.8485 5.45085 11.898 5.39727 11.8927 5.33567ZM7.44355 4.5562V1.1196C8.28721 1.29618 9.06143 1.71399 9.67212 2.32227C10.2842 2.93299 10.7034 3.70977 10.8775 4.54683L7.44355 4.5562Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_82_1859">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
frontend/app/svg/icons/memory-ios.svg
Normal file
3
frontend/app/svg/icons/memory-ios.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-memory" viewBox="0 0 16 16">
|
||||
<path d="M1 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4.586a1 1 0 0 0 .707-.293l.353-.353a.5.5 0 0 1 .708 0l.353.353a1 1 0 0 0 .707.293H15a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H1Zm.5 1h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm5 0h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm4.5.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4ZM2 10v2H1v-2h1Zm2 0v2H3v-2h1Zm2 0v2H5v-2h1Zm3 0v2H8v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue