diff --git a/.github/workflows/api-ee.yaml b/.github/workflows/api-ee.yaml index 7c48fc7d9..9de1c53d3 100644 --- a/.github/workflows/api-ee.yaml +++ b/.github/workflows/api-ee.yaml @@ -6,6 +6,7 @@ on: - dev paths: - ee/api/** + - api/** name: Build and Deploy Chalice EE diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index c0a540efb..dba71939f 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -3,9 +3,13 @@ on: workflow_dispatch: push: branches: - - api-v1.5.5 + - dev paths: - frontend/** +# Disable previous workflows for this action. +concurrency: + group: ${{ github.workflow }} #-${{ github.ref }} + cancel-in-progress: true jobs: build: @@ -23,6 +27,10 @@ jobs: ${{ runner.OS }}-build- ${{ runner.OS }}- + - name: Docker login + run: | + docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}" + - uses: azure/k8s-set-context@v1 with: method: kubeconfig @@ -31,16 +39,60 @@ jobs: # - name: Install # run: npm install - - name: Build and deploy + - name: Building and Pushing frontend image + id: build-image + env: + DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }} + IMAGE_TAG: ${{ github.sha }} + ENVIRONMENT: staging run: | cd frontend - bash build.sh - cp -arl public frontend - minio_pod=$(kubectl get po -n db -l app.kubernetes.io/name=minio -n db --output custom-columns=name:.metadata.name | tail -n+2) - echo $minio_pod - echo copying frontend to container. - kubectl -n db cp frontend $minio_pod:/data/ - rm -rf frontend + mv .env.sample .env + docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:14-stretch-slim /bin/bash -c "yarn && yarn build" + # https://github.com/docker/cli/issues/1134#issuecomment-613516912 + DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} . + docker push $DOCKER_REPO/frontend:${IMAGE_TAG} + - name: Creating old image input + run: | + # + # Create yaml with existing image tags + # + kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\ + tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt + + echo > /tmp/image_override.yaml + + for line in `cat /tmp/image_tag.txt`; + do + image_array=($(echo "$line" | tr ':' '\n')) + cat <> /tmp/image_override.yaml + ${image_array[0]}: + image: + tag: ${image_array[1]} + EOF + done + + - name: Deploy to kubernetes + run: | + cd scripts/helmcharts/ + + ## Update secerts + sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.OSS_PG_PASSWORD }}\"/g" vars.yaml + sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\"/g" vars.yaml + sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\"/g" vars.yaml + sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.OSS_JWT_SECRET }}\"/g" vars.yaml + sed -i "s/domainName: \"\"/domainName: \"${{ secrets.OSS_DOMAIN_NAME }}\"/g" vars.yaml + + # Update changed image tag + sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml + + cat /tmp/image_override.yaml + # Deploy command + helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic + env: + DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }} + IMAGE_TAG: ${{ github.sha }} + ENVIRONMENT: staging # - name: Debug Job # if: ${{ failure() }} diff --git a/.github/workflows/workers-ee.yaml b/.github/workflows/workers-ee.yaml index 170f0761a..e42089602 100644 --- a/.github/workflows/workers-ee.yaml +++ b/.github/workflows/workers-ee.yaml @@ -7,6 +7,7 @@ on: - dev paths: - ee/backend/** + - backend/** name: Build and deploy workers EE @@ -118,7 +119,7 @@ jobs: ## Update images for image in $(cat /tmp/images_to_build.txt); do - sed -i "/${image}/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml + sed -i "/${image}/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml done cat /tmp/image_override.yaml diff --git a/.github/workflows/workers.yaml b/.github/workflows/workers.yaml index f9ac362a4..f84909a39 100644 --- a/.github/workflows/workers.yaml +++ b/.github/workflows/workers.yaml @@ -33,11 +33,12 @@ jobs: method: kubeconfig kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret. id: setcontext - + # Caching docker images - - uses: satackey/action-docker-layer-caching@v0.0.11 - # Ignore the failure of a step and avoid terminating the job. - continue-on-error: true + # - uses: satackey/action-docker-layer-caching@v0.0.11 + # # Ignore the failure of a step and avoid terminating the job. + # continue-on-error: true + - name: Build, tag id: build-image diff --git a/backend/Dockerfile b/backend/Dockerfile index 8de0b0279..40377b6fe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,13 +6,14 @@ WORKDIR /root COPY go.mod . COPY go.sum . -RUN go mod download +RUN go mod tidy && go mod download FROM prepare AS build COPY cmd cmd COPY pkg pkg COPY internal internal +RUN go mod tidy ARG SERVICE_NAME RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openreplay/backend/cmd/$SERVICE_NAME @@ -27,7 +28,7 @@ ENV TZ=UTC \ MAXMINDDB_FILE=/root/geoip.mmdb \ UAPARSER_FILE=/root/regexes.yaml \ HTTP_PORT=80 \ - BEACON_SIZE_LIMIT=7000000 \ + BEACON_SIZE_LIMIT=1000000 \ KAFKA_USE_SSL=true \ KAFKA_MAX_POLL_INTERVAL_MS=400000 \ REDIS_STREAMS_MAX_LEN=10000 \ @@ -50,8 +51,8 @@ ENV TZ=UTC \ FS_CLEAN_HRS=72 \ FILE_SPLIT_SIZE=300000 \ LOG_QUEUE_STATS_INTERVAL_SEC=60 \ - BATCH_QUEUE_LIMIT=20 \ - BATCH_SIZE_LIMIT=10000000 \ + DB_BATCH_QUEUE_LIMIT=20 \ + DB_BATCH_SIZE_LIMIT=10000000 \ PARTITIONS_NUMBER=1 diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index c863fdbeb..98bddef45 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -32,7 +32,7 @@ func main() { cfg := db.New() // Init database - pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres), cfg.ProjectExpirationTimeoutMs) + pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres, cfg.BatchQueueLimit, cfg.BatchSizeLimit), cfg.ProjectExpirationTimeoutMs) defer pg.Close() // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. diff --git a/backend/cmd/ender/main.go b/backend/cmd/ender/main.go index 82f9b51d0..6898b6f27 100644 --- a/backend/cmd/ender/main.go +++ b/backend/cmd/ender/main.go @@ -4,6 +4,8 @@ import ( "log" "openreplay/backend/internal/config/ender" "openreplay/backend/internal/sessionender" + "openreplay/backend/pkg/db/cache" + "openreplay/backend/pkg/db/postgres" "openreplay/backend/pkg/monitoring" "time" @@ -30,6 +32,9 @@ func main() { // Load service configuration cfg := ender.New() + pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres, 0, 0), cfg.ProjectExpirationTimeoutMs) + defer pg.Close() + // Init all modules statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) sessions, err := sessionender.New(metrics, intervals.EVENTS_SESSION_END_TIMEOUT, cfg.PartitionsNumber) @@ -44,8 +49,17 @@ func main() { cfg.TopicRawWeb, }, func(sessionID uint64, msg messages.Message, meta *types.Meta) { + switch msg.(type) { + case *messages.SessionStart, *messages.SessionEnd: + // Skip several message types + return + } + // Test debug + if msg.Meta().Timestamp == 0 { + log.Printf("ZERO TS, sessID: %d, msgType: %d", sessionID, msg.TypeID()) + } statsLogger.Collect(sessionID, meta) - sessions.UpdateSession(sessionID, meta.Timestamp) + sessions.UpdateSession(sessionID, meta.Timestamp, msg.Meta().Timestamp) }, false, ) @@ -70,8 +84,12 @@ func main() { // Find ended sessions and send notification to other services sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool { msg := &messages.SessionEnd{Timestamp: uint64(timestamp)} + if err := pg.InsertSessionEnd(sessionID, msg.Timestamp); err != nil { + log.Printf("can't save sessionEnd to database, sessID: %d", sessionID) + return false + } if err := producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(msg)); err != nil { - log.Printf("can't send SessionEnd to trigger topic: %s; sessID: %d", err, sessionID) + log.Printf("can't send sessionEnd to topic: %s; sessID: %d", err, sessionID) return false } return true diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index 0aa0415c4..d173d7be2 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -4,8 +4,6 @@ import ( "log" "openreplay/backend/internal/config/heuristics" "openreplay/backend/pkg/handlers" - "openreplay/backend/pkg/handlers/custom" - ios2 "openreplay/backend/pkg/handlers/ios" web2 "openreplay/backend/pkg/handlers/web" "openreplay/backend/pkg/intervals" logger "openreplay/backend/pkg/log" @@ -39,12 +37,12 @@ func main() { &web2.MemoryIssueDetector{}, &web2.NetworkIssueDetector{}, &web2.PerformanceAggregator{}, - // iOS handlers - &ios2.AppNotResponding{}, - &ios2.ClickRageDetector{}, - &ios2.PerformanceAggregator{}, + // iOS's handlers + //&ios2.AppNotResponding{}, + //&ios2.ClickRageDetector{}, + //&ios2.PerformanceAggregator{}, // Other handlers (you can add your custom handlers here) - &custom.CustomHandler{}, + //&custom.CustomHandler{}, } } diff --git a/backend/cmd/http/main.go b/backend/cmd/http/main.go index 0c9603601..96562aee1 100644 --- a/backend/cmd/http/main.go +++ b/backend/cmd/http/main.go @@ -35,7 +35,7 @@ func main() { defer producer.Close(15000) // Connect to database - dbConn := cache.NewPGCache(postgres.NewConn(cfg.Postgres), 1000*60*20) + dbConn := cache.NewPGCache(postgres.NewConn(cfg.Postgres, 0, 0), 1000*60*20) defer dbConn.Close() // Build all services diff --git a/backend/cmd/integrations/main.go b/backend/cmd/integrations/main.go index 246e2c1fc..fd06a14c3 100644 --- a/backend/cmd/integrations/main.go +++ b/backend/cmd/integrations/main.go @@ -26,7 +26,7 @@ func main() { cfg := config.New() - pg := postgres.NewConn(cfg.PostgresURI) + pg := postgres.NewConn(cfg.PostgresURI, 0, 0) defer pg.Close() tokenizer := token.NewTokenizer(cfg.TokenSecret) @@ -74,7 +74,7 @@ func main() { log.Printf("Requesting all...\n") manager.RequestAll() case event := <-manager.Events: - log.Printf("New integration event: %+v\n", *event.RawErrorEvent) + log.Printf("New integration event: %+v\n", *event.IntegrationEvent) sessionID := event.SessionID if sessionID == 0 { sessData, err := tokenizer.Parse(event.Token) @@ -84,8 +84,7 @@ func main() { } sessionID = sessData.ID } - // TODO: send to ready-events topic. Otherwise it have to go through the events worker. - producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(event.RawErrorEvent)) + producer.Produce(cfg.TopicAnalytics, sessionID, messages.Encode(event.IntegrationEvent)) case err := <-manager.Errors: log.Printf("Integration error: %v\n", err) case i := <-manager.RequestDataUpdates: diff --git a/backend/internal/config/db/config.go b/backend/internal/config/db/config.go index f467b0c5c..25767afbf 100644 --- a/backend/internal/config/db/config.go +++ b/backend/internal/config/db/config.go @@ -26,7 +26,7 @@ func New() *Config { TopicRawWeb: env.String("TOPIC_RAW_WEB"), TopicAnalytics: env.String("TOPIC_ANALYTICS"), CommitBatchTimeout: 15 * time.Second, - BatchQueueLimit: env.Int("BATCH_QUEUE_LIMIT"), - BatchSizeLimit: env.Int("BATCH_SIZE_LIMIT"), + BatchQueueLimit: env.Int("DB_BATCH_QUEUE_LIMIT"), + BatchSizeLimit: env.Int("DB_BATCH_SIZE_LIMIT"), } } diff --git a/backend/internal/config/ender/config.go b/backend/internal/config/ender/config.go index 5898c69ec..7203eae33 100644 --- a/backend/internal/config/ender/config.go +++ b/backend/internal/config/ender/config.go @@ -5,19 +5,23 @@ import ( ) type Config struct { - GroupEnder string - LoggerTimeout int - TopicRawWeb string - ProducerTimeout int - PartitionsNumber int + Postgres string + ProjectExpirationTimeoutMs int64 + GroupEnder string + LoggerTimeout int + TopicRawWeb string + ProducerTimeout int + PartitionsNumber int } func New() *Config { return &Config{ - GroupEnder: env.String("GROUP_ENDER"), - LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), - TopicRawWeb: env.String("TOPIC_RAW_WEB"), - ProducerTimeout: 2000, - PartitionsNumber: env.Int("PARTITIONS_NUMBER"), + Postgres: env.String("POSTGRES_STRING"), + ProjectExpirationTimeoutMs: 1000 * 60 * 20, + GroupEnder: env.String("GROUP_ENDER"), + LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), + TopicRawWeb: env.String("TOPIC_RAW_WEB"), + ProducerTimeout: 2000, + PartitionsNumber: env.Int("PARTITIONS_NUMBER"), } } diff --git a/backend/internal/config/integrations/config.go b/backend/internal/config/integrations/config.go index 037b26f68..290acde1d 100644 --- a/backend/internal/config/integrations/config.go +++ b/backend/internal/config/integrations/config.go @@ -10,8 +10,8 @@ type Config struct { func New() *Config { return &Config{ - TopicRawWeb: env.String("TOPIC_RAW_WEB"), - PostgresURI: env.String("POSTGRES_STRING"), - TokenSecret: env.String("TOKEN_SECRET"), + TopicAnalytics: env.String("TOPIC_ANALYTICS"), + PostgresURI: env.String("POSTGRES_STRING"), + TokenSecret: env.String("TOKEN_SECRET"), } } diff --git a/backend/internal/db/datasaver/messages.go b/backend/internal/db/datasaver/messages.go index 0f24d237e..4197ffb77 100644 --- a/backend/internal/db/datasaver/messages.go +++ b/backend/internal/db/datasaver/messages.go @@ -19,9 +19,9 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error { // Web case *SessionStart: - return mi.pg.InsertWebSessionStart(sessionID, m) + return mi.pg.HandleWebSessionStart(sessionID, m) case *SessionEnd: - return mi.pg.InsertWebSessionEnd(sessionID, m) + return mi.pg.HandleWebSessionEnd(sessionID, m) case *UserID: return mi.pg.InsertWebUserID(sessionID, m) case *UserAnonymousID: @@ -42,6 +42,15 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error { return mi.pg.InsertWebFetchEvent(sessionID, m) case *GraphQLEvent: return mi.pg.InsertWebGraphQLEvent(sessionID, m) + case *IntegrationEvent: + return mi.pg.InsertWebErrorEvent(sessionID, &ErrorEvent{ + MessageID: m.Meta().Index, + Timestamp: m.Timestamp, + Source: m.Source, + Name: m.Name, + Message: m.Message, + Payload: m.Payload, + }) // IOS case *IOSSessionStart: @@ -66,15 +75,6 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error { case *IOSCrash: return mi.pg.InsertIOSCrash(sessionID, m) - case *RawErrorEvent: - return mi.pg.InsertWebErrorEvent(sessionID, &ErrorEvent{ - MessageID: m.Meta().Index, // TODO: is it possible to catch panic here??? - Timestamp: m.Timestamp, - Source: m.Source, - Name: m.Name, - Message: m.Message, - Payload: m.Payload, - }) } return nil // "Not implemented" } diff --git a/backend/internal/http/router/handlers-web.go b/backend/internal/http/router/handlers-web.go index 7514a78fc..69cbe4675 100644 --- a/backend/internal/http/router/handlers-web.go +++ b/backend/internal/http/router/handlers-web.go @@ -98,7 +98,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) expTime := startTime.Add(time.Duration(p.MaxSessionDuration) * time.Millisecond) tokenData = &token.TokenData{ID: sessionID, ExpTime: expTime.UnixMilli()} - e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(&SessionStart{ + sessionStart := &SessionStart{ Timestamp: req.Timestamp, ProjectID: uint64(p.ProjectID), TrackerVersion: req.TrackerVersion, @@ -115,7 +115,13 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) UserDeviceMemorySize: req.DeviceMemory, UserDeviceHeapSize: req.JsHeapSizeLimit, UserID: req.UserID, - })) + } + + // Save sessionStart to db + e.services.Database.InsertWebSessionStart(sessionID, sessionStart) + + // Send sessionStart message to kafka + e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(sessionStart)) } ResponseWithJSON(w, &StartSessionResponse{ diff --git a/backend/internal/integrations/integration/bugsnag.go b/backend/internal/integrations/integration/bugsnag.go index 118cdb84d..ae8657c39 100644 --- a/backend/internal/integrations/integration/bugsnag.go +++ b/backend/internal/integrations/integration/bugsnag.go @@ -97,7 +97,7 @@ func (b *bugsnag) Request(c *client) error { c.evChan <- &SessionErrorEvent{ SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "bugsnag", Timestamp: timestamp, Name: e.Exceptions[0].Message, diff --git a/backend/internal/integrations/integration/client.go b/backend/internal/integrations/integration/client.go index 315bfe4e9..632861607 100644 --- a/backend/internal/integrations/integration/client.go +++ b/backend/internal/integrations/integration/client.go @@ -40,7 +40,7 @@ type client struct { type SessionErrorEvent struct { SessionID uint64 Token string - *messages.RawErrorEvent + *messages.IntegrationEvent } type ClientMap map[string]*client diff --git a/backend/internal/integrations/integration/cloudwatch.go b/backend/internal/integrations/integration/cloudwatch.go index 9974f485b..a069b18cb 100644 --- a/backend/internal/integrations/integration/cloudwatch.go +++ b/backend/internal/integrations/integration/cloudwatch.go @@ -68,7 +68,7 @@ func (cw *cloudwatch) Request(c *client) error { c.evChan <- &SessionErrorEvent{ //SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "cloudwatch", Timestamp: timestamp, // e.IngestionTime ?? Name: name, diff --git a/backend/internal/integrations/integration/datadog.go b/backend/internal/integrations/integration/datadog.go index 096c3b822..edbbd6d83 100644 --- a/backend/internal/integrations/integration/datadog.go +++ b/backend/internal/integrations/integration/datadog.go @@ -115,7 +115,7 @@ func (d *datadog) Request(c *client) error { c.evChan <- &SessionErrorEvent{ //SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "datadog", Timestamp: timestamp, Name: ddLog.Content.Attributes.Error.Message, diff --git a/backend/internal/integrations/integration/elasticsearch.go b/backend/internal/integrations/integration/elasticsearch.go index 6b8181073..5331f850e 100644 --- a/backend/internal/integrations/integration/elasticsearch.go +++ b/backend/internal/integrations/integration/elasticsearch.go @@ -181,7 +181,7 @@ func (es *elasticsearch) Request(c *client) error { //SessionID: sessionID, SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "elasticsearch", Timestamp: timestamp, Name: fmt.Sprintf("%v", docID), diff --git a/backend/internal/integrations/integration/newrelic.go b/backend/internal/integrations/integration/newrelic.go index 2dce79aa5..97426de22 100644 --- a/backend/internal/integrations/integration/newrelic.go +++ b/backend/internal/integrations/integration/newrelic.go @@ -89,7 +89,7 @@ func (nr *newrelic) Request(c *client) error { c.setLastMessageTimestamp(e.Timestamp) c.evChan <- &SessionErrorEvent{ Token: e.OpenReplaySessionToken, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "newrelic", Timestamp: e.Timestamp, Name: e.ErrorClass, diff --git a/backend/internal/integrations/integration/rollbar.go b/backend/internal/integrations/integration/rollbar.go index 53a5c6d5b..a683a687c 100644 --- a/backend/internal/integrations/integration/rollbar.go +++ b/backend/internal/integrations/integration/rollbar.go @@ -156,7 +156,7 @@ func (rb *rollbar) Request(c *client) error { c.setLastMessageTimestamp(timestamp) c.evChan <- &SessionErrorEvent{ Token: e["body.message.openReplaySessionToken"], - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "rollbar", Timestamp: timestamp, Name: e["item.title"], diff --git a/backend/internal/integrations/integration/sentry.go b/backend/internal/integrations/integration/sentry.go index 1c5bfdaad..ea3118449 100644 --- a/backend/internal/integrations/integration/sentry.go +++ b/backend/internal/integrations/integration/sentry.go @@ -115,7 +115,7 @@ PageLoop: c.evChan <- &SessionErrorEvent{ SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "sentry", Timestamp: timestamp, Name: e.Title, diff --git a/backend/internal/integrations/integration/stackdriver.go b/backend/internal/integrations/integration/stackdriver.go index e852d5d36..45b769aa7 100644 --- a/backend/internal/integrations/integration/stackdriver.go +++ b/backend/internal/integrations/integration/stackdriver.go @@ -89,7 +89,7 @@ func (sd *stackdriver) Request(c *client) error { c.evChan <- &SessionErrorEvent{ //SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "stackdriver", Timestamp: timestamp, Name: e.InsertID, // not sure about that diff --git a/backend/internal/integrations/integration/sumologic.go b/backend/internal/integrations/integration/sumologic.go index 8ff39ec9e..c5ee63249 100644 --- a/backend/internal/integrations/integration/sumologic.go +++ b/backend/internal/integrations/integration/sumologic.go @@ -193,7 +193,7 @@ func (sl *sumologic) Request(c *client) error { c.evChan <- &SessionErrorEvent{ //SessionID: sessionID, Token: token, - RawErrorEvent: &messages.RawErrorEvent{ + IntegrationEvent: &messages.IntegrationEvent{ Source: "sumologic", Timestamp: e.Timestamp, Name: name, diff --git a/backend/internal/sessionender/ender.go b/backend/internal/sessionender/ender.go index 107618f59..f43a9029b 100644 --- a/backend/internal/sessionender/ender.go +++ b/backend/internal/sessionender/ender.go @@ -16,6 +16,7 @@ type EndedSessionHandler func(sessionID uint64, timestamp int64) bool type session struct { lastTimestamp int64 lastUpdate int64 + lastUserTime int64 isEnded bool } @@ -51,7 +52,7 @@ func New(metrics *monitoring.Metrics, timeout int64, parts int) (*SessionEnder, } // UpdateSession save timestamp for new sessions and update for existing sessions -func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) { +func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp, msgTimestamp int64) { localTS := time.Now().UnixMilli() currTS := timestamp if currTS == 0 { @@ -62,11 +63,12 @@ func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) { sess, ok := se.sessions[sessionID] if !ok { se.sessions[sessionID] = &session{ - lastTimestamp: currTS, // timestamp from message broker - lastUpdate: localTS, // local timestamp + lastTimestamp: currTS, // timestamp from message broker + lastUpdate: localTS, // local timestamp + lastUserTime: msgTimestamp, // last timestamp from user's machine isEnded: false, } - log.Printf("added new session: %d", sessionID) + //log.Printf("added new session: %d", sessionID) se.activeSessions.Add(context.Background(), 1) se.totalSessions.Add(context.Background(), 1) return @@ -74,6 +76,7 @@ func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) { if currTS > sess.lastTimestamp { sess.lastTimestamp = currTS sess.lastUpdate = localTS + sess.lastUserTime = msgTimestamp sess.isEnded = false } } @@ -86,10 +89,12 @@ func (se *SessionEnder) HandleEndedSessions(handler EndedSessionHandler) { if sess.isEnded || (se.timeCtrl.LastTimestamp(sessID)-sess.lastTimestamp > se.timeout) || (currTime-sess.lastUpdate > se.timeout) { sess.isEnded = true - if handler(sessID, sess.lastTimestamp) { + if handler(sessID, sess.lastUserTime) { delete(se.sessions, sessID) se.activeSessions.Add(context.Background(), -1) removedSessions++ + } else { + log.Printf("sessID: %d, userTime: %d", sessID, sess.lastUserTime) } } } diff --git a/backend/pkg/db/cache/messages-common.go b/backend/pkg/db/cache/messages-common.go index 8ca7b2f85..cebdaf5e7 100644 --- a/backend/pkg/db/cache/messages-common.go +++ b/backend/pkg/db/cache/messages-common.go @@ -1,22 +1,25 @@ package cache import ( + "log" . "openreplay/backend/pkg/messages" + "time" // . "openreplay/backend/pkg/db/types" ) -func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64) error { - //duration, err := c.Conn.InsertSessionEnd(sessionID, timestamp) +func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) error { _, err := c.Conn.InsertSessionEnd(sessionID, timestamp) if err != nil { return err } + return nil +} + +func (c *PGCache) HandleSessionEnd(sessionID uint64) error { + if err := c.Conn.HandleSessionEnd(sessionID); err != nil { + log.Printf("can't handle session end: %s", err) + } c.DeleteSession(sessionID) - // session, err := c.GetSession(sessionID) - // if err != nil { - // return err - // } - // session.Duration = &duration return nil } @@ -45,6 +48,12 @@ func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error { return nil } if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { + // Try to insert metadata after one minute + time.AfterFunc(time.Minute, func() { + if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { + log.Printf("metadata retry err: %s", err) + } + }) return err } session.SetMetadata(keyNo, metadata.Value) diff --git a/backend/pkg/db/cache/messages-ios.go b/backend/pkg/db/cache/messages-ios.go index 4bbc8c1f5..4195976c3 100644 --- a/backend/pkg/db/cache/messages-ios.go +++ b/backend/pkg/db/cache/messages-ios.go @@ -32,7 +32,7 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er } func (c *PGCache) InsertIOSSessionEnd(sessionID uint64, e *IOSSessionEnd) error { - return c.insertSessionEnd(sessionID, e.Timestamp) + return c.InsertSessionEnd(sessionID, e.Timestamp) } func (c *PGCache) InsertIOSScreenEnter(sessionID uint64, screenEnter *IOSScreenEnter) error { diff --git a/backend/pkg/db/cache/messages-web.go b/backend/pkg/db/cache/messages-web.go index 71f2c38d0..7da7006af 100644 --- a/backend/pkg/db/cache/messages-web.go +++ b/backend/pkg/db/cache/messages-web.go @@ -7,6 +7,30 @@ import ( ) func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error { + return c.Conn.InsertSessionStart(sessionID, &Session{ + SessionID: sessionID, + Platform: "web", + Timestamp: s.Timestamp, + ProjectID: uint32(s.ProjectID), + TrackerVersion: s.TrackerVersion, + RevID: s.RevID, + UserUUID: s.UserUUID, + UserOS: s.UserOS, + UserOSVersion: s.UserOSVersion, + UserDevice: s.UserDevice, + UserCountry: s.UserCountry, + // web properties (TODO: unite different platform types) + UserAgent: s.UserAgent, + UserBrowser: s.UserBrowser, + UserBrowserVersion: s.UserBrowserVersion, + UserDeviceType: s.UserDeviceType, + UserDeviceMemorySize: s.UserDeviceMemorySize, + UserDeviceHeapSize: s.UserDeviceHeapSize, + UserID: &s.UserID, + }) +} + +func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error { if c.sessions[sessionID] != nil { return errors.New("This session already in cache!") } @@ -31,7 +55,7 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error UserDeviceHeapSize: s.UserDeviceHeapSize, UserID: &s.UserID, } - if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil { + if err := c.Conn.HandleSessionStart(sessionID, c.sessions[sessionID]); err != nil { c.sessions[sessionID] = nil return err } @@ -39,7 +63,11 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error } func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error { - return c.insertSessionEnd(sessionID, e.Timestamp) + return c.InsertSessionEnd(sessionID, e.Timestamp) +} + +func (c *PGCache) HandleWebSessionEnd(sessionID uint64, e *SessionEnd) error { + return c.HandleSessionEnd(sessionID) } func (c *PGCache) InsertWebErrorEvent(sessionID uint64, e *ErrorEvent) error { diff --git a/backend/pkg/db/postgres/connector.go b/backend/pkg/db/postgres/connector.go index 9b9724e58..61f274aed 100644 --- a/backend/pkg/db/postgres/connector.go +++ b/backend/pkg/db/postgres/connector.go @@ -3,6 +3,7 @@ package postgres import ( "context" "log" + "strings" "time" "github.com/jackc/pgx/v4" @@ -14,24 +15,33 @@ func getTimeoutContext() context.Context { return ctx } +type batchItem struct { + query string + arguments []interface{} +} + type Conn struct { c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?) batches map[uint64]*pgx.Batch batchSizes map[uint64]int + rawBatches map[uint64][]*batchItem batchQueueLimit int batchSizeLimit int } -func NewConn(url string) *Conn { +func NewConn(url string, queueLimit, sizeLimit int) *Conn { c, err := pgxpool.Connect(context.Background(), url) if err != nil { log.Println(err) log.Fatalln("pgxpool.Connect Error") } return &Conn{ - c: c, - batches: make(map[uint64]*pgx.Batch), - batchSizes: make(map[uint64]int), + c: c, + batches: make(map[uint64]*pgx.Batch), + batchSizes: make(map[uint64]int), + rawBatches: make(map[uint64][]*batchItem), + batchQueueLimit: queueLimit, + batchSizeLimit: sizeLimit, } } @@ -44,9 +54,17 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) batch, ok := conn.batches[sessionID] if !ok { conn.batches[sessionID] = &pgx.Batch{} + conn.rawBatches[sessionID] = make([]*batchItem, 0) batch = conn.batches[sessionID] } batch.Queue(sql, args...) + // Temp raw batch store + raw := conn.rawBatches[sessionID] + raw = append(raw, &batchItem{ + query: sql, + arguments: args, + }) + conn.rawBatches[sessionID] = raw } func (conn *Conn) CommitBatches() { @@ -56,12 +74,16 @@ func (conn *Conn) CommitBatches() { for i := 0; i < l; i++ { if ct, err := br.Exec(); err != nil { log.Printf("Error in PG batch (command tag %s, session: %d): %v \n", ct.String(), sessID, err) + failedSql := conn.rawBatches[sessID][i] + query := strings.ReplaceAll(failedSql.query, "\n", " ") + log.Println("failed sql req:", query, failedSql.arguments) } } br.Close() // returns err } conn.batches = make(map[uint64]*pgx.Batch) conn.batchSizes = make(map[uint64]int) + conn.rawBatches = make(map[uint64][]*batchItem) } func (conn *Conn) updateBatchSize(sessionID uint64, reqSize int) { @@ -83,6 +105,9 @@ func (conn *Conn) commitBatch(sessionID uint64) { for i := 0; i < l; i++ { if ct, err := br.Exec(); err != nil { log.Printf("Error in PG batch (command tag %s, session: %d): %v \n", ct.String(), sessionID, err) + failedSql := conn.rawBatches[sessionID][i] + query := strings.ReplaceAll(failedSql.query, "\n", " ") + log.Println("failed sql req:", query, failedSql.arguments) } } br.Close() @@ -90,6 +115,7 @@ func (conn *Conn) commitBatch(sessionID uint64) { // Clean batch info delete(conn.batches, sessionID) delete(conn.batchSizes, sessionID) + delete(conn.rawBatches, sessionID) } func (conn *Conn) query(sql string, args ...interface{}) (pgx.Rows, error) { diff --git a/backend/pkg/db/postgres/messages-common.go b/backend/pkg/db/postgres/messages-common.go index c21221ebf..a68d2c814 100644 --- a/backend/pkg/db/postgres/messages-common.go +++ b/backend/pkg/db/postgres/messages-common.go @@ -2,6 +2,7 @@ package postgres import ( "fmt" + "log" "strings" "openreplay/backend/pkg/db/types" @@ -31,14 +32,17 @@ func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value str FROM sessions WHERE session_id = $3 ) ON CONFLICT DO NOTHING` - conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID) + if err := conn.exec(sqlRequest, value, tp, sessionID); err != nil { + log.Printf("can't insert autocomplete: %s", err) + } + //conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID) // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8) + //conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8) } func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { - if err := conn.exec(` + return conn.exec(` INSERT INTO sessions ( session_id, project_id, start_ts, user_uuid, user_device, user_device_type, user_country, @@ -66,9 +70,10 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { s.Platform, s.UserAgent, s.UserBrowser, s.UserBrowserVersion, s.UserDeviceMemorySize, s.UserDeviceHeapSize, s.UserID, - ); err != nil { - return err - } + ) +} + +func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error { conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS) conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry) @@ -79,6 +84,20 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { } func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) { + var dur uint64 + if err := conn.queryRow(` + UPDATE sessions SET duration=$2 - start_ts + WHERE session_id=$1 + RETURNING duration + `, + sessionID, timestamp, + ).Scan(&dur); err != nil { + return 0, err + } + return dur, nil +} + +func (conn *Conn) HandleSessionEnd(sessionID uint64) error { // TODO: search acceleration? sqlRequest := ` UPDATE sessions @@ -96,18 +115,7 @@ func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, // Record approximate message size conn.updateBatchSize(sessionID, len(sqlRequest)+8) - - var dur uint64 - if err := conn.queryRow(` - UPDATE sessions SET duration=$2 - start_ts - WHERE session_id=$1 - RETURNING duration - `, - sessionID, timestamp, - ).Scan(&dur); err != nil { - return 0, err - } - return dur, nil + return nil } func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error { @@ -115,7 +123,7 @@ func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64 INSERT INTO events_common.requests ( session_id, timestamp, seq_index, url, duration, success ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, left($4, 2700), $5, $6 )` conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), url, duration, success) @@ -129,7 +137,7 @@ func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index ui INSERT INTO events_common.customs ( session_id, timestamp, seq_index, name, payload ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, left($4, 2700), $5 )` conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), name, payload) @@ -222,7 +230,7 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag INSERT INTO events_common.customs (session_id, seq_index, timestamp, name, payload, level) VALUES - ($1, $2, $3, $4, $5, 'error') + ($1, $2, $3, left($4, 2700), $5, 'error') `, sessionID, getSqIdx(e.MessageID), e.Timestamp, e.ContextString, e.Payload, ); err != nil { diff --git a/backend/pkg/db/postgres/messages-web-stats.go b/backend/pkg/db/postgres/messages-web-stats.go index 5f0b11e87..2a5a11750 100644 --- a/backend/pkg/db/postgres/messages-web-stats.go +++ b/backend/pkg/db/postgres/messages-web-stats.go @@ -1,6 +1,7 @@ package postgres import ( + "log" . "openreplay/backend/pkg/messages" "openreplay/backend/pkg/url" ) @@ -27,16 +28,25 @@ func (conn *Conn) InsertWebStatsPerformance(sessionID uint64, p *PerformanceTrac $10, $11, $12, $13, $14, $15 )` - conn.batchQueue(sessionID, sqlRequest, + //conn.batchQueue(sessionID, sqlRequest, + // sessionID, timestamp, timestamp, // ??? TODO: primary key by timestamp+session_id + // p.MinFPS, p.AvgFPS, p.MaxFPS, + // p.MinCPU, p.AvgCPU, p.MinCPU, + // p.MinTotalJSHeapSize, p.AvgTotalJSHeapSize, p.MaxTotalJSHeapSize, + // p.MinUsedJSHeapSize, p.AvgUsedJSHeapSize, p.MaxUsedJSHeapSize, + //) + if err := conn.exec(sqlRequest, sessionID, timestamp, timestamp, // ??? TODO: primary key by timestamp+session_id p.MinFPS, p.AvgFPS, p.MaxFPS, p.MinCPU, p.AvgCPU, p.MinCPU, p.MinTotalJSHeapSize, p.AvgTotalJSHeapSize, p.MaxTotalJSHeapSize, p.MinUsedJSHeapSize, p.AvgUsedJSHeapSize, p.MaxUsedJSHeapSize, - ) + ); err != nil { + log.Printf("can't insert perf: %s", err) + } // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+8*15) + //conn.updateBatchSize(sessionID, len(sqlRequest)+8*15) return nil } @@ -57,7 +67,7 @@ func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent ) VALUES ( $1, $2, $3, $4, - $5, $6, $7, + left($5, 2700), $6, $7, $8, $9, NULLIF($10, '')::events.resource_method, NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), NULLIF($14, 0), NULLIF($15, 0) diff --git a/backend/pkg/db/postgres/messages-web.go b/backend/pkg/db/postgres/messages-web.go index 495ca53e4..e703ee933 100644 --- a/backend/pkg/db/postgres/messages-web.go +++ b/backend/pkg/db/postgres/messages-web.go @@ -229,7 +229,7 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet duration, success ) VALUES ( $1, $2, $3, - $4, $5, $6, $7, + left($4, 2700), $5, $6, $7, $8, $9, $10::smallint, NULLIF($11, '')::http_method, $12, $13 ) ON CONFLICT DO NOTHING` @@ -242,7 +242,7 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet // Record approximate message size conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.URL)+len(host)+len(path)+len(query)+ - len(*request)+len(*response)+len(url.EnsureMethod(e.Method))+8*5+1) + len(e.Request)+len(e.Response)+len(url.EnsureMethod(e.Method))+8*5+1) return nil } @@ -261,7 +261,7 @@ func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *G request_body, response_body ) VALUES ( $1, $2, $3, - $4, + left($4, 2700), $5, $6 ) ON CONFLICT DO NOTHING` conn.batchQueue(sessionID, sqlRequest, sessionID, e.Timestamp, e.MessageID, @@ -269,6 +269,6 @@ func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *G ) // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(*request)+len(*response)+8*3) + conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(e.Variables)+len(e.Response)+8*3) return nil } diff --git a/backend/pkg/messages/batch.go b/backend/pkg/messages/batch.go index fe283d25d..2d531c429 100644 --- a/backend/pkg/messages/batch.go +++ b/backend/pkg/messages/batch.go @@ -1,11 +1,8 @@ package messages import ( - "fmt" - "io" - "strings" - "github.com/pkg/errors" + "io" ) func ReadBatchReader(reader io.Reader, messageHandler func(Message)) error { @@ -16,15 +13,7 @@ func ReadBatchReader(reader io.Reader, messageHandler func(Message)) error { if err == io.EOF { return nil } else if err != nil { - if strings.HasPrefix(err.Error(), "Unknown message code:") { - code := strings.TrimPrefix(err.Error(), "Unknown message code: ") - msg, err = DecodeExtraMessage(code, reader) - if err != nil { - return fmt.Errorf("can't decode msg: %s", err) - } - } else { - return errors.Wrapf(err, "Batch Message decoding error on message with index %v", index) - } + return errors.Wrapf(err, "Batch Message decoding error on message with index %v", index) } msg = transformDeprecated(msg) diff --git a/backend/pkg/messages/facade.go b/backend/pkg/messages/facade.go index 5c024f2f6..ebc9e7983 100644 --- a/backend/pkg/messages/facade.go +++ b/backend/pkg/messages/facade.go @@ -1,41 +1,5 @@ package messages -import ( - "bytes" - //"io" -) - func Encode(msg Message) []byte { return msg.Encode() } - -// -// func EncodeList(msgs []Message) []byte { - -// } -// - -// func Decode(b []byte) (Message, error) { -// return ReadMessage(bytes.NewReader(b)) -// } - -// func DecodeEach(b []byte, callback func(Message)) error { -// var err error -// reader := bytes.NewReader(b) -// for { -// msg, err := ReadMessage(reader) -// if err != nil { -// break -// } -// callback(msg) -// } -// if err == io.EOF { -// return nil -// } -// return err -// } - -func GetMessageTypeID(b []byte) (uint64, error) { - reader := bytes.NewReader(b) - return ReadUint(reader) -} diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index ba10eb026..6c4d75bfc 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -571,7 +571,7 @@ func (msg *JSException) TypeID() int { return 25 } -type RawErrorEvent struct { +type IntegrationEvent struct { message Timestamp uint64 Source string @@ -580,7 +580,7 @@ type RawErrorEvent struct { Payload string } -func (msg *RawErrorEvent) Encode() []byte { +func (msg *IntegrationEvent) Encode() []byte { buf := make([]byte, 51+len(msg.Source)+len(msg.Name)+len(msg.Message)+len(msg.Payload)) buf[0] = 26 p := 1 @@ -592,7 +592,7 @@ func (msg *RawErrorEvent) Encode() []byte { return buf[:p] } -func (msg *RawErrorEvent) TypeID() int { +func (msg *IntegrationEvent) TypeID() int { return 26 } @@ -1396,7 +1396,7 @@ type IssueEvent struct { Type string ContextString string Context string - Payload string // TODO: check, maybe it's better to use empty interface here + Payload string } func (msg *IssueEvent) Encode() []byte { diff --git a/backend/pkg/messages/read-message.go b/backend/pkg/messages/read-message.go index 59c8e739c..5009994f5 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -369,7 +369,7 @@ func ReadMessage(reader io.Reader) (Message, error) { return msg, nil case 26: - msg := &RawErrorEvent{} + msg := &IntegrationEvent{} if msg.Timestamp, err = ReadUint(reader); err != nil { return nil, err } diff --git a/backend/pkg/messages/trigger.go b/backend/pkg/messages/trigger.go deleted file mode 100644 index 0fe33340e..000000000 --- a/backend/pkg/messages/trigger.go +++ /dev/null @@ -1,35 +0,0 @@ -package messages - -import ( - "fmt" - "io" -) - -type SessionFinished struct { - message - Timestamp uint64 -} - -func (msg *SessionFinished) Encode() []byte { - buf := make([]byte, 11) - buf[0] = 127 - p := 1 - p = WriteUint(msg.Timestamp, buf, p) - return buf[:p] -} - -func (msg *SessionFinished) TypeID() int { - return 127 -} - -func DecodeExtraMessage(code string, reader io.Reader) (Message, error) { - var err error - if code != "127" { - return nil, fmt.Errorf("unknown message code: %s", code) - } - trigger := &SessionFinished{} - if trigger.Timestamp, err = ReadUint(reader); err != nil { - return nil, fmt.Errorf("can't read message timestamp: %s", err) - } - return trigger, nil -} diff --git a/backend/pkg/sessions/builder.go b/backend/pkg/sessions/builder.go index e764a7f20..eed3d8229 100644 --- a/backend/pkg/sessions/builder.go +++ b/backend/pkg/sessions/builder.go @@ -49,11 +49,16 @@ func (b *builder) handleMessage(message Message, messageID uint64) { } timestamp := GetTimestamp(message) if timestamp == 0 { - log.Printf("skip message with empty timestamp, sessID: %d, msgID: %d, msgType: %d", b.sessionID, messageID, message.TypeID()) + switch message.(type) { + case *SessionEnd, *IssueEvent, *PerformanceTrackAggr: + break + default: + log.Printf("skip message with empty timestamp, sessID: %d, msgID: %d, msgType: %d", b.sessionID, messageID, message.TypeID()) + } return } if timestamp < b.timestamp { - log.Printf("skip message with wrong timestamp, sessID: %d, msgID: %d, type: %d, msgTS: %d, lastTS: %d", b.sessionID, messageID, message.TypeID(), timestamp, b.timestamp) + //log.Printf("skip message with wrong timestamp, sessID: %d, msgID: %d, type: %d, msgTS: %d, lastTS: %d", b.sessionID, messageID, message.TypeID(), timestamp, b.timestamp) } else { b.timestamp = timestamp } diff --git a/backend/pkg/url/url.go b/backend/pkg/url/url.go index 0ac0f9e08..654e803eb 100644 --- a/backend/pkg/url/url.go +++ b/backend/pkg/url/url.go @@ -10,6 +10,7 @@ func DiscardURLQuery(url string) string { } func GetURLParts(rawURL string) (string, string, string, error) { + rawURL = strings.Replace(rawURL, "\t", "", -1) // Other chars? u, err := _url.Parse(rawURL) if err != nil { return "", "", "", err diff --git a/ee/api/.gitignore b/ee/api/.gitignore index a0bd649f3..12a468ef1 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -247,7 +247,6 @@ Pipfile /db_changes.sql /Dockerfile.bundle /entrypoint.bundle.sh -#/entrypoint.sh /chalicelib/core/heatmaps.py /routers/subs/insights.py /schemas.py @@ -258,5 +257,4 @@ Pipfile /build_alerts.sh /routers/subs/metrics.py /routers/subs/v1_api.py -/chalicelib/core/dashboards.py -entrypoint.sh \ No newline at end of file +/chalicelib/core/dashboards.py \ No newline at end of file diff --git a/ee/api/entrypoint.sh b/ee/api/entrypoint.sh new file mode 100755 index 000000000..d8ca9fa4f --- /dev/null +++ b/ee/api/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +bash env_vars.sh +cd sourcemap-reader +nohup npm start &> /tmp/sourcemap-reader.log & +cd .. +uvicorn app:app --host 0.0.0.0 --reload --proxy-headers diff --git a/ee/api/env_vars.sh b/ee/api/env_vars.sh new file mode 100755 index 000000000..a809e3262 --- /dev/null +++ b/ee/api/env_vars.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [[ -z "${ENV_CONFIG_OVERRIDE_PATH}" ]]; then + echo 'no env-override' +else + override=$ENV_CONFIG_OVERRIDE_PATH + if [ -f "$override" ]; then + # to remove endOfLine form sed result + echo "" >> $override + sed 's/=.*//;/^$/d' $override > .replacements + + # to remove all defined os-env-vars + cat .replacements | while read line + do + unset $line + done + rm .replacements + + # to merge predefined .env with the override.env + cp .env .env.d + sort -u -t '=' -k 1,1 $override .env.d > .env + rm .env.d + else + echo "$override does not exist." + fi + +fi \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 000000000..8afa1c5f0 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +.git +.cache +**/build.sh +**/build_*.sh +**/*deploy.sh diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..198c03c7a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +from node:14-stretch-slim AS builder +workdir /work +COPY . . +RUN cp .env.sample .env +RUN yarn +RUN yarn build + +FROM nginx:alpine as cicd +LABEL maintainer=Rajesh +COPY public /var/www/openreplay +COPY nginx.conf /etc/nginx/conf.d/default.conf + + +# Default step in docker build +FROM nginx:alpine +LABEL maintainer=Rajesh +COPY --from=builder /work/public /var/www/openreplay +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/README.md b/frontend/README.md index c7fcc1885..e2b4a3898 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,7 +2,7 @@ OpenReplay prototype UI On new icon addition: -`npm run generate:icons` +`yarn gen:icons` ## Documentation diff --git a/frontend/_webpack.config.js b/frontend/_webpack.config.js deleted file mode 100644 index ba4539fa1..000000000 --- a/frontend/_webpack.config.js +++ /dev/null @@ -1,181 +0,0 @@ -const webpack = require('webpack'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -// const CircularDependencyPlugin = require('circular-dependency-plugin') -// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); //TODO: replace Moment with date-fns ?? -const path = require('path'); -const fs = require('fs'); -const alias = require('./path-alias'); -const environments = require('./env'); - - -const DIST_DIR = 'public'; - -const GLOBAL_STYLES_DIR = 'app/styles'; - -const cssEntrypoints = [ - 'codemirror/lib/codemirror.css', - 'codemirror/theme/yeti.css', - 'codemirror/addon/lint/lint.css', - 'react-daterange-picker/dist/css/react-calendar.css', - 'react-datepicker/dist/react-datepicker.css', - 'rc-time-picker/assets/index.css', -]; - -const babelLoader = { - loader: 'babel-loader', - options: { - presets: [ - [ '@babel/preset-env', { // probably, use dynamic imports for polifills in future - "targets": "> 4%, not dead", - useBuiltIns: 'entry', - corejs: 3 - }], - '@babel/preset-react', - "@babel/preset-flow", //TODO: remove, use ts - ], - plugins: [ - "@babel/plugin-syntax-bigint", - ["@babel/plugin-proposal-private-property-in-object", { "loose": true }], - [ '@babel/plugin-proposal-decorators', { legacy: true } ], - [ '@babel/plugin-proposal-class-properties', { loose: true }], - [ '@babel/plugin-proposal-private-methods', { loose: true }], - // 'recharts' - ] - } -}; - -const cssFiles = fs.readdirSync(GLOBAL_STYLES_DIR, { withFileTypes: true }); -cssFiles.forEach(file => { - if (/.css$/.test(file.name)) { - const pathFullName = path.join(__dirname, GLOBAL_STYLES_DIR, file.name); - cssEntrypoints.push(pathFullName); - } -}); - - -function prepareEnv(env) { - const pEnv = {}; - Object.keys(env).forEach(key => { - pEnv[ `window.ENV.${ key }` ] = typeof env[ key ] === 'function' ? env[ key ]() : JSON.stringify(env[ key ]); - }); - return pEnv; -} - -module.exports = (envName = 'local') => { - const env = environments[ envName ]; - const cssFileLoader = { - loader: MiniCssExtractPlugin.loader, - options: { - hmr: !env.PRODUCTION, - }, - } - return { - // Polyfill only for async (TODO) - entry: [ './app/initialize.js' ].concat(cssEntrypoints), - output: { - path: path.join(__dirname, DIST_DIR), - filename: 'app-[contenthash:7].js', - publicPath: '/', - }, - plugins: [ - new webpack.ProvidePlugin({ - 'React': 'react' // back code compatability - }), - new MiniCssExtractPlugin({ - path: path.join(__dirname, DIST_DIR), - filename: 'app-[contenthash:7].css' - }), - new CopyWebpackPlugin([ 'app/assets' ]), - new HtmlWebpackPlugin({ - template: 'app/assets/index.html' - }), - new MomentLocalesPlugin(), - new webpack.DefinePlugin(prepareEnv(env)), - // new BundleAnalyzerPlugin({ analyzerMode: 'static'}), - // new CircularDependencyPlugin({ - // // exclude detection of files based on a RegExp - // exclude: /node_modules/, - // // add errors to webpack instead of warnings - // failOnError: true, - // // allow import cycles that include an asyncronous import, - // // e.g. via import(/* webpackMode: "weak" */ './file.js') - // allowAsyncCycles: false, - // // set the current working directory for displaying module paths - // cwd: process.cwd(), - // }) - ], - module: { - rules: [ - - // global and module css separation. TODO more beautyfull - { - test: /\.css$/, - include: [ path.join(__dirname, "app/components"), path.join(__dirname, "app/player") ], - use: [ - cssFileLoader, - { - loader: 'css-loader', - options: { - importLoaders: 1, - modules: { - localIdentName: '[name]_[local]_[hash:base64:7]' - }, - } - }, - 'postcss-loader' - ] - }, - { - test: /\.css$/, - include: [ path.join(__dirname, "node_modules"), path.join(__dirname, "app/styles") ], - use: [ - cssFileLoader, - { - loader: 'css-loader', - options: { - importLoaders: 1, - } - }, - 'postcss-loader' - ] - }, - - { - test: /\.svg$/, - use: ['@svgr/webpack'], - }, - { - test: /\.js$/, - include: [ path.join(__dirname, "app"), path.join(__dirname, ".storybook") ], - use: babelLoader, - }, - { - test: /\.tsx?$/, - include: path.join(__dirname, "app"), - use: [ 'ts-loader' ] - }, - ] - }, - resolve: { - alias, - extensions: ['.js', '.json', '.ts', '.tsx' ], - }, - mode: env.PRODUCTION ? 'production' : 'development', - optimization: { - splitChunks: { - chunks: 'all', - }, - }, - devServer: { - contentBase: path.join(__dirname, DIST_DIR), - //compress: true, - port: 3333, - historyApiFallback: true, - }, - stats: 'errors-only', - devtool: env.SOURCEMAP && 'source-map' - }; -} diff --git a/frontend/app/Router.js b/frontend/app/Router.js index c5fe4b36c..143fda226 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -145,7 +145,7 @@ class Router extends React.Component { } componentDidUpdate(prevProps, prevState) { - this.props.setSessionPath(prevProps.location.pathname) + this.props.setSessionPath(prevProps.location) if (prevProps.email !== this.props.email && !this.props.email) { this.props.fetchTenants(); } diff --git a/frontend/app/assets/integrations/assist.svg b/frontend/app/assets/integrations/assist.svg new file mode 100644 index 000000000..9563278c4 --- /dev/null +++ b/frontend/app/assets/integrations/assist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/bugsnag.svg b/frontend/app/assets/integrations/bugsnag.svg new file mode 100644 index 000000000..26a3a13b8 --- /dev/null +++ b/frontend/app/assets/integrations/bugsnag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/cloudwatch.svg b/frontend/app/assets/integrations/cloudwatch.svg new file mode 100644 index 000000000..3c6be67f9 --- /dev/null +++ b/frontend/app/assets/integrations/cloudwatch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/datadog.svg b/frontend/app/assets/integrations/datadog.svg new file mode 100644 index 000000000..129dd8309 --- /dev/null +++ b/frontend/app/assets/integrations/datadog.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/frontend/app/assets/integrations/elasticsearch.svg b/frontend/app/assets/integrations/elasticsearch.svg new file mode 100644 index 000000000..b95507cd5 --- /dev/null +++ b/frontend/app/assets/integrations/elasticsearch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/github.svg b/frontend/app/assets/integrations/github.svg new file mode 100644 index 000000000..53bd7b2d2 --- /dev/null +++ b/frontend/app/assets/integrations/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/graphql.svg b/frontend/app/assets/integrations/graphql.svg new file mode 100644 index 000000000..714f38846 --- /dev/null +++ b/frontend/app/assets/integrations/graphql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/jira-text.svg b/frontend/app/assets/integrations/jira-text.svg new file mode 100644 index 000000000..defb226cf --- /dev/null +++ b/frontend/app/assets/integrations/jira-text.svg @@ -0,0 +1 @@ +jira-logo-gradient-blue \ No newline at end of file diff --git a/frontend/app/assets/integrations/jira.svg b/frontend/app/assets/integrations/jira.svg new file mode 100644 index 000000000..36b328d35 --- /dev/null +++ b/frontend/app/assets/integrations/jira.svg @@ -0,0 +1,23 @@ + + + + Jira Software-blue + Created with Sketch. + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/assets/integrations/mobx.svg b/frontend/app/assets/integrations/mobx.svg new file mode 100644 index 000000000..2747797bd --- /dev/null +++ b/frontend/app/assets/integrations/mobx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/newrelic.svg b/frontend/app/assets/integrations/newrelic.svg new file mode 100644 index 000000000..cc4aea514 --- /dev/null +++ b/frontend/app/assets/integrations/newrelic.svg @@ -0,0 +1 @@ +NewRelic-logo-square \ No newline at end of file diff --git a/frontend/app/assets/integrations/ngrx.svg b/frontend/app/assets/integrations/ngrx.svg new file mode 100644 index 000000000..0e9ea2c19 --- /dev/null +++ b/frontend/app/assets/integrations/ngrx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/openreplay.svg b/frontend/app/assets/integrations/openreplay.svg new file mode 100644 index 000000000..667be8f22 --- /dev/null +++ b/frontend/app/assets/integrations/openreplay.svg @@ -0,0 +1,12 @@ + + + Group + + + + + + + + + \ No newline at end of file diff --git a/frontend/app/assets/integrations/redux.svg b/frontend/app/assets/integrations/redux.svg new file mode 100644 index 000000000..e02bb3a2d --- /dev/null +++ b/frontend/app/assets/integrations/redux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/assets/integrations/rollbar.svg b/frontend/app/assets/integrations/rollbar.svg new file mode 100644 index 000000000..2f6538118 --- /dev/null +++ b/frontend/app/assets/integrations/rollbar.svg @@ -0,0 +1,20 @@ + + + + +rollbar-logo-color-vertical + + + + + + + diff --git a/frontend/app/assets/integrations/segment.svg b/frontend/app/assets/integrations/segment.svg new file mode 100644 index 000000000..e631af69b --- /dev/null +++ b/frontend/app/assets/integrations/segment.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/app/assets/integrations/sentry-text.svg b/frontend/app/assets/integrations/sentry-text.svg new file mode 100644 index 000000000..59b79bc58 --- /dev/null +++ b/frontend/app/assets/integrations/sentry-text.svg @@ -0,0 +1 @@ +sentry-logo-black \ No newline at end of file diff --git a/frontend/app/assets/integrations/sentry.svg b/frontend/app/assets/integrations/sentry.svg new file mode 100644 index 000000000..ea1955275 --- /dev/null +++ b/frontend/app/assets/integrations/sentry.svg @@ -0,0 +1,6 @@ + + Untitled + + + + \ No newline at end of file diff --git a/frontend/app/assets/integrations/slack-bw.svg b/frontend/app/assets/integrations/slack-bw.svg new file mode 100644 index 000000000..a486d5d96 --- /dev/null +++ b/frontend/app/assets/integrations/slack-bw.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app/assets/integrations/slack.svg b/frontend/app/assets/integrations/slack.svg new file mode 100644 index 000000000..f65d81b52 --- /dev/null +++ b/frontend/app/assets/integrations/slack.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/app/assets/integrations/stackdriver.svg b/frontend/app/assets/integrations/stackdriver.svg new file mode 100644 index 000000000..05eb29650 --- /dev/null +++ b/frontend/app/assets/integrations/stackdriver.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/app/assets/integrations/sumologic.svg b/frontend/app/assets/integrations/sumologic.svg new file mode 100644 index 000000000..f4569061f --- /dev/null +++ b/frontend/app/assets/integrations/sumologic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app/assets/integrations/vuejs.svg b/frontend/app/assets/integrations/vuejs.svg new file mode 100644 index 000000000..420bd1140 --- /dev/null +++ b/frontend/app/assets/integrations/vuejs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js index adefd3c22..1701c8e0a 100644 --- a/frontend/app/components/Alerts/AlertForm.js +++ b/frontend/app/components/Alerts/AlertForm.js @@ -50,7 +50,7 @@ const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS); const AlertForm = props => { const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId, style={ width: '580px', height: '100vh' } } = props; const write = ({ target: { value, name } }) => props.edit({ [ name ]: value }) - const writeOption = (e, { name, value }) => props.edit({ [ name ]: value }); + const writeOption = (e, { name, value }) => props.edit({ [ name ]: value.value }); const onChangeCheck = ({ target: { checked, name }}) => props.edit({ [ name ]: checked }) // const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked }) // const onChangeCheck = (e) => { console.log(e) } @@ -96,7 +96,7 @@ const AlertForm = props => { primary name="detectionMethod" className="my-3" - onSelect={ writeOption } + onSelect={ (e, { name, value }) => props.edit({ [ name ]: value }) } value={{ value: instance.detectionMethod }} list={ [ { name: 'Threshold', value: 'threshold' }, @@ -144,7 +144,7 @@ const AlertForm = props => { name="left" value={ triggerOptions.find(i => i.value === instance.query.left) } // onChange={ writeQueryOption } - onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value }) } + onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value: value.value }) } /> @@ -157,29 +157,32 @@ const AlertForm = props => { name="operator" defaultValue={ instance.query.operator } // onChange={ writeQueryOption } - onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value }) } + onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value: value.value }) } /> { unit && ( - + <> + + {'test'} + )} { !unit && ( - + )} @@ -309,7 +312,7 @@ const AlertForm = props => { {instance.exists() ? 'Update' : 'Create'}
- +
{instance.exists() && ( diff --git a/frontend/app/components/Alerts/AlertsList.js b/frontend/app/components/Alerts/AlertsList.js index 21fbce249..21ea6448d 100644 --- a/frontend/app/components/Alerts/AlertsList.js +++ b/frontend/app/components/Alerts/AlertsList.js @@ -21,7 +21,6 @@ const AlertsList = props => {
setQuery(value)} /> diff --git a/frontend/app/components/Alerts/Notifications/Notifications.tsx b/frontend/app/components/Alerts/Notifications/Notifications.tsx index c4fad52ca..d6327d530 100644 --- a/frontend/app/components/Alerts/Notifications/Notifications.tsx +++ b/frontend/app/components/Alerts/Notifications/Notifications.tsx @@ -16,14 +16,11 @@ interface Props { fetchList: any; } function Notifications(props: Props) { - // const { notifications } = props; const { showModal } = useModal(); - // const unReadNotificationsCount = notifications.filter(({viewed}: any) => !viewed).size const { notificationStore } = useStore(); const count = useObserver(() => notificationStore.notificationsCount); useEffect(() => { - notificationStore.fetchNotificationsCount(); const interval = setInterval(() => { notificationStore.fetchNotificationsCount() }, AUTOREFRESH_INTERVAL); diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.tsx b/frontend/app/components/Assist/ChatControls/ChatControls.tsx index 16f79b3af..42866ed4b 100644 --- a/frontend/app/components/Assist/ChatControls/ChatControls.tsx +++ b/frontend/app/components/Assist/ChatControls/ChatControls.tsx @@ -29,14 +29,14 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props
-
- diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx index 8af2e0614..8eb2a3620 100644 --- a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -1,7 +1,6 @@ //@ts-nocheck import React, { useState, FC, useEffect } from 'react' import VideoContainer from '../components/VideoContainer' -import { Icon, Popup, Button } from 'UI' import cn from 'classnames' import Counter from 'App/components/shared/SessionItem/Counter' import stl from './chatWindow.module.css' diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 3bbd55943..10404a6e2 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -6,19 +6,14 @@ import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions'; import { applyFilter, clearEvents, addAttribute } from 'Duck/filters'; -import { fetchList as fetchFunnelsList } from 'Duck/funnels'; 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 { setFunnelPage } from 'Duck/sessions'; import { setActiveTab } from 'Duck/search'; import SessionsMenu from './SessionsMenu/SessionsMenu'; -import { LAST_7_DAYS } from 'Types/app/period'; -import { resetFunnel } from 'Duck/funnels'; -import { resetFunnelFilters } from 'Duck/funnelFilters' import NoSessionsMessage from 'Shared/NoSessionsMessage'; import SessionSearch from 'Shared/SessionSearch'; import MainSearchBar from 'Shared/MainSearchBar'; @@ -65,10 +60,6 @@ const allowedQueryKeys = [ fetchSources, clearEvents, setActiveTab, - fetchFunnelsList, - resetFunnel, - resetFunnelFilters, - setFunnelPage, clearSearch, fetchSessions, }) @@ -94,9 +85,6 @@ export default class BugFinder extends React.PureComponent { if (props.sessions.size === 0) { props.fetchSessions(); } - props.resetFunnel(); - props.resetFunnelFilters(); - props.fetchFunnelsList(LAST_7_DAYS) const queryFilter = this.props.query.all(allowedQueryKeys); if (queryFilter.hasOwnProperty('userId')) { @@ -104,10 +92,6 @@ export default class BugFinder extends React.PureComponent { } } - componentDidMount() { - this.props.setFunnelPage(false); - } - toggleRehydratePanel = () => { this.setState({ showRehydratePanel: !this.state.showRehydratePanel }) } diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index 47f02e5f5..ea5532211 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -95,42 +95,48 @@ export default class SessionList extends React.PureComponent { const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID); return ( - - - {this.getNoContentMessage(activeTab)} -
} - // subtext="Please try changing your search parameters." - // animatedIcon="no-results" - show={ !loading && list.size === 0} - subtext={ -
-
Please try changing your search parameters.
-
- } - > - - { list.map(session => ( - - ))} - -
- this.props.updateCurrentPage(page)} - limit={PER_PAGE} - debounceRequest={1000} - /> +
+ + + {this.getNoContentMessage(activeTab)} +
} + // subtext="Please try changing your search parameters." + // animatedIcon="no-results" + show={ !loading && list.size === 0} + subtext={ +
+
Please try changing your search parameters.
+
+ } + > + + + { list.map(session => ( + <> + +
+ + ))} + +
+ this.props.updateCurrentPage(page)} + limit={PER_PAGE} + debounceRequest={1000} + /> +
+
- ); } diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js index c4b7f884b..c30b57953 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; import { edit } from 'Duck/integrations/actions'; -import { Dropdown } from 'UI'; +import Select from 'Shared/Select'; import { withRequest } from 'HOCs'; @connect(state => ({ @@ -50,7 +50,7 @@ export default class ProjectListDropdown extends React.PureComponent { this.fetchProjectList(); } } - onChange = (e, target) => { + onChange = (target) => { if (typeof this.props.onChange === 'function') { this.props.onChange({ target }); } @@ -65,11 +65,11 @@ export default class ProjectListDropdown extends React.PureComponent { } = this.props; const options = projects.map(({ name, id }) => ({ text: name, value: id })); return ( - o.value === value) } placeholder={ placeholder } onChange={ this.onChange } loading={ loading } diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js index 640aa3de5..d1d306244 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; import { edit } from 'Duck/integrations/actions'; -import { Dropdown } from 'UI'; +import Select from 'Shared/Select'; import { withRequest } from 'HOCs'; @connect(state => ({ @@ -48,7 +48,7 @@ export default class LogGroupDropdown extends React.PureComponent { this.fetchLogGroups(); } } - onChange = (e, target) => { + onChange = (target) => { if (typeof this.props.onChange === 'function') { this.props.onChange({ target }); } @@ -63,11 +63,11 @@ export default class LogGroupDropdown extends React.PureComponent { } = this.props; const options = values.map(g => ({ text: g, value: g })); return ( - o.value === value) } placeholder={ placeholder } onChange={ this.onChange } loading={ loading } diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.js b/frontend/app/components/Client/Integrations/IntegrationItem.js index e4179855e..b0bfa258a 100644 --- a/frontend/app/components/Client/Integrations/IntegrationItem.js +++ b/frontend/app/components/Client/Integrations/IntegrationItem.js @@ -18,7 +18,7 @@ const IntegrationItem = ({
)} - + integration

{ title }

) diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index 18e01fc51..010b8cdea 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -4,7 +4,8 @@ import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; import { Form, IconButton, SlideModal, Input, Button, Loader, - NoContent, Popup, CopyButton, Dropdown } from 'UI'; + NoContent, Popup, CopyButton } from 'UI'; +import Select from 'Shared/Select'; import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member'; import { fetchList as fetchRoles } from 'Duck/roles'; import styles from './manageUsers.module.css'; @@ -39,7 +40,7 @@ class ManageUsers extends React.PureComponent { state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false } // writeOption = (e, { name, value }) => this.props.edit({ [ name ]: value }); - onChange = (e, { name, value }) => this.props.edit({ [ name ]: value }); + onChange = ({ name, value }) => this.props.edit({ [ name ]: value.value }); onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked }); setFocus = () => this.focusElement && this.focusElement.focus(); closeModal = () => this.setState({ showModal: false }); @@ -138,12 +139,12 @@ class ManageUsers extends React.PureComponent { { isEnterprise && ( - r.value === member.roleId) } onChange={ this.onChange } /> diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index 0b1c70dca..63c0d5d83 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -127,7 +127,7 @@ const RoleForm = (props: Props) => { isSearchable name="permissions" options={ permissions } - onChange={ ({ value }: any) => writeOption({ name: 'permissions', value }) } + onChange={ ({ value }: any) => writeOption({ name: 'permissions', value: value.value }) } value={null} /> { role.permissions.size > 0 && ( diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx new file mode 100644 index 000000000..078122bb2 --- /dev/null +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Popup, IconButton } from 'UI'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; +const LIMIT_WARNING = 'You have reached site limit.'; + +function AddProjectButton({ isAdmin = false, onClick }: any ) { + const { userStore } = useStore(); + const limtis = useObserver(() => userStore.limits); + const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); + return ( + + + + ); +} + +export default AddProjectButton; \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/AddProjectButton/index.ts b/frontend/app/components/Client/Sites/AddProjectButton/index.ts new file mode 100644 index 000000000..66beb9cf8 --- /dev/null +++ b/frontend/app/components/Client/Sites/AddProjectButton/index.ts @@ -0,0 +1 @@ +export { default } from './AddUserButton'; \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/Sites.js b/frontend/app/components/Client/Sites/Sites.js index 0dd140add..5a6791191 100644 --- a/frontend/app/components/Client/Sites/Sites.js +++ b/frontend/app/components/Client/Sites/Sites.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { Loader, SlideModal, IconButton, Icon, Button, Popup, TextLink } from 'UI'; +import { Loader, SlideModal, Icon, Button, Popup, TextLink } from 'UI'; import { init, remove, fetchGDPR } from 'Duck/site'; import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site'; import stl from './sites.module.css'; @@ -10,8 +10,9 @@ import NewSiteForm from './NewSiteForm'; import GDPRForm from './GDPRForm'; import TrackingCodeModal from 'Shared/TrackingCodeModal'; import BlockedIps from './BlockedIps'; -import { confirm } from 'UI'; +import { confirm, PageTitle } from 'UI'; import SiteSearch from './SiteSearch'; +import AddProjectButton from './AddProjectButton'; const STATUS_MESSAGE_MAP = { [ RED ]: ' There seems to be an issue (please verify your installation)', @@ -19,9 +20,6 @@ const STATUS_MESSAGE_MAP = { [ GREEN ]: 'All good!', }; -const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; -const LIMIT_WARNING = 'You have reached site limit.'; - const BLOCKED_IPS = 'BLOCKED_IPS'; const NONE = 'NONE'; @@ -143,21 +141,14 @@ class Sites extends React.PureComponent { />
-

{ 'Projects' }

- -
- -
-
+ {/*

{ 'Projects' }

*/} + Projects
} + actionButton={( + + )} + /> +
Team {userCount}
} actionButton={( - - editHandler(null) } - /> - + editHandler(null)} /> )} />
diff --git a/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx new file mode 100644 index 000000000..6907a7b12 --- /dev/null +++ b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Popup, IconButton } from 'UI'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; +const LIMIT_WARNING = 'You have reached users limit.'; + +function AddUserButton({ isAdmin = false, onClick }: any ) { + const { userStore } = useStore(); + const limtis = useObserver(() => userStore.limits); + const cannAddUser = useObserver(() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0)); + return ( + + + + ); +} + +export default AddUserButton; \ No newline at end of file diff --git a/frontend/app/components/Client/Users/components/AddUserButton/index.ts b/frontend/app/components/Client/Users/components/AddUserButton/index.ts new file mode 100644 index 000000000..66beb9cf8 --- /dev/null +++ b/frontend/app/components/Client/Users/components/AddUserButton/index.ts @@ -0,0 +1 @@ +export { default } from './AddUserButton'; \ No newline at end of file diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index 7e47f5a98..eb25b069e 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -27,6 +27,7 @@ function UserForm(props: Props) { const onSave = () => { userStore.saveUser(user).then(() => { hideModal(); + userStore.fetchLimits(); }); } @@ -42,6 +43,7 @@ function UserForm(props: Props) { })) { userStore.deleteUser(user.userId).then(() => { hideModal(); + userStore.fetchLimits(); }); } } diff --git a/frontend/app/components/Client/Users/components/UserListItem/UserListItem.tsx b/frontend/app/components/Client/Users/components/UserListItem/UserListItem.tsx index 521bd3be4..8628f029f 100644 --- a/frontend/app/components/Client/Users/components/UserListItem/UserListItem.tsx +++ b/frontend/app/components/Client/Users/components/UserListItem/UserListItem.tsx @@ -47,29 +47,32 @@ function UserListItem(props: Props) {
- {!user.isJoined && user.invitationLink ? ( - - - - ) :
} - {!user.isJoined && user.isExpiredInvite && ( - - - - )} +
+ {!user.isJoined && user.invitationLink && !user.isExpiredInvite && ( + + + + )} + + {!user.isJoined && user.isExpiredInvite && ( + + + + )} +
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx index b6d0ff995..3038813e4 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx @@ -1,14 +1,11 @@ import React from 'react' import { Styles } from '../../common'; -import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts'; -import { LineChart, Line, Legend } from 'recharts'; -import cn from 'classnames'; +import { AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip } from 'recharts'; import CountBadge from '../../common/CountBadge'; import { numberWithCommas } from 'App/utils'; interface Props { data: any; - // onClick?: (event, index) => void; } function CustomMetricOverviewChart(props: Props) { const { data } = props; @@ -33,7 +30,7 @@ function CustomMetricOverviewChart(props: Props) { {gradientDef} @@ -60,7 +57,7 @@ function CustomMetricOverviewChart(props: Props) { export default CustomMetricOverviewChart -const countView = (avg, unit) => { +const countView = (avg: any, unit: any) => { if (unit === 'mb') { if (!avg) return 0; const count = Math.trunc(avg / 1024 / 1024); @@ -72,4 +69,4 @@ const countView = (avg, unit) => { return numberWithCommas(count > 1000 ? count +'k' : count); } return avg ? numberWithCommas(avg): 0; - } \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx index ffce73783..916911d84 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx @@ -12,7 +12,7 @@ function CustomMetriPercentage(props: Props) { return (
{numberWithCommas(data.count)}
-
{`${parseInt(data.previousCount || 0)} ( ${parseInt(data.countProgress || 0).toFixed(1)}% )`}
+
{`${parseInt(data.previousCount || 0)} ( ${Math.floor(parseInt(data.countProgress || 0))}% )`}
from previous period.
) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx index 3806d55f5..b8e982b62 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx @@ -1,15 +1,80 @@ -import React from 'react'; - +import React, { useEffect } from 'react'; +import { Pagination, NoContent } from 'UI'; +import ErrorListItem from '../../../components/Errors/ErrorListItem'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { useModal } from 'App/components/Modal'; +import ErrorDetailsModal from '../../../components/Errors/ErrorDetailsModal'; +const PER_PAGE = 5; interface Props { - + metric: any; + isTemplate?: boolean; + isEdit?: boolean; + history: any, + location: any, } -function CustomMetricTableErrors(props) { +function CustomMetricTableErrors(props: RouteComponentProps) { + const { metric, isEdit = false } = props; + const errorId = new URLSearchParams(props.location.search).get("errorId"); + const { showModal, hideModal } = useModal(); + + const onErrorClick = (e: any, error: any) => { + e.stopPropagation(); + props.history.replace({search: (new URLSearchParams({errorId : error.errorId})).toString()}); + } + + useEffect(() => { + if (!errorId) return; + + showModal(, { right: true, onClose: () => { + if (props.history.location.pathname.includes("/dashboard")) { + props.history.replace({search: ""}); + } + }}); + + return () => { + hideModal(); + } + }, [errorId]) + return ( -
- -
+ +
+ {metric.data.errors && metric.data.errors.map((error: any, index: any) => ( + onErrorClick(e, error)} /> + ))} + + {isEdit && ( +
+ metric.updateKey('page', page)} + limit={metric.limit} + debounceRequest={500} + /> +
+ )} + + {!isEdit && ( + + )} +
+
); } -export default CustomMetricTableErrors; \ No newline at end of file +export default withRouter(CustomMetricTableErrors) as React.FunctionComponent>; + +const ViewMore = ({ total, limit }: any) => total > limit && ( +
+
+
+ All {total} errors +
+
+
+); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx index 45cbd198a..4c06664c7 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx @@ -1,48 +1,54 @@ -import React from 'react'; +import { useObserver } from 'mobx-react-lite'; +import React, { useEffect } from 'react'; import SessionItem from 'Shared/SessionItem'; -import { Pagination } from 'UI'; +import { Pagination, NoContent } from 'UI'; +import { useModal } from 'App/components/Modal'; -const PER_PAGE = 10; interface Props { - data: any - metric?: any + metric: any; isTemplate?: boolean; isEdit?: boolean; } function CustomMetricTableSessions(props: Props) { - const { data = { sessions: [], total: 0 }, isEdit = false, metric = {}, isTemplate } = props; - const currentPage = 1; + const { isEdit = false, metric } = props; - return ( -
- {data.sessions && data.sessions.map((session: any, index: any) => ( - - ))} - - {isEdit && ( -
- this.props.updateCurrentPage(page)} - limit={PER_PAGE} - debounceRequest={500} - /> -
- )} + return useObserver(() => ( + +
+ {metric.data.sessions && metric.data.sessions.map((session: any, index: any) => ( +
+ +
+ ))} + + {isEdit && ( +
+ metric.updateKey('page', page)} + limit={metric.data.total} + debounceRequest={500} + /> +
+ )} - {!isEdit && ( - - )} -
- ); + {!isEdit && ( + + )} +
+ + )); } export default CustomMetricTableSessions; -const ViewMore = ({ total }: any) => total > PER_PAGE && ( -
+const ViewMore = ({ total, limit }: any) => total > limit && ( +
All {total} sessions diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.module.css b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.module.css deleted file mode 100644 index 2088330ba..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.wrapper { - background-color: $gray-light; - /* border: solid thin $gray-medium; */ - border-radius: 3px; - padding: 20px; -} - -.innerWapper { - border-radius: 3px; - width: 70%; - margin: 0 auto; - background-color: white; - min-height: 220px; -} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx deleted file mode 100644 index 5275a9147..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { connect } from 'react-redux'; -import { Loader, NoContent, SegmentSelection } from 'UI'; -import { Styles } from '../../common'; -import Period from 'Types/app/period'; -import stl from './CustomMetricWidgetPreview.module.css'; -import { remove } from 'Duck/customMetrics'; -import DateRange from 'Shared/DateRange'; -import { edit } from 'Duck/customMetrics'; -import CustomMetriLineChart from '../CustomMetriLineChart'; -import CustomMetricPercentage from '../CustomMetricPercentage'; -import CustomMetricTable from '../CustomMetricTable'; -import CustomMetricPieChart from '../CustomMetricPieChart'; - -const customParams = (rangeName: string) => { - const params = { density: 70 } - - // if (rangeName === LAST_24_HOURS) params.density = 70 - // if (rangeName === LAST_30_MINUTES) params.density = 70 - // if (rangeName === YESTERDAY) params.density = 70 - // if (rangeName === LAST_7_DAYS) params.density = 70 - - return params -} - -interface Props { - metric: any; - data?: any; - onClickEdit?: (e) => void; - remove: (id) => void; - edit: (metric) => void; -} -function CustomMetricWidget(props: Props) { - const { metric } = props; - const [loading, setLoading] = useState(false) - const [data, setData] = useState({ chart: [{}] }) - const [period, setPeriod] = useState(Period({ rangeName: metric.rangeName, startDate: metric.startDate, endDate: metric.endDate })); - - const colors = Styles.customMetricColors; - const params = customParams(period.rangeName) - const prevMetricRef = useRef(); - const isTimeSeries = metric.metricType === 'timeseries'; - const isTable = metric.metricType === 'table'; - - useEffect(() => { - // Check for title change - if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) { - prevMetricRef.current = metric; - return - }; - prevMetricRef.current = metric; - setLoading(true); - }, [metric]) - - const onDateChange = (changedDates) => { - setPeriod({ ...changedDates, rangeName: changedDates.rangeValue }) - props.edit({ ...changedDates, rangeName: changedDates.rangeValue }); - } - - const chagneViewType = (e, { name, value }) => { - props.edit({ [ name ]: value }); - } - - return ( -
-
-
Preview
-
- {isTimeSeries && ( - <> - Visualization - - - )} - - {isTable && ( - <> - Visualization - - - )} -
- Time Range - -
-
-
-
- - -
- {metric.name} -
-
- { isTimeSeries && ( - <> - { metric.viewType === 'progress' && ( - - )} - { metric.viewType === 'lineChart' && ( - - )} - - )} - - { isTable && ( - <> - { metric.viewType === 'table' ? ( - - ) : ( - - )} - - )} -
-
-
-
-
-
- ); -} - -export default connect(null, { remove, edit })(CustomMetricWidget); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts deleted file mode 100644 index 9595513c4..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CustomMetricWidgetPreview'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/MissingResources.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/MissingResources.tsx index 69517328c..ae2f1d27e 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/MissingResources.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/MissingResources.tsx @@ -7,7 +7,7 @@ import Chart from './Chart'; import ResourceInfo from './ResourceInfo'; import CopyPath from './CopyPath'; -const cols = [ +const cols: Array = [ { key: 'resource', title: 'Resource', @@ -17,7 +17,7 @@ const cols = [ { key: 'sessions', title: 'Sessions', - toText: count => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`, + toText: (count: number) => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`, width: '20%', }, { @@ -25,16 +25,17 @@ const cols = [ title: 'Trend', Component: Chart, width: '20%', - }, - { - key: 'copy-path', - title: '', - Component: CopyPath, - cellClass: 'invisible group-hover:visible text-right', - width: '20%', } ]; +const copyPathCol = { + key: 'copy-path', + title: '', + Component: CopyPath, + cellClass: 'invisible group-hover:visible text-right', + width: '20%', +} + interface Props { data: any metric?: any @@ -43,6 +44,10 @@ interface Props { function MissingResources(props: Props) { const { data, metric, isTemplate } = props; + if (!isTemplate) { + cols.push(copyPathCol); + } + return ( - + = [ { key: 'type', title: 'Type', @@ -43,16 +43,17 @@ const cols = [ title: 'Trend', Component: Chart, width: '15%', - }, - { - key: 'copy-path', - title: '', - Component: CopyPath, - cellClass: 'invisible group-hover:visible text-right', - width: '15%', } ]; +const copyPathCol = { + key: 'copy-path', + title: '', + Component: CopyPath, + cellClass: 'invisible group-hover:visible text-right', + width: '15%', +} + interface Props { data: any metric?: any @@ -61,6 +62,10 @@ interface Props { function SlowestResources(props: Props) { const { data, metric, isTemplate } = props; + if (!isTemplate) { + cols.push(copyPathCol); + } + return ( - + { !isTemplate && rows.size > (small ? 3 : 5) && !showAll && -
+
+ + { children } + + ), Placeholder: (): any => null, diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx index 9b81a6d1c..e0908c6f4 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx @@ -1,30 +1,62 @@ import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; -import React from 'react'; +import React, { useEffect } from 'react'; import FunnelIssuesListItem from '../FunnelIssuesListItem'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { NoContent } from 'UI'; +import { useModal } from 'App/components/Modal'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import FunnelIssueModal from '../FunnelIssueModal'; interface Props { + loading?: boolean; issues: any; + history: any; + location: any; } -function FunnelIssuesList(props: Props) { - const { issues } = props; +function FunnelIssuesList(props: RouteComponentProps) { + const { issues, loading } = props; const { funnelStore } = useStore(); const issuesSort = useObserver(() => funnelStore.issuesSort); const issuesFilter = useObserver(() => funnelStore.issuesFilter.map((issue: any) => issue.value)); + const { showModal } = useModal(); + const issueId = new URLSearchParams(props.location.search).get("issueId"); + + const onIssueClick = (issue: any) => { + props.history.replace({search: (new URLSearchParams({issueId : issue.issueId})).toString()}); + } + + useEffect(() => { + if (!issueId) return; + + showModal(, { right: true, onClose: () => { + if (props.history.location.pathname.includes("/metric")) { + props.history.replace({search: ""}); + } + }}); + }, [issueId]); let filteredIssues = useObserver(() => issuesFilter.length > 0 ? issues.filter((issue: any) => issuesFilter.includes(issue.type)) : issues); filteredIssues = useObserver(() => issuesSort.sort ? filteredIssues.slice().sort((a: { [x: string]: number; }, b: { [x: string]: number; }) => a[issuesSort.sort] - b[issuesSort.sort]): filteredIssues); filteredIssues = useObserver(() => issuesSort.order === 'desc' ? filteredIssues.reverse() : filteredIssues); return useObserver(() => ( -
+ + +
No issues found
+
+ } + > {filteredIssues.map((issue: any, index: React.Key) => (
- + onIssueClick(issue)} />
))} -
- )); + + )) } -export default FunnelIssuesList; \ No newline at end of file +export default withRouter(FunnelIssuesList) as React.FunctionComponent>; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx index 5779ec541..89aa09be3 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx @@ -8,13 +8,14 @@ import FunnelIssueModal from '../FunnelIssueModal'; interface Props { issue: any; inDetails?: boolean; + onClick?: () => void; } -function FunnelIssuesListItem(props) { - const { issue, inDetails = false } = props; - const { showModal } = useModal(); - const onClick = () => { - showModal(, { right: true }); - } +function FunnelIssuesListItem(props: Props) { + const { issue, inDetails = false, onClick } = props; + // const { showModal } = useModal(); + // const onClick = () => { + // showModal(, { right: true }); + // } return (
null}> {/* {inDetails && ( diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index cbeb4ee4e..50bb99164 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -1,19 +1,22 @@ import React from 'react'; import { Icon, NoContent, Label, Link, Pagination } from 'UI'; import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date'; +import { getIcon } from 'react-toastify/dist/components'; interface Props { metric: any; } -function DashboardLink({ dashboards}) { +function DashboardLink({ dashboards}: any) { return ( - dashboards.map(dashboard => ( + dashboards.map((dashboard: any) => ( - -
-
·
- {dashboard.name} + +
+
+ +
+ {dashboard.name}
@@ -23,14 +26,30 @@ function DashboardLink({ dashboards}) { function MetricListItem(props: Props) { const { metric } = props; + + const getIcon = (metricType: string) => { + switch (metricType) { + case 'funnel': + return 'filter'; + case 'table': + return 'list-alt'; + case 'timeseries': + return 'bar-chart-line'; + } + } return (
-
- - {metric.name} - +
+
+
+ +
+ + {metric.name} + +
-
+ {/*
*/}
diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 8ed8df601..3cc6dff40 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -39,8 +39,8 @@ function MetricsList(props: Props) { >
-
Title
-
Type
+
Metric
+ {/*
Type
*/}
Dashboards
Owner
Visibility
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index e16b19a1d..a8c1d96c4 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -18,7 +18,7 @@ function MetricsView(props: Props) { metricStore.fetchList(); }, []); return useObserver(() => ( -
+
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 83894d299..e6553dcc8 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -12,11 +12,13 @@ import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMe import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted' +import { FilterKey } from 'Types/filter/filterType'; import FunnelWidget from 'App/components/Funnels/FunnelWidget'; import ErrorsWidget from '../Errors/ErrorsWidget'; import SessionWidget from '../Sessions/SessionWidget'; -import CustomMetricTableSessions from '../../Widgets/CustomMetricsWidgets/CustomMetricTableSessions'; +import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions'; +import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors'; interface Props { metric: any; isWidget?: boolean; @@ -27,6 +29,7 @@ function WidgetChart(props: Props) { const { dashboardStore, metricStore } = useStore(); const _metric: any = useObserver(() => metricStore.instance); const period = useObserver(() => dashboardStore.period); + const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod); const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter); const colors = Styles.customMetricColors; const [loading, setLoading] = useState(true) @@ -39,18 +42,17 @@ function WidgetChart(props: Props) { const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table'; const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart'; - const isFunnel = metric.metricType === 'funnel'; const onChartClick = (event: any) => { if (event) { - if (isTableWidget || isPieChart) { + if (isTableWidget || isPieChart) { // get the filter of clicked row const periodTimestamps = period.toTimestamps() drillDownFilter.merge({ filters: event, startTimestamp: periodTimestamps.startTimestamp, endTimestamp: periodTimestamps.endTimestamp, }); - } else { + } else { // get the filter of clicked chart point const payload = event.activePayload[0].payload; const timestamp = payload.timestamp; const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density); @@ -64,7 +66,6 @@ function WidgetChart(props: Props) { } const depsString = JSON.stringify(_metric.series); - const fetchMetricChartData = (metric: any, payload: any, isWidget: any) => { if (!isMounted()) return; setLoading(true) @@ -82,9 +83,11 @@ function WidgetChart(props: Props) { return }; prevMetricRef.current = metric; - const payload = isWidget ? { ...params } : { ...metricParams, ...metric.toJson() }; + const timestmaps = drillDownPeriod.toTimestamps(); + const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() }; debounceRequest(metric, payload, isWidget); - }, [period, depsString]); + }, [drillDownPeriod, period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]); + const renderChart = () => { const { metricType, viewType, metricOf } = metric; @@ -98,7 +101,7 @@ function WidgetChart(props: Props) { } if (metricType === 'funnel') { - return + return } if (metricType === 'predefined') { @@ -130,27 +133,39 @@ function WidgetChart(props: Props) { } if (metricType === 'table') { - if (metricOf === 'SESSIONS') { - return + ) + } + if (metricOf === FilterKey.ERRORS) { + return ( + + ) } if (viewType === 'table') { - return ; + return ( + + ) } else if (viewType === 'pieChart') { return ( ) @@ -160,7 +175,7 @@ function WidgetChart(props: Props) { return
Unknown
; } return useObserver(() => ( - + {renderChart()} )); diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx new file mode 100644 index 000000000..3034aecba --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import SelectDateRange from 'Shared/SelectDateRange'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +interface Props { + +} +function WidgetDateRange(props: Props) { + const { dashboardStore } = useStore(); + const period = useObserver(() => dashboardStore.drillDownPeriod); + const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter); + + const onChangePeriod = (period: any) => { + dashboardStore.setDrillDownPeriod(period); + const periodTimestamps = period.toTimestamps(); + drillDownFilter.merge({ + startTimestamp: periodTimestamps.startTimestamp, + endTimestamp: periodTimestamps.endTimestamp, + }) + } + + return ( + <> + Time Range + metric.setPeriod(period)} + onChange={onChangePeriod} + right={true} + /> + + ); +} + +export default WidgetDateRange; \ No newline at end of file diff --git a/frontend/app/components/Header/components/index.js b/frontend/app/components/Dashboard/components/WidgetDateRange/index.ts similarity index 100% rename from frontend/app/components/Header/components/index.js rename to frontend/app/components/Dashboard/components/WidgetDateRange/index.ts diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 9d95fc503..88e0a59b4 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; import { Button, Icon } from 'UI' import FilterSeries from '../FilterSeries'; -import { confirm } from 'UI'; +import { confirm, Popup } from 'UI'; import Select from 'Shared/Select' import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes' import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal'; @@ -28,13 +28,11 @@ function WidgetForm(props: Props) { const tableOptions = metricOf.filter(i => i.type === 'table'); const isTable = metric.metricType === 'table'; const isFunnel = metric.metricType === 'funnel'; - const isErrors = metric.metricType === 'errors'; - const isSessions = metric.metricType === 'sessions'; - const _issueOptions = [{ label: 'All', value: 'all' }].concat(issueOptions); const canAddToDashboard = metric.exists() && dashboards.length > 0; const canAddSeries = metric.series.length < 3; + const eventsLength = useObserver(() => metric.series[0].filter.filters.filter((i: any) => i.isEvent).length) + const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); - // const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value }); const writeOption = ({ value, name }: any) => { value = Array.isArray(value) ? value : value.value const obj: any = { [ name ]: value }; @@ -69,15 +67,16 @@ function WidgetForm(props: Props) { const onSave = () => { const wasCreating = !metric.exists() - metricStore.save(metric, dashboardId).then((metric: any) => { - if (wasCreating) { - if (parseInt(dashboardId) > 0) { - history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId)); - } else { - history.replace(withSiteId(metricDetails(metric.metricId), siteId)); + metricStore.save(metric, dashboardId) + .then((metric: any) => { + if (wasCreating) { + if (parseInt(dashboardId) > 0) { + history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId)); + } else { + history.replace(withSiteId(metricDetails(metric.metricId), siteId)); + } } - } - }); + }); } const onDelete = async () => { @@ -90,10 +89,6 @@ function WidgetForm(props: Props) { } } - const onObserveChanges = () => { - // metricStore.fetchMetricChartData(metric); - } - return useObserver(() => (
@@ -144,7 +139,7 @@ function WidgetForm(props: Props) { )} - {metric.metricType === 'table' && ( + {metric.metricType === 'table' && !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( <> showing @@ -92,7 +90,7 @@ function WidgetSessions(props: Props) { )}
-
+
} show={filteredSessions.sessions.length === 0} - // animatedIcon="no-results" > {filteredSessions.sessions.map((session: any) => ( - + <> + +
+ ))}
metricStore.updateKey('sessionsPage', page)} + onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)} limit={metricStore.sessionsPageSize} debounceRequest={500} /> @@ -124,12 +124,12 @@ function WidgetSessions(props: Props) { )); } -const getListSessionsBySeries = (data, seriesId) => { +const getListSessionsBySeries = (data: any, seriesId: any) => { const arr: any = { sessions: [], total: 0 }; - data.forEach(element => { + data.forEach((element: any) => { if (seriesId === 'all') { - const sessionIds = arr.sessions.map(i => i.sessionId); - arr.sessions.push(...element.sessions.filter(i => !sessionIds.includes(i.sessionId))); + const sessionIds = arr.sessions.map((i: any) => i.sessionId); + arr.sessions.push(...element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId))); arr.total = element.total } else { if (element.seriesId === seriesId) { diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index 27d3b292c..91d47751a 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -10,6 +10,9 @@ import WidgetName from '../WidgetName'; import { withSiteId } from 'App/routes'; import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues'; import Breadcrumb from 'Shared/Breadcrumb'; +import { FilterKey } from 'Types/filter/filterType'; +import { Prompt } from 'react-router' + interface Props { history: any; match: any @@ -17,14 +20,19 @@ interface Props { } function WidgetView(props: Props) { const { match: { params: { siteId, dashboardId, metricId } } } = props; - const { metricStore } = useStore(); + const { metricStore, dashboardStore } = useStore(); const widget = useObserver(() => metricStore.instance); const loading = useObserver(() => metricStore.isLoading); const [expanded, setExpanded] = useState(!metricId || metricId === 'create'); + const hasChanged = useObserver(() => widget.hasChanged) + + const dashboards = useObserver(() => dashboardStore.dashboards); + const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId)); + const dashboardName = dashboard ? dashboard.name : null; React.useEffect(() => { if (metricId && metricId !== 'create') { - metricStore.fetch(metricId); + metricStore.fetch(metricId, dashboardStore.period); } else if (metricId === 'create') { metricStore.init(); } @@ -41,10 +49,19 @@ function WidgetView(props: Props) { return useObserver(() => ( + { + if (location.pathname.includes('/metrics/') || location.pathname.includes('/metric/')) { + return true; + } + return 'You have unsaved changes. Are you sure you want to leave?'; + }} + />
@@ -53,22 +70,21 @@ function WidgetView(props: Props) { className={cn( "px-6 py-4 flex justify-between items-center", { - 'cursor-pointer hover:bg-active-blue hover:shadow-border-blue': !expanded, + 'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded, } )} onClick={openEdit} - > -

+ > +

metricStore.merge({ name })} canEdit={expanded} />

-
+
setExpanded(!expanded)}>
setExpanded(!expanded)} - className="flex items-center cursor-pointer select-none" + className="flex items-center select-none w-fit ml-auto" > {expanded ? 'Close' : 'Edit'} @@ -80,7 +96,7 @@ function WidgetView(props: Props) {
- { widget.metricOf !== 'SESSIONS' && widget.metricOf !== 'ERRORS' && ( + { widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && ( <> { (widget.metricType === 'table' || widget.metricType === 'timeseries') && } { widget.metricType === 'funnel' && } diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index 3e0b930ea..807d43c85 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -5,11 +5,12 @@ import { useDrag, useDrop } from 'react-dnd'; import WidgetChart from '../WidgetChart'; import { useObserver } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -import { withRouter } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withSiteId, dashboardMetricDetails } from 'App/routes'; import TemplateOverlay from './TemplateOverlay'; import AlertButton from './AlertButton'; import stl from './widgetWrapper.module.css'; +import { FilterKey } from 'App/types/filter/filterType'; interface Props { className?: string; @@ -25,10 +26,11 @@ interface Props { onClick?: () => void; isWidget?: boolean; } -function WidgetWrapper(props: Props) { +function WidgetWrapper(props: Props & RouteComponentProps) { const { dashboardStore } = useStore(); const { isWidget = false, active = false, index = 0, moveListItem = null, isPreview = false, isTemplate = false, dashboardId, siteId } = props; const widget: any = useObserver(() => props.widget); + const isTimeSeries = widget.metricType === 'timeseries'; const isPredefined = widget.metricType === 'predefined'; const dashboard = useObserver(() => dashboardStore.selectedDashboard); @@ -65,8 +67,7 @@ function WidgetWrapper(props: Props) { const ref: any = useRef(null) const dragDropRef: any = dragRef(dropRef(ref)) - - const addOverlay = isTemplate || (!isPredefined && isWidget) + const addOverlay = isTemplate || (!isPredefined && isWidget && widget.metricOf !== FilterKey.ERRORS && widget.metricOf !== FilterKey.SESSIONS) return useObserver(() => (
-
{widget.name}
+
{widget.name}
{isWidget && (
- {!isPredefined && ( + {!isPredefined && isTimeSeries && ( <>
diff --git a/frontend/app/components/Errors/Error/DistributionBar.js b/frontend/app/components/Errors/Error/DistributionBar.js index 35778b283..6df611d0a 100644 --- a/frontend/app/components/Errors/Error/DistributionBar.js +++ b/frontend/app/components/Errors/Error/DistributionBar.js @@ -36,6 +36,7 @@ function DistributionBar({ className, title, partitions }) { {`${ Math.round(p.prc) }%`}
} + className="w-full" >
-
+ {/*
+
-
+
*/}
diff --git a/frontend/app/components/Errors/Error/MainSection.js b/frontend/app/components/Errors/Error/MainSection.js index 2e291a62b..4a81ca062 100644 --- a/frontend/app/components/Errors/Error/MainSection.js +++ b/frontend/app/components/Errors/Error/MainSection.js @@ -103,7 +103,7 @@ export default class MainSection extends React.PureComponent {
- + {/*
{ error.status === UNRESOLVED ? } /> -
+
*/}

Last session with this error

diff --git a/frontend/app/components/Errors/List/ListItem/ListItem.js b/frontend/app/components/Errors/List/ListItem/ListItem.js index 2752fcfb4..1dde46e59 100644 --- a/frontend/app/components/Errors/List/ListItem/ListItem.js +++ b/frontend/app/components/Errors/List/ListItem/ListItem.js @@ -12,10 +12,9 @@ import Label from 'Components/Errors/ui/Label'; import stl from './listItem.module.css'; import { Styles } from '../../../Dashboard/Widgets/common'; - const CustomTooltip = ({ active, payload, label }) => { if (active) { - const p = payload[0].payload; + const p = payload[0].payload; return (

{`${moment(p.timestamp).format('l')}`}

diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index b846ed94b..b84fff021 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -1,13 +1,13 @@ import React from 'react'; import FunnelStepText from './FunnelStepText'; -import { Icon } from 'UI'; +import { Icon, Popup } from 'UI'; interface Props { filter: any; } function FunnelBar(props: Props) { const { filter } = props; - const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues); + // const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues); return (
@@ -32,6 +32,28 @@ function FunnelBar(props: Props) { }}>
{filter.completedPercentage}%
+ {filter.dropDueToIssues > 0 && ( +
+ +
{filter.dropDueToIssuesPercentage}%
+
+
+ + )}
diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index 4653a6687..2f7f06bcf 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -1,34 +1,57 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import Widget from 'App/mstore/types/widget'; import Funnelbar from './FunnelBar'; import cn from 'classnames'; import stl from './FunnelWidget.module.css'; -import { Icon } from 'UI'; import { useObserver } from 'mobx-react-lite'; +import { NoContent, Icon } from 'UI'; +import { useModal } from 'App/components/Modal'; interface Props { metric: Widget; + isWidget?: boolean } function FunnelWidget(props: Props) { - const { metric } = props; + const { metric, isWidget = false } = props; const funnel = metric.data.funnel || { stages: [] }; + const totalSteps = funnel.stages.length; + const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages; + const hasMoreSteps = funnel.stages.length > 2; + const lastStage = funnel.stages[funnel.stages.length - 1]; + const remainingSteps = totalSteps - 2; + const { hideModal } = useModal(); + + useEffect(() => { + return () => { + if (isWidget) return; + hideModal(); + } + }, []); return useObserver(() => ( - <> +
- {funnel.stages.map((filter: any, index: any) => ( -
-
- {index + 1} -
- -
- -
-
- ))} + { !isWidget && ( + stages.map((filter: any, index: any) => ( + + )) + )} + + { isWidget && ( + <> + + + { hasMoreSteps && ( + <> + + + )} + + {funnel.stages.length > 1 && ( + + )} + + )}
@@ -55,8 +78,51 @@ function FunnelWidget(props: Props) {
- + )); } +function EmptyStage({ total }: any) { + return useObserver( () => ( +
+ +
+ {`+${total} ${total > 1 ? 'steps' : 'step'}`} +
+
+
+ )) +} + +function Stage({ stage, index, isWidget }: any) { + return useObserver(() => stage ? ( +
+ + + {!isWidget && ( + + )} +
+ ) : <>) +} + +function IndexNumber({ index }: any) { + return ( +
+ {index === 0 ? : index} +
+ ); +} + + +function BarActions({ bar }: any) { + return useObserver(() => ( +
+ +
+ )) +} + export default FunnelWidget; \ No newline at end of file diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index 9c5493d0e..293799976 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -26,6 +26,8 @@ import ErrorGenPanel from 'App/dev/components'; import Alerts from '../Alerts/Alerts'; import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG'; import { fetchList as fetchMetadata } from 'Duck/customField'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; const DASHBOARD_PATH = dashboard(); const SESSIONS_PATH = sessions(); @@ -41,18 +43,33 @@ const Header = (props) => { const { sites, location, account, onLogoutClick, siteId, - boardingCompletion = 100, fetchSiteList, showAlerts = false + boardingCompletion = 100, fetchSiteList, showAlerts = false, } = props; const name = account.get('name').split(" ")[0]; const [hideDiscover, setHideDiscover] = useState(false) + const { userStore, notificationStore } = useStore(); + const initialDataFetched = useObserver(() => userStore.initialDataFetched); let activeSite = null; + useEffect(() => { + if (!account.id || initialDataFetched) return; + + setTimeout(() => { + Promise.all([ + userStore.fetchLimits(), + notificationStore.fetchNotificationsCount(), + ]).then(() => { + userStore.updateKey('initialDataFetched', true); + }); + }, 0); + }, [account]); + useEffect(() => { activeSite = sites.find(s => s.id == siteId); props.initSite(activeSite); props.fetchMetadata(); - }, [sites]) + }, [siteId]) return (
diff --git a/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx new file mode 100644 index 000000000..695139fa3 --- /dev/null +++ b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Icon } from 'UI'; +import cn from 'classnames'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +function NewProjectButton({ onClick, isAdmin = false }: any) { + const { userStore } = useStore(); + const limtis = useObserver(() => userStore.limits); + const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); + + return ( +
+ + Add New Project +
+ ); +} + +export default NewProjectButton; \ No newline at end of file diff --git a/frontend/app/components/Header/NewProjectButton/index.ts b/frontend/app/components/Header/NewProjectButton/index.ts new file mode 100644 index 000000000..b9d180076 --- /dev/null +++ b/frontend/app/components/Header/NewProjectButton/index.ts @@ -0,0 +1 @@ +export { default } from './NewProjectButton' \ No newline at end of file diff --git a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js index 9e003145f..71359120d 100644 --- a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js +++ b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js @@ -46,8 +46,10 @@ const styles = { @withToggle('display', 'toggleModal') @withRouter class OnboardingExplore extends React.PureComponent { - componentWillMount() { - this.props.getOnboard(); + UNSAFE_componentWillMount() { + if (this.props.boarding.size === 0) { + this.props.getOnboard(); + } } componentDidMount() { diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index d46c347fe..d6df6be31 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -14,6 +14,7 @@ import { clearSearch } from 'Duck/search'; import { fetchList as fetchIntegrationVariables } from 'Duck/customField'; import { withStore } from 'App/mstore' import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG'; +import NewProjectButton from './NewProjectButton'; @withStore @withRouter @@ -57,10 +58,8 @@ export default class SiteDropdown extends React.PureComponent { const activeSite = sites.find(s => s.id == siteId); const disabled = !siteChangeAvaliable(pathname); const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname); - const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0; + // const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0; - // const signslGreenSvg = - // const signslRedSvg = return (
{ @@ -87,18 +86,7 @@ export default class SiteDropdown extends React.PureComponent { )) } -
- - Add New Project -
+
+ {component} , document.querySelector("#modal-root"), - ) : null; + ) : <>; } \ No newline at end of file diff --git a/frontend/app/components/Modal/ModalOverlay.tsx b/frontend/app/components/Modal/ModalOverlay.tsx index 762782427..398e27f2f 100644 --- a/frontend/app/components/Modal/ModalOverlay.tsx +++ b/frontend/app/components/Modal/ModalOverlay.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { useModal } from 'App/components/Modal'; import stl from './ModalOverlay.module.css' import cn from 'classnames'; -function ModalOverlay({ children, left = false, right = false }) { - let modal = useModal(); - +function ModalOverlay({ hideModal, children, left = false, right = false }: any) { return (
modal.hideModal()} + onClick={hideModal} className={stl.overlay} style={{ background: "rgba(0,0,0,0.5)" }} /> diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx index dba7d0c08..55d18b6f9 100644 --- a/frontend/app/components/Modal/index.tsx +++ b/frontend/app/components/Modal/index.tsx @@ -6,6 +6,7 @@ const ModalContext = createContext({ component: null, props: { right: false, + onClose: () => {}, }, showModal: (component: any, props: any) => {}, hideModal: () => {} @@ -19,7 +20,7 @@ export class ModalProvider extends Component { } } - showModal = (component, props = {}) => { + showModal = (component, props = { }) => { this.setState({ component, props @@ -28,11 +29,15 @@ export class ModalProvider extends Component { }; hideModal = () => { + document.removeEventListener('keydown', this.handleKeyDown); + const { props } = this.state; + if (props.onClose) { + props.onClose(); + }; this.setState({ component: null, props: {} }); - document.removeEventListener('keydown', this.handleKeyDown); } state = { @@ -45,7 +50,7 @@ export class ModalProvider extends Component { render() { return ( - + {this.props.children} ); diff --git a/frontend/app/components/Session/Layout/Player/Controls.module.css b/frontend/app/components/Session/Layout/Player/Controls.module.css index ec00e8766..8879514dd 100644 --- a/frontend/app/components/Session/Layout/Player/Controls.module.css +++ b/frontend/app/components/Session/Layout/Player/Controls.module.css @@ -27,7 +27,7 @@ .skipIntervalButton { transition: all 0.2s; - font-size: 12px; + font-size: 14px; padding: 0 10px; height: 30px; border-radius: 3px; diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index 1106f6dfa..5793e2d52 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -25,7 +25,7 @@ const InitLoader = connectPlayer(state => ({ function LivePlayer ({ session, toggleFullscreen, closeBottomBlock, fullscreen, jwt, loadingCredentials, assistCredendials, request, isEnterprise, hasErrors }) { useEffect(() => { if (!loadingCredentials) { - initPlayer(session, jwt, assistCredendials, true); + initPlayer(session, assistCredendials, true); } return () => cleanPlayer() }, [ session.sessionId, loadingCredentials, assistCredendials ]); diff --git a/frontend/app/components/Session/LiveSession.js b/frontend/app/components/Session/LiveSession.js index 08d941be5..673e0c701 100644 --- a/frontend/app/components/Session/LiveSession.js +++ b/frontend/app/components/Session/LiveSession.js @@ -47,7 +47,7 @@ function LiveSession({ export default withPermissions(['ASSIST_LIVE'], '', true)(connect((state, props) => { const { match: { params: { sessionId } } } = props; const isAssist = state.getIn(['sessions', 'activeTab']).type === 'live'; - const hasSessiosPath = state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions'); + const hasSessiosPath = state.getIn([ 'sessions', 'sessionPath' ]).pathname.includes('/sessions'); return { sessionId, loading: state.getIn([ 'sessions', 'loading' ]), diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.js index f7e93c493..c86d7d3e4 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.js @@ -1,5 +1,5 @@ import React from 'react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import usePageTitle from 'App/hooks/usePageTitle'; import { fetch as fetchSession } from 'Duck/sessions'; @@ -21,18 +21,14 @@ function Session({ fetchSlackList, }) { usePageTitle("OpenReplay Session Player"); - // useEffect(() => { - // fetchSlackList() - // }, []); + const [ initializing, setInitializing ] = useState(true) useEffect(() => { if (sessionId != null) { fetchSession(sessionId) } else { console.error("No sessionID in route.") } - return () => { - if (!session.exists()) return; - } + setInitializing(false) },[ sessionId ]); return ( @@ -46,7 +42,7 @@ function Session({ } > - + { session.isIOS ? : diff --git a/frontend/app/components/Session/Tabs/tabs.module.css b/frontend/app/components/Session/Tabs/tabs.module.css index 72e5aa775..55f50749c 100644 --- a/frontend/app/components/Session/Tabs/tabs.module.css +++ b/frontend/app/components/Session/Tabs/tabs.module.css @@ -15,7 +15,6 @@ cursor: pointer; transition: all 0.2s; color: $gray-darkest; - border-bottom: solid thin transparent; font-weight: 500; white-space: nowrap; diff --git a/frontend/app/components/Session_/EventsBlock/Metadata/SessionList.js b/frontend/app/components/Session_/EventsBlock/Metadata/SessionList.js index 679e6b401..bc0868933 100644 --- a/frontend/app/components/Session_/EventsBlock/Metadata/SessionList.js +++ b/frontend/app/components/Session_/EventsBlock/Metadata/SessionList.js @@ -36,7 +36,13 @@ class SessionList extends React.PureComponent { { site.name }
- { site.sessions.map(session => ) } +
+ { site.sessions.map(session => ( +
+ +
+ )) } +
)) } diff --git a/frontend/app/components/Session_/Issues/IssuesSortDropdown.js b/frontend/app/components/Session_/Issues/IssuesSortDropdown.js deleted file mode 100644 index 16effb366..000000000 --- a/frontend/app/components/Session_/Issues/IssuesSortDropdown.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Dropdown } from 'semantic-ui-react'; -import { IconButton } from 'UI'; - -const sessionSortOptions = { -// '': 'All', - 'open': 'Open', - 'closed': 'Closed', -}; - -const sortOptions = Object.entries(sessionSortOptions) - .map(([ value, text ]) => ({ value, text })); - -const IssuesSortDropdown = ({ onChange, value }) => { - // sort = (e, { 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); - // } - - return ( - - } - pointing="top right" - options={ sortOptions } - onChange={ onChange } - // defaultValue={ sortOptions[ 0 ].value } - icon={ null } - /> - ); -} - -export default IssuesSortDropdown; diff --git a/frontend/app/components/Session_/Issues/SessionIssuesPanel.js b/frontend/app/components/Session_/Issues/SessionIssuesPanel.js deleted file mode 100644 index 851b443c9..000000000 --- a/frontend/app/components/Session_/Issues/SessionIssuesPanel.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { getRE } from 'App/utils'; -import { Input } from 'UI'; -import IssueListItem from './IssueListItem'; -import IssuesSortDropdown from './IssuesSortDropdown'; - -class SessionIssuesPanel extends React.Component { - state = { search: '', closed: false, issueType: 'open' } - write = ({ target: { value, name } }) => this.setState({ [ name ]: value }); - writeOption = (e, { name, value }) => this.setState({ [ name ]: value }); - - render() { - const { issueTypeIcons, users, activeIssue, issues = [], onIssueClick = () => null } = this.props; - const { search, closed, issueType } = this.state; - - let filteredIssues = issues.filter(({ closed, title }) => getRE(search, 'i').test(title)) - if (!issueType !== '') { - filteredIssues = filteredIssues.filter(({ closed }) => closed === ( this.state.issueType === 'closed')) - } - // .filter(({ closed }) => closed == this.state.closed); - - filteredIssues = filteredIssues.map(issue => { - issue.user = users.filter(user => user.id === issue.assignee).first(); - return issue; - }); - - return ( -
-
- - -
-
- { filteredIssues.map(issue => ( - onIssueClick(issue) } - issue={ issue } - icon={ issueTypeIcons[issue.issueType] } - user={ issue.user } - active={ activeIssue && activeIssue.id === issue.id } - /> - )) - } -
-
- ); - } -} - -export default connect(state => ({ - issues: state.getIn(['assignments', 'list']), - issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']), - users: state.getIn(['assignments', 'users']), -}))(SessionIssuesPanel); diff --git a/frontend/app/components/Session_/Issues/issuesModal.stories.js b/frontend/app/components/Session_/Issues/issuesModal.stories.js index 6e71a3705..60769df9f 100644 --- a/frontend/app/components/Session_/Issues/issuesModal.stories.js +++ b/frontend/app/components/Session_/Issues/issuesModal.stories.js @@ -5,7 +5,6 @@ import IssueHeader from './IssueHeader'; import IssueComment from './IssueComment'; import IssueCommentForm from './IssueCommentForm'; import IssueDetails from './IssueDetails'; -import SessionIssuesPanel from './SessionIssuesPanel'; import IssueForm from './IssueForm'; import IssueListItem from './IssueListItem'; import IssueDescription from './IssueDescription'; @@ -298,13 +297,4 @@ storiesOf('Issues', module) )) - .add('SessionIssuesPanel', () => ( -
- -
- )) diff --git a/frontend/app/components/Session_/Issues/sessionIssuesPanel.module.css b/frontend/app/components/Session_/Issues/sessionIssuesPanel.module.css deleted file mode 100644 index d13bcfbbf..000000000 --- a/frontend/app/components/Session_/Issues/sessionIssuesPanel.module.css +++ /dev/null @@ -1,9 +0,0 @@ - - -.searchInput { - padding: 10px 6px !important; - - &:focus { - border-color: $teal !important; - } - } \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 48f738247..d42cff222 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -164,7 +164,6 @@ export default class Controls extends React.Component { } onKeyDown = (e) => { - console.log(e.key, e.target) if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 1dffbee1b..51efa8b8e 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -155,6 +155,7 @@ export default class Timeline extends React.PureComponent { className={ stl.progress } onClick={ disabled ? null : this.seekProgress } ref={ this.progressRef } + role="button" > @@ -199,7 +200,7 @@ export default class Timeline extends React.PureComponent { } > - + )) @@ -224,7 +225,7 @@ export default class Timeline extends React.PureComponent { } > - + ))} @@ -246,7 +247,7 @@ export default class Timeline extends React.PureComponent { } > - + } @@ -269,7 +270,7 @@ export default class Timeline extends React.PureComponent { } > - + )) @@ -297,7 +298,7 @@ export default class Timeline extends React.PureComponent { } > - + )) diff --git a/frontend/app/components/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index 55f59e73d..7f9aed6c7 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -55,10 +55,10 @@ export default class PlayerBlockHeader extends React.PureComponent { backHandler = () => { const { history, siteId, sessionPath, isAssist } = this.props; - if (sessionPath === history.location.pathname || sessionPath.includes("/session/") || isAssist) { + if (sessionPath.pathname === history.location.pathname || sessionPath.pathname.includes("/session/") || isAssist) { history.push(withSiteId(isAssist ? ASSIST_ROUTE: SESSIONS_ROUTE, siteId)); } else { - history.push(sessionPath ? sessionPath : withSiteId(SESSIONS_ROUTE, siteId)); + history.push(sessionPath ? sessionPath.pathname + sessionPath.search : withSiteId(SESSIONS_ROUTE, siteId)); } } diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js index 0e017d748..37050eec0 100644 --- a/frontend/app/components/Signup/SignupForm/SignupForm.js +++ b/frontend/app/components/Signup/SignupForm/SignupForm.js @@ -1,10 +1,11 @@ import React from 'react' -import { Form, Input, Icon, Button, Link, Dropdown, CircularLoader } from 'UI' +import { Form, Input, Icon, Button, Link, CircularLoader } from 'UI' import { login } from 'App/routes' import ReCAPTCHA from 'react-google-recaptcha' import stl from './signup.module.css' import { signup } from 'Duck/user'; import { connect } from 'react-redux' +import Select from 'Shared/Select' const LOGIN_ROUTE = login() const recaptchaRef = React.createRef() @@ -48,7 +49,7 @@ export default class SignupForm extends React.Component { } write = ({ target: { value, name } }) => this.setState({ [ name ]: value }) - writeOption = (e, { name, value }) => this.setState({ [ name ]: value }); + writeOption = ({ name, value }) => this.setState({ [ name ]: value.value }); onSubmit = (e) => { e.preventDefault(); @@ -82,7 +83,7 @@ export default class SignupForm extends React.Component { { tenants.length > 0 && ( - }
-
diff --git a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx index 67a1a120d..6b8e10972 100644 --- a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx +++ b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx @@ -5,21 +5,20 @@ import { Link } from 'react-router-dom'; interface Props { items: any } -function Breadcrumb(props) { +function Breadcrumb(props: Props) { const { items } = props; return (
- {items.map((item, index) => { + {items.map((item: any, index: any) => { if (index === items.length - 1) { return ( - {item.label} + {item.label} ); } return (
- {item.label} + {item.label} / - {/* */}
); })} diff --git a/frontend/app/components/shared/CodeSnippet/CodeSnippet.tsx b/frontend/app/components/shared/CodeSnippet/CodeSnippet.tsx index 8302800f9..76f752f70 100644 --- a/frontend/app/components/shared/CodeSnippet/CodeSnippet.tsx +++ b/frontend/app/components/shared/CodeSnippet/CodeSnippet.tsx @@ -10,8 +10,6 @@ const inputModeOptions = [ const inputModeOptionsMap: any = {} inputModeOptions.forEach((o: any, i: any) => inputModeOptionsMap[o.value] = i) -console.log('inputModeOptionsMap', inputModeOptionsMap) - interface Props { host: string; diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx deleted file mode 100644 index 06b292eb2..000000000 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import { Form, Button, IconButton, HelpText } from 'UI'; -import FilterSeries from '../FilterSeries'; -import { connect } from 'react-redux'; -import { edit as editMetric, save, addSeries, removeSeries, remove } from 'Duck/customMetrics'; -import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview'; -import { confirm } from 'UI'; -import { toast } from 'react-toastify'; -import cn from 'classnames'; -import DropdownPlain from '../../DropdownPlain'; -import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions'; -import { FilterKey } from 'Types/filter/filterType'; -interface Props { - metric: any; - editMetric: (metric, shouldFetch?) => void; - save: (metric) => Promise; - loading: boolean; - addSeries: (series?) => void; - onClose: () => void; - remove: (id) => Promise; - removeSeries: (seriesIndex) => void; -} - -function CustomMetricForm(props: Props) { - const { metric, loading } = props; - // const metricOfOptions = metricOf.filter(i => i.key === metric.metricType); - const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); - const tableOptions = metricOf.filter(i => i.type === 'table'); - const isTable = metric.metricType === 'table'; - const isTimeSeries = metric.metricType === 'timeseries'; - const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions); - - - const addSeries = () => { - props.addSeries(); - } - - const removeSeries = (index) => { - props.removeSeries(index); - } - - const write = ({ target: { value, name } }) => props.editMetric({ [ name ]: value }, false); - const writeOption = (e, { value, name }) => { - props.editMetric({ [ name ]: value }, false); - - if (name === 'metricValue') { - props.editMetric({ metricValue: [value] }, false); - } - - if (name === 'metricOf') { - if (value === FilterKey.ISSUE) { - props.editMetric({ metricValue: ['all'] }, false); - } - } - - if (name === 'metricType') { - if (value === 'timeseries') { - props.editMetric({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }, false); - } else if (value === 'table') { - props.editMetric({ metricOf: tableOptions[0].value, viewType: 'table' }, false); - } - } - }; - - // const changeConditionTab = (e, { name, value }) => { - // props.editMetric({[ 'viewType' ]: value }); - // }; - - const save = () => { - props.save(metric).then(() => { - toast.success(metric.exists() ? 'Updated succesfully.' : 'Created succesfully.'); - props.onClose() - }); - } - - const deleteHandler = async () => { - if (await confirm({ - header: 'Custom Metric', - confirmButton: 'Delete', - confirmation: `Are you sure you want to delete ${metric.name}` - })) { - props.remove(metric.metricId).then(() => { - toast.success('Deleted succesfully.'); - props.onClose(); - }); - } - } - - return ( -
-
-
- - -
- -
- -
- - - {metric.metricType === 'timeseries' && ( - <> - of - - - )} - - {metric.metricType === 'table' && ( - <> - of - - - )} - - {metric.metricOf === FilterKey.ISSUE && ( - <> - issue type - - - )} - - {metric.metricType === 'table' && ( - <> - showing - - - )} -
-
- -
- - {metric.series && metric.series.size > 0 && metric.series.take(isTable ? 1 : metric.series.size).map((series: any, index: number) => ( -
- removeSeries(index)} - canDelete={metric.series.size > 1} - emptyMessage={isTable ? - 'Filter data using any event or attribute. Use Add Step button below to do so.' : - 'Add user event or filter to define the series by clicking Add Step.' - } - /> -
- ))} -
- - { isTimeSeries && ( -
2})}> - -
- )} - -
- - -
- -
-
- - - -
-
- { metric.exists() && } -
-
- - ); -} - -export default connect(state => ({ - metric: state.getIn(['customMetrics', 'instance']), - loading: state.getIn(['customMetrics', 'saveRequest', 'loading']), -}), { editMetric, save, addSeries, remove, removeSeries })(CustomMetricForm); \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts deleted file mode 100644 index e6ffb605b..000000000 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CustomMetricForm'; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/CustomMetricsModal.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/CustomMetricsModal.tsx deleted file mode 100644 index 9783ceca0..000000000 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/CustomMetricsModal.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' -import { IconButton, SlideModal } from 'UI'; -import CustomMetricForm from '../CustomMetricForm'; -import { connect } from 'react-redux' -import { init } from 'Duck/customMetrics'; - -interface Props { - metric: any; - init: (instance?, setDefault?) => void; -} -function CustomMetricsModal(props: Props) { - const { metric } = props; - return ( - <> - - { metric && metric.exists() ? 'Update Custom Metric' : 'Create Custom Metric' } -
- } - isDisplayed={ !!metric } - onClose={ () => props.init(null, true)} - content={ (!!metric) && ( -
- props.init(null, true)} /> -
- )} - /> - - ) -} - - -export default connect(state => ({ - metric: state.getIn(['customMetrics', 'instance']), - alertInstance: state.getIn(['alerts', 'instance']), - showModal: state.getIn(['customMetrics', 'showModal']), - }), { init })(CustomMetricsModal); \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/index.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/index.tsx deleted file mode 100644 index 251375d3b..000000000 --- a/frontend/app/components/shared/CustomMetrics/CustomMetricsModal/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CustomMetricsModal'; \ No newline at end of file diff --git a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js b/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js deleted file mode 100644 index da1460362..000000000 --- a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import { Dropdown } from 'semantic-ui-react'; -import cn from 'classnames'; -import { - getDateRangeFromValue, - getDateRangeLabel, - dateRangeValues, - getDateRangeFromTs, - CUSTOM_RANGE, - DATE_RANGE_VALUES, -} from 'App/dateRange'; -import { Icon } from 'UI'; -import DateRangePopup from './DateRangePopup'; -import DateOptionLabel from './DateOptionLabel'; -import styles from './dateRangeDropdown.module.css'; - -const getDateRangeOptions = (customRange = getDateRangeFromValue(CUSTOM_RANGE)) => dateRangeValues.map(value => ({ - value, - text: , - content: getDateRangeLabel(value), -})); - -export default class DateRangeDropdown extends React.PureComponent { - state = { - showDateRangePopup: false, - range: null, - value: DATE_RANGE_VALUES.TODAY, - }; - - static getDerivedStateFromProps(props) { - const { rangeValue, startDate, endDate } = props; - if (rangeValue) { - const range = rangeValue === CUSTOM_RANGE - ? getDateRangeFromTs(startDate, endDate) - : getDateRangeFromValue(rangeValue); - return { - value: rangeValue, - range, - }; - } - return null; - } - - onCancelDateRange = () => this.setState({ showDateRangePopup: false }); - - onApplyDateRange = (range, value) => { - this.setState({ - showDateRangePopup: false, - range, - value, - }); - - this.props.onChange({ - startDate: range.start.unix() * 1000, - endDate: range.end.unix() * 1000, - rangeValue: value, - }); - } - - onItemClick = (event, { value }) => { - if (value !== CUSTOM_RANGE) { - const range = getDateRangeFromValue(value); - this.onApplyDateRange(range, value); - } else { - this.setState({ showDateRangePopup: true }); - } - } - - render() { - const { customRangeRight, button = false, className, direction = 'right', customHidden=false, show30Minutes=false } = this.props; - const { showDateRangePopup, value, range } = this.state; - - let options = getDateRangeOptions(range); - - if (customHidden) { - options.pop(); - } - - if (!show30Minutes) { - options.shift() - } - - return ( -
- - { value === CUSTOM_RANGE ? range.start.format('MMM Do YY, hh:mm A') + ' - ' + range.end.format('MMM Do YY, hh:mm A') : getDateRangeLabel(value) } - -
: null - } - // selection={!button} - name="sessionDateRange" - direction={ direction } - className={ button ? "" : "customDropdown" } - // pointing="top left" - placeholder="Select..." - icon={ null } - > - - { options.map((props, i) => - - ) } - - - { - showDateRangePopup && -
- -
- } -
- ); - } -} diff --git a/frontend/app/components/shared/DateRangeDropdown/dateRangeDropdown.module.css b/frontend/app/components/shared/DateRangeDropdown/dateRangeDropdown.module.css deleted file mode 100644 index 1cf17bf82..000000000 --- a/frontend/app/components/shared/DateRangeDropdown/dateRangeDropdown.module.css +++ /dev/null @@ -1,69 +0,0 @@ -.button { - padding: 0 4px; - border-radius: 3px; - color: $teal; - cursor: pointer; - display: flex !important; - align-items: center !important; - & span { - white-space: nowrap; - margin-right: 5px; - } -} - -.dropdownTrigger { - padding: 4px; - &:hover { - background-color: $gray-light; - } -} -.dateRangeOptions { - position: relative; - - display: flex !important; - border-radius: 3px; - color: $gray-darkest; - font-weight: 500; - - - & .dateRangePopup { - top: 38px; - bottom: 0; - z-index: 999; - position: absolute; - background-color: white; - border: solid thin $gray-light; - border-radius: 3px; - min-height: fit-content; - min-width: 773px; - box-shadow: 0px 2px 10px 0 $gray-light; - } -} - -.dropdown { - display: flex !important; - padding: 4px 4px; - border-radius: 3px; - color: $gray-darkest; - font-weight: 500; - &:hover { - background-color: $gray-light; - } -} - -.dropdownTrigger { - padding: 4px 4px; - border-radius: 3px; - &:hover { - background-color: $gray-light; - } -} - -.dropdownIcon { - margin-top: 1px; - margin-left: 3px; -} - -.customRangeRight { - right: 0 !important; -} \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js index 34e2fe3b1..2e7ed2984 100644 --- a/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js +++ b/frontend/app/components/shared/Filters/FilterDuration/FilterDuration.js @@ -1,5 +1,4 @@ import React from 'react'; -// import { Input, Label } from 'semantic-ui-react'; import styles from './FilterDuration.module.css'; import { Input } from 'UI' diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx index a75843709..01388946b 100644 --- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -5,11 +5,10 @@ import { List } from 'immutable'; import { useObserver } from 'mobx-react-lite'; interface Props { - // filters: any[]; // event/filter filter?: any; // event/filter - onUpdateFilter: (filterIndex, filter) => void; - onRemoveFilter: (filterIndex) => void; - onChangeEventsOrder: (e, { name, value }) => void; + onUpdateFilter: (filterIndex: any, filter: any) => void; + onRemoveFilter: (filterIndex: any) => void; + onChangeEventsOrder: (e: any, { name, value }: any) => void; hideEventsOrder?: boolean; observeChanges?: () => void; saveRequestPayloads?: boolean; @@ -23,7 +22,7 @@ function FilterList(props: Props) { useEffect(observeChanges, [filters]); - const onRemoveFilter = (filterIndex) => { + const onRemoveFilter = (filterIndex: any) => { props.onRemoveFilter(filterIndex); } diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx index 5ed5b228f..ecb9e3b52 100644 --- a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -122,9 +122,13 @@ function FilterModal(props: Props) { ); } -export default connect(state => ({ - // filters: state.getIn([ 'search', 'filterListLive' ]), - filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), - metaOptions: state.getIn([ 'customFields', 'list' ]), - fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]), -}))(FilterModal); +export default connect((state: any, props: any) => { + return ({ + filters: props.isLive ? state.getIn([ 'search', 'filterListLive' ]) : state.getIn([ 'search', 'filterList' ]), + filterSearchList: props.isLive ? state.getIn([ 'liveSearch', 'filterSearchList' ]) : state.getIn([ 'search', 'filterSearchList' ]), + // filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), + // liveFilterSearchList: state.getIn([ 'liveSearch', 'filterSearchList' ]), + metaOptions: state.getIn([ 'customFields', 'list' ]), + fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]), + }) +})(FilterModal); diff --git a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx index b52a91939..c10dd9396 100644 --- a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx +++ b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx @@ -21,7 +21,9 @@ const dropdownStyles = { ...provided, paddingRight: '0px', width: 'fit-content', - // height: '26px' + '& input': { + marginTop: '-3px', + }, }), placeholder: (provided: any) => ({ ...provided, @@ -40,6 +42,7 @@ const dropdownStyles = { top: 20, left: 0, minWidth: 'fit-content', + overflow: 'hidden', }), container: (provided: any) => ({ ...provided, @@ -49,11 +52,16 @@ const dropdownStyles = { const opacity = state.isDisabled ? 0.5 : 1; const transition = 'opacity 300ms'; - return { ...provided, opacity, transition }; + return { + ...provided, + opacity, + transition, + marginTop: '-3px', + }; } } interface Props { - onChange: (e, { name, value }) => void; + onChange: (e: any, { name, value }: any) => void; className?: string; options?: any; value?: string; @@ -70,7 +78,7 @@ function FilterOperator(props: Props) { styles={dropdownStyles} placeholder="Select" isDisabled={isDisabled} - defaultValue={ value } + value={value ? options.find((i: any) => i.value === value) : null} onChange={({ value }: any) => onChange(null, { name: 'operator', value })} /> diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx index 91be41af0..d4e20d322 100644 --- a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -11,12 +11,12 @@ interface Props { filter?: any; // event/filter filterList: any; filterListLive: any; - onFilterClick: (filter) => void; + onFilterClick: (filter: any) => void; children?: any; isLive?: boolean; } function FilterSelection(props: Props) { - const { filter, onFilterClick, children, isLive = true } = props; + const { filter, onFilterClick, children } = props; const [showModal, setShowModal] = useState(false); return ( @@ -45,8 +45,9 @@ function FilterSelection(props: Props) { {showModal && (
)} diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index df36a1046..7087926ff 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -88,7 +88,7 @@ function FilterValue(props: Props) { case FilterType.DROPDOWN: return ( onChange(null, { value }, valueIndex)} onAddValue={onAddValue} diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx index 6123f931d..13c40cbd8 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -43,6 +43,7 @@ const dropdownStyles = { top: 20, left: 0, minWidth: 'fit-content', + overflow: 'hidden', }), container: (provided: any) => ({ ...provided, @@ -50,9 +51,10 @@ const dropdownStyles = { }), input: (provided: any) => ({ ...provided, - // padding: '0px', - // margin: '0px', height: '22px', + '& input:focus': { + border: 'none !important', + } }), singleValue: (provided: any, state: { isDisabled: any; }) => { const opacity = state.isDisabled ? 0.5 : 1; @@ -67,14 +69,14 @@ const dropdownStyles = { } } interface Props { - filter: any; // event/filter + // filter: any; // event/filter // options: any[]; value: string; onChange: (value: any) => void; className?: string; options: any[]; search?: boolean; - multiple?: boolean; + // multiple?: boolean; showCloseButton?: boolean; showOrButton?: boolean; onRemoveValue?: () => void; @@ -82,7 +84,7 @@ interface Props { isMultilple?: boolean; } function FilterValueDropdown(props: Props) { - const { filter, multiple = false, isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props; + const { isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props; // const options = [] return ( diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index eae686134..0fe4b0b9a 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -124,40 +124,46 @@ function LiveSessionList(props: Props) { props.applyFilter({ order: state })} sortOrder={filter.order} /> - + - See how to {'enable Assist'} and ensure you're using tracker-assist v3.5.0 or higher. - - } - image={} - show={ !loading && list.size === 0} - > - - {list.map(session => ( - - ))} + title={"No live sessions."} + subtext={ + + See how to {'enable Assist'} and ensure you're using tracker-assist v3.5.0 or higher. + + } + image={} + show={ !loading && list.size === 0} + > +
+ + {list.map(session => ( + <> + +
+ + ))} -
- props.updateCurrentPage(page)} - limit={PER_PAGE} - /> -
- - +
+ props.updateCurrentPage(page)} + limit={PER_PAGE} + /> +
+ + +
+
) } diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index 44d13f2ac..9f59c92d9 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -13,9 +13,10 @@ interface Props { styles?: any; onChange: (value: any) => void; name?: string; + placeholder?: string; [x:string]: any; } -export default function({ name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', ...rest }: Props) { +export default function({ placeholder='Select', name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', ...rest }: Props) { const defaultSelected = defaultValue ? (options.find(o => o.value === defaultValue) || options[0]): null; const customStyles = { option: (provided: any, state: any) => ({ @@ -24,6 +25,7 @@ export default function({ name = '', onChange, right = false, plain = false, opt transition: 'all 0.3s', backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent', color: state.isFocused ? colors.teal : 'black', + fontSize: '14px', '&:hover': { transition: 'all 0.2s', backgroundColor: colors['active-blue'], @@ -98,6 +100,12 @@ export default function({ name = '', onChange, right = false, plain = false, opt return { ...provided, opacity, transition }; }, + input: (provided: any) => ({ + ...provided, + '& input:focus': { + border: 'none !important', + } + }), noOptionsMessage: (provided: any) => ({ ...provided, whiteSpace: 'nowrap !important', @@ -127,7 +135,7 @@ export default function({ name = '', onChange, right = false, plain = false, opt } })} blurInputOnSelect={true} - // menuPosition="fixed" + placeholder={placeholder} {...rest} /> ); @@ -159,4 +167,4 @@ const CustomValueContainer = ({ children, ...rest }: any) => { {!conditional && ` and ${selectedCount - 1} others`} ) - } \ No newline at end of file + } diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index 13573b4d5..107c87337 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -5,6 +5,7 @@ import Period, { LAST_7_DAYS } from 'Types/app/period'; import { components } from 'react-select'; import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; +import cn from 'classnames'; interface Props { period: any, diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx index 203b72089..2234a5242 100644 --- a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -36,7 +36,7 @@ export default function PlayLink(props: Props) { onMouseEnter={() => toggleHover(true)} onMouseLeave={() => toggleHover(false)} > - + ) } \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index b6b0f4832..d787aa310 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -5,6 +5,7 @@ import { Avatar, TextEllipsis, Label, + Icon, } from 'UI'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; @@ -104,7 +105,7 @@ function SessionItem(props: RouteComponentProps) { }); return ( -
+
e.stopPropagation()}>
@@ -114,36 +115,38 @@ function SessionItem(props: RouteComponentProps) { className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, [stl.userName]: !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})} onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)} > - +
-
+
{formatTimeOrDate(startedAt, timezone) }
-
+
{!isAssist && ( <>
{ eventsCount } { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }
-
·
+ )}
{ live ? : formattedDuration }
-
-
+
+ +
+
-
·
+ -
·
+ diff --git a/frontend/app/components/shared/SessionItem/sessionItem.module.css b/frontend/app/components/shared/SessionItem/sessionItem.module.css index 897ee327f..34ed53ad9 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.module.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.module.css @@ -1,8 +1,8 @@ .sessionItem { background-color: #fff; user-select: none; - border-radius: 3px; - border: solid thin #EEEEEE; + /* border-radius: 3px; */ + /* border: solid thin #EEEEEE; */ transition: all 0.4s; & .favorite { @@ -14,7 +14,7 @@ &:hover { background-color: $active-blue; - border: solid thin $active-blue-border; + /* border: solid thin $active-blue-border; */ transition: all 0.2s; & .playLink { diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 5833db31c..d7d74aac1 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -5,7 +5,6 @@ import SaveFilterButton from 'Shared/SaveFilterButton'; import { connect } from 'react-redux'; import { Button } from 'UI'; import { edit, addFilter } from 'Duck/search'; -import SaveFunnelButton from '../SaveFunnelButton'; interface Props { appliedFilter: any; @@ -15,15 +14,15 @@ interface Props { } function SessionSearch(props: Props) { const { appliedFilter, saveRequestPayloads = false } = props; - const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0; - const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0; + const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).size > 0; + const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0; - const onAddFilter = (filter) => { + const onAddFilter = (filter: any) => { props.addFilter(filter); } - const onUpdateFilter = (filterIndex, filter) => { - const newFilters = appliedFilter.filters.map((_filter, i) => { + const onUpdateFilter = (filterIndex: any, filter: any) => { + const newFilters = appliedFilter.filters.map((_filter: any, i: any) => { if (i === filterIndex) { return filter; } else { @@ -37,8 +36,8 @@ function SessionSearch(props: Props) { }); } - const onRemoveFilter = (filterIndex) => { - const newFilters = appliedFilter.filters.filter((_filter, i) => { + const onRemoveFilter = (filterIndex: any) => { + const newFilters = appliedFilter.filters.filter((_filter: any, i: any) => { return i !== filterIndex; }); @@ -47,7 +46,7 @@ function SessionSearch(props: Props) { }); } - const onChangeEventsOrder = (e, { name, value }) => { + const onChangeEventsOrder = (e: any, { value }: any) => { props.edit({ eventsOrder: value, }); @@ -82,7 +81,6 @@ function SessionSearch(props: Props) {
-
@@ -90,7 +88,7 @@ function SessionSearch(props: Props) { ) : <>; } -export default connect(state => ({ +export default connect((state: any) => ({ saveRequestPayloads: state.getIn(['site', 'active', 'saveRequestPayloads']), appliedFilter: state.getIn([ 'search', 'instance' ]), }), { edit, addFilter })(SessionSearch); \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx index 28d1af364..7f38a0fed 100644 --- a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx +++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx @@ -11,6 +11,8 @@ interface Props { addFilterByKeyAndValue: (key: string, value: string) => void; filterList: any; filterListLive: any; + filterSearchListLive: any; + filterSearchList: any; } function SessionSearchField(props: Props) { const debounceFetchFilterSearch = React.useCallback(debounce(props.fetchFilterSearch, 1000), []); @@ -45,7 +47,9 @@ function SessionSearchField(props: Props) { searchQuery={searchQuery} isMainSearch={true} onFilterClick={onAddFilter} - filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList } + isLive={isRoute(ASSIST_ROUTE, window.location.pathname)} + // filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList } + // filterSearchList={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterSearchListLive : props.filterSearchList } />
)} @@ -54,6 +58,8 @@ function SessionSearchField(props: Props) { } export default connect((state: any) => ({ + filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), + filterSearchListLive: state.getIn([ 'liveSearch', 'filterSearchList' ]), filterList: state.getIn([ 'search', 'filterList' ]), filterListLive: state.getIn([ 'search', 'filterListLive' ]), }), { })(SessionSearchField); \ No newline at end of file diff --git a/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx b/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx index 07db9a2ad..0d7ae52e2 100644 --- a/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx +++ b/frontend/app/components/shared/SessionSettings/components/ListingVisibility.tsx @@ -51,7 +51,7 @@ function ListingVisibility(props) { name="count" placeholder="E.g 10" // style={{ height: '38px', width: '100%'}} - onChange={(e, { value }) => { + onChange={({ target: { value } }) => { changeSettings({ count: value }) }} /> diff --git a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js index b7c65d14d..cd8c23707 100644 --- a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js +++ b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js @@ -44,7 +44,7 @@ class TrackingCodeModal extends React.PureComponent {
- + { + React.useEffect(() => { + const handleEsc = (e) => (e.key === 'Escape' || e.key === 'Esc') && proceed(false); + document.addEventListener('keydown', handleEsc, false); + + return () => { + document.removeEventListener('keydown', handleEsc, false); + } + }, []) return ( {knownCountry - ?
+ ?
: (
Unknown Country
)} - { knownCountry && label &&
{ countryName }
} + { knownCountry && label &&
{ countryName }
}
); }) diff --git a/frontend/app/components/ui/Dropdown/Dropdown.js b/frontend/app/components/ui/Dropdown/Dropdown.js deleted file mode 100644 index 91ec07180..000000000 --- a/frontend/app/components/ui/Dropdown/Dropdown.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { Dropdown } from 'semantic-ui-react'; - -export default props => ( - -); diff --git a/frontend/app/components/ui/Dropdown/Dropdown.stories.js b/frontend/app/components/ui/Dropdown/Dropdown.stories.js deleted file mode 100644 index 335d9056d..000000000 --- a/frontend/app/components/ui/Dropdown/Dropdown.stories.js +++ /dev/null @@ -1,8 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import Dropdown from '.'; - -storiesOf('Dropdown', module) - .add('Pure', () => ( - - )) - diff --git a/frontend/app/components/ui/Dropdown/index.js b/frontend/app/components/ui/Dropdown/index.js deleted file mode 100644 index 0179bfccc..000000000 --- a/frontend/app/components/ui/Dropdown/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Dropdown'; \ No newline at end of file diff --git a/frontend/app/components/ui/DropdownPlain/DropdownPlain.js b/frontend/app/components/ui/DropdownPlain/DropdownPlain.js deleted file mode 100644 index 4b6cf2fb2..000000000 --- a/frontend/app/components/ui/DropdownPlain/DropdownPlain.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import { Dropdown } from 'semantic-ui-react' -import { Icon } from 'UI'; -import stl from './dropdownPlain.module.css' - -const sessionSortOptions = { - 'latest': 'Newest', - 'editedAt': 'Last Modified' -}; -const sortOptions = Object.entries(sessionSortOptions) - .map(([ value, text ]) => ({ value, text })); - -function DropdownPlain({ name, label, options, onChange, defaultValue, wrapperStyle = {}, disabled = false }) { - return ( -
- { label && {label} } - } - /> -
- ) -} - -export default DropdownPlain diff --git a/frontend/app/components/ui/DropdownPlain/dropdownPlain.module.css b/frontend/app/components/ui/DropdownPlain/dropdownPlain.module.css deleted file mode 100644 index 87e26bc68..000000000 --- a/frontend/app/components/ui/DropdownPlain/dropdownPlain.module.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/frontend/app/components/ui/DropdownPlain/index.js b/frontend/app/components/ui/DropdownPlain/index.js deleted file mode 100644 index e7c8ecebf..000000000 --- a/frontend/app/components/ui/DropdownPlain/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DropdownPlain'; diff --git a/frontend/app/components/ui/Form/Form.tsx b/frontend/app/components/ui/Form/Form.tsx index 185ce1aaf..c9ab7c036 100644 --- a/frontend/app/components/ui/Form/Form.tsx +++ b/frontend/app/components/ui/Form/Form.tsx @@ -2,6 +2,7 @@ import React from 'react'; interface Props { children: React.ReactNode; + onSubmit?: any [x: string]: any } @@ -23,7 +24,12 @@ function FormField (props: FormFieldProps) { function Form(props: Props) { const { children, ...rest } = props; return ( -
+ { + e.preventDefault(); + if (props.onSubmit) { + props.onSubmit(e); + } + }}> {children}
); diff --git a/frontend/app/components/ui/Icon/Icon.js b/frontend/app/components/ui/Icon/Icon.tsx similarity index 74% rename from frontend/app/components/ui/Icon/Icon.js rename to frontend/app/components/ui/Icon/Icon.tsx index 7fc6ffee9..745d6412d 100644 --- a/frontend/app/components/ui/Icon/Icon.js +++ b/frontend/app/components/ui/Icon/Icon.tsx @@ -3,7 +3,19 @@ import cn from 'classnames'; import SVG from 'UI/SVG'; import styles from './icon.module.css'; -const Icon = ({ +interface IProps { + name: string + size?: number | string + height?: number + width?: number + color?: string + className?: string + style?: object + marginRight?: number + inline?: boolean +} + +const Icon: React.FunctionComponent = ({ name, size = 12, height = size, @@ -11,7 +23,7 @@ const Icon = ({ color = 'gray-medium', className = '', style={}, - marginRight, + marginRight = 0, inline = false, ...props }) => { @@ -21,6 +33,7 @@ const Icon = ({ ...style, }; if (marginRight){ + // @ts-ignore _style.marginRight = `${ marginRight }px`; } diff --git a/frontend/app/components/ui/Input/Input.tsx b/frontend/app/components/ui/Input/Input.tsx index 502142508..a2ad36f1b 100644 --- a/frontend/app/components/ui/Input/Input.tsx +++ b/frontend/app/components/ui/Input/Input.tsx @@ -7,14 +7,21 @@ interface Props { className: string; icon?: string; leadingButton?: React.ReactNode; + type?: string; + rows?: number; [x:string]: any; } function Input(props: Props) { - const { className, leadingButton = "", wrapperClassName = "", icon = "", ...rest } = props; + const { className, leadingButton = "", wrapperClassName = "", icon = "", type="text", rows=4, ...rest } = props; return (
{icon && } - + { type === 'textarea' ? ( +