remote remove-immutable pull and resolved conflcits
This commit is contained in:
commit
58c3732fa2
193 changed files with 1864 additions and 4401 deletions
2
.github/workflows/api-ee.yaml
vendored
2
.github/workflows/api-ee.yaml
vendored
|
|
@ -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: |
|
||||
#
|
||||
|
|
|
|||
2
.github/workflows/api.yaml
vendored
2
.github/workflows/api.yaml
vendored
|
|
@ -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: |
|
||||
#
|
||||
|
|
|
|||
5
.github/workflows/workers-ee.yaml
vendored
5
.github/workflows/workers-ee.yaml
vendored
|
|
@ -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 -
|
||||
|
||||
|
|
|
|||
5
.github/workflows/workers.yaml
vendored
5
.github/workflows/workers.yaml
vendored
|
|
@ -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 -
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
22
backend/pkg/messages/cache.go
Normal file
22
backend/pkg/messages/cache.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
31
frontend/.storybook/config.DEPRECATED.js
Normal file
31
frontend/.storybook/config.DEPRECATED.js
Normal 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
|
||||
);
|
||||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ListItem';
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Announcements';
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AutoComplete';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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} /> }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Filters';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionList';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionsMenu';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './TabItem'
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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]} />
|
||||
))
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './BugFinder';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.header {
|
||||
padding: 3px 10px;
|
||||
letter-spacing: 1.5px;
|
||||
color: $gray-medium;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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))
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './LastPerformance';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
185
frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
Normal file
185
frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
Normal 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))
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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']),
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ListItem';
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AnnouncementModal';
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SeriesName';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FilterSeries'
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue