remote remove-immutable pull and resolved conflcits

This commit is contained in:
Shekar Siri 2023-01-03 15:08:32 +01:00
commit 58c3732fa2
193 changed files with 1864 additions and 4401 deletions

View file

@ -67,7 +67,7 @@ jobs:
} && {
echo "Skipping Security Checks"
}
PUSH_IMAGE=1 bash -x ./build.sh ee
docker push $DOCKER_REPO/$image:$IMAGE_TAG
- name: Creating old image input
run: |
#

View file

@ -66,7 +66,7 @@ jobs:
} && {
echo "Skipping Security Checks"
}
PUSH_IMAGE=1 bash -x ./build.sh
docker push $DOCKER_REPO/$image:$IMAGE_TAG
- name: Creating old image input
run: |
#

View file

@ -95,6 +95,7 @@ jobs:
# Pushing image to registry
#
cd backend
cat /tmp/images_to_build.txt
for image in $(cat /tmp/images_to_build.txt);
do
echo "Bulding $image"
@ -109,7 +110,7 @@ jobs:
} && {
echo "Skipping Security Checks"
}
PUSH_IMAGE=1 bash -x ./build.sh ee $image
docker push $DOCKER_REPO/$image:$IMAGE_TAG
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
done
@ -156,8 +157,6 @@ jobs:
mv /tmp/helmcharts/* openreplay/charts/
ls openreplay/charts
cat /tmp/image_override.yaml
# Deploy command
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -

View file

@ -95,6 +95,7 @@ jobs:
# Pushing image to registry
#
cd backend
cat /tmp/images_to_build.txt
for image in $(cat /tmp/images_to_build.txt);
do
echo "Bulding $image"
@ -109,7 +110,7 @@ jobs:
} && {
echo "Skipping Security Checks"
}
PUSH_IMAGE=1 bash -x ./build.sh skip $image
docker push $DOCKER_REPO/$image:$IMAGE_TAG
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
done
@ -154,8 +155,6 @@ jobs:
mv /tmp/helmcharts/* openreplay/charts/
ls openreplay/charts
cat /tmp/image_override.yaml
# Deploy command
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -

View file

@ -41,6 +41,8 @@ type Conn struct {
webIssues Bulk
webIssueEvents Bulk
webCustomEvents Bulk
webClickEvents Bulk
webNetworkRequest Bulk
sessionUpdates map[uint64]*sessionUpdates
batchQueueLimit int
batchSizeLimit int
@ -111,25 +113,25 @@ func (conn *Conn) initBulks() {
"autocomplete",
"(value, type, project_id)",
"($%d, $%d, $%d)",
3, 100)
3, 200)
if err != nil {
log.Fatalf("can't create autocomplete bulk")
log.Fatalf("can't create autocomplete bulk: %s", err)
}
conn.requests, err = NewBulk(conn.c,
"events_common.requests",
"(session_id, timestamp, seq_index, url, duration, success)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d)",
6, 100)
6, 200)
if err != nil {
log.Fatalf("can't create requests bulk")
log.Fatalf("can't create requests bulk: %s", err)
}
conn.customEvents, err = NewBulk(conn.c,
"events_common.customs",
"(session_id, timestamp, seq_index, name, payload)",
"($%d, $%d, $%d, left($%d, 2700), $%d)",
5, 100)
5, 200)
if err != nil {
log.Fatalf("can't create customEvents bulk")
log.Fatalf("can't create customEvents bulk: %s", err)
}
conn.webPageEvents, err = NewBulk(conn.c,
"events.pages",
@ -138,73 +140,89 @@ func (conn *Conn) initBulks() {
"time_to_interactive, response_time, dom_building_time)",
"($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0),"+
" NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0))",
18, 100)
18, 200)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
log.Fatalf("can't create webPageEvents bulk: %s", err)
}
conn.webInputEvents, err = NewBulk(conn.c,
"events.inputs",
"(session_id, message_id, timestamp, value, label)",
"($%d, $%d, $%d, $%d, NULLIF($%d,''))",
5, 100)
5, 200)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
log.Fatalf("can't create webPageEvents bulk: %s", err)
}
conn.webGraphQL, err = NewBulk(conn.c,
"events.graphql",
"(session_id, timestamp, message_id, name, request_body, response_body)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d)",
6, 100)
6, 200)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
log.Fatalf("can't create webPageEvents bulk: %s", err)
}
conn.webErrors, err = NewBulk(conn.c,
"errors",
"(error_id, project_id, source, name, message, payload)",
"($%d, $%d, $%d, $%d, $%d, $%d::jsonb)",
6, 100)
6, 200)
if err != nil {
log.Fatalf("can't create webErrors bulk")
log.Fatalf("can't create webErrors bulk: %s", err)
}
conn.webErrorEvents, err = NewBulk(conn.c,
"events.errors",
"(session_id, message_id, timestamp, error_id)",
"($%d, $%d, $%d, $%d)",
4, 100)
4, 200)
if err != nil {
log.Fatalf("can't create webErrorEvents bulk")
log.Fatalf("can't create webErrorEvents bulk: %s", err)
}
conn.webErrorTags, err = NewBulk(conn.c,
"public.errors_tags",
"(session_id, message_id, error_id, key, value)",
"($%d, $%d, $%d, $%d, $%d)",
5, 100)
5, 200)
if err != nil {
log.Fatalf("can't create webErrorEvents bulk")
log.Fatalf("can't create webErrorEvents bulk: %s", err)
}
conn.webIssues, err = NewBulk(conn.c,
"issues",
"(project_id, issue_id, type, context_string)",
"($%d, $%d, $%d, $%d)",
4, 100)
4, 200)
if err != nil {
log.Fatalf("can't create webIssues bulk")
log.Fatalf("can't create webIssues bulk: %s", err)
}
conn.webIssueEvents, err = NewBulk(conn.c,
"events_common.issues",
"(session_id, issue_id, timestamp, seq_index, payload)",
"($%d, $%d, $%d, $%d, CAST($%d AS jsonb))",
5, 100)
5, 200)
if err != nil {
log.Fatalf("can't create webIssueEvents bulk")
log.Fatalf("can't create webIssueEvents bulk: %s", err)
}
conn.webCustomEvents, err = NewBulk(conn.c,
"events_common.customs",
"(session_id, seq_index, timestamp, name, payload, level)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d)",
6, 100)
6, 200)
if err != nil {
log.Fatalf("can't create webCustomEvents bulk")
log.Fatalf("can't create webCustomEvents bulk: %s", err)
}
conn.webClickEvents, err = NewBulk(conn.c,
"events.clicks",
"(session_id, message_id, timestamp, label, selector, url, path)",
"($%d, $%d, $%d, NULLIF($%d, ''), $%d, $%d, $%d)",
7, 200)
if err != nil {
log.Fatalf("can't create webClickEvents bulk: %s", err)
}
conn.webNetworkRequest, err = NewBulk(conn.c,
"events_common.requests",
"(session_id, timestamp, seq_index, url, host, path, query, request_body, response_body, status_code, method, duration, success)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d, $%d, $%d, $%d, $%d::smallint, NULLIF($%d, '')::http_method, $%d, $%d)",
13, 200)
if err != nil {
log.Fatalf("can't create webNetworkRequest bulk: %s", err)
}
}
@ -296,6 +314,12 @@ func (conn *Conn) sendBulks() {
if err := conn.webCustomEvents.Send(); err != nil {
log.Printf("webCustomEvents bulk send err: %s", err)
}
if err := conn.webClickEvents.Send(); err != nil {
log.Printf("webClickEvents bulk send err: %s", err)
}
if err := conn.webNetworkRequest.Send(); err != nil {
log.Printf("webNetworkRequest bulk send err: %s", err)
}
}
func (conn *Conn) CommitBatches() {

View file

@ -58,16 +58,12 @@ func (conn *Conn) InsertWebPageEvent(sessionID uint64, projectID uint32, e *Page
}
func (conn *Conn) InsertWebClickEvent(sessionID uint64, projectID uint32, e *ClickEvent) error {
sqlRequest := `
INSERT INTO events.clicks
(session_id, message_id, timestamp, label, selector, url)
(SELECT
$1, $2, $3, NULLIF($4, ''), $5, host || path
FROM events.pages
WHERE session_id = $1 AND timestamp <= $3 ORDER BY timestamp DESC LIMIT 1
)
`
conn.batchQueue(sessionID, sqlRequest, sessionID, truncSqIdx(e.MessageID), e.Timestamp, e.Label, e.Selector)
var host, path string
host, path, _, _ = url.GetURLParts(e.Url)
log.Println("insert web click:", host, path)
if err := conn.webClickEvents.Append(sessionID, truncSqIdx(e.MessageID), e.Timestamp, e.Label, e.Selector, host+path, path); err != nil {
log.Printf("insert web click err: %s", err)
}
// Accumulate session updates and exec inside batch with another sql commands
conn.updateSessionEvents(sessionID, 1, 0)
// Add new value set to autocomplete bulk
@ -119,29 +115,8 @@ func (conn *Conn) InsertWebNetworkRequest(sessionID uint64, projectID uint32, sa
if err != nil {
return err
}
sqlRequest := `
INSERT INTO events_common.requests (
session_id, timestamp, seq_index,
url, host, path, query,
request_body, response_body, status_code, method,
duration, success
) VALUES (
$1, $2, $3,
left($4, 2700), $5, $6, $7,
$8, $9, $10::smallint, NULLIF($11, '')::http_method,
$12, $13
) ON CONFLICT DO NOTHING`
conn.batchQueue(sessionID, sqlRequest,
sessionID, e.Meta().Timestamp, truncSqIdx(e.Meta().Index),
e.URL, host, path, query,
request, response, e.Status, url.EnsureMethod(e.Method),
e.Duration, e.Status < 400,
)
// Record approximate message size
conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.URL)+len(host)+len(path)+len(query)+
len(e.Request)+len(e.Response)+len(url.EnsureMethod(e.Method))+8*5+1)
conn.webNetworkRequest.Append(sessionID, e.Meta().Timestamp, truncSqIdx(e.Meta().Index), e.URL, host, path, query,
request, response, e.Status, url.EnsureMethod(e.Method), e.Duration, e.Status < 400)
return nil
}

View file

@ -19,20 +19,6 @@ func (f *NetworkIssueDetector) Build() Message {
func (f *NetworkIssueDetector) Handle(message Message, messageID uint64, timestamp uint64) Message {
switch msg := message.(type) {
// case *ResourceTiming:
// success := msg.Duration != 0 // The only available way here
// if !success {
// issueType := "missing_resource"
// if msg.Initiator == "fetch" || msg.Initiator == "xmlhttprequest" {
// issueType = "bad_request"
// }
// return &IssueEvent{
// Type: issueType,
// MessageID: messageID,
// Timestamp: msg.Timestamp,
// ContextString: msg.URL,
// }
// }
case *NetworkRequest:
if msg.Status >= 400 {
return &IssueEvent{

View file

@ -0,0 +1,22 @@
package messages
type pageLocations struct {
urls map[uint64]string
}
func NewPageLocations() *pageLocations {
return &pageLocations{urls: make(map[uint64]string)}
}
func (p *pageLocations) Set(sessID uint64, url string) {
p.urls[sessID] = url
}
func (p *pageLocations) Get(sessID uint64) string {
url := p.urls[sessID]
return url
}
func (p *pageLocations) Delete(sessID uint64) {
delete(p.urls, sessID)
}

View file

@ -24,10 +24,15 @@ type messageIteratorImpl struct {
broken bool
messageInfo *message
batchInfo *BatchInfo
urls *pageLocations
}
func NewMessageIterator(messageHandler MessageHandler, messageFilter []int, autoDecode bool) MessageIterator {
iter := &messageIteratorImpl{handler: messageHandler, autoDecode: autoDecode}
iter := &messageIteratorImpl{
handler: messageHandler,
autoDecode: autoDecode,
urls: NewPageLocations(),
}
if len(messageFilter) != 0 {
filter := make(map[int]struct{}, len(messageFilter))
for _, msgType := range messageFilter {
@ -125,7 +130,7 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
if m.Timestamp == 0 {
i.zeroTsLog("BatchMetadata")
}
i.messageInfo.Url = m.Url
i.messageInfo.Url = m.Location
i.version = m.Version
i.batchInfo.version = m.Version
@ -138,6 +143,10 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
if m.Timestamp == 0 {
i.zeroTsLog("BatchMeta")
}
// Try to get saved session's page url
if savedURL := i.urls.Get(i.messageInfo.batch.sessionID); savedURL != "" {
i.messageInfo.Url = savedURL
}
case *Timestamp:
i.messageInfo.Timestamp = int64(m.Timestamp)
@ -158,9 +167,13 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
if m.Timestamp == 0 {
i.zeroTsLog("SessionEnd")
}
// Delete session from urls cache layer
i.urls.Delete(i.messageInfo.batch.sessionID)
case *SetPageLocation:
i.messageInfo.Url = m.URL
// Save session page url in cache for using in next batches
i.urls.Set(i.messageInfo.batch.sessionID, m.URL)
}
return nil
}

View file

@ -289,7 +289,13 @@ func (c *connectorImpl) InsertWebErrorEvent(session *types.Session, msg *types.E
keys = append(keys, k)
values = append(values, v)
}
// Check error source before insert to avoid panic from clickhouse lib
switch msg.Source {
case "js_exception", "bugsnag", "cloudwatch", "datadog", "elasticsearch", "newrelic", "rollbar", "sentry", "stackdriver", "sumologic":
default:
return fmt.Errorf("unknown error source: %s", msg.Source)
}
// Insert event to batch
if err := c.batches["errors"].Append(
session.SessionID,
uint16(session.ProjectID),

View file

@ -0,0 +1,31 @@
import { configure, addDecorator } from '@storybook/react';
import { Provider } from 'react-redux';
import store from '../app/store';
import { MemoryRouter } from "react-router"
const withProvider = (story) => (
<Provider store={store}>
{ story() }
</Provider>
)
// const req = require.context('../app/components/ui', true, /\.stories\.js$/);
// const issues = require.context('../app/components/Session/Issues', true, /\.stories\.js$/);
addDecorator(withProvider);
addDecorator(story => <MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>);
// function loadStories() {
// req.keys().forEach(filename => req(filename));
// }
// configure(loadStories, module);
configure(
[
// require.context('../app', true, /\.stories\.mdx$/),
require.context('../app', true, /\.stories\.js$/),
],
module
);

View file

@ -8,7 +8,6 @@ import { fetchUserInfo } from 'Duck/user';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import Header from 'Components/Header/Header';
import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { withStore } from 'App/mstore';
@ -30,7 +29,7 @@ const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
const ClientPure = lazy(() => import('Components/Client/Client'));
const AssistPure = lazy(() => import('Components/Assist'));
const BugFinderPure = lazy(() => import('Components/Overview'));
const SessionsOverviewPure = lazy(() => import('Components/Overview'));
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails'));
@ -38,7 +37,7 @@ const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDeta
const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage'));
const MultiviewPure = lazy(() => import('Components/Session_/Multiview/Multiview.tsx'));
const BugFinder = withSiteIdUpdater(BugFinderPure);
const SessionsOverview = withSiteIdUpdater(SessionsOverviewPure);
const Dashboard = withSiteIdUpdater(DashboardPure);
const Session = withSiteIdUpdater(SessionPure);
const LiveSession = withSiteIdUpdater(LiveSessionPure);
@ -115,7 +114,6 @@ const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
fetchTenants,
setSessionPath,
fetchSiteList,
fetchAnnouncements,
fetchAlerts,
}
)
@ -237,7 +235,7 @@ class Router extends React.Component {
<Route exact strict path={withSiteId(FUNNEL_PATH, siteIdList)} component={FunnelPage} />
<Route exact strict path={withSiteId(FUNNEL_CREATE_PATH, siteIdList)} component={FunnelsDetails} />
<Route exact strict path={withSiteId(FUNNEL_ISSUE_PATH, siteIdList)} component={FunnelIssue} />
<Route exact strict path={withSiteId(SESSIONS_PATH, siteIdList)} component={BugFinder} />
<Route exact strict path={withSiteId(SESSIONS_PATH, siteIdList)} component={SessionsOverview} />
<Route exact strict path={withSiteId(SESSION_PATH, siteIdList)} component={Session} />
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} component={LiveSession} />
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)} render={(props) => <Session {...props} live />} />

View file

@ -88,14 +88,22 @@ export default class APIClient {
if (
path !== '/targets_temp' &&
!path.includes('/metadata/session_search') &&
!path.includes('/watchdogs/rules') &&
!path.includes('/assist/credentials') &&
!!this.siteId &&
siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath))
) {
edp = `${ edp }/${ this.siteId }`
}
return fetch(edp + path, this.init);
return fetch(edp + path, this.init)
.then(response => {
if (response.ok) {
return response
} else {
throw new Error(
`! ${this.init.method} error on ${path}; ${response.status}`
)
}
})
}
get(path, params, options) {

View file

@ -41,9 +41,9 @@ export default (store) => (next) => (action) => {
function parseError(e) {
try {
return JSON.parse(e).errors || [];
return [...JSON.parse(e).errors] || [];
} catch {
return e;
return Array.isArray(e) ? e : [e];
}
}

View file

@ -1,34 +1,27 @@
import React, { useEffect } from 'react';
import stl from './notifications.module.css';
import { connect } from 'react-redux';
import { Icon, Tooltip } from 'UI';
import { fetchList, setViewed, clearAll } from 'Duck/notifications';
import { setLastRead } from 'Duck/announcements';
import { useModal } from 'App/components/Modal';
import AlertTriggersModal from 'Shared/AlertTriggersModal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
interface Props {
notifications: any;
fetchList: any;
}
function Notifications(props: Props) {
function Notifications() {
const { showModal } = useModal();
const { notificationStore } = useStore();
const count = useObserver(() => notificationStore.notificationsCount);
const count = notificationStore.notificationsCount;
useEffect(() => {
const interval = setInterval(() => {
notificationStore.fetchNotificationsCount();
void notificationStore.fetchNotificationsCount();
}, AUTOREFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return useObserver(() => (
return (
<Tooltip title={`Alerts`}>
<div
className={stl.button}
@ -40,12 +33,7 @@ function Notifications(props: Props) {
<Icon name="bell" size="18" color="gray-dark" />
</div>
</Tooltip>
));
);
}
export default connect(
(state: any) => ({
notifications: state.getIn(['notifications', 'list']),
}),
{ fetchList, setLastRead, setViewed, clearAll }
)(Notifications);
export default observer(Notifications)

View file

@ -1,99 +0,0 @@
import React from 'react';
import stl from './announcements.module.css';
import ListItem from './ListItem';
import { connect } from 'react-redux';
import { SlideModal, Icon, NoContent, Tooltip } from 'UI';
import { fetchList, setLastRead } from 'Duck/announcements';
import withToggle from 'Components/hocs/withToggle';
import { withRouter } from 'react-router-dom';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@withToggle('visible', 'toggleVisisble')
@withRouter
class Announcements extends React.Component {
navigateToUrl = url => {
if (url) {
if (url.startsWith(window.env.ORIGIN || window.location.origin)) {
const { history } = this.props;
var path = new URL(url).pathname
if (path.includes('/metrics')) {
const { siteId, sites } = this.props;
const activeSite = sites.find(s => s.id == siteId);
history.push(`/${activeSite.id + path}`);
} else {
history.push(path)
}
} else {
window.open(url, "_blank")
}
this.toggleModal()
}
}
toggleModal = () => {
if (!this.props.visible) {
const { setLastRead, fetchList } = this.props;
fetchList().then(() => { setTimeout(() => { setLastRead() }, 5000); });
}
this.props.toggleVisisble(!this.props.visible);
}
render() {
const { announcements, visible, loading } = this.props;
const unReadNotificationsCount = announcements.filter(({viewed}) => !viewed).size
return (
<div>
<Tooltip content={ `Announcements` } >
<div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
<div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
{ unReadNotificationsCount }
</div>
<Icon name="bullhorn" size="18" />
</div>
</Tooltip>
<SlideModal
title="Announcements"
right
isDisplayed={ visible }
onClose={ visible && this.toggleModal }
bgColor="gray-lightest"
size="small"
content={
<div className="mx-4">
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_ANNOUNCEMENTS} size={80} />
<div className="text-center text-gray-600 my-4">No announcements to show.</div>
</div>
}
size="small"
show={ !loading && announcements.size === 0 }
>
{
announcements.map(item => (
<ListItem
key={item.key}
announcement={item}
onButtonClick={this.navigateToUrl}
/>
))
}
</NoContent>
</div>
}
/>
</div>
);
}
}
export default connect(state => ({
announcements: state.getIn(['announcements', 'list']),
loading: state.getIn(['announcements', 'fetchList', 'loading']),
siteId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
}), { fetchList, setLastRead })(Announcements);

View file

@ -1,31 +0,0 @@
import React from 'react';
import { Button, Label } from 'UI';
import stl from './listItem.module.css';
const ListItem = ({ announcement, onButtonClick }) => {
return (
<div className={stl.wrapper}>
<div className="flex justify-between items-center mb-3">
<div className="text-sm">{announcement.createdAt && announcement.createdAt.toFormat('LLL dd, yyyy')}</div>
<Label><span className="capitalize">{announcement.type}</span></Label>
</div>
{announcement.imageUrl &&
<img className="w-full border mb-3" src={announcement.imageUrl} />
}
<div>
<h2 className="text-xl mb-2">{announcement.title}</h2>
<div className="mb-2 text-sm text-justify">{announcement.description}</div>
{announcement.buttonUrl &&
<Button
variant="outline"
onClick={() => onButtonClick(announcement.buttonUrl) }
>
<span className="capitalize">{announcement.buttonText}</span>
</Button>
}
</div>
</div>
)
}
export default ListItem

View file

@ -1 +0,0 @@
export { default } from './ListItem';

View file

@ -1,5 +0,0 @@
.wrapper {
background-color: white;
margin-bottom: 20px;
padding: 15px;
}

View file

@ -1,39 +0,0 @@
.wrapper {
position: relative;
}
.button {
position: relative;
cursor: pointer;
display: flex;
align-items: center;
padding: 0 15px;
height: 50px;
transition: all 0.3s;
&:hover {
background-color: $gray-lightest;
transition: all 0.2s;
}
&[data-active=true] {
background-color: $gray-lightest;
}
}
.counter {
position: absolute;
top: 8px;
left: 24px;
background-color: #CC0000;
color: white;
font-size: 9px;
font-weight: 300;
min-width: 16px;
height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
}

View file

@ -1 +0,0 @@
export { default } from './Announcements';

View file

@ -82,5 +82,5 @@ function RequestingWindow({ userDisplayName, getWindowType }: Props) {
}
export default connect((state: any) => ({
userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']),
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
}))(RequestingWindow);

View file

@ -255,7 +255,7 @@ const con = connect(
return {
hasPermission: permissions.includes('ASSIST_CALL'),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']),
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
};
},
{ toggleChatWindow }

View file

@ -36,7 +36,7 @@ function SessionList(props: Props) {
</div>
<Loader loading={props.loading}>
<NoContent
show={!props.loading && props.list.size === 0}
show={!props.loading && props.list.length === 0}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />
@ -56,7 +56,7 @@ function SessionList(props: Props) {
<span className="ml-2 font-medium">{session.pageTitle}</span>
</div>
)}
<SessionItem onClick={() => hideModal()} key={session.sessionId} session={session} />
<SessionItem compact={true} onClick={() => hideModal()} key={session.sessionId} session={session} />
</div>
))}
</div>

View file

@ -1,199 +0,0 @@
import React from 'react';
import APIClient from 'App/api_client';
import cn from 'classnames';
import { Input, Icon } from 'UI';
import { debounce } from 'App/utils';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import EventSearchInput from 'Shared/EventSearchInput';
import stl from './autoComplete.module.css';
import FilterItem from '../CustomFilters/FilterItem';
const TYPE_TO_SEARCH_MSG = "Start typing to search...";
const NO_RESULTS_MSG = "No results found.";
const SOME_ERROR_MSG = "Some error occured.";
const defaultValueToText = value => value;
const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value }));
const hiddenStyle = {
whiteSpace: 'pre-wrap',
opacity: 0, position: 'fixed', left: '-3000px'
};
let pasted = false;
let changed = false;
class AutoComplete extends React.PureComponent {
static defaultProps = {
method: 'GET',
params: {},
}
state = {
values: [],
noResultsMessage: TYPE_TO_SEARCH_MSG,
ddOpen: false,
query: this.props.value,
loading: false,
error: false
}
componentWillReceiveProps(newProps) {
if (this.props.value !== newProps.value) {
this.setState({ query: newProps.value});
}
}
onClickOutside = () => {
this.setState({ ddOpen: false });
}
requestValues = (q) => {
const { params, endpoint, method } = this.props;
this.setState({
loading: true,
error: false,
});
return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
this.setError();
} else {
this.setState({
ddOpen: true,
values: data,
loading: false,
noResultsMessage: NO_RESULTS_MSG,
});
}
})
.catch(this.setError);
}
debouncedRequestValues = debounce(this.requestValues, 1000)
setError = () => this.setState({
loading: false,
error: true,
noResultsMessage: SOME_ERROR_MSG,
})
onInputChange = ({ target: { value } }) => {
changed = true;
this.setState({ query: value, updated: true })
const _value = value ? value.trim() : undefined;
if (_value !== '' && _value !== ' ') {
this.debouncedRequestValues(_value)
}
}
onBlur = ({ target: { value } }) => {
// to avoid sending unnecessary request on focus in/out without changing
if (!changed && !pasted) return;
value = pasted ? this.hiddenInput.value : value;
const { onSelect, name } = this.props;
if (value !== this.props.value) {
const _value = value ? value.trim() : undefined;
onSelect(null, {name, value: _value});
}
changed = false;
pasted = false;
}
onItemClick = (e, item) => {
e.stopPropagation();
e.preventDefault();
const { onSelect, name } = this.props;
this.setState({ query: item.value, ddOpen: false})
onSelect(e, {name, ...item.toJS()});
}
render() {
const { ddOpen, query, loading, values } = this.state;
const {
optionMapping = defaultOptionMapping,
valueToText = defaultValueToText,
placeholder = 'Type to search...',
headerText = '',
fullWidth = false,
onRemoveValue = () => {},
onAddValue = () => {},
showCloseButton = false,
} = this.props;
const options = optionMapping(values, valueToText)
return (
<OutsideClickDetectingDiv
className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
onClickOutside={this.onClickOutside}
>
{/* <EventSearchInput /> */}
<div className={stl.inputWrapper}>
<input
name="query"
// className={cn(stl.input)}
onFocus={ () => this.setState({ddOpen: true})}
onChange={ this.onInputChange }
onBlur={ this.onBlur }
value={ query }
autoFocus={ true }
type="text"
placeholder={ placeholder }
onPaste={(e) => {
const text = e.clipboardData.getData('Text');
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
autocomplete="do-not-autofill-bad-chrome"
/>
<div className={stl.right} onClick={showCloseButton ? onRemoveValue : onAddValue}>
{ showCloseButton ? <Icon name="close" size="14" /> : <span className="px-1">or</span>}
</div>
</div>
{showCloseButton && <div className='ml-2'>or</div>}
{/* <Input
className={ cn(stl.searchInput, { [ stl.fullWidth] : fullWidth }) }
onChange={ this.onInputChange }
onBlur={ this.onBlur }
onFocus={ () => this.setState({ddOpen: true})}
value={ query }
// icon="search"
label={{ basic: true, content: <div>test</div> }}
labelPosition='right'
loading={ loading }
autoFocus={ true }
type="search"
placeholder={ placeholder }
onPaste={(e) => {
const text = e.clipboardData.getData('Text');
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
/> */}
<textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea>
{ ddOpen && options.length > 0 &&
<div className={ stl.menu }>
{ headerText && headerText }
{
options.map(item => (
<FilterItem
label={ item.value }
icon={ item.icon }
onClick={ (e) => this.onItemClick(e, item) }
/>
))
}
</div>
}
</OutsideClickDetectingDiv>
);
}
}
export default AutoComplete;

View file

@ -1,10 +0,0 @@
import React from 'react';
import stl from './dropdownItem.module.css';
const DropdownItem = ({ value, onSelect }) => {
return (
<div className={ stl.wrapper } onClick={ onSelect } >{ value }</div>
);
};
export default DropdownItem;

View file

@ -1,64 +0,0 @@
.menu {
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 10px 0 $gray-light;
padding: 20px;
background-color: white;
max-height: 350px;
overflow-y: auto;
position: absolute;
top: 28px;
left: 0;
width: 500px;
z-index: 99;
}
.searchInput {
& input {
font-size: 13px !important;
padding: 5px !important;
color: $gray-darkest !important;
font-size: 14px !important;
background-color: rgba(255, 255, 255, 0.8) !important;
& .label {
padding: 0px !important;
display: flex;
align-items: center;
justify-content: center;
}
}
height: 28px !important;
width: 280px;
color: $gray-darkest !important;
}
.fullWidth {
width: 100% !important;
}
.inputWrapper {
border: solid thin $gray-light !important;
border-radius: 3px;
border-radius: 3px;
display: flex;
align-items: center;
& input {
height: 28px;
font-size: 13px !important;
padding: 0 5px !important;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
& .right {
height: 28px;
display: flex;
align-items: center;
padding: 0 5px;
background-color: $gray-lightest;
border-left: solid thin $gray-light !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
}
}

View file

@ -1,11 +0,0 @@
.wrapper {
padding: 8px;
border-bottom: solid thin rgba(0, 0, 0, 0.05);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: $active-blue;
}
}

View file

@ -1 +0,0 @@
export { default } from './AutoComplete';

View file

@ -1,131 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import withPageTitle from 'HOCs/withPageTitle';
import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions';
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
import { KEYS } from 'Types/filter/customFilter';
import SessionList from './SessionList';
import stl from './bugFinder.module.css';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { fetch as fetchFilterVariables } from 'Duck/sources';
import { fetchSources } from 'Duck/customField';
import { setActiveTab } from 'Duck/search';
import SessionsMenu from './SessionsMenu/SessionsMenu';
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import SessionSearch from 'Shared/SessionSearch';
import MainSearchBar from 'Shared/MainSearchBar';
import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search';
import { FilterKey } from 'Types/filter/filterType';
const weakEqual = (val1, val2) => {
if (!!val1 === false && !!val2 === false) return true;
if (!val1 !== !val2) return false;
return `${val1}` === `${val2}`;
};
const allowedQueryKeys = [
'userOs',
'userId',
'userBrowser',
'userDevice',
'userCountry',
'startDate',
'endDate',
'minDuration',
'maxDuration',
'referrer',
'sort',
'order',
];
@withLocationHandlers()
@connect(
(state) => ({
filter: state.getIn(['filters', 'appliedFilter']),
variables: state.getIn(['customFields', 'list']),
sources: state.getIn(['customFields', 'sources']),
filterValues: state.get('filterValues'),
favoriteList: state.getIn(['sessions', 'favoriteList']),
currentProjectId: state.getIn(['site', 'siteId']),
sites: state.getIn(['site', 'list']),
watchdogs: state.getIn(['watchdogs', 'list']),
activeFlow: state.getIn(['filters', 'activeFlow']),
sessions: state.getIn(['sessions', 'list']),
}),
{
fetchFavoriteSessionList,
applyFilter,
addAttribute,
fetchFilterVariables,
fetchSources,
clearEvents,
setActiveTab,
clearSearch,
fetchSessions,
addFilterByKeyAndValue,
}
)
@withPageTitle('Sessions - OpenReplay')
export default class BugFinder extends React.PureComponent {
state = { showRehydratePanel: false };
constructor(props) {
super(props);
// TODO should cache the response
// props.fetchSources().then(() => {
// defaultFilters[6] = {
// category: 'Collaboration',
// type: 'CUSTOM',
// keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
// };
// defaultFilters[7] = {
// category: 'Logging Tools',
// type: 'ERROR',
// keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
// };
// });
// if (props.sessions.size === 0) {
// props.fetchSessions();
// }
const queryFilter = this.props.query.all(allowedQueryKeys);
if (queryFilter.hasOwnProperty('userId')) {
props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId);
} else {
if (props.sessions.size === 0) {
props.fetchSessions();
}
}
}
toggleRehydratePanel = () => {
this.setState({ showRehydratePanel: !this.state.showRehydratePanel });
};
setActiveTab = (tab) => {
this.props.setActiveTab(tab);
};
render() {
const { showRehydratePanel } = this.state;
return (
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
<div className="side-menu">
<SessionsMenu onMenuItemClick={this.setActiveTab} toggleRehydratePanel={this.toggleRehydratePanel} />
</div>
<div className={cn('side-menu-margined', stl.searchWrapper)}>
<NoSessionsMessage />
<div className="mb-5">
<MainSearchBar />
<SessionSearch />
</div>
<SessionList onMenuItemClick={this.setActiveTab} />
</div>
</div>
</div>
);
}
}

View file

@ -1,31 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { applyFilter } from 'Duck/search';
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
import DateRangeDropdown from 'Shared/DateRangeDropdown';
@connect(state => ({
filter: state.getIn([ 'search', 'instance' ]),
}), {
applyFilter, fetchFunnelsList
})
export default class DateRange extends React.PureComponent {
onDateChange = (e) => {
// this.props.fetchFunnelsList(e.rangeValue)
this.props.applyFilter(e)
}
render() {
const { filter: { rangeValue, startDate, endDate }, className } = this.props;
return (
<DateRangeDropdown
button
onChange={ this.onDateChange }
rangeValue={ rangeValue }
startDate={ startDate }
endDate={ endDate }
className={ className }
/>
);
}
}

View file

@ -1,14 +0,0 @@
import React from 'react';
import { Icon } from 'UI';
import stl from './filterSelectionButton.module.css';
const FilterSelectionButton = ({ label }) => {
return (
<div className={ stl.wrapper }>
<span className="capitalize">{ label } </span>
<Icon name="chevron-down"/>
</div>
);
};
export default FilterSelectionButton;

View file

@ -1,37 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import Select from 'Shared/Select';
import { Icon } from 'UI';
import { sort } from 'Duck/sessions';
import { applyFilter } from 'Duck/search';
import stl from './sortDropdown.module.css';
@connect(null, { sort, applyFilter })
export default class SortDropdown extends React.PureComponent {
state = { value: null }
sort = ({ value }) => {
value = value.value
this.setState({ value: value })
const [ sort, order ] = value.split('-');
const sign = order === 'desc' ? -1 : 1;
this.props.applyFilter({ order, sort });
this.props.sort(sort, sign)
setTimeout(() => this.props.sort(sort, sign), 3000); //AAA
}
render() {
const { options } = this.props;
return (
<Select
name="sortSessions"
plain
right
options={ options }
onChange={ this.sort }
defaultValue={ options[ 0 ].value }
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
/>
);
}
}

View file

@ -1 +0,0 @@
export { default } from './Filters';

View file

@ -1,23 +0,0 @@
.dropdown {
display: flex !important;
padding: 4px 6px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;
&:hover {
background-color: $gray-light;
}
}
.dropdownTrigger {
padding: 4px 8px;
border-radius: 3px;
&:hover {
background-color: $gray-light;
}
}
.dropdownIcon {
margin-top: 2px;
margin-left: 3px;
}

View file

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import styles from './insights.module.css';
const Insights = ({ insights }) => (
<div className={ styles.notes }>
<div className={ styles.tipText }>
<i className={ styles.tipIcon } />
{'This journey is only 2% of all the journeys but represents 20% of problems.'}
</div>
<div className={ styles.tipText }>
<i className={ styles.tipIcon } />
{'Lorem Ipsum 1290 events of 1500 events.'}
</div>
</div>
);
Insights.displayName = 'Insights';
export default connect(state => ({
insights: state.getIn([ 'sessions', 'insights' ]),
}))(Insights);

View file

@ -1,10 +0,0 @@
import React from 'react';
import stl from './listHeader.module.css';
const ListHeader = ({ title }) => {
return (
<div className={ stl.header }>{ title }</div>
);
};
export default ListHeader;

View file

@ -1,150 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Pagination } from 'UI';
import { applyFilter, addAttribute, addEvent } from 'Duck/filters';
import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage, setScrollPosition } from 'Duck/search';
import SessionItem from 'Shared/SessionItem';
import SessionListHeader from './SessionListHeader';
import { FilterKey } from 'Types/filter/filterType';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
// const ALL = 'all';
const PER_PAGE = 10;
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
var timeoutId;
@connect(state => ({
shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0,
savedFilters: state.getIn([ 'filters', 'list' ]),
loading: state.getIn([ 'sessions', 'loading' ]),
activeTab: state.getIn([ 'search', 'activeTab' ]),
allList: state.getIn([ 'sessions', 'list' ]),
total: state.getIn([ 'sessions', 'total' ]),
filters: state.getIn([ 'search', 'instance', 'filters' ]),
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
currentPage: state.getIn([ 'search', 'currentPage' ]),
scrollY: state.getIn([ 'search', 'scrollY' ]),
lastPlayedSessionId: state.getIn([ 'sessions', 'lastPlayedSessionId' ]),
}), {
applyFilter,
addAttribute,
addEvent,
fetchSessions,
addFilterByKeyAndValue,
updateCurrentPage,
setScrollPosition,
})
export default class SessionList extends React.PureComponent {
constructor(props) {
super(props);
this.timeout();
}
onUserClick = (userId, userAnonymousId) => {
if (userId) {
this.props.addFilterByKeyAndValue(FilterKey.USERID, userId);
} else {
this.props.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
}
}
timeout = () => {
timeoutId = setTimeout(function () {
if (this.props.shouldAutorefresh) {
// this.props.applyFilter();
this.props.fetchSessions();
}
this.timeout();
}.bind(this), AUTOREFRESH_INTERVAL);
}
getNoContentMessage = activeTab => {
let str = "No recordings found";
if (activeTab.type !== 'all') {
str += ' with ' + activeTab.name;
return str;
}
return str + '!';
}
componentWillUnmount() {
this.props.setScrollPosition(window.scrollY)
clearTimeout(timeoutId)
}
componentDidMount() {
const { scrollY } = this.props;
window.scrollTo(0, scrollY);
}
renderActiveTabContent(list) {
const {
loading,
filters,
activeTab,
metaList,
currentPage,
total,
lastPlayedSessionId,
} = this.props;
const _filterKeys = filters.map(i => i.key);
const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
return (
<div className="bg-white p-3 rounded border">
<NoContent
title={<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
{this.getNoContentMessage(activeTab)}
</div>}
// subtext="Please try changing your search parameters."
// animatedIcon="no-results"
show={ !loading && list.size === 0}
subtext={
<div>
<div>Please try changing your search parameters.</div>
</div>
}
>
<Loader loading={ loading }>
{ list.map(session => (
<React.Fragment key={ session.sessionId }>
<SessionItem
session={ session }
hasUserFilter={hasUserFilter}
onUserClick={this.onUserClick}
metaList={metaList}
lastPlayedSessionId={lastPlayedSessionId}
/>
<div className="border-b" />
</React.Fragment>
))}
</Loader>
<div className="w-full flex items-center justify-center py-6">
<Pagination
page={currentPage}
totalPages={Math.ceil(total / PER_PAGE)}
onPageChange={(page) => this.props.updateCurrentPage(page)}
limit={PER_PAGE}
debounceRequest={1000}
/>
</div>
</NoContent>
</div>
);
}
render() {
const { activeTab, allList, total } = this.props;
return (
<div className="">
<SessionListHeader activeTab={activeTab} count={total}/>
{ this.renderActiveTabContent(allList) }
</div>
);
}
}

View file

@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { Button } from 'UI';
import styles from './sessionListFooter.module.css';
const SessionListFooter = ({
displayedCount, totalCount, loading, onLoadMoreClick,
}) => (
<div className={ styles.pageLoading }>
<div className={ styles.countInfo }>
{ `Displaying ${ displayedCount } of ${ totalCount }` }
</div>
{ totalCount > displayedCount &&
<Button
onClick={ onLoadMoreClick }
disabled={ loading }
loading={ loading }
outline
>
{ 'Load more...' }
</Button>
}
</div>
);
SessionListFooter.displayName = 'SessionListFooter';
export default connect(state => ({
loading: state.getIn([ 'sessions', 'loading' ])
}))(SessionListFooter);

View file

@ -1,78 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import SortDropdown from '../Filters/SortDropdown';
import { numberWithCommas } from 'App/utils';
import SelectDateRange from 'Shared/SelectDateRange';
import { applyFilter } from 'Duck/search';
import Record from 'Types/app/period';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { moment } from 'App/dateRange';
const sortOptionsMap = {
'startTs-desc': 'Newest',
'startTs-asc': 'Oldest',
'eventsCount-asc': 'Events Ascending',
'eventsCount-desc': 'Events Descending',
};
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
function SessionListHeader({ activeTab, count, applyFilter, filter }) {
const { settingsStore } = useStore();
const label = useObserver(() => settingsStore.sessionSettings.timezone.label);
const getTimeZoneOffset = React.useCallback(() => {
return label.slice(-6);
}, [label]);
const { startDate, endDate, rangeValue } = filter;
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() });
const onDateChange = (e) => {
const dateValues = e.toJSON();
dateValues.startDate = moment(dateValues.startDate).utcOffset(getTimeZoneOffset(), true).valueOf();
dateValues.endDate = moment(dateValues.endDate).utcOffset(getTimeZoneOffset(), true).valueOf();
applyFilter(dateValues);
};
React.useEffect(() => {
if (label) {
const dateValues = period.toJSON();
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
// applyFilter(dateValues);
}
}, [label]);
return (
<div className="flex mb-2 justify-between items-end">
<div className="flex items-baseline">
<h3 className="text-2xl capitalize">
<span>{activeTab.name}</span>
<span className="ml-2 font-normal color-gray-medium">{count ? numberWithCommas(count) : 0}</span>
</h3>
{
<div className="ml-3 flex items-center">
<span className="mr-2 color-gray-medium">Sessions Captured in</span>
<SelectDateRange period={period} onChange={onDateChange} timezone={getTimeZoneOffset()} />
</div>
}
</div>
<div className="flex items-center">
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Sort By</span>
<SortDropdown options={sortOptions} />
</div>
</div>
</div>
);
}
export default connect(
(state) => ({
activeTab: state.getIn(['search', 'activeTab']),
period: state.getIn(['search', 'period']),
filter: state.getIn(['search', 'instance']),
}),
{ applyFilter }
)(SessionListHeader);

View file

@ -1 +0,0 @@
export { default } from './SessionList';

View file

@ -1,12 +0,0 @@
.customMessage {
padding: 5px 10px !important;
box-shadow: none !important;
font-size: 12px !important;
color: $gray-medium !important;
font-weight: 300;
display: flex;
justify-content: center;
& > div {
flex: none !important;
}
}

View file

@ -1,18 +0,0 @@
@import 'mixins.css';
.pageLoading {
display: flex;
flex-flow: column;
align-items: center;
margin: 20px 0 30px;
}
.loadMoreButton {
@mixin basicButton;
}
.countInfo {
font-size: 10px;
color: #999;
margin-bottom: 10px;
}

View file

@ -1,72 +0,0 @@
import React from 'react'
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, Tooltip } from 'UI'
import stl from './sessionMenu.module.css';
import { clearEvents } from 'Duck/filters';
import { issues_types } from 'Types/session/issue'
import { fetchList as fetchSessionList } from 'Duck/sessions';
import { useModal } from 'App/components/Modal';
import SessionSettings from 'Shared/SessionSettings/SessionSettings'
function SessionsMenu(props) {
const { activeTab, isEnterprise } = props;
const { showModal } = useModal();
const onMenuItemClick = (filter) => {
props.onMenuItemClick(filter)
}
return (
<div className={stl.wrapper}>
<div className={ cn(stl.header, 'flex items-center') }>
<div className={ stl.label }>
<span>Sessions</span>
</div>
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>
<Tooltip
title={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
>
Settings
</Tooltip>
</span>
</div>
<div>
<SideMenuitem
active={activeTab.type === 'all'}
title="All"
iconName="play-circle"
onClick={() => onMenuItemClick({ name: 'All', type: 'all' })}
/>
</div>
{ issues_types.filter(item => item.visible).map(item => (
<SideMenuitem
key={item.key}
active={activeTab.type === item.type}
title={item.name} iconName={item.icon}
onClick={() => onMenuItemClick(item)}
/>
))}
<div className={cn(stl.divider, 'my-4')} />
<SideMenuitem
title={ isEnterprise ? "Vault" : "Bookmarks" }
iconName={ isEnterprise ? "safe" : "star" }
active={activeTab.type === 'bookmark'}
onClick={() => onMenuItemClick({ name: isEnterprise ? 'Vault' : 'Bookmarks', type: 'bookmark', description: isEnterprise ? 'Sessions saved to vault never get\'s deleted from records.' : '' })}
/>
</div>
)
}
export default connect(state => ({
activeTab: state.getIn([ 'search', 'activeTab' ]),
captureRate: state.getIn(['watchdogs', 'captureRate']),
filters: state.getIn([ 'filters', 'appliedFilter' ]),
sessionsLoading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
}), {
clearEvents, fetchSessionList
})(SessionsMenu);

View file

@ -1 +0,0 @@
export { default } from './SessionsMenu';

View file

@ -1,29 +0,0 @@
.header {
margin-bottom: 15px;
& .label {
text-transform: uppercase;
color: gray;
letter-spacing: 0.2em;
}
& .manageButton {
margin-left: 5px;
font-size: 12px;
color: $teal;
cursor: pointer;
padding: 2px 5px;
border: solid thin transparent;
border-radius: 3px;
margin-bottom: -3px;
&:hover {
background-color: $gray-light;
color: $gray-darkest;
}
}
}
.divider {
height: 1px;
width: 100%;
background-color: $gray-light;
}

View file

@ -1,27 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import stl from './tabItem.module.css';
const TabItem = ({ icon, label, count, iconColor = 'teal', active = false, leading, ...rest }) => {
return (
<div
className={
count === 0 ? stl.disabled : '',
cn(stl.wrapper,
active ? stl.active : '',
"flex items-center py-2 justify-between")
}
{ ...rest }
>
<div className="flex items-center">
{ icon && <Icon name={ icon } size="16" color={ iconColor } /> }
<span className="ml-3 mr-1">{ label }</span>
{ count && <span>({ count })</span>}
</div>
{ !!leading && leading }
</div>
);
}
export default TabItem;

View file

@ -1 +0,0 @@
export { default } from './TabItem'

View file

@ -1,23 +0,0 @@
.wrapper {
color: $teal;
cursor: pointer;
padding: 5px;
border: solid thin transparent;
border-radius: 3px;
margin-left: -5px;
&.active,
&:hover {
background-color: $active-blue;
border-color: $active-blue-border;
& .actionWrapper {
opacity: 1;
}
}
}
.disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}

View file

@ -1,44 +0,0 @@
@import 'mixins.css';
.searchWrapper {
flex: 1;
border-radius: 3px;
margin-bottom: 30px;
}
.bottom {
display: flex;
align-items: center;
border-top: solid thin #EDEDED;
& > div {
cursor: pointer;
padding: 0 10px;
border-right: solid thin $gray-light;
&:hover {
background-color: $active-blue;
}
&:last-child {
border-right: solid thin transparent;
}
&:first-child {
flex: 1;
text-align: center;
}
}
}
.savedSearchesWrapper {
width: 200px;
margin-left: 20px;
}
.header {
text-transform: uppercase;
font-size: 12px;
margin-bottom: 10px;
letter-spacing: 1px;
color: $gray-medium;
}

View file

@ -1,70 +0,0 @@
import { storiesOf } from '@storybook/react';
import SessionsMenu from './SessionsMenu/SessionsMenu';
import SessionItem from 'Shared/SessionItem';
import SessionStack from 'Shared/SessionStack';
import Session from 'Types/session';
import SessionListHeader from './SessionList/SessionListHeader';
import SavedFilter from 'Types/filter/savedFilter';
import { List } from 'immutable';
var items = [
{
"watchdogId": 140,
"projectId": 1,
"type": "errors",
"payload": {
"threshold": 0,
"captureAll": true
}
},
{
"watchdogId": 139,
"projectId": 1,
"type": "bad_request",
"payload": {
"threshold": 0,
"captureAll": true
}
},
]
var session = Session({
"projectId": 1,
"sessionId": "2236890417118217",
"userUuid": "1e4bec88-fe8d-4f51-9806-716e92384ffc",
"userId": null,
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"userOs": "Mac OS X",
"userBrowser": "Chrome",
"userDevice": "Mac",
"userCountry": "FR",
"startTs": 1584132239030,
"duration": 618469,
"eventsCount": 24,
"pagesCount": 18,
"errorsCount": 0,
"watchdogs": [
137,
143
],
"favorite": false,
"viewed": false
})
var savedFilters = [
SavedFilter({filterId: 1, name: 'Something', count: 10, watchdogs: []})
]
storiesOf('Bug Finder', module)
.add('Sessions Menu', () => (
<SessionsMenu items={ items } />
))
.add('Sessions Item', () => (
<SessionItem key={1} session={session}/>
))
.add('Session List Header', () => (
<SessionListHeader />
))
.add('Sessions Stack', () => (
<SessionStack flow={savedFilters[0]} />
))

View file

@ -1,24 +0,0 @@
.wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
border: solid thin rgba(34, 36, 38, 0.15) !important;
border-radius: 4px;
padding: 0 10px;
width: 150px;
color: $gray-darkest;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.1) !important;
&:hover {
background-color: white;
}
& span {
margin-right: 5px;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View file

@ -1 +0,0 @@
export { default } from './BugFinder';

View file

@ -1,18 +0,0 @@
@import 'icons.css';
.notes {
margin: 15px 0;
font-weight: 300;
}
.tipIcon {
@mixin icon lightbulb, $gray-medium, 13px;
margin-right: 5px;
}
.tipText {
display: flex;
align-items: center;
color: $gray-medium;
font-size: 12px;
}

View file

@ -1,7 +0,0 @@
.header {
padding: 3px 10px;
letter-spacing: 1.5px;
color: $gray-medium;
font-size: 12px;
text-transform: uppercase;
}

View file

@ -1,9 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { Switch, Route, Redirect } from 'react-router';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
import { fetchList as fetchMemberList } from 'Duck/member';
import ProfileSettings from './ProfileSettings';
import Integrations from './Integrations';
@ -18,7 +16,6 @@ import PreferencesMenu from './PreferencesMenu';
import Notifications from './Notifications';
import Roles from './Roles';
@connect(null, { fetchMemberList, })
@withRouter
export default class Client extends React.PureComponent {
constructor(props){

View file

@ -1,50 +0,0 @@
import React, { useEffect } from 'react';
import cn from 'classnames';
import stl from './notifications.module.css';
import { Checkbox, Toggler } from 'UI';
import { connect } from 'react-redux';
import { withRequest } from 'HOCs';
import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config';
import withPageTitle from 'HOCs/withPageTitle';
function Notifications(props) {
const { config } = props;
useEffect(() => {
props.fetchConfig();
}, []);
const onChange = () => {
const _config = { weeklyReport: !config.weeklyReport };
props.editConfig(_config);
props.saveConfig(_config);
};
return (
<div className="p-5">
<div className={stl.tabHeader}>{<h3 className={cn(stl.tabTitle, 'text-2xl')}>{'Notifications'}</h3>}</div>
<div className="">
<div className="text-lg font-medium">Weekly project summary</div>
<div className="mb-4">Receive wekly report for each project on email.</div>
<Toggler checked={config.weeklyReport} name="test" onChange={onChange} label={config.weeklyReport ? 'Yes' : 'No'} />
{/* <Checkbox
name="isPublic"
className="font-medium"
type="checkbox"
checked={config.weeklyReport}
onClick={onChange}
className="mr-8"
label="Send me a weekly report for each project."
/> */}
{/* <img src="/assets/img/img-newsletter.png" style={{ width: '400px'}}/> */}
</div>
</div>
);
}
export default connect(
(state) => ({
config: state.getIn(['config', 'options']),
}),
{ fetchConfig, editConfig, saveConfig }
)(withPageTitle('Notifications - OpenReplay Preferences')(Notifications));

View file

@ -0,0 +1,39 @@
import React, { useEffect } from 'react';
import cn from 'classnames';
import stl from './notifications.module.css';
import { Toggler } from 'UI';
import { useStore } from "App/mstore";
import { observer } from 'mobx-react-lite'
import withPageTitle from 'HOCs/withPageTitle';
function Notifications() {
const { weeklyReportStore } = useStore()
useEffect(() => {
void weeklyReportStore.fetchReport()
}, []);
const onChange = () => {
const newValue = !weeklyReportStore.weeklyReport
void weeklyReportStore.fetchEditReport(newValue)
};
return (
<div className="p-5">
<div className={stl.tabHeader}>{<h3 className={cn(stl.tabTitle, 'text-2xl')}>{'Notifications'}</h3>}</div>
<div className="">
<div className="text-lg font-medium">Weekly project summary</div>
<div className="mb-4">Receive weekly report for each project on email.</div>
<Toggler
checked={weeklyReportStore.weeklyReport}
name="test"
onChange={onChange}
label={weeklyReportStore.weeklyReport ? 'Yes' : 'No'}
/>
</div>
</div>
);
}
export default withPageTitle('Notifications - OpenReplay Preferences')(observer(Notifications))

View file

@ -9,7 +9,6 @@ import {
init,
edit,
remove,
setAlertMetricId,
setActiveWidget,
updateActiveState,
} from 'Duck/customMetrics';
@ -181,7 +180,6 @@ export default connect(
{
remove,
setShowAlerts,
setAlertMetricId,
edit,
setActiveWidget,
updateActiveState,

View file

@ -1,28 +0,0 @@
import React from 'react';
import { Loader, NoContent } from 'UI';
import { widgetHOC, SessionLine } from '../common';
@widgetHOC('sessionsPerformance', { fitContent: true })
export default class LastFeedbacks extends React.PureComponent {
render() {
const { data: sessions, loading } = this.props;
return (
<Loader loading={ loading } size="small">
<NoContent
title="No data available."
size="small"
show={ sessions.size === 0 }
>
{ sessions.map(({ sessionId, missedResources }) => (
<SessionLine
sessionId={ sessionId }
icon="file"
info={ missedResources.get(0).name }
subInfo="Missing File"
/>
))}
</NoContent>
</Loader>
);
}
}

View file

@ -1 +0,0 @@
export { default } from './LastPerformance';

View file

@ -1,14 +1,5 @@
import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {
edit,
updateSeries,
addSeriesFilterFilter,
removeSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
} from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { Button, Icon } from 'UI';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SeriesName from './SeriesName';
@ -18,21 +9,22 @@ import { observer } from 'mobx-react-lite';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
addSeriesFilterFilter: typeof addSeriesFilterFilter;
editSeriesFilterFilter: typeof editSeriesFilterFilter;
editSeriesFilter: typeof editSeriesFilter;
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
}
function FilterSeries(props: Props) {
const { observeChanges = () => {}, canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
const {
observeChanges = () => {
},
canDelete,
hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.'
} = props;
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
@ -46,7 +38,7 @@ function FilterSeries(props: Props) {
observeChanges()
}
const onChangeEventsOrder = (e, { name, value }: any) => {
const onChangeEventsOrder = (_: any, { name, value }: any) => {
series.filter.updateKey(name, value)
observeChanges()
}
@ -60,11 +52,11 @@ function FilterSeries(props: Props) {
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name) } />
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name)} />
</div>
<div className="flex items-center cursor-pointer">
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
<div onClick={props.onRemoveSeries} className={cn("ml-3", { 'disabled': !canDelete })}>
<Icon name="trash" size="16" />
</div>
@ -73,17 +65,17 @@ function FilterSeries(props: Props) {
</div>
</div>
</div>
{ expanded && (
{expanded && (
<>
<div className="p-5">
{ series.filter.filters.length > 0 ? (
{series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
) : (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
@ -103,11 +95,4 @@ function FilterSeries(props: Props) {
);
}
export default connect(null, {
edit,
updateSeries,
addSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
removeSeriesFilterFilter,
})(observer(FilterSeries));
export default observer(FilterSeries);

View file

@ -4,8 +4,7 @@ import { Set, List as ImmutableList } from "immutable";
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain, Pagination } from 'UI';
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
import { applyFilter } from 'Duck/filters';
import { IGNORED, RESOLVED, UNRESOLVED } from 'Types/errorInfo';
import SortDropdown from 'Components/BugFinder/Filters/SortDropdown';
import { IGNORED, UNRESOLVED } from 'Types/errorInfo';
import Divider from 'Components/Errors/ui/Divider';
import ListItem from './ListItem/ListItem';
import { debounce } from 'App/utils';

View file

@ -1,33 +1,30 @@
import React from 'react'
import EventsBlock from '../Session_/EventsBlock';
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import stl from './rightblock.module.css';
function RightBlock(props: any) {
const { activeTab } = props;
const { player, store } = React.useContext(PlayerContext)
const { eventListNow, playing } = store.get()
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const EventsBlockConnected = () => <EventsBlock playing={playing} player={player} setActiveTab={props.setActiveTab} currentTimeEventIndex={currentTimeEventIndex} />
const renderActiveTab = (tab: string) => {
switch(tab) {
case props.tabs.EVENTS:
return <EventsBlockConnected />
case props.tabs.CLICKMAP:
return <PageInsightsPanel setActiveTab={props.setActiveTab} />
}
if (activeTab === props.tabs.EVENTS) {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<EventsBlock
setActiveTab={props.setActiveTab}
/>
</div>
)
}
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
{renderActiveTab(activeTab)}
</div>
)
if (activeTab === props.tabs.HEATMAPS) {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<PageInsightsPanel setActiveTab={props.setActiveTab} />
</div>
)
}
return null
}
export default observer(RightBlock)
export default RightBlock

View file

@ -104,7 +104,7 @@ function WebPlayer(props: any) {
contextValue.player.togglePlay();
};
if (!contextValue.player) return null;
if (!contextValue.player || !session) return null;
return (
<PlayerContext.Provider value={contextValue}>

View file

@ -1,12 +1,15 @@
import { createContext } from 'react';
import {
IWebPlayer,
IWebPlayerStore
IWebPlayerStore,
IWebLivePlayer,
IWebLivePlayerStore,
} from 'Player'
export interface IPlayerContext {
player: IWebPlayer
store: IWebPlayerStore,
player: IWebPlayer | IWebLivePlayer
store: IWebPlayerStore | IWebLivePlayerStore,
}
export const defaultContextValue: IPlayerContext = { player: undefined, store: undefined}
export const defaultContextValue = { player: undefined, store: undefined}
// @ts-ignore
export const PlayerContext = createContext<IPlayerContext>(defaultContextValue);

View file

@ -147,14 +147,6 @@ export default class Event extends React.PureComponent {
</button>
}
<div className={ cls.topBlock }>
{/* <div className={ cls.checkbox }>
<Checkbox
className="customCheckbox"
name={ event.key }
checked={ selected }
onClick={ onCheckboxClick }
/>
</div> */}
<div className={ cls.firstLine }>
{ this.renderBody() }
</div>

View file

@ -5,16 +5,10 @@ import { PlayerContext } from 'App/components/Session/playerContext';
function EventSearch(props) {
const { player } = React.useContext(PlayerContext)
const { onChange, clearSearch, value, header, setActiveTab } = props;
const { onChange, value, header, setActiveTab } = props;
const toggleEvents = () => player.toggleEvents()
useEffect(() => {
return () => {
clearSearch()
}
}, [])
return (
<div className="flex items-center w-full relative">
<div className="flex flex-1 flex-col">

View file

@ -1,249 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { Icon } from 'UI';
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized";
import { TYPES } from 'Types/session/event';
import { setSelected } from 'Duck/events';
import { setEventFilter, filterOutNote } from 'Duck/sessions';
import { show as showTargetDefiner } from 'Duck/components/targetDefiner';
import EventGroupWrapper from './EventGroupWrapper';
import styles from './eventsBlock.module.css';
import EventSearch from './EventSearch/EventSearch';
@connect(state => ({
session: state.getIn([ 'sessions', 'current' ]),
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
selectedEvents: state.getIn([ 'events', 'selected' ]),
targetDefinerDisplayed: state.getIn([ 'components', 'targetDefiner', 'isDisplayed' ]),
testsAvaliable: false,
}), {
showTargetDefiner,
setSelected,
setEventFilter,
filterOutNote
})
export default class EventsBlock extends React.Component {
state = {
editingEvent: null,
mouseOver: false,
query: ''
}
scroller = React.createRef();
cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 300
});
write = ({ target: { value, name } }) => {
const { filter } = this.state;
this.setState({ query: value })
this.props.setEventFilter({ query: value, filter })
setTimeout(() => {
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
clearSearch = () => {
const { filter } = this.state;
this.setState({ query: '' })
this.props.setEventFilter({ query: '', filter })
if (this.scroller.current) {
this.scroller.current.forceUpdateGrid();
}
setTimeout(() => {
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
onSetEventFilter = (e, { name, value }) => {
const { query } = this.state;
this.setState({ filter: value })
this.props.setEventFilter({ filter: value, query });
}
componentDidUpdate(prevProps) {
if (prevProps.targetDefinerDisplayed && !this.props.targetDefinerDisplayed) {
this.setState({ editingEvent: null });
}
if (prevProps.session !== this.props.session) { // Doesn't happen
this.cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 300
});
}
if (prevProps.currentTimeEventIndex !== this.props.currentTimeEventIndex &&
this.scroller.current !== null) {
this.scroller.current.forceUpdateGrid();
if (!this.state.mouseOver) {
this.scroller.current.scrollToRow(this.props.currentTimeEventIndex);
}
}
}
onCheckboxClick(e, event) {
e.stopPropagation();
const {
session: { events },
selectedEvents,
} = this.props;
this.props.player.pause();
let newSelectedSet;
const wasSelected = selectedEvents.contains(event);
if (wasSelected) {
newSelectedSet = selectedEvents.remove(event);
} else {
newSelectedSet = selectedEvents.add(event);
}
let selectNextLoad = false;
events.reverse().forEach((sessEvent) => {
if (sessEvent.type === TYPES.LOCATION) {
if (selectNextLoad) {
newSelectedSet = newSelectedSet.add(sessEvent);
}
selectNextLoad = false;
} else if (newSelectedSet.contains(sessEvent)) {
selectNextLoad = true;
}
});
this.props.setSelected(newSelectedSet);
}
onEventClick = (e, event) => this.props.player.jump(event.time)
onMouseOver = () => this.setState({ mouseOver: true })
onMouseLeave = () => this.setState({ mouseOver: false })
get eventsList() {
const { session: { notesWithEvents }, filteredEvents } = this.props
const usedEvents = filteredEvents || notesWithEvents
return usedEvents
}
renderGroup = ({ index, key, style, parent }) => {
const {
selectedEvents,
currentTimeEventIndex,
testsAvaliable,
playing,
eventsIndex,
filterOutNote,
} = this.props;
const { query } = this.state;
const _events = this.eventsList
const isLastEvent = index === _events.size - 1;
const isLastInGroup = isLastEvent || _events.get(index + 1).type === TYPES.LOCATION;
const event = _events.get(index);
const isNote = !!event.noteId
const isSelected = selectedEvents.includes(event);
const isCurrent = index === currentTimeEventIndex;
const isEditing = this.state.editingEvent === event;
const heightBug = index === 0 && event.type === TYPES.LOCATION && event.referrer ? { top: 2 } : {}
return (
<CellMeasurer
key={key}
cache={this.cache}
parent={parent}
rowIndex={index}
>
{({measure, registerChild}) => (
<div style={{ ...style, ...heightBug }} ref={registerChild}>
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}
onEventClick={ this.onEventClick }
onCheckboxClick={ this.onCheckboxClick }
event={ event }
isLastEvent={ isLastEvent }
isLastInGroup={ isLastInGroup }
isSelected={ isSelected }
isCurrent={ isCurrent }
isEditing={ isEditing }
showSelection={ testsAvaliable && !playing }
isNote={isNote}
filterOutNote={filterOutNote}
/>
</div>
)}
</CellMeasurer>
);
}
render() {
const { query } = this.state;
const {
session: {
events,
},
setActiveTab,
} = this.props;
const _events = this.eventsList
const isEmptySearch = query && (_events.size === 0 || !_events)
return (
<>
<div className={ cn(styles.header, 'p-4') }>
<div className={ cn(styles.hAndProgress, 'mt-3') }>
<EventSearch
onChange={this.write}
clearSearch={this.clearSearch}
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.size }</span></div>
}
/>
</div>
</div>
<div
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
id="eventList"
data-openreplay-masked
onMouseOver={ this.onMouseOver }
onMouseLeave={ this.onMouseLeave }
>
{isEmptySearch && (
<div className='flex items-center'>
<Icon name="binoculars" size={18} />
<span className='ml-2'>No Matching Results</span>
</div>
)}
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={this.scroller}
className={ styles.eventsList }
height={height + 10}
width={248}
overscanRowCount={6}
itemSize={230}
rowCount={_events.size}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
rowRenderer={this.renderGroup}
scrollToAlignment="start"
/>
)}
</AutoSizer>
</div>
</>
);
}
}

View file

@ -0,0 +1,185 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { Icon } from 'UI';
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
import { TYPES } from 'Types/session/event';
import { setEventFilter, filterOutNote } from 'Duck/sessions';
import EventGroupWrapper from './EventGroupWrapper';
import styles from './eventsBlock.module.css';
import EventSearch from './EventSearch/EventSearch';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { RootStore } from 'App/duck'
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
import { InjectedEvent } from 'Types/session/event'
import Session from 'Types/session'
interface IProps {
setEventFilter: (filter: { query: string }) => void
filteredEvents: InjectedEvent[]
setActiveTab: (tab?: string) => void
query: string
session: Session
filterOutNote: (id: string) => void
eventsIndex: number[]
}
function EventsBlock(props: IProps) {
const [mouseOver, setMouseOver] = React.useState(true)
const scroller = React.useRef<List>(null)
const cache = useCellMeasurerCache(undefined, {
fixedWidth: true,
defaultHeight: 300
});
const { store, player } = React.useContext(PlayerContext)
const { eventListNow, playing } = store.get()
const { session: { events, notesWithEvents }, filteredEvents,
eventsIndex,
filterOutNote,
query,
setActiveTab,
} = props
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const usedEvents = filteredEvents || notesWithEvents
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
props.setEventFilter({ query: value })
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
const clearSearch = () => {
props.setEventFilter({ query: '' })
if (scroller.current) {
scroller.current.forceUpdateGrid();
}
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
React.useEffect(() => {
return () => {
clearSearch()
}
}, [])
React.useEffect(() => {
if (scroller.current) {
scroller.current.forceUpdateGrid();
if (!mouseOver) {
scroller.current.scrollToRow(currentTimeEventIndex);
}
}
}, [currentTimeEventIndex])
const onEventClick = (_: React.MouseEvent, event: { time: number }) => player.jump(event.time)
const onMouseOver = () => setMouseOver(true)
const onMouseLeave = () => setMouseOver(false)
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: React.CSSProperties; parent: any }) => {
const isLastEvent = index === usedEvents.length - 1;
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
const event = usedEvents[index];
const isNote = 'noteId' in event
const isCurrent = index === currentTimeEventIndex;
const heightBug = index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {}
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
rowIndex={index}
>
{({measure, registerChild}) => (
<div style={{ ...style, ...heightBug }} ref={registerChild}>
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}
onEventClick={ onEventClick }
event={ event }
isLastEvent={ isLastEvent }
isLastInGroup={ isLastInGroup }
isCurrent={ isCurrent }
showSelection={ !playing }
isNote={isNote}
filterOutNote={filterOutNote}
/>
</div>
)}
</CellMeasurer>
);
}
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents)
return (
<>
<div className={ cn(styles.header, 'p-4') }>
<div className={ cn(styles.hAndProgress, 'mt-3') }>
<EventSearch
onChange={write}
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.length }</span></div>
}
/>
</div>
</div>
<div
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
id="eventList"
data-openreplay-masked
onMouseOver={ onMouseOver }
onMouseLeave={ onMouseLeave }
>
{isEmptySearch && (
<div className='flex items-center'>
<Icon name="binoculars" size={18} />
<span className='ml-2'>No Matching Results</span>
</div>
)}
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={scroller}
className={ styles.eventsList }
height={height + 10}
width={248}
overscanRowCount={6}
itemSize={230}
rowCount={usedEvents.length}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowRenderer={renderGroup}
scrollToAlignment="start"
/>
)}
</AutoSizer>
</div>
</>
);
}
export default connect((state: RootStore) => ({
session: state.getIn([ 'sessions', 'current' ]),
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
query: state.getIn(['sessions', 'eventsQuery']),
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
}), {
setEventFilter,
filterOutNote
})(observer(EventsBlock))

View file

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import MetadataItem from './MetadataItem';
export default connect(state => ({
metadata: state.getIn([ 'sessions', 'current', 'metadata' ]),
metadata: state.getIn([ 'sessions', 'current' ]).metadata,
}))(function Metadata ({ metadata }) {
const metaLenth = Object.keys(metadata).length;

View file

@ -7,7 +7,7 @@ import stl from './sessionList.module.css';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect((state) => ({
currentSessionId: state.getIn(['sessions', 'current', 'sessionId']),
currentSessionId: state.getIn(['sessions', 'current']).sessionId,
}))
class SessionList extends React.PureComponent {
render() {
@ -17,7 +17,7 @@ class SessionList extends React.PureComponent {
.map(({ sessions, ...rest }) => {
return {
...rest,
sessions: sessions.map(Session).filter(({ sessionId }) => sessionId !== currentSessionId),
sessions: sessions.map(s => new Session(s)).filter(({ sessionId }) => sessionId !== currentSessionId),
};
})
.filter((site) => site.sessions.length > 0);

View file

@ -53,5 +53,5 @@ export default connect(state => ({
users: state.getIn(['assignments', 'users']),
loading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']),
issuesIntegration: state.getIn([ 'issues', 'list']).first() || {},
issuesIntegration: state.getIn([ 'issues', 'list'])[0] || {},
}))(IssueDetails);

View file

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Form, Input, Button, CircularLoader, Loader } from 'UI';
//import { } from 'Duck/issues';
import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assignments';
import Select from 'Shared/Select';
@ -119,7 +118,6 @@ class IssueForm extends React.PureComponent {
selection
name="assignee"
options={userOptions}
// value={ instance.assignee }
fluid
onChange={this.writeOption}
placeholder="Select a user"
@ -153,7 +151,7 @@ class IssueForm extends React.PureComponent {
<Button
loading={creating}
variant="primary"
disabled={!instance.validate()}
disabled={!instance.isValid}
className="float-left mr-2"
type="submit"
>

View file

@ -1,9 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { Icon, Popover, Button } from 'UI';
import { Popover, Button } from 'UI';
import IssuesModal from './IssuesModal';
import { fetchProjects, fetchMeta } from 'Duck/assignments';
import stl from './issues.module.css';
@connect(
(state) => ({
@ -15,9 +14,7 @@ import stl from './issues.module.css';
fetchIssueLoading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
fetchIssuesLoading: state.getIn(['assignments', 'fetchAssignments', 'loading']),
projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']),
issuesIntegration: state.getIn(['issues', 'list']).first() || {},
jiraConfig: state.getIn(['issues', 'list']).first(),
issuesIntegration: state.getIn(['issues', 'list']) || {},
issuesFetched: state.getIn(['issues', 'issuesFetched']),
}),
{ fetchMeta, fetchProjects }
@ -58,13 +55,9 @@ class Issues extends React.Component {
render() {
const {
sessionId,
isModalDisplayed,
projectsLoading,
metaLoading,
fetchIssuesLoading,
issuesIntegration,
} = this.props;
const provider = issuesIntegration.provider;
const provider = issuesIntegration.first()?.provider || '';
return (
<Popover
@ -80,13 +73,6 @@ class Issues extends React.Component {
Create Issue
</Button>
</div>
{/* <div
className="flex items-center cursor-pointer"
disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}
>
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size="16" />
<span className="ml-2 whitespace-nowrap">Create Issue</span>
</div> */}
</Popover>
);
}

View file

@ -132,7 +132,7 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
export default connect(
(state: any) => ({
issuesList: state.getIn(['sessions', 'current', 'issues']),
issuesList: state.getIn(['sessions', 'current']).issues,
}),
{
toggleBottomBlock,

View file

@ -492,5 +492,5 @@ function Performance({
}
export const ConnectedPerformance = connect((state: any) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
userDeviceHeapSize: state.getIn(['sessions', 'current']).userDeviceHeapSize,
}))(observer(Performance));

View file

@ -1,435 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import {
connectPlayer,
STORAGE_TYPES,
selectStorageType,
selectStorageListNow,
} from 'Player';
import LiveTag from 'Shared/LiveTag';
import { jumpToLive } from 'Player';
import { Icon, Tooltip } from 'UI';
import { toggleInspectorMode } from 'Player';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
INSPECTOR,
} from 'Duck/components/player';
import { AssistDuration } from './Time';
import Timeline from './Timeline';
import ControlButton from './ControlButton';
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';
import XRayButton from 'Shared/XRayButton';
const SKIP_INTERVALS = {
2: 2e3,
5: 5e3,
10: 1e4,
15: 15e3,
20: 2e4,
30: 3e4,
60: 6e4,
};
function getStorageName(type) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.ZUSTAND:
return 'ZUSTAND';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
@connectPlayer((state) => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
// logCount: state.logList.length,
logRedCount: state.logMarkedCount,
showExceptions: state.exceptionsList.length > 0,
resourceRedCount: state.resourceMarkedCount,
fetchRedCount: state.fetchMarkedCount,
showStack: state.stackList.length > 0,
stackCount: state.stackList.length,
stackRedCount: state.stackMarkedCount,
profilesCount: state.profilesList.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCount,
graphqlCount: state.graphqlList.length,
liveTimeTravel: state.liveTimeTravel,
}))
@connect(
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage:
props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
}
)
export default class Controls extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
// nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
nextProps.skipInterval !== this.props.skipInterval
)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
};
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(1, time - SKIP_INTERVALS[skipInterval]));
};
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
return (
<Tooltip
title={label}
className="mr-4"
>
<div
onClick={this.props.togglePlay}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
>
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
<div
onClick={action}
className={cn('py-2 px-2 hover-main cursor-pointer bg-gray-lightest', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
);
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logRedCount,
showExceptions,
resourceRedCount,
fetchRedCount,
showStack,
stackRedCount,
showStorage,
storageType,
showProfiler,
showGraphql,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
skipInterval,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={styles.controls}>
<Timeline
live={live}
jump={this.props.jump}
liveTimeTravel={liveTimeTravel}
pause={this.props.pause}
togglePlay={this.props.togglePlay}
/>
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}
/>
<div className={cn('mx-2')} />
<XRayButton
isActive={bottomBlock === OVERVIEW && !inspectorMode}
onClick={() => toggleBottomTools(OVERVIEW)}
/>
</>
)}
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
</div>
)}
</div>
<div className="flex items-center h-full">
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
hasErrors={logRedCount > 0 || showExceptions}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0 || fetchRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
16,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
</div>
);
}
}

View file

@ -386,7 +386,7 @@ export default connect(
session: state.getIn(['sessions', 'current']),
totalAssistSessions: state.getIn(['liveSearch', 'total']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current']).live,
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},

View file

@ -12,6 +12,7 @@ import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { DateTime, Duration } from 'luxon';
import Issue from "Types/session/issue";
function getTimelinePosition(value: number, scale: number) {
const pos = value * scale;
@ -19,7 +20,14 @@ function getTimelinePosition(value: number, scale: number) {
return pos > 100 ? 99 : pos;
}
function Timeline(props) {
interface IProps {
issues: Issue[]
setTimelineHoverTime: (t: number) => void
startedAt: number
tooltipVisible: boolean
}
function Timeline(props: IProps) {
const { player, store } = useContext(PlayerContext)
const [wasPlaying, setWasPlaying] = useState(false)
const { notesStore, settingsStore } = useStore();
@ -35,17 +43,17 @@ function Timeline(props) {
live,
liveTimeTravel,
} = store.get()
const { issues } = props;
const notes = notesStore.sessionNotes
const progressRef = useRef<HTMLDivElement>()
const timelineRef = useRef<HTMLDivElement>()
const progressRef = useRef<HTMLDivElement>(null)
const timelineRef = useRef<HTMLDivElement>(null)
const scale = 100 / endTime;
useEffect(() => {
const { issues } = props;
const firstIssue = issues.get(0);
const firstIssue = issues[0];
if (firstIssue && skipToIssue) {
player.jump(firstIssue.time);
@ -64,7 +72,7 @@ function Timeline(props) {
};
const onDrag: OnDragCallback = (offset) => {
if (live && !liveTimeTravel) return;
if ((live && !liveTimeTravel) || !progressRef.current) return;
const p = (offset.x) / progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
@ -76,7 +84,7 @@ function Timeline(props) {
}
};
const getLiveTime = (e) => {
const getLiveTime = (e: React.MouseEvent) => {
const duration = new Date().getTime() - props.startedAt;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * duration), 0);
@ -84,7 +92,7 @@ function Timeline(props) {
return [time, duration];
};
const showTimeTooltip = (e) => {
const showTimeTooltip = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target !== progressRef.current && e.target !== timelineRef.current) {
return props.tooltipVisible && hideTimeTooltip();
}
@ -118,13 +126,13 @@ function Timeline(props) {
debouncedTooltipChange(timeLineTooltip);
};
const seekProgress = (e) => {
const seekProgress = (e: React.MouseEvent<HTMLDivElement>) => {
const time = getTime(e);
player.jump(time);
hideTimeTooltip();
};
const loadAndSeek = async (e) => {
const loadAndSeek = async (e: React.MouseEvent<HTMLDivElement>) => {
e.persist();
await player.toggleTimetravel();
@ -133,7 +141,7 @@ function Timeline(props) {
});
};
const jumpToTime: React.MouseEventHandler = (e) => {
const jumpToTime = (e: React.MouseEvent<HTMLDivElement>) => {
if (live && !liveTimeTravel) {
loadAndSeek(e);
} else {
@ -141,7 +149,7 @@ function Timeline(props) {
}
};
const getTime = (e: React.MouseEvent, customEndTime?: number) => {
const getTime = (e: React.MouseEvent<HTMLDivElement>, customEndTime?: number) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const targetTime = customEndTime || endTime;
const time = Math.max(Math.round(p * targetTime), 0);
@ -161,7 +169,7 @@ function Timeline(props) {
>
<div
className={stl.progress}
onClick={ready ? jumpToTime : null }
onClick={ready ? jumpToTime : undefined }
ref={progressRef}
role="button"
onMouseMoveCapture={showTimeTooltip}
@ -197,11 +205,19 @@ function Timeline(props) {
{events.map((e) => (
<div
/*@ts-ignore TODO */
key={e.key}
className={stl.event}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/>
))}
{issues.map((i: Issue) => (
<div
key={i.key}
className={stl.redEvent}
style={{ left: `${getTimelinePosition(i.time, scale)}%` }}
/>
))}
{notes.map((note) => note.timestamp > 0 ? (
<div
key={note.noteId}
@ -224,9 +240,9 @@ function Timeline(props) {
}
export default connect(
(state) => ({
issues: state.getIn(['sessions', 'current', 'issues']),
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
(state: any) => ({
issues: state.getIn(['sessions', 'current']).issues || [],
startedAt: state.getIn(['sessions', 'current']).startedAt || 0,
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }

View file

@ -298,7 +298,7 @@ export default connect(
} = state.getIn(['sessions', 'createNoteTooltip']);
const slackChannels = state.getIn(['slack', 'list']);
const teamsChannels = state.getIn(['teams', 'list']);
const sessionId = state.getIn(['sessions', 'current', 'sessionId']);
const sessionId = state.getIn(['sessions', 'current']).sessionId;
return { isVisible, time, sessionId, isEdit, editNote, slackChannels, teamsChannels };
},
{ setCreateNoteTooltip, addNote, updateNote, fetchSlack, fetchTeams }

View file

@ -73,6 +73,22 @@
.event.location {
background: $blue;
} */
.redEvent {
position: absolute;
width: 2px;
height: 10px;
background: $red;
z-index: 3;
pointer-events: none;
/* top: 0; */
/* bottom: 0; */
/* &:hover {
width: 10px;
height: 10px;
margin-left: -6px;
z-index: 1;
};*/
}
.markup {
position: absolute;

View file

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { findDOMNode } from 'react-dom';
import cn from 'classnames';
import { EscapeButton } from 'UI';
import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner';
import {
NONE,
CONSOLE,
@ -109,15 +108,14 @@ export default connect(
return {
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
nextId: state.getIn(['sessions', 'nextId']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
sessionId: state.getIn(['sessions', 'current']).sessionId,
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
closedLive:
!!state.getIn(['sessions', 'errors']) ||
(isAssist && !state.getIn(['sessions', 'current', 'live'])),
(isAssist && !state.getIn(['sessions', 'current']).live),
};
},
{
hideTargetDefiner,
fullscreenOff,
updateLastPlayedSession,
}

View file

@ -8,9 +8,9 @@ import styles from './playerBlock.module.css';
@connect((state) => ({
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
sessionId: state.getIn(['sessions', 'current']).sessionId,
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
jiraConfig: state.getIn(['issues', 'list']).first(),
jiraConfig: state.getIn(['issues', 'list'])[0],
}))
export default class PlayerBlock extends React.PureComponent {
render() {

View file

@ -120,5 +120,5 @@ function ScreenRecorder({
export default connect((state: any) => ({
siteId: state.getIn(['site', 'siteId']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
sessionId: state.getIn(['sessions', 'current']).sessionId,
}))(observer(ScreenRecorder))

View file

@ -179,9 +179,9 @@ export default class TimeTable extends React.PureComponent<Props, State> {
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render ? render(row) : row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
{columns.map((column, key) => (
<div key={column.label.replace(' ', '')} className={stl.cell} style={{ width: `${column.width}px` }}>
{column.render ? column.render(row) : row[column.dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
@ -262,7 +262,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width }) => (
<div className={stl.headerCell} style={{ width: `${width}px` }}>
<div key={label.replace(' ', '')} className={stl.headerCell} style={{ width: `${width}px` }}>
{label}
</div>
))}
@ -282,14 +282,15 @@ export default class TimeTable extends React.PureComponent<Props, State> {
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
{visibleRefLines.map((line, key) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
key={line.time+key}
className={cn(stl.refLine, `bg-${line.color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
left: `${percentOf(line.time - timestart, timewidth)}%`,
cursor: typeof line.onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
onClick={line.onClick}
/>
))}
</div>

View file

@ -1,55 +0,0 @@
import React from 'react';
import { Button, NoContent } from 'UI';
import { connect } from 'react-redux';
import { fetchList, setLastRead } from 'Duck/announcements';
import cn from 'classnames';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import ListItem from './ListItem'
interface Props {
unReadNotificationsCount: number;
setLastRead: Function;
list: any;
}
function AnnouncementModal(props: Props) {
const { list, unReadNotificationsCount } = props;
// const onClear = (notification: any) => {
// console.log('onClear', notification);
// props.setViewed(notification.notificationId)
// }
return (
<div className="bg-white box-shadow h-screen overflow-y-auto" style={{ width: '450px'}}>
<div className="flex items-center justify-between p-5 text-2xl">
<div>Announcements</div>
</div>
<div className="pb-5">
<NoContent
title={
<div className="flex items-center justify-between">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="100" />
</div>
}
subtext="There are no alerts to show."
// show={ !loading && unReadNotificationsCount === 0 }
size="small"
>
{list.map((item: any, i: any) => (
<div className="border-b" key={i}>
{/* <ListItem alert={item} onClear={() => onClear(item)} loading={false} /> */}
</div>
))}
</NoContent>
</div>
</div>
);
}
export default connect((state: any) => ({
list: state.getIn(['announcements', 'list']),
}), {
fetchList,
setLastRead,
})(AnnouncementModal);

View file

@ -1,31 +0,0 @@
import React from 'react';
import { Button, Label } from 'UI';
import stl from './listItem.module.css';
const ListItem = ({ announcement, onButtonClick }) => {
return (
<div className={stl.wrapper}>
<div className="flex justify-between items-center mb-3">
<div className="text-sm">{announcement.createdAt && announcement.createdAt.toFormat('LLL dd, yyyy')}</div>
<Label><span className="capitalize">{announcement.type}</span></Label>
</div>
{announcement.imageUrl &&
<img className="w-full border mb-3" src={announcement.imageUrl} />
}
<div>
<h2 className="text-xl mb-2">{announcement.title}</h2>
<div className="mb-2 text-sm text-justify">{announcement.description}</div>
{announcement.buttonUrl &&
<Button
variant="outline"
onClick={() => onButtonClick(announcement.buttonUrl) }
>
<span className="capitalize">{announcement.buttonText}</span>
</Button>
}
</div>
</div>
)
}
export default ListItem

View file

@ -1 +0,0 @@
export { default } from './ListItem';

View file

@ -1,5 +0,0 @@
.wrapper {
background-color: white;
margin-bottom: 20px;
padding: 15px;
}

View file

@ -1 +0,0 @@
export { default } from './AnnouncementModal';

View file

@ -65,7 +65,7 @@ function Bookmark(props: Props) {
export default connect(
(state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
favorite: state.getIn(['sessions', 'current', 'favorite']),
favorite: state.getIn(['sessions', 'current']).favorite,
}),
{ toggleFavorite }
)(Bookmark);

View file

@ -1,112 +0,0 @@
import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import {
edit,
updateSeries,
addSeriesFilterFilter,
removeSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
} from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { IconButton, Icon } from 'UI';
import FilterSelection from '../../Filters/FilterSelection';
import SeriesName from './SeriesName';
import cn from 'classnames';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex) => void;
canDelete?: boolean;
addSeriesFilterFilter: typeof addSeriesFilterFilter;
editSeriesFilterFilter: typeof editSeriesFilterFilter;
editSeriesFilter: typeof editSeriesFilter;
removeSeriesFilterFilter: typeof removeSeriesFilterFilter;
hideHeader?: boolean;
emptyMessage?: any;
}
function FilterSeries(props: Props) {
const { canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props;
const [expanded, setExpanded] = useState(true)
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
filter.value = [""]
if (filter.hasOwnProperty('filters') && Array.isArray(filter.filters)) {
filter.filters = filter.filters.map(i => ({ ...i, value: [""] }))
}
props.addSeriesFilterFilter(seriesIndex, filter);
}
const onUpdateFilter = (filterIndex, filter) => {
props.editSeriesFilterFilter(seriesIndex, filterIndex, filter);
}
const onChangeEventsOrder = (e, { name, value }) => {
props.editSeriesFilter(seriesIndex, { eventsOrder: value });
}
const onRemoveFilter = (filterIndex) => {
props.removeSeriesFilterFilter(seriesIndex, filterIndex);
}
return (
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
</div>
<div className="flex items-center cursor-pointer" >
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
<Icon name="trash" size="16" />
</div>
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
{ expanded && (
<>
<div className="p-5">
{ series.filter.filters.size > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
</div>
</>
)}
</div>
);
}
export default connect(null, {
edit,
updateSeries,
addSeriesFilterFilter,
editSeriesFilterFilter,
editSeriesFilter,
removeSeriesFilterFilter,
})(FilterSeries);

View file

@ -1,57 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { Icon } from 'UI';
interface Props {
name: string;
onUpdate: (name) => void;
seriesIndex?: number;
}
function SeriesName(props: Props) {
const { seriesIndex = 1 } = props;
const [editing, setEditing] = useState(false)
const [name, setName] = useState(props.name)
const ref = useRef<any>(null)
const write = ({ target: { value, name } }) => {
setName(value)
}
const onBlur = () => {
setEditing(false)
props.onUpdate(name)
}
useEffect(() => {
if (editing) {
ref.current.focus()
}
}, [editing])
useEffect(() => {
setName(props.name)
}, [props.name])
// const { name } = props;
return (
<div className="flex items-center">
{ editing ? (
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2 h-8"
value={name}
// readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
) : (
<div className="text-base h-8 flex items-center border-transparent">{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }</div>
)}
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
</div>
);
}
export default SeriesName;

View file

@ -1 +0,0 @@
export { default } from './SeriesName';

View file

@ -1 +0,0 @@
export { default } from './FilterSeries'

View file

@ -12,7 +12,7 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD
import { useModal } from 'App/components/Modal';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
import useCellMeasurerCache from '../useCellMeasurerCache'
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
const ALL = 'ALL';
const INFO = 'INFO';

View file

@ -384,5 +384,5 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
}
export default connect((state: any) => ({
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
startedAt: state.getIn(['sessions', 'current']).startedAt,
}))(observer(NetworkPanel));

View file

@ -12,7 +12,7 @@ import StackEventRow from 'Shared/DevTools/StackEventRow';
import StackEventModal from '../StackEventModal';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
import useCellMeasurerCache from '../useCellMeasurerCache'
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
const INDEX_KEY = 'stackEvent';
const ALL = 'ALL';

View file

@ -223,7 +223,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
{columns
.filter((i: any) => !i.hidden)
.map(({ dataKey, render, width, label }) => (
<div key={parseInt(label, 36)} className={stl.cell} style={{ width: `${width}px` }}>
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={stl.cell} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
@ -327,7 +327,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
<div className={stl.infoHeaders}>
{columns.map(({ label, width, dataKey, onClick = null }) => (
<div
key={parseInt(label, 36)}
key={parseInt(label.replace(' ', ''), 36)}
className={cn(stl.headerCell, 'flex items-center select-none', {
'cursor-pointer': typeof onClick === 'function',
})}
@ -355,6 +355,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
key={time}
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,

Some files were not shown because too many files have changed in this diff Show more