Merge remote-tracking branch 'origin/dev' into api-v1.7.0
This commit is contained in:
commit
747487cc4c
59 changed files with 1963 additions and 1575 deletions
4
.github/workflows/api-ee.yaml
vendored
4
.github/workflows/api-ee.yaml
vendored
|
|
@ -82,11 +82,11 @@ jobs:
|
|||
sed -i "s/enterpriseEditionLicense: \"\"/enterpriseEditionLicense: \"${{ secrets.EE_LICENSE_KEY }}\"/g" vars.yaml
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/chalice/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
sed -i "/chalice/{n;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
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set skipMigration=true
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
|
|
|
|||
2
.github/workflows/api.yaml
vendored
2
.github/workflows/api.yaml
vendored
|
|
@ -84,7 +84,7 @@ jobs:
|
|||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set skipMigration=true
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
|
|
|
|||
73
.github/workflows/frontend.yaml
vendored
73
.github/workflows/frontend.yaml
vendored
|
|
@ -36,8 +36,6 @@ jobs:
|
|||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
# - name: Install
|
||||
# run: npm install
|
||||
|
||||
- name: Building and Pushing frontend image
|
||||
id: build-image
|
||||
|
|
@ -46,14 +44,19 @@ jobs:
|
|||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
set -x
|
||||
cd 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 tag $DOCKER_REPO/frontend:${IMAGE_TAG} $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
|
||||
- name: Creating old image input
|
||||
run: |
|
||||
set -x
|
||||
#
|
||||
# Create yaml with existing image tags
|
||||
#
|
||||
|
|
@ -72,7 +75,7 @@ jobs:
|
|||
EOF
|
||||
done
|
||||
|
||||
- name: Deploy to kubernetes
|
||||
- name: Deploy to kubernetes foss
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
|
|
@ -88,12 +91,74 @@ jobs:
|
|||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic --set skipMigration=true
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
|
||||
### Enterprise code deployment
|
||||
|
||||
- name: cleaning old assets
|
||||
run: |
|
||||
rm -rf /tmp/image_*
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontextee
|
||||
|
||||
- name: Creating old image input
|
||||
env:
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
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 <<EOF >> /tmp/image_override.yaml
|
||||
${image_array[0]}:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: `echo ${image_array[1]} | cut -d '-' -f 1`
|
||||
EOF
|
||||
done
|
||||
|
||||
- name: Resetting vars file
|
||||
run: |
|
||||
git checkout -- scripts/helmcharts/vars.yaml
|
||||
- name: Deploy to kubernetes ee
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
## Update secerts
|
||||
sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.EE_PG_PASSWORD }}\"/g" vars.yaml
|
||||
sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.EE_MINIO_ACCESS_KEY }}\"/g" vars.yaml
|
||||
sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.EE_MINIO_SECRET_KEY }}\"/g" vars.yaml
|
||||
sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.EE_JWT_SECRET }}\"/g" vars.yaml
|
||||
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.EE_DOMAIN_NAME }}\"/g" vars.yaml
|
||||
sed -i "s/enterpriseEditionLicense: \"\"/enterpriseEditionLicense: \"${{ secrets.EE_LICENSE_KEY }}\"/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 --set skipMigration=true
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
|
|
|
|||
10
.github/workflows/workers-ee.yaml
vendored
10
.github/workflows/workers-ee.yaml
vendored
|
|
@ -35,10 +35,10 @@ jobs:
|
|||
kubeconfig: ${{ secrets.EE_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
|
||||
# # 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
|
||||
|
||||
- name: Build, tag
|
||||
id: build-image
|
||||
|
|
@ -125,7 +125,7 @@ jobs:
|
|||
cat /tmp/image_override.yaml
|
||||
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set skipMigration=true
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
|
|
|
|||
2
.github/workflows/workers.yaml
vendored
2
.github/workflows/workers.yaml
vendored
|
|
@ -119,7 +119,7 @@ jobs:
|
|||
done
|
||||
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set skipMigration=true
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package integrations
|
|||
import "openreplay/backend/pkg/env"
|
||||
|
||||
type Config struct {
|
||||
TopicRawWeb string
|
||||
PostgresURI string
|
||||
TokenSecret string
|
||||
TopicAnalytics string
|
||||
PostgresURI string
|
||||
TokenSecret string
|
||||
}
|
||||
|
||||
func New() *Config {
|
||||
|
|
|
|||
|
|
@ -118,10 +118,14 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
// Save sessionStart to db
|
||||
e.services.Database.InsertWebSessionStart(sessionID, sessionStart)
|
||||
if err := e.services.Database.InsertWebSessionStart(sessionID, sessionStart); err != nil {
|
||||
log.Printf("can't insert session start: %s", err)
|
||||
}
|
||||
|
||||
// Send sessionStart message to kafka
|
||||
e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(sessionStart))
|
||||
if err := e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(sessionStart)); err != nil {
|
||||
log.Printf("can't send session start: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
ResponseWithJSON(w, &StartSessionResponse{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"log"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/url"
|
||||
)
|
||||
|
|
@ -28,25 +27,16 @@ func (conn *Conn) InsertWebStatsPerformance(sessionID uint64, p *PerformanceTrac
|
|||
$10, $11, $12,
|
||||
$13, $14, $15
|
||||
)`
|
||||
//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,
|
||||
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,
|
||||
); 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ func ReadBatchReader(reader io.Reader, messageHandler func(Message)) error {
|
|||
// No skipping here for making it easy to encode back the same sequence of message
|
||||
// continue readLoop
|
||||
case *SessionStart:
|
||||
// Save session start timestamp for collecting "empty" sessions
|
||||
timestamp = int64(m.Timestamp)
|
||||
case *SessionEnd:
|
||||
timestamp = int64(m.Timestamp)
|
||||
}
|
||||
msg.Meta().Index = index
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) {
|
|||
timestamp := GetTimestamp(message)
|
||||
if timestamp == 0 {
|
||||
switch message.(type) {
|
||||
case *SessionEnd, *IssueEvent, *PerformanceTrackAggr:
|
||||
case *IssueEvent, *PerformanceTrackAggr:
|
||||
break
|
||||
default:
|
||||
log.Printf("skip message with empty timestamp, sessID: %d, msgID: %d, msgType: %d", b.sessionID, messageID, message.TypeID())
|
||||
|
|
|
|||
|
|
@ -9,23 +9,16 @@ import (
|
|||
)
|
||||
|
||||
type Connector struct {
|
||||
sessionsIOS *bulk
|
||||
//viewsIOS *bulk
|
||||
clicksIOS *bulk
|
||||
inputsIOS *bulk
|
||||
crashesIOS *bulk
|
||||
performanceIOS *bulk
|
||||
resourcesIOS *bulk
|
||||
sessions *bulk
|
||||
metadata *bulk // TODO: join sessions, sessions_metadata & sessions_ios
|
||||
resources *bulk
|
||||
pages *bulk
|
||||
clicks *bulk
|
||||
inputs *bulk
|
||||
errors *bulk
|
||||
performance *bulk
|
||||
longtasks *bulk
|
||||
db *sql.DB
|
||||
sessions *bulk
|
||||
metadata *bulk // TODO: join sessions, sessions_metadata & sessions_ios
|
||||
resources *bulk
|
||||
pages *bulk
|
||||
clicks *bulk
|
||||
inputs *bulk
|
||||
errors *bulk
|
||||
performance *bulk
|
||||
longtasks *bulk
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewConnector(url string) *Connector {
|
||||
|
|
@ -37,35 +30,6 @@ func NewConnector(url string) *Connector {
|
|||
}
|
||||
return &Connector{
|
||||
db: db,
|
||||
// sessionsIOS: newBulk(db, `
|
||||
// INSERT INTO sessions_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, duration, views_count, events_count, crashes_count, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// viewsIOS: newBulk(db, `
|
||||
// INSERT INTO views_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, url, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// clicksIOS: newBulk(db, `
|
||||
// INSERT INTO clicks_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, label)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// inputsIOS: newBulk(db, `
|
||||
// INSERT INTO inputs_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, label)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// crashesIOS: newBulk(db, `
|
||||
// INSERT INTO crashes_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, name, reason, crash_id)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// performanceIOS: newBulk(db, `
|
||||
// INSERT INTO performance_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_memory, avg_memory, max_memory)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
// `),
|
||||
// resourcesIOS: newBulk(db, `
|
||||
// INSERT INTO resources_ios (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, url, duration, body_size, success, method, status)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, nullIf(?, ''), ?)
|
||||
// `),
|
||||
|
||||
sessions: newBulk(db, `
|
||||
INSERT INTO sessions (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, duration, pages_count, events_count, errors_count, user_browser, user_browser_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
|
|
@ -107,27 +71,6 @@ func NewConnector(url string) *Connector {
|
|||
}
|
||||
|
||||
func (conn *Connector) Prepare() error {
|
||||
// if err := conn.sessionsIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // if err := conn.viewsIOS.prepare(); err != nil {
|
||||
// // return err
|
||||
// // }
|
||||
// if err := conn.clicksIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.inputsIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.crashesIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.performanceIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.resourcesIOS.prepare(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := conn.sessions.prepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -159,27 +102,6 @@ func (conn *Connector) Prepare() error {
|
|||
}
|
||||
|
||||
func (conn *Connector) Commit() error {
|
||||
// if err := conn.sessionsIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // if err := conn.viewsIOS.commit(); err != nil {
|
||||
// // return err
|
||||
// // }
|
||||
// if err := conn.clicksIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.inputsIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.crashesIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.performanceIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := conn.resourcesIOS.commit(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := conn.sessions.commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,46 +2,41 @@ package clickhouse
|
|||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"openreplay/backend/pkg/hashid"
|
||||
"openreplay/backend/pkg/url"
|
||||
. "openreplay/backend/pkg/db/types"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
|
||||
// TODO: join sessions & sessions_ios clcikhouse tables
|
||||
func (conn *Connector) InsertIOSSession(session *Session) error {
|
||||
if (session.Duration == nil) {
|
||||
if session.Duration == nil {
|
||||
return errors.New("Clickhouse: trying to insert session with ")
|
||||
}
|
||||
return conn.sessionsIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(session.Timestamp),
|
||||
uint32(*session.Duration),
|
||||
session.PagesCount,
|
||||
session.EventsCount,
|
||||
session.ErrorsCount,
|
||||
session.Metadata1,
|
||||
session.Metadata2,
|
||||
session.Metadata3,
|
||||
session.Metadata4,
|
||||
session.Metadata5,
|
||||
session.Metadata6,
|
||||
session.Metadata7,
|
||||
session.Metadata8,
|
||||
session.Metadata9,
|
||||
session.Metadata10,
|
||||
)
|
||||
//return conn.sessionsIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(session.Timestamp),
|
||||
// uint32(*session.Duration),
|
||||
// session.PagesCount,
|
||||
// session.EventsCount,
|
||||
// session.ErrorsCount,
|
||||
// session.Metadata1,
|
||||
// session.Metadata2,
|
||||
// session.Metadata3,
|
||||
// session.Metadata4,
|
||||
// session.Metadata5,
|
||||
// session.Metadata6,
|
||||
// session.Metadata7,
|
||||
// session.Metadata8,
|
||||
// session.Metadata9,
|
||||
// session.Metadata10,
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (conn *Connector) IOSScreenEnter(session *Session, msg *PageEvent) error {
|
||||
|
|
@ -76,105 +71,110 @@ func (conn *Connector) InsertIOSClickEvent(session *Session, msg *IOSClickEvent)
|
|||
if msg.Label == "" {
|
||||
return nil
|
||||
}
|
||||
return conn.clicksIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(msg.Timestamp),
|
||||
msg.Label,
|
||||
)
|
||||
//return conn.clicksIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(msg.Timestamp),
|
||||
// msg.Label,
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connector) InsertIOSInputEvent(session *Session, msg *IOSInputEvent) error {
|
||||
if msg.Label == "" {
|
||||
return nil
|
||||
}
|
||||
return conn.inputsIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(msg.Timestamp),
|
||||
msg.Label,
|
||||
)
|
||||
//return conn.inputsIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(msg.Timestamp),
|
||||
// msg.Label,
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connector) InsertIOSCrash(session *Session, msg *IOSCrash) error {
|
||||
return conn.crashesIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(msg.Timestamp),
|
||||
msg.Name,
|
||||
msg.Reason,
|
||||
hashid.IOSCrashID(session.ProjectID, msg),
|
||||
)
|
||||
//return conn.crashesIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(msg.Timestamp),
|
||||
// msg.Name,
|
||||
// msg.Reason,
|
||||
// hashid.IOSCrashID(session.ProjectID, msg),
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connector) InsertIOSNetworkCall(session *Session, msg *IOSNetworkCall) error {
|
||||
return conn.resourcesIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(msg.Timestamp),
|
||||
url.DiscardURLQuery(msg.URL),
|
||||
nullableUint16(uint16(msg.Duration)),
|
||||
nullableUint32(uint32(len(msg.Body))),
|
||||
msg.Success,
|
||||
url.EnsureMethod(msg.Method), // nullableString causes error "unexpected type *string"
|
||||
nullableUint16(uint16(msg.Status)),
|
||||
)
|
||||
//return conn.resourcesIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(msg.Timestamp),
|
||||
// url.DiscardURLQuery(msg.URL),
|
||||
// nullableUint16(uint16(msg.Duration)),
|
||||
// nullableUint32(uint32(len(msg.Body))),
|
||||
// msg.Success,
|
||||
// url.EnsureMethod(msg.Method), // nullableString causes error "unexpected type *string"
|
||||
// nullableUint16(uint16(msg.Status)),
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connector) InsertIOSPerformanceAggregated(session *Session, msg *IOSPerformanceAggregated) error {
|
||||
var timestamp uint64 = (msg.TimestampStart + msg.TimestampEnd) / 2
|
||||
return conn.performanceIOS.exec(
|
||||
session.SessionID,
|
||||
session.ProjectID,
|
||||
session.TrackerVersion,
|
||||
nullableString(session.RevID),
|
||||
session.UserUUID,
|
||||
session.UserOS,
|
||||
nullableString(session.UserOSVersion),
|
||||
nullableString(session.UserDevice),
|
||||
session.UserDeviceType,
|
||||
session.UserCountry,
|
||||
datetime(timestamp),
|
||||
uint8(msg.MinFPS),
|
||||
uint8(msg.AvgFPS),
|
||||
uint8(msg.MaxFPS),
|
||||
uint8(msg.MinCPU),
|
||||
uint8(msg.AvgCPU),
|
||||
uint8(msg.MaxCPU),
|
||||
msg.MinMemory,
|
||||
msg.AvgMemory,
|
||||
msg.MaxMemory,
|
||||
)
|
||||
//var timestamp uint64 = (msg.TimestampStart + msg.TimestampEnd) / 2
|
||||
//return conn.performanceIOS.exec(
|
||||
// session.SessionID,
|
||||
// session.ProjectID,
|
||||
// session.TrackerVersion,
|
||||
// nullableString(session.RevID),
|
||||
// session.UserUUID,
|
||||
// session.UserOS,
|
||||
// nullableString(session.UserOSVersion),
|
||||
// nullableString(session.UserDevice),
|
||||
// session.UserDeviceType,
|
||||
// session.UserCountry,
|
||||
// datetime(timestamp),
|
||||
// uint8(msg.MinFPS),
|
||||
// uint8(msg.AvgFPS),
|
||||
// uint8(msg.MaxFPS),
|
||||
// uint8(msg.MinCPU),
|
||||
// uint8(msg.AvgCPU),
|
||||
// uint8(msg.MaxCPU),
|
||||
// msg.MinMemory,
|
||||
// msg.AvgMemory,
|
||||
// msg.MaxMemory,
|
||||
//)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ SENTRY_URL = ''
|
|||
|
||||
# CAPTCHA
|
||||
CAPTCHA_ENABLED = false
|
||||
CAPTCHA_SITE_KEY = 'asdad'
|
||||
CAPTCHA_SITE_KEY = ''
|
||||
|
||||
# MINIO
|
||||
MINIO_ENDPOINT = ''
|
||||
|
|
@ -22,5 +22,5 @@ MINIO_ACCESS_KEY = ''
|
|||
MINIO_SECRET_KEY = ''
|
||||
|
||||
# APP and TRACKER VERSIONS
|
||||
VERSION = '1.6.0'
|
||||
TRACKER_VERSION = '3.5.10'
|
||||
VERSION = '1.7.0'
|
||||
TRACKER_VERSION = '3.5.12'
|
||||
|
|
|
|||
4
frontend/.prettierrc
Normal file
4
frontend/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ const siteIdRequiredPaths = [
|
|||
'/custom_metrics',
|
||||
'/dashboards',
|
||||
'/metrics',
|
||||
'/trails',
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -16,9 +16,10 @@ function SlackChannelList(props) {
|
|||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div>
|
||||
<div className="text-base text-left p-5">Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.</div>
|
||||
<DocLink className="mt-4" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.</div>
|
||||
{/* <DocLink className="mt-4" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" /> */}
|
||||
<DocLink className="mt-4 text-base" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
|
|||
errors: state.getIn([ 'members', 'saveRequest', 'errors' ]),
|
||||
loading: state.getIn([ 'members', 'loading' ]),
|
||||
saving: state.getIn([ 'members', 'saveRequest', 'loading' ]),
|
||||
roles: state.getIn(['roles', 'list']).filter(r => !r.protected).map(r => ({ text: r.name, value: r.roleId })).toJS(),
|
||||
roles: state.getIn(['roles', 'list']).filter(r => !r.protected).map(r => ({ label: r.name, value: r.roleId })).toJS(),
|
||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
||||
}), {
|
||||
init,
|
||||
|
|
|
|||
|
|
@ -109,10 +109,11 @@ function Roles(props: Props) {
|
|||
icon
|
||||
>
|
||||
<div className={''}>
|
||||
<div className={cn(stl.wrapper, 'flex items-start py-3 border-b px-3 pr-20')}>
|
||||
<div className="flex" style={{ width: '20%'}}>Title</div>
|
||||
<div className="flex" style={{ width: '30%'}}>Project Access</div>
|
||||
<div className="flex" style={{ width: '50%'}}>Feature Access</div>
|
||||
<div className={cn('flex items-start py-3 border-b px-3 pr-20')}>
|
||||
<div className="" style={{ width: '20%'}}>Title</div>
|
||||
<div className="" style={{ width: '30%'}}>Project Access</div>
|
||||
<div className="" style={{ width: '50%'}}>Feature Access</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{roles.map(role => (
|
||||
<RoleItem
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ const RoleForm = (props: Props) => {
|
|||
isSearchable
|
||||
name="projects"
|
||||
options={ projectOptions }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'projects', value }) }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'projects', value: value.value }) }
|
||||
value={null}
|
||||
/>
|
||||
{ role.projects.size > 0 && (
|
||||
|
|
@ -181,10 +181,10 @@ export default connect((state: any) => {
|
|||
key: p.get('id'),
|
||||
value: p.get('id'),
|
||||
label: p.get('name'),
|
||||
isDisabled: role.projects.includes(p.get('id')),
|
||||
})).toJS(),
|
||||
permissions: state.getIn(['roles', 'permissions'])
|
||||
.map(({ text, value }: any) => ({ label: text, value, isDisabled: role.permissions.includes(value) })).toJS(),
|
||||
// isDisabled: role.projects.includes(p.get('id')),
|
||||
})).filter(({ value }: any) => !role.projects.includes(value)).toJS(),
|
||||
permissions: state.getIn(['roles', 'permissions']).filter(({ value }: any) => !role.permissions.includes(value))
|
||||
.map(({ text, value }: any) => ({ label: text, value })).toJS(),
|
||||
saving: state.getIn([ 'roles', 'saveRequest', 'loading' ]),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[ p.get('id') ] = p.get('name')
|
||||
|
|
|
|||
|
|
@ -42,20 +42,21 @@ function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions, proj
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-start flex-wrap" style={{ width: '50%'}}>
|
||||
{role.permissions.map((permission: any) => (
|
||||
<PermisionLabel label={permissions[permission]} key={permission.id} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap">
|
||||
{role.permissions.map((permission: any) => (
|
||||
<PermisionLabel label={permissions[permission]} key={permission.id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{ isAdmin && (
|
||||
<div className={ cn(stl.actions, 'absolute right-0 top-0 bottom-0 mr-8') }>
|
||||
{ !!editHandler &&
|
||||
{isAdmin && !!editHandler &&
|
||||
<div className={ cn(stl.button, {[stl.disabled] : role.protected }) } onClick={ () => editHandler(role) }>
|
||||
<Icon name="edit" size="16" color="teal"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
|
|||
function AddUserButton({ isAdmin = false, onClick }: any ) {
|
||||
const { userStore } = useStore();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
console.log('limtis', limtis)
|
||||
const cannAddUser = useObserver(() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0));
|
||||
return (
|
||||
<Popup
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ function UserForm(props: Props) {
|
|||
options={ roles }
|
||||
name="roleId"
|
||||
defaultValue={ user.roleId }
|
||||
onChange={({ value }) => user.updateKey('roleId', value)}
|
||||
onChange={({ value }) => user.updateKey('roleId', value.value)}
|
||||
className="block"
|
||||
isDisabled={user.isSuperAdmin}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Loader } from 'UI';
|
|||
import DashboardRouter from './components/DashboardRouter';
|
||||
import cn from 'classnames';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<{}>) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
|
|
@ -27,7 +28,6 @@ function NewDashboard(props: RouteComponentProps<{}>) {
|
|||
props.history.push(withSiteId('/dashboard', siteId));
|
||||
})
|
||||
}
|
||||
|
||||
}, [siteId]);
|
||||
|
||||
return useObserver(() => (
|
||||
|
|
@ -49,4 +49,4 @@ function NewDashboard(props: RouteComponentProps<{}>) {
|
|||
));
|
||||
}
|
||||
|
||||
export default withRouter(NewDashboard);
|
||||
export default withRouter(withPermissions(['METRICS'])(NewDashboard));
|
||||
|
|
|
|||
|
|
@ -1,58 +1,75 @@
|
|||
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;
|
||||
import React, { useEffect } from "react";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import ErrorListItem from "App/components/Dashboard/components/Errors/ErrorListItem";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { useModal } from "App/components/Modal";
|
||||
import ErrorDetailsModal from "App/components/Dashboard/components/Errors/ErrorDetailsModal";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
interface Props {
|
||||
metric: any;
|
||||
isTemplate?: boolean;
|
||||
isEdit?: boolean;
|
||||
history: any,
|
||||
location: any,
|
||||
isEdit: any;
|
||||
history: any;
|
||||
location: any;
|
||||
}
|
||||
function CustomMetricTableErrors(props: RouteComponentProps<Props>) {
|
||||
const { metric, isEdit = false } = props;
|
||||
const errorId = new URLSearchParams(props.location.search).get("errorId");
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { dashboardStore } = useStore();
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const onErrorClick = (e: any, error: any) => {
|
||||
e.stopPropagation();
|
||||
props.history.replace({search: (new URLSearchParams({errorId : error.errorId})).toString()});
|
||||
}
|
||||
props.history.replace({
|
||||
search: new URLSearchParams({ errorId: error.errorId }).toString(),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!errorId) return;
|
||||
|
||||
showModal(<ErrorDetailsModal errorId={errorId} />, { right: true, onClose: () => {
|
||||
if (props.history.location.pathname.includes("/dashboard")) {
|
||||
props.history.replace({search: ""});
|
||||
}
|
||||
}});
|
||||
showModal(<ErrorDetailsModal errorId={errorId} />, {
|
||||
right: true,
|
||||
onClose: () => {
|
||||
if (props.history.location.pathname.includes("/dashboard")) {
|
||||
props.history.replace({ search: "" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
hideModal();
|
||||
}
|
||||
}, [errorId])
|
||||
};
|
||||
}, [errorId]);
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title={`No errors found ${overPastString(period)}`}
|
||||
show={!metric.data.errors || metric.data.errors.length === 0}
|
||||
size="small"
|
||||
>
|
||||
<div className="pb-4">
|
||||
{metric.data.errors && metric.data.errors.map((error: any, index: any) => (
|
||||
<ErrorListItem key={index} error={error} onClick={(e) => onErrorClick(e, error)} />
|
||||
))}
|
||||
{metric.data.errors &&
|
||||
metric.data.errors.map((error: any, index: any) => (
|
||||
<div key={index} className="broder-b last:border-none">
|
||||
<ErrorListItem
|
||||
error={error}
|
||||
onClick={(e) => onErrorClick(e, error)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isEdit && (
|
||||
<div className="my-6 flex items-center justify-center">
|
||||
<Pagination
|
||||
page={metric.page}
|
||||
totalPages={Math.ceil(metric.data.total / metric.limit)}
|
||||
onPageChange={(page: any) => metric.updateKey('page', page)}
|
||||
totalPages={Math.ceil(
|
||||
metric.data.total / metric.limit
|
||||
)}
|
||||
onPageChange={(page: any) =>
|
||||
metric.updateKey("page", page)
|
||||
}
|
||||
limit={metric.limit}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
|
|
@ -67,14 +84,17 @@ function CustomMetricTableErrors(props: RouteComponentProps<Props>) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withRouter(CustomMetricTableErrors) as React.FunctionComponent<RouteComponentProps<Props>>;
|
||||
export default withRouter(CustomMetricTableErrors) as React.FunctionComponent<
|
||||
RouteComponentProps<Props>
|
||||
>;
|
||||
|
||||
const ViewMore = ({ total, limit }: any) => total > limit && (
|
||||
<div className="mt-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="color-teal text-lg">
|
||||
All <span className="font-medium">{total}</span> errors
|
||||
const ViewMore = ({ total, limit }: any) =>
|
||||
total > limit && (
|
||||
<div className="mt-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="color-teal text-lg">
|
||||
All <span className="font-medium">{total}</span> errors
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import { Pagination, NoContent } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import SessionItem from "Shared/SessionItem";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -12,25 +13,41 @@ interface Props {
|
|||
|
||||
function CustomMetricTableSessions(props: Props) {
|
||||
const { isEdit = false, metric } = props;
|
||||
|
||||
const { dashboardStore } = useStore();
|
||||
const period = dashboardStore.period;
|
||||
|
||||
return useObserver(() => (
|
||||
<NoContent
|
||||
show={!metric || !metric.data || !metric.data.sessions || metric.data.sessions.length === 0}
|
||||
show={
|
||||
!metric ||
|
||||
!metric.data ||
|
||||
!metric.data.sessions ||
|
||||
metric.data.sessions.length === 0
|
||||
}
|
||||
size="small"
|
||||
title={`No sessions found ${overPastString(period)}`}
|
||||
>
|
||||
<div className="pb-4">
|
||||
{metric.data.sessions && metric.data.sessions.map((session: any, index: any) => (
|
||||
<div className="border-b last:border-none">
|
||||
<SessionItem session={session} key={session.sessionId} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{metric.data.sessions &&
|
||||
metric.data.sessions.map((session: any, index: any) => (
|
||||
<div
|
||||
className="border-b last:border-none"
|
||||
key={session.sessionId}
|
||||
>
|
||||
<SessionItem session={session} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isEdit && (
|
||||
<div className="mt-6 flex items-center justify-center">
|
||||
<Pagination
|
||||
page={metric.page}
|
||||
totalPages={Math.ceil(metric.data.total / metric.limit)}
|
||||
onPageChange={(page: any) => metric.updateKey('page', page)}
|
||||
totalPages={Math.ceil(
|
||||
metric.data.total / metric.limit
|
||||
)}
|
||||
onPageChange={(page: any) =>
|
||||
metric.updateKey("page", page)
|
||||
}
|
||||
limit={metric.data.total}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
|
|
@ -47,12 +64,13 @@ function CustomMetricTableSessions(props: Props) {
|
|||
|
||||
export default CustomMetricTableSessions;
|
||||
|
||||
const ViewMore = ({ total, limit }: any) => total > limit && (
|
||||
<div className="mt-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="color-teal text-lg">
|
||||
All <span className="font-medium">{total}</span> sessions
|
||||
const ViewMore = ({ total, limit }: any) =>
|
||||
total > limit && (
|
||||
<div className="mt-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="color-teal text-lg">
|
||||
All <span className="font-medium">{total}</span> sessions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function DashboardEditModal(props: Props) {
|
|||
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
|
||||
|
||||
return useObserver(() => (
|
||||
<Modal open={ show }>
|
||||
<Modal open={ show } onClose={closeHandler}>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Edit Dashboard' }</div>
|
||||
<Icon
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function DashboardSelectionModal(props: Props) {
|
|||
}, [])
|
||||
|
||||
return useObserver(() => (
|
||||
<Modal size="small" open={ show }>
|
||||
<Modal size="small" open={ show } onClose={closeHandler}>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Add to selected dashboard' }</div>
|
||||
<Icon
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, PageTitle, Loader, NoContent } from 'UI';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
import { confirm } from 'UI';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withReport from 'App/components/hocs/withReport';
|
||||
import DashboardOptions from '../DashboardOptions';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import DashboardIcon from '../../../../svg/dashboard-icn.svg';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useStore } from "App/mstore";
|
||||
import { Button, PageTitle, Loader, NoContent } from "UI";
|
||||
import { withSiteId } from "App/routes";
|
||||
import withModal from "App/components/Modal/withModal";
|
||||
import DashboardWidgetGrid from "../DashboardWidgetGrid";
|
||||
import { confirm } from "UI";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { useModal } from "App/components/Modal";
|
||||
import DashboardModal from "../DashboardModal";
|
||||
import DashboardEditModal from "../DashboardEditModal";
|
||||
import AlertFormModal from "App/components/Alerts/AlertFormModal";
|
||||
import withPageTitle from "HOCs/withPageTitle";
|
||||
import withReport from "App/components/hocs/withReport";
|
||||
import DashboardOptions from "../DashboardOptions";
|
||||
import SelectDateRange from "Shared/SelectDateRange";
|
||||
import DashboardIcon from "../../../../svg/dashboard-icn.svg";
|
||||
import { Tooltip } from "react-tippy";
|
||||
|
||||
interface IProps {
|
||||
siteId: string;
|
||||
dashboardId: any
|
||||
renderReport?: any
|
||||
dashboardId: any;
|
||||
renderReport?: any;
|
||||
}
|
||||
|
||||
type Props = IProps & RouteComponentProps;
|
||||
|
|
@ -39,76 +39,108 @@ function DashboardView(props: Props) {
|
|||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const queryParams = new URLSearchParams(props.location.search)
|
||||
const queryParams = new URLSearchParams(props.location.search);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboard || !dashboard.dashboardId) return;
|
||||
dashboardStore.fetch(dashboard.dashboardId)
|
||||
dashboardStore.fetch(dashboard.dashboardId);
|
||||
}, [dashboard]);
|
||||
|
||||
const trimQuery = () => {
|
||||
if (!queryParams.has('modal')) return;
|
||||
queryParams.delete('modal')
|
||||
if (!queryParams.has("modal")) return;
|
||||
queryParams.delete("modal");
|
||||
props.history.replace({
|
||||
search: queryParams.toString(),
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const pushQuery = () => {
|
||||
if (!queryParams.has('modal')) props.history.push('?modal=addMetric')
|
||||
}
|
||||
if (!queryParams.has("modal")) props.history.push("?modal=addMetric");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboardId) dashboardStore.selectDefaultDashboard();
|
||||
|
||||
if (queryParams.has('modal')) {
|
||||
if (queryParams.has("modal")) {
|
||||
onAddWidgets();
|
||||
trimQuery();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard)
|
||||
showModal(<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId} />, { right: true })
|
||||
}
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
showModal(
|
||||
<DashboardModal
|
||||
siteId={siteId}
|
||||
onMetricAdd={pushQuery}
|
||||
dashboardId={dashboardId}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
|
||||
const onEdit = (isTitle: boolean) => {
|
||||
dashboardStore.initDashboard(dashboard)
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
setFocusedInput(isTitle);
|
||||
setShowEditModal(true)
|
||||
}
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`
|
||||
})) {
|
||||
if (
|
||||
await confirm({
|
||||
header: "Confirm",
|
||||
confirmButton: "Yes, delete",
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
|
||||
})
|
||||
) {
|
||||
dashboardStore.deleteDashboard(dashboard).then(() => {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
props.history.push(withSiteId(`/dashboard/${dashboardId}`, siteId));
|
||||
}, () => {
|
||||
props.history.push(withSiteId('/dashboard', siteId));
|
||||
})
|
||||
dashboardStore.selectDefaultDashboard().then(
|
||||
({ dashboardId }) => {
|
||||
props.history.push(
|
||||
withSiteId(`/dashboard/${dashboardId}`, siteId)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
props.history.push(withSiteId("/dashboard", siteId));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={dashboards.length === 0 || !dashboard || !dashboard.dashboardId}
|
||||
show={
|
||||
dashboards.length === 0 ||
|
||||
!dashboard ||
|
||||
!dashboard.dashboardId
|
||||
}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<object style={{ width: '180px' }} type="image/svg+xml" data={DashboardIcon} className="no-result-icon" />
|
||||
<span>Gather and analyze <br /> important metrics in one place.</span>
|
||||
<object
|
||||
style={{ width: "180px" }}
|
||||
type="image/svg+xml"
|
||||
data={DashboardIcon}
|
||||
className="no-result-icon"
|
||||
/>
|
||||
<span>
|
||||
Gather and analyze <br /> important metrics in one
|
||||
place.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
subtext={
|
||||
<Button variant="primary" size="small" onClick={onAddWidgets}>+ Create Dashboard</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onAddWidgets}
|
||||
>
|
||||
+ Create Dashboard
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}}>
|
||||
<div style={{ maxWidth: "1300px", margin: "auto" }}>
|
||||
<DashboardEditModal
|
||||
show={showEditModal}
|
||||
closeHandler={() => setShowEditModal(false)}
|
||||
|
|
@ -118,23 +150,41 @@ function DashboardView(props: Props) {
|
|||
<div className="flex items-center" style={{ flex: 3 }}>
|
||||
<PageTitle
|
||||
// @ts-ignore
|
||||
title={<Tooltip delay={100} arrow title="Double click to rename">{dashboard?.name}</Tooltip>}
|
||||
title={
|
||||
<Tooltip
|
||||
delay={100}
|
||||
arrow
|
||||
title="Double click to rename"
|
||||
>
|
||||
{dashboard?.name}
|
||||
</Tooltip>
|
||||
}
|
||||
onDoubleClick={() => onEdit(true)}
|
||||
className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer"
|
||||
actionButton={
|
||||
<Button variant="primary" onClick={onAddWidgets}>Add Metric</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddWidgets}
|
||||
>
|
||||
Add Metric
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
|
||||
<div className="flex items-center flex-shrink-0 justify-end" style={{ width: '300px'}}>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ flex: 1, justifyContent: "end" }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 justify-end"
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<SelectDateRange
|
||||
style={{ width: '300px'}}
|
||||
fluid
|
||||
plain
|
||||
style={{ width: "300px" }}
|
||||
period={period}
|
||||
onChange={(period: any) => dashboardStore.setPeriod(period)}
|
||||
onChange={(period: any) =>
|
||||
dashboardStore.setPeriod(period)
|
||||
}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -150,7 +200,9 @@ function DashboardView(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="my-4 font-normal color-gray-dark">{dashboard?.description}</h2>
|
||||
<h2 className="my-4 font-normal color-gray-dark">
|
||||
{dashboard?.description}
|
||||
</h2>
|
||||
</div>
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
|
|
@ -160,7 +212,9 @@ function DashboardView(props: Props) {
|
|||
/>
|
||||
<AlertFormModal
|
||||
showModal={showAlertModal}
|
||||
onClose={() => dashboardStore.updateKey('showAlertModal', false)}
|
||||
onClose={() =>
|
||||
dashboardStore.updateKey("showAlertModal", false)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
|
|
@ -168,6 +222,6 @@ function DashboardView(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('Dashboards - OpenReplay')(
|
||||
export default withPageTitle("Dashboards - OpenReplay")(
|
||||
withReport(withRouter(withModal(observer(DashboardView))))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function ErrorListItem(props: Props) {
|
|||
// }
|
||||
return (
|
||||
<div
|
||||
className={ cn("p-3 border-b grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue", className) }
|
||||
className={ cn("p-3 grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue", className) }
|
||||
id="error-item"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
|
||||
import { Icon, NoContent, Label, Link, Pagination, Popup } from 'UI';
|
||||
import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date';
|
||||
import { getIcon } from 'react-toastify/dist/components';
|
||||
|
||||
|
|
@ -24,11 +24,22 @@ function DashboardLink({ dashboards}: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric } = props;
|
||||
function MetricTypeIcon({ type }: any) {
|
||||
const PopupWrapper = (props: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={<div className="capitalize">{type}</div>}
|
||||
position="top center"
|
||||
on="hover"
|
||||
hideOnScroll={true}
|
||||
>
|
||||
{props.children}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
const getIcon = (metricType: string) => {
|
||||
switch (metricType) {
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'funnel':
|
||||
return 'filter';
|
||||
case 'table':
|
||||
|
|
@ -37,13 +48,28 @@ function MetricListItem(props: Props) {
|
|||
return 'bar-chart-line';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupWrapper>
|
||||
<div className="w-8 h-8 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon()} size="14" color="tealx" />
|
||||
</div>
|
||||
</PopupWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric } = props;
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 p-3 border-t select-none">
|
||||
<div className="col-span-3 flex items-start">
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
{/* <div className="w-8 h-8 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon(metric.metricType)} size="14" color="tealx" />
|
||||
</div>
|
||||
</div> */}
|
||||
<MetricTypeIcon type={metric.metricType} />
|
||||
<Link to={`/metrics/${metric.metricId}`} className="link capitalize-first">
|
||||
{metric.name}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ function WidgetChart(props: Props) {
|
|||
return (
|
||||
<CustomMetricTableErrors
|
||||
metric={metric}
|
||||
isTemplate={isTemplate}
|
||||
// isTemplate={isTemplate}
|
||||
isEdit={!isWidget && !isTemplate}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ function WidgetName(props: Props) {
|
|||
setEditing(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keypress', handler, false)
|
||||
document.addEventListener('keydown', handler, false)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keypress', handler, false)
|
||||
document.removeEventListener('keydown', handler, false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { NoContent, Loader, Pagination } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { DateTime } from 'luxon';
|
||||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted'
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NoContent, Loader, Pagination } from "UI";
|
||||
import Select from "Shared/Select";
|
||||
import cn from "classnames";
|
||||
import { useStore } from "App/mstore";
|
||||
import SessionItem from "Shared/SessionItem";
|
||||
import { observer, useObserver } from "mobx-react-lite";
|
||||
import { DateTime } from "luxon";
|
||||
import { debounce } from "App/utils";
|
||||
import useIsMounted from "App/hooks/useIsMounted";
|
||||
import AnimatedSVG, { ICONS } from "Shared/AnimatedSVG/AnimatedSVG";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
function WidgetSessions(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const [activeSeries, setActiveSeries] = useState('all');
|
||||
const { className = "" } = props;
|
||||
const [activeSeries, setActiveSeries] = useState("all");
|
||||
const [data, setData] = useState<any>([]);
|
||||
const isMounted = useIsMounted();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -23,15 +23,14 @@ function WidgetSessions(props: Props) {
|
|||
const { dashboardStore, metricStore } = useStore();
|
||||
const filter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
const widget: any = useObserver(() => metricStore.instance);
|
||||
// const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
// const [timestamps, setTimestamps] = useState<any>({
|
||||
// startTimestamp: 0,
|
||||
// endTimestamp: 0,
|
||||
// });
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat(
|
||||
"LLL dd, yyyy HH:mm"
|
||||
);
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat(
|
||||
"LLL dd, yyyy HH:mm"
|
||||
);
|
||||
const [seriesOptions, setSeriesOptions] = useState([
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: "All", value: "all" },
|
||||
]);
|
||||
|
||||
const writeOption = ({ value }: any) => setActiveSeries(value.value);
|
||||
|
|
@ -41,49 +40,68 @@ function WidgetSessions(props: Props) {
|
|||
label: item.seriesName,
|
||||
value: item.seriesId,
|
||||
}));
|
||||
setSeriesOptions([
|
||||
{ label: 'All', value: 'all' },
|
||||
...seriesOptions,
|
||||
]);
|
||||
setSeriesOptions([{ label: "All", value: "all" }, ...seriesOptions]);
|
||||
}, [data]);
|
||||
|
||||
const fetchSessions = (metricId: any, filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true)
|
||||
widget.fetchSessions(metricId, filter).then((res: any) => {
|
||||
setData(res)
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []);
|
||||
setLoading(true);
|
||||
widget
|
||||
.fetchSessions(metricId, filter)
|
||||
.then((res: any) => {
|
||||
setData(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
const debounceRequest: any = React.useCallback(
|
||||
debounce(fetchSessions, 1000),
|
||||
[]
|
||||
);
|
||||
|
||||
const depsString = JSON.stringify(widget.series);
|
||||
useEffect(() => {
|
||||
debounceRequest(widget.metricId, { ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
|
||||
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const timestamps = drillDownPeriod.toTimestamps();
|
||||
// // console.log('timestamps', timestamps);
|
||||
// debounceRequest(widget.metricId, { startTime: timestamps.startTimestamp, endTime: timestamps.endTimestamp, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
|
||||
// }, [drillDownPeriod]);
|
||||
debounceRequest(widget.metricId, {
|
||||
...filter,
|
||||
series: widget.toJsonDrilldown(),
|
||||
page: metricStore.sessionsPage,
|
||||
limit: metricStore.sessionsPageSize,
|
||||
});
|
||||
}, [
|
||||
filter.startTimestamp,
|
||||
filter.endTimestamp,
|
||||
filter.filters,
|
||||
depsString,
|
||||
metricStore.sessionsPage,
|
||||
]);
|
||||
|
||||
return useObserver(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-2xl">Sessions</h2>
|
||||
<div className="ml-2 color-gray-medium">between <span className="font-medium color-gray-darkest">{startTime}</span> and <span className="font-medium color-gray-darkest">{endTime}</span> </div>
|
||||
<div className="ml-2 color-gray-medium">
|
||||
between{" "}
|
||||
<span className="font-medium color-gray-darkest">
|
||||
{startTime}
|
||||
</span>{" "}
|
||||
and{" "}
|
||||
<span className="font-medium color-gray-darkest">
|
||||
{endTime}
|
||||
</span>{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ widget.metricType !== 'table' && (
|
||||
{widget.metricType !== "table" && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Filter by Series</span>
|
||||
<span className="mr-2 color-gray-medium">
|
||||
Filter by Series
|
||||
</span>
|
||||
<Select
|
||||
options={ seriesOptions }
|
||||
defaultValue={ 'all' }
|
||||
onChange={ writeOption }
|
||||
options={seriesOptions}
|
||||
defaultValue={"all"}
|
||||
onChange={writeOption}
|
||||
plain
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -95,24 +113,34 @@ function WidgetSessions(props: Props) {
|
|||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
|
||||
<div className="mt-6 text-2xl">No recordings found</div>
|
||||
<AnimatedSVG
|
||||
name={ICONS.NO_RESULTS}
|
||||
size="170"
|
||||
/>
|
||||
<div className="mt-6 text-2xl">
|
||||
No recordings found
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
show={filteredSessions.sessions.length === 0}
|
||||
>
|
||||
{filteredSessions.sessions.map((session: any) => (
|
||||
<>
|
||||
<SessionItem key={ session.sessionId } session={ session } />
|
||||
<React.Fragment key={session.sessionId}>
|
||||
<SessionItem session={session} />
|
||||
<div className="border-b" />
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
totalPages={Math.ceil(filteredSessions.total / metricStore.sessionsPageSize)}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
totalPages={Math.ceil(
|
||||
filteredSessions.total /
|
||||
metricStore.sessionsPageSize
|
||||
)}
|
||||
onPageChange={(page: any) =>
|
||||
metricStore.updateKey("sessionsPage", page)
|
||||
}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
|
|
@ -127,18 +155,22 @@ function WidgetSessions(props: Props) {
|
|||
const getListSessionsBySeries = (data: any, seriesId: any) => {
|
||||
const arr: any = { sessions: [], total: 0 };
|
||||
data.forEach((element: any) => {
|
||||
if (seriesId === 'all') {
|
||||
if (seriesId === "all") {
|
||||
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
|
||||
arr.sessions.push(
|
||||
...element.sessions.filter(
|
||||
(i: any) => !sessionIds.includes(i.sessionId)
|
||||
)
|
||||
);
|
||||
arr.total = element.total;
|
||||
} else {
|
||||
if (element.seriesId === seriesId) {
|
||||
arr.sessions.push(...element.sessions)
|
||||
arr.total = element.total
|
||||
arr.sessions.push(...element.sessions);
|
||||
arr.total = element.total;
|
||||
}
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
|
||||
export default observer(WidgetSessions);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default class FunnelSaveModal extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal size="small" open={ show }>
|
||||
<Modal size="small" open={ show } onClose={this.props.closeHandler}>
|
||||
<Modal.Header className={ styles.modalHeader }>
|
||||
<div>{ 'Save Funnel' }</div>
|
||||
<Icon
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ function FunnelWidget(props: Props) {
|
|||
<span className="text-xl mr-2">Affected users</span>
|
||||
<div className="rounded px-2 py-1 bg-gray-lightest">
|
||||
<span className="text-xl font-medium">{funnel.affectedUsers}</span>
|
||||
{/* <span className="text-sm">(12%)</span> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,7 +88,7 @@ function EmptyStage({ total }: any) {
|
|||
<div className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal" style={{ width: '100px'}}>
|
||||
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
|
||||
</div>
|
||||
<div className="border-b w-full border-dotted"></div>
|
||||
<div className="border-b w-full border-dashed"></div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import {
|
|||
sessions,
|
||||
assist,
|
||||
client,
|
||||
errors,
|
||||
// funnels,
|
||||
dashboard,
|
||||
withSiteId,
|
||||
CLIENT_DEFAULT_TAB,
|
||||
isRoute,
|
||||
} from 'App/routes';
|
||||
import { logout } from 'Duck/user';
|
||||
import { Icon, Popup } from 'UI';
|
||||
|
|
@ -20,7 +17,7 @@ import styles from './header.module.css';
|
|||
import OnboardingExplore from './OnboardingExplore/OnboardingExplore'
|
||||
import Announcements from '../Announcements';
|
||||
import Notifications from '../Alerts/Notifications';
|
||||
import { init as initSite, fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { init as initSite } from 'Duck/site';
|
||||
|
||||
import ErrorGenPanel from 'App/dev/components';
|
||||
import Alerts from '../Alerts/Alerts';
|
||||
|
|
@ -32,18 +29,13 @@ import { useObserver } from 'mobx-react-lite';
|
|||
const DASHBOARD_PATH = dashboard();
|
||||
const SESSIONS_PATH = sessions();
|
||||
const ASSIST_PATH = assist();
|
||||
const ERRORS_PATH = errors();
|
||||
// const FUNNELS_PATH = funnels();
|
||||
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
|
||||
const AUTOREFRESH_INTERVAL = 30 * 1000;
|
||||
|
||||
let interval = null;
|
||||
|
||||
const Header = (props) => {
|
||||
const {
|
||||
sites, location, account,
|
||||
onLogoutClick, siteId,
|
||||
boardingCompletion = 100, fetchSiteList, showAlerts = false,
|
||||
boardingCompletion = 100, showAlerts = false,
|
||||
} = props;
|
||||
|
||||
const name = account.get('name').split(" ")[0];
|
||||
|
|
@ -98,20 +90,6 @@ const Header = (props) => {
|
|||
>
|
||||
{ 'Assist' }
|
||||
</NavLink>
|
||||
{/* <NavLink
|
||||
to={ withSiteId(ERRORS_PATH, siteId) }
|
||||
className={ styles.nav }
|
||||
activeClassName={ styles.active }
|
||||
>
|
||||
{ 'Errors' }
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={ withSiteId(FUNNELS_PATH, siteId) }
|
||||
className={ styles.nav }
|
||||
activeClassName={ styles.active }
|
||||
>
|
||||
{ 'Funnels' }
|
||||
</NavLink> */}
|
||||
<NavLink
|
||||
to={ withSiteId(DASHBOARD_PATH, siteId) }
|
||||
className={ styles.nav }
|
||||
|
|
@ -162,5 +140,5 @@ export default withRouter(connect(
|
|||
showAlerts: state.getIn([ 'dashboard', 'showAlerts' ]),
|
||||
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ])
|
||||
}),
|
||||
{ onLogoutClick: logout, initSite, fetchSiteList, fetchMetadata },
|
||||
{ onLogoutClick: logout, initSite, fetchMetadata },
|
||||
)(Header));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { Loader } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import {
|
||||
PlayerProvider,
|
||||
connectPlayer,
|
||||
|
|
@ -40,7 +41,7 @@ function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) {
|
|||
}
|
||||
|
||||
function WebPlayer (props) {
|
||||
const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, config } = props;
|
||||
const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props;
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'Events',
|
||||
|
|
@ -50,6 +51,7 @@ function WebPlayer (props) {
|
|||
const [activeTab, setActiveTab] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchList('issues')
|
||||
initPlayer(session, jwt);
|
||||
|
||||
const jumptTime = props.query.get('jumpto');
|
||||
|
|
@ -89,4 +91,5 @@ export default connect(state => ({
|
|||
}), {
|
||||
toggleFullscreen,
|
||||
closeBottomBlock,
|
||||
fetchList,
|
||||
})(withLocationHandlers()(WebPlayer));
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Popup, Button } from 'UI';
|
||||
import { Popup, Button, Icon } from 'UI';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import IssuesModal from './IssuesModal';
|
||||
import { fetchProjects, fetchMeta } from 'Duck/assignments';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import stl from './issues.module.css';
|
||||
import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
||||
|
||||
@connect(state => ({
|
||||
issues: state.getIn(['assignments', 'list']),
|
||||
|
|
@ -21,16 +19,10 @@ import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
|||
|
||||
jiraConfig: state.getIn([ 'issues', 'list' ]).first(),
|
||||
issuesFetched: state.getIn([ 'issues', 'issuesFetched' ]),
|
||||
}), { fetchMeta, fetchProjects, fetchListIntegration })
|
||||
@withToggle('isModalDisplayed', 'toggleModal')
|
||||
}), { fetchMeta, fetchProjects })
|
||||
class Issues extends React.Component {
|
||||
state = {showModal: false };
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.issuesFetched)
|
||||
this.props.fetchListIntegration('issues')
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { showModal: false };
|
||||
|
|
@ -46,12 +38,7 @@ class Issues extends React.Component {
|
|||
this.setState({ showModal: true });
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
alert('test')
|
||||
this.setState({ showModal: true });
|
||||
if (!this.props.projectsFetched) { // cache projects fetch
|
||||
this.props.fetchProjects().then(function() {
|
||||
|
|
@ -67,52 +54,33 @@ class Issues extends React.Component {
|
|||
const {
|
||||
sessionId, isModalDisplayed, projectsLoading, metaLoading, fetchIssuesLoading, issuesIntegration
|
||||
} = this.props;
|
||||
const { showModal } = this.state;
|
||||
const provider = issuesIntegration.provider
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={ stl.buttonWrapper}>
|
||||
<Popup
|
||||
open={ isModalDisplayed }
|
||||
onOpen={ this.handleOpen }
|
||||
onClose={ this.handleClose }
|
||||
trigger={
|
||||
<div className="flex items-center" onClick={this.props.toggleModal} disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}>
|
||||
<Icon name={ `integrations/${ provider === 'jira' ? 'jira' : 'github'}` } size="16" />
|
||||
<span className="ml-2">Create Issue</span>
|
||||
</div>
|
||||
}
|
||||
on="click"
|
||||
position="top right"
|
||||
content={
|
||||
<OutsideClickDetectingDiv onClickOutside={this.closeModal}>
|
||||
<IssuesModal
|
||||
provider={provider}
|
||||
sessionId={ sessionId }
|
||||
closeHandler={ this.closeModal }
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
}
|
||||
// trigger="click"
|
||||
theme="tippy-light"
|
||||
>
|
||||
{
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={ () => this.setState({ showModal: true }) }
|
||||
className={ stl.button }
|
||||
disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}
|
||||
icon={`integrations/${ provider === 'jira' ? 'jira' : 'github'}`}
|
||||
>
|
||||
<div className="h-full flex items-center">
|
||||
Report Issue
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
</Popup>
|
||||
<div className="relative">
|
||||
<div className={ stl.buttonWrapper} onClick={this.handleOpen}>
|
||||
<Popup
|
||||
open={this.state.showModal}
|
||||
position="top right"
|
||||
interactive
|
||||
content={
|
||||
<OutsideClickDetectingDiv onClickOutside={this.closeModal}>
|
||||
<IssuesModal
|
||||
provider={provider}
|
||||
sessionId={ sessionId }
|
||||
closeHandler={ this.closeModal }
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
}
|
||||
theme="tippy-light"
|
||||
>
|
||||
<div className="flex items-center" onClick={this.handleOpen} disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}>
|
||||
<Icon name={ `integrations/${ provider === 'jira' ? 'jira' : 'github'}` } size="16" />
|
||||
<span className="ml-2">Create Issue</span>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import stl from './issuesModal.module.css';
|
||||
import IssueForm from './IssueForm';
|
||||
import { Icon } from 'UI';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from 'App/store';
|
||||
|
||||
const IssuesModal = ({
|
||||
sessionId,
|
||||
|
|
@ -14,7 +15,9 @@ const IssuesModal = ({
|
|||
{/* <Icon name={headerIcon} size="18" color="color-gray-darkest" /> */}
|
||||
<span>{`Report an Issue on ${provider === 'jira' ? 'Jira' : 'Github'}`}</span>
|
||||
</h3>
|
||||
<IssueForm sessionId={ sessionId } closeHandler={ closeHandler } />
|
||||
<Provider store={store}>
|
||||
<IssueForm sessionId={ sessionId } closeHandler={ closeHandler } />
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ function PageInsightsPanel({
|
|||
|
||||
return (
|
||||
<div className="p-4 bg-white">
|
||||
<div className="pt-2 pb-3 flex items-center" style={{ maxWidth: '241px' }}>
|
||||
<div className="-ml-1 text-lg">
|
||||
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-xl">Clicks</span>
|
||||
<SelectDateRange period={period} onChange={onDateChange} disableCustom />
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
tabs={ TABS }
|
||||
active={ activeTab }
|
||||
onClick={ (tab) => { setActiveTab(tab); !showEvents && toggleEvents(true) } }
|
||||
border={ true }
|
||||
border={ false }
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { NoPermission, NoSessionPermission } from 'UI';
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { NoPermission, NoSessionPermission } from "UI";
|
||||
|
||||
export default (requiredPermissions, className, isReplay = false) => BaseComponent =>
|
||||
@connect((state, props) => ({
|
||||
permissions: state.getIn([ 'user', 'account', 'permissions' ]) || [],
|
||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
||||
}))
|
||||
class extends React.PureComponent {
|
||||
render() {
|
||||
const hasPermission = requiredPermissions.every(permission => this.props.permissions.includes(permission));
|
||||
export default (requiredPermissions, className, isReplay = false) =>
|
||||
(BaseComponent) =>
|
||||
(
|
||||
@connect((state, props) => ({
|
||||
permissions:
|
||||
state.getIn(["user", "account", "permissions"]) || [],
|
||||
isEnterprise:
|
||||
state.getIn(["user", "account", "edition"]) === "ee",
|
||||
}))
|
||||
class extends React.PureComponent {
|
||||
render() {
|
||||
const hasPermission = requiredPermissions.every(
|
||||
(permission) =>
|
||||
this.props.permissions.includes(permission)
|
||||
);
|
||||
|
||||
return (
|
||||
(!this.props.isEnterprise || hasPermission) ?
|
||||
<BaseComponent {...this.props} /> :
|
||||
<div className={className}>
|
||||
{ isReplay ? <NoSessionPermission /> : <NoPermission /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return !this.props.isEnterprise || hasPermission ? (
|
||||
<BaseComponent {...this.props} />
|
||||
) : (
|
||||
<div className={className}>
|
||||
{isReplay ? (
|
||||
<NoSessionPermission />
|
||||
) : (
|
||||
<NoPermission />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function SaveSearchModal(props: Props) {
|
|||
|
||||
|
||||
return (
|
||||
<Modal size="small" open={ show }>
|
||||
<Modal size="small" open={ show } onClose={closeHandler}>
|
||||
<Modal.Header className={ stl.modalHeader }>
|
||||
<div>{ 'Save Search' }</div>
|
||||
<Icon
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const Confirmation = ({
|
|||
return (
|
||||
<Modal
|
||||
open={show}
|
||||
onClose={() => proceed(false)}
|
||||
>
|
||||
<Modal.Header>{header}</Modal.Header>
|
||||
<Modal.Content>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ interface Props {
|
|||
children: React.ReactNode;
|
||||
open?: boolean;
|
||||
size ?: 'tiny' | 'small' | 'large' | 'fullscreen';
|
||||
onClose?: () => void;
|
||||
}
|
||||
function Modal(props: Props) {
|
||||
const { children, open = false, size = 'small' } = props;
|
||||
|
|
@ -28,10 +29,17 @@ function Modal(props: Props) {
|
|||
style.width = '100%';
|
||||
}
|
||||
|
||||
const handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
props.onClose && props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return open ? (
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center box-shadow animate-fade-in"
|
||||
style={{ zIndex: '999', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div className="absolute z-10 bg-white rounded border" style={style}>
|
||||
{children}
|
||||
|
|
@ -85,4 +93,4 @@ Modal.Header = ModalHeader;
|
|||
Modal.Footer = ModalFooter;
|
||||
Modal.Content = ModalContent;
|
||||
|
||||
export default Modal;
|
||||
export default Modal;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,77 @@
|
|||
import React from 'react';
|
||||
import stl from './NoSessionPermission.module.css'
|
||||
import { Icon, Button, Link } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import React from "react";
|
||||
import stl from "./NoSessionPermission.module.css";
|
||||
import { Icon, Button, Link } from "UI";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
sessions as sessionsRoute,
|
||||
assist as assistRoute,
|
||||
withSiteId,
|
||||
} from "App/routes";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
interface Props {
|
||||
session: any
|
||||
interface Props extends RouteComponentProps {
|
||||
session: any;
|
||||
siteId: string;
|
||||
history: any;
|
||||
sessionPath: any;
|
||||
isAssist: boolean;
|
||||
}
|
||||
function NoSessionPermission({ session }: Props) {
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<Icon name="shield-lock" size="50" className="py-16"/>
|
||||
<div className={ stl.title }>Not allowed</div>
|
||||
{ session.isLive ?
|
||||
<span>This session is still live, and you don’t have the necessary permissions to access this feature. Please check with your admin.</span> :
|
||||
<span>You don’t have the necessary permissions to access this feature. Please check with your admin.</span>
|
||||
}
|
||||
<Link to="/">
|
||||
<Button variant="primary" className="mt-6">GO BACK</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
function NoSessionPermission(props: Props) {
|
||||
const { session, history, siteId, sessionPath, isAssist } = props;
|
||||
|
||||
const backHandler = () => {
|
||||
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.pathname + sessionPath.search
|
||||
: withSiteId(SESSIONS_ROUTE, siteId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<Icon name="shield-lock" size="50" className="py-16" />
|
||||
<div className={stl.title}>Not allowed</div>
|
||||
{session.isLive ? (
|
||||
<span>
|
||||
This session is still live, and you don’t have the necessary
|
||||
permissions to access this feature. Please check with your
|
||||
admin.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
You don’t have the necessary permissions to access this
|
||||
feature. Please check with your admin.
|
||||
</span>
|
||||
)}
|
||||
{/* <Link to="/"> */}
|
||||
<Button variant="primary" onClick={backHandler} className="mt-6">
|
||||
GO BACK
|
||||
</Button>
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
}))(NoSessionPermission);
|
||||
export default withRouter(
|
||||
connect((state: any) => {
|
||||
const isAssist = window.location.pathname.includes("/assist/");
|
||||
return {
|
||||
isAssist,
|
||||
session: state.getIn(["sessions", "current"]),
|
||||
siteId: state.getIn(["site", "siteId"]),
|
||||
sessionPath: state.getIn(["sessions", "sessionPath"]),
|
||||
};
|
||||
})(NoSessionPermission)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,92 +1,82 @@
|
|||
import origMoment from 'moment';
|
||||
import { extendMoment } from 'moment-range';
|
||||
|
||||
import origMoment from "moment";
|
||||
import { extendMoment } from "moment-range";
|
||||
export const moment = extendMoment(origMoment);
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export const CUSTOM_RANGE = 'CUSTOM_RANGE';
|
||||
export const CUSTOM_RANGE = "CUSTOM_RANGE";
|
||||
|
||||
const DATE_RANGE_LABELS = {
|
||||
// LAST_30_MINUTES: '30 Minutes',
|
||||
// TODAY: 'Today',
|
||||
LAST_24_HOURS: 'Last 24 Hours',
|
||||
// YESTERDAY: 'Yesterday',
|
||||
LAST_7_DAYS: 'Past 7 Days',
|
||||
LAST_30_DAYS: 'Past 30 Days',
|
||||
//THIS_MONTH: 'This Month',
|
||||
//LAST_MONTH: 'Previous Month',
|
||||
//THIS_YEAR: 'This Year',
|
||||
[ CUSTOM_RANGE ]: 'Custom Range',
|
||||
// LAST_30_MINUTES: '30 Minutes',
|
||||
// TODAY: 'Today',
|
||||
LAST_24_HOURS: "Last 24 Hours",
|
||||
// YESTERDAY: 'Yesterday',
|
||||
LAST_7_DAYS: "Past 7 Days",
|
||||
LAST_30_DAYS: "Past 30 Days",
|
||||
//THIS_MONTH: 'This Month',
|
||||
//LAST_MONTH: 'Previous Month',
|
||||
//THIS_YEAR: 'This Year',
|
||||
[CUSTOM_RANGE]: "Custom Range",
|
||||
};
|
||||
|
||||
const DATE_RANGE_VALUES = {};
|
||||
Object.keys(DATE_RANGE_LABELS).forEach((key) => { DATE_RANGE_VALUES[ key ] = key; });
|
||||
Object.keys(DATE_RANGE_LABELS).forEach((key) => {
|
||||
DATE_RANGE_VALUES[key] = key;
|
||||
});
|
||||
|
||||
export { DATE_RANGE_VALUES };
|
||||
export const dateRangeValues = Object.keys(DATE_RANGE_VALUES);
|
||||
|
||||
export const DATE_RANGE_OPTIONS = Object.keys(DATE_RANGE_LABELS).map((key) => {
|
||||
return {
|
||||
label: DATE_RANGE_LABELS[ key ],
|
||||
value: key,
|
||||
};
|
||||
return {
|
||||
label: DATE_RANGE_LABELS[key],
|
||||
value: key,
|
||||
};
|
||||
});
|
||||
|
||||
export function getDateRangeFromTs(start, end) {
|
||||
return moment.range(
|
||||
moment(start),
|
||||
moment(end),
|
||||
);
|
||||
return moment.range(moment(start), moment(end));
|
||||
}
|
||||
|
||||
export function getDateRangeLabel(value) {
|
||||
return DATE_RANGE_LABELS[ value ];
|
||||
return DATE_RANGE_LABELS[value];
|
||||
}
|
||||
|
||||
export function getDateRangeFromValue(value) {
|
||||
switch (value) {
|
||||
case DATE_RANGE_VALUES.LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf('hour').subtract(30, 'minutes'),
|
||||
moment().startOf('hour'),
|
||||
);
|
||||
case DATE_RANGE_VALUES.TODAY:
|
||||
return moment.range(
|
||||
moment().startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case DATE_RANGE_VALUES.YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, 'days').startOf('day'),
|
||||
moment().subtract(1, 'days').endOf('day'),
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_24_HOURS:
|
||||
return moment.range(
|
||||
moment().subtract(24, 'hours'),
|
||||
moment(),
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, 'days').startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, 'days').startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case DATE_RANGE_VALUES.THIS_MONTH:
|
||||
return moment().range('month');
|
||||
case DATE_RANGE_VALUES.LAST_MONTH:
|
||||
return moment().subtract(1, 'months').range('month');
|
||||
case DATE_RANGE_VALUES.THIS_YEAR:
|
||||
return moment().range('year');
|
||||
case DATE_RANGE_VALUES.CUSTOM_RANGE:
|
||||
return moment.range(
|
||||
moment(),
|
||||
moment(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
switch (value) {
|
||||
case DATE_RANGE_VALUES.LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf("hour").subtract(30, "minutes"),
|
||||
moment().startOf("hour")
|
||||
);
|
||||
case DATE_RANGE_VALUES.TODAY:
|
||||
return moment.range(moment().startOf("day"), moment().endOf("day"));
|
||||
case DATE_RANGE_VALUES.YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, "days").startOf("day"),
|
||||
moment().subtract(1, "days").endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_24_HOURS:
|
||||
return moment.range(moment().subtract(24, "hours"), moment());
|
||||
case DATE_RANGE_VALUES.LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.THIS_MONTH:
|
||||
return moment().range("month");
|
||||
case DATE_RANGE_VALUES.LAST_MONTH:
|
||||
return moment().subtract(1, "months").range("month");
|
||||
case DATE_RANGE_VALUES.THIS_YEAR:
|
||||
return moment().range("year");
|
||||
case DATE_RANGE_VALUES.CUSTOM_RANGE:
|
||||
return moment.range(moment(), moment());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -96,13 +86,25 @@ export function getDateRangeFromValue(value) {
|
|||
* @return {String} Formated date string.
|
||||
*/
|
||||
export const checkForRecent = (date, format) => {
|
||||
const d = new Date();
|
||||
// Today
|
||||
if (date.hasSame(d, 'day')) return 'Today';
|
||||
const d = new Date();
|
||||
// Today
|
||||
if (date.hasSame(d, "day")) return "Today";
|
||||
|
||||
// Yesterday
|
||||
if (date.hasSame(d.setDate(d.getDate() - 1), 'day')) return 'Yesterday';
|
||||
// Yesterday
|
||||
if (date.hasSame(d.setDate(d.getDate() - 1), "day")) return "Yesterday";
|
||||
|
||||
// Formatted
|
||||
return date.toFormat(format);
|
||||
// Formatted
|
||||
return date.toFormat(format);
|
||||
};
|
||||
|
||||
export const overPastString = (period) => {
|
||||
if (period.rangeName === DATE_RANGE_VALUES.CUSTOM_RANGE) {
|
||||
const format = "LLL dd, yyyy HH:mm";
|
||||
const { startTimestamp, endTimestamp } = period.toTimestamps();
|
||||
const start = DateTime.fromMillis(startTimestamp).toFormat(format);
|
||||
const end = DateTime.fromMillis(endTimestamp).toFormat(format);
|
||||
return ` between ${start} - ${end}`;
|
||||
}
|
||||
|
||||
return ' over the ' + DATE_RANGE_LABELS[period.rangeName];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { List, Map } from 'immutable';
|
|||
import Role from 'Types/role';
|
||||
import crudDuckGenerator from './tools/crudDuck';
|
||||
import { reduceDucks } from 'Duck/tools';
|
||||
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
|
||||
import { createListUpdater } from './funcTools/tools';
|
||||
|
||||
const crudDuck = crudDuckGenerator('client/role', Role, { idKey: 'roleId' });
|
||||
export const { fetchList, init, edit, remove, } = crudDuck.actions;
|
||||
|
|
@ -14,14 +14,14 @@ const initialState = Map({
|
|||
permissions: List([
|
||||
{ text: 'Session Replay', value: 'SESSION_REPLAY' },
|
||||
{ text: 'Developer Tools', value: 'DEV_TOOLS' },
|
||||
{ text: 'Errors', value: 'ERRORS' },
|
||||
{ text: 'Metrics', value: 'METRICS' },
|
||||
// { text: 'Errors', value: 'ERRORS' },
|
||||
{ text: 'Dashboard', value: 'METRICS' },
|
||||
{ text: 'Assist (Live)', value: 'ASSIST_LIVE' },
|
||||
{ text: 'Assist (Call)', value: 'ASSIST_CALL' },
|
||||
])
|
||||
});
|
||||
|
||||
const name = "role";
|
||||
// const name = "role";
|
||||
const idKey = "roleId";
|
||||
|
||||
const updateItemInList = createListUpdater(idKey);
|
||||
|
|
|
|||
|
|
@ -1,104 +1,114 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
import Dashboard, { IDashboard } from "./types/dashboard"
|
||||
import {
|
||||
makeAutoObservable,
|
||||
runInAction,
|
||||
observable,
|
||||
action,
|
||||
} from "mobx";
|
||||
import Dashboard, { IDashboard } from "./types/dashboard";
|
||||
import Widget, { IWidget } from "./types/widget";
|
||||
import { dashboardService, metricService } from "App/services";
|
||||
import { toast } from 'react-toastify';
|
||||
import Period, { LAST_24_HOURS, LAST_7_DAYS, LAST_30_DAYS } from 'Types/app/period';
|
||||
import { getChartFormatter } from 'Types/dashboard/helper';
|
||||
import { toast } from "react-toastify";
|
||||
import Period, {
|
||||
LAST_24_HOURS,
|
||||
LAST_7_DAYS,
|
||||
} from "Types/app/period";
|
||||
import { getChartFormatter } from "Types/dashboard/helper";
|
||||
import Filter, { IFilter } from "./types/filter";
|
||||
import Funnel from "./types/funnel";
|
||||
import Session from "./types/session";
|
||||
import Error from "./types/error";
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { FilterKey } from "Types/filter/filterType";
|
||||
|
||||
export interface IDashboardSotre {
|
||||
dashboards: IDashboard[]
|
||||
selectedDashboard: IDashboard | null
|
||||
dashboardInstance: IDashboard
|
||||
selectedWidgets: IWidget[]
|
||||
startTimestamp: number
|
||||
endTimestamp: number
|
||||
period: Period
|
||||
drillDownFilter: IFilter
|
||||
drillDownPeriod: Period
|
||||
dashboards: IDashboard[];
|
||||
selectedDashboard: IDashboard | null;
|
||||
dashboardInstance: IDashboard;
|
||||
selectedWidgets: IWidget[];
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
period: Period;
|
||||
drillDownFilter: IFilter;
|
||||
drillDownPeriod: Period;
|
||||
|
||||
siteId: any
|
||||
currentWidget: Widget
|
||||
widgetCategories: any[]
|
||||
widgets: Widget[]
|
||||
metricsPage: number
|
||||
metricsPageSize: number
|
||||
metricsSearch: string
|
||||
siteId: any;
|
||||
currentWidget: Widget;
|
||||
widgetCategories: any[];
|
||||
widgets: Widget[];
|
||||
metricsPage: number;
|
||||
metricsPageSize: number;
|
||||
metricsSearch: string;
|
||||
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
isDeleting: boolean
|
||||
fetchingDashboard: boolean
|
||||
sessionsLoading: boolean
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
isDeleting: boolean;
|
||||
fetchingDashboard: boolean;
|
||||
sessionsLoading: boolean;
|
||||
|
||||
showAlertModal: boolean
|
||||
showAlertModal: boolean;
|
||||
|
||||
selectWidgetsByCategory: (category: string) => void
|
||||
toggleAllSelectedWidgets: (isSelected: boolean) => void
|
||||
removeSelectedWidgetByCategory(category: string): void
|
||||
toggleWidgetSelection(widget: IWidget): void
|
||||
selectWidgetsByCategory: (category: string) => void;
|
||||
toggleAllSelectedWidgets: (isSelected: boolean) => void;
|
||||
removeSelectedWidgetByCategory(category: string): void;
|
||||
toggleWidgetSelection(widget: IWidget): void;
|
||||
|
||||
initDashboard(dashboard?: IDashboard): void
|
||||
updateKey(key: string, value: any): void
|
||||
resetCurrentWidget(): void
|
||||
editWidget(widget: any): void
|
||||
fetchList(): Promise<any>
|
||||
fetch(dashboardId: string): Promise<any>
|
||||
save(dashboard: IDashboard): Promise<any>
|
||||
saveDashboardWidget(dashboard: Dashboard, widget: Widget)
|
||||
deleteDashboard(dashboard: IDashboard): Promise<any>
|
||||
toJson(): void
|
||||
fromJson(json: any): void
|
||||
// initDashboard(dashboard: IDashboard): void
|
||||
addDashboard(dashboard: IDashboard): void
|
||||
removeDashboard(dashboard: IDashboard): void
|
||||
getDashboard(dashboardId: string): IDashboard|null
|
||||
getDashboardCount(): void
|
||||
updateDashboard(dashboard: IDashboard): void
|
||||
selectDashboardById(dashboardId: string): void
|
||||
setSiteId(siteId: any): void
|
||||
selectDefaultDashboard(): Promise<IDashboard>
|
||||
initDashboard(dashboard?: IDashboard): void;
|
||||
updateKey(key: string, value: any): void;
|
||||
resetCurrentWidget(): void;
|
||||
editWidget(widget: any): void;
|
||||
fetchList(): Promise<any>;
|
||||
fetch(dashboardId: string): Promise<any>;
|
||||
save(dashboard: IDashboard): Promise<any>;
|
||||
deleteDashboard(dashboard: IDashboard): Promise<any>;
|
||||
toJson(): void;
|
||||
fromJson(json: any): void;
|
||||
addDashboard(dashboard: IDashboard): void;
|
||||
removeDashboard(dashboard: IDashboard): void;
|
||||
getDashboard(dashboardId: string): IDashboard | null;
|
||||
getDashboardCount(): void;
|
||||
updateDashboard(dashboard: IDashboard): void;
|
||||
selectDashboardById(dashboardId: string): void;
|
||||
setSiteId(siteId: any): void;
|
||||
selectDefaultDashboard(): Promise<IDashboard>;
|
||||
|
||||
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>
|
||||
fetchTemplates(hardRefresh: boolean): Promise<any>
|
||||
deleteDashboardWidget(dashboardId: string, widgetId: string): Promise<any>
|
||||
addWidgetToDashboard(dashboard: IDashboard, metricIds: any): Promise<any>
|
||||
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>;
|
||||
fetchTemplates(hardRefresh: boolean): Promise<any>;
|
||||
deleteDashboardWidget(dashboardId: string, widgetId: string): Promise<any>;
|
||||
addWidgetToDashboard(dashboard: IDashboard, metricIds: any): Promise<any>;
|
||||
|
||||
updatePinned(dashboardId: string): Promise<any>
|
||||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise<any>
|
||||
setPeriod(period: any): void
|
||||
updatePinned(dashboardId: string): Promise<any>;
|
||||
fetchMetricChartData(
|
||||
metric: IWidget,
|
||||
data: any,
|
||||
isWidget: boolean
|
||||
): Promise<any>;
|
||||
setPeriod(period: any): void;
|
||||
}
|
||||
export default class DashboardStore implements IDashboardSotre {
|
||||
siteId: any = null
|
||||
siteId: any = null;
|
||||
// Dashbaord / Widgets
|
||||
dashboards: Dashboard[] = []
|
||||
selectedDashboard: Dashboard | null = null
|
||||
dashboardInstance: IDashboard = new Dashboard()
|
||||
dashboards: Dashboard[] = [];
|
||||
selectedDashboard: Dashboard | null = null;
|
||||
dashboardInstance: IDashboard = new Dashboard();
|
||||
selectedWidgets: IWidget[] = [];
|
||||
currentWidget: Widget = new Widget()
|
||||
widgetCategories: any[] = []
|
||||
widgets: Widget[] = []
|
||||
period: Period = Period({ rangeName: LAST_24_HOURS })
|
||||
drillDownFilter: Filter = new Filter()
|
||||
currentWidget: Widget = new Widget();
|
||||
widgetCategories: any[] = [];
|
||||
widgets: Widget[] = [];
|
||||
period: Period = Period({ rangeName: LAST_24_HOURS });
|
||||
drillDownFilter: Filter = new Filter();
|
||||
drillDownPeriod: Period = Period({ rangeName: LAST_7_DAYS });
|
||||
startTimestamp: number = 0
|
||||
endTimestamp: number = 0
|
||||
startTimestamp: number = 0;
|
||||
endTimestamp: number = 0;
|
||||
|
||||
// Metrics
|
||||
metricsPage: number = 1
|
||||
metricsPageSize: number = 10
|
||||
metricsSearch: string = ''
|
||||
metricsPage: number = 1;
|
||||
metricsPageSize: number = 10;
|
||||
metricsSearch: string = "";
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean = true
|
||||
isSaving: boolean = false
|
||||
isDeleting: boolean = false
|
||||
fetchingDashboard: boolean = false
|
||||
isLoading: boolean = true;
|
||||
isSaving: boolean = false;
|
||||
isDeleting: boolean = false;
|
||||
fetchingDashboard: boolean = false;
|
||||
sessionsLoading: boolean = false;
|
||||
|
||||
showAlertModal: boolean = false;
|
||||
|
|
@ -135,378 +145,469 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
setPeriod: action,
|
||||
setDrillDownPeriod: action,
|
||||
|
||||
fetchMetricChartData: action
|
||||
})
|
||||
fetchMetricChartData: action,
|
||||
});
|
||||
|
||||
this.drillDownPeriod = Period({ rangeName: LAST_7_DAYS });
|
||||
const timeStamps = this.drillDownPeriod.toTimestamps();
|
||||
this.drillDownFilter.updateKey('startTimestamp', timeStamps.startTimestamp)
|
||||
this.drillDownFilter.updateKey('endTimestamp', timeStamps.endTimestamp)
|
||||
this.drillDownFilter.updateKey(
|
||||
"startTimestamp",
|
||||
timeStamps.startTimestamp
|
||||
);
|
||||
this.drillDownFilter.updateKey("endTimestamp", timeStamps.endTimestamp);
|
||||
}
|
||||
|
||||
toggleAllSelectedWidgets(isSelected: boolean) {
|
||||
if (isSelected) {
|
||||
const allWidgets = this.widgetCategories.reduce((acc, cat) => {
|
||||
return acc.concat(cat.widgets)
|
||||
}, [])
|
||||
return acc.concat(cat.widgets);
|
||||
}, []);
|
||||
|
||||
this.selectedWidgets = allWidgets
|
||||
this.selectedWidgets = allWidgets;
|
||||
} else {
|
||||
this.selectedWidgets = []
|
||||
this.selectedWidgets = [];
|
||||
}
|
||||
}
|
||||
|
||||
selectWidgetsByCategory(category: string) {
|
||||
const selectedWidgetIds = this.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const widgets = this.widgetCategories.find(cat => cat.name === category)?.widgets.filter(widget => !selectedWidgetIds.includes(widget.metricId))
|
||||
this.selectedWidgets = this.selectedWidgets.concat(widgets) || []
|
||||
const selectedWidgetIds = this.selectedWidgets.map(
|
||||
(widget: any) => widget.metricId
|
||||
);
|
||||
const widgets = this.widgetCategories
|
||||
.find((cat) => cat.name === category)
|
||||
?.widgets.filter(
|
||||
(widget: any) => !selectedWidgetIds.includes(widget.metricId)
|
||||
);
|
||||
this.selectedWidgets = this.selectedWidgets.concat(widgets) || [];
|
||||
}
|
||||
|
||||
removeSelectedWidgetByCategory = (category: any) => {
|
||||
const categoryWidgetIds = category.widgets.map(w => w.metricId)
|
||||
this.selectedWidgets = this.selectedWidgets.filter((widget: any) => !categoryWidgetIds.includes(widget.metricId));
|
||||
}
|
||||
const categoryWidgetIds = category.widgets.map((w) => w.metricId);
|
||||
this.selectedWidgets = this.selectedWidgets.filter(
|
||||
(widget: any) => !categoryWidgetIds.includes(widget.metricId)
|
||||
);
|
||||
};
|
||||
|
||||
toggleWidgetSelection = (widget: any) => {
|
||||
const selectedWidgetIds = this.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const selectedWidgetIds = this.selectedWidgets.map(
|
||||
(widget: any) => widget.metricId
|
||||
);
|
||||
if (selectedWidgetIds.includes(widget.metricId)) {
|
||||
this.selectedWidgets = this.selectedWidgets.filter((w: any) => w.metricId !== widget.metricId);
|
||||
this.selectedWidgets = this.selectedWidgets.filter(
|
||||
(w: any) => w.metricId !== widget.metricId
|
||||
);
|
||||
} else {
|
||||
this.selectedWidgets.push(widget);
|
||||
}
|
||||
};
|
||||
|
||||
findByIds(ids: string[]) {
|
||||
return this.dashboards.filter(d => ids.includes(d.dashboardId))
|
||||
return this.dashboards.filter((d) => ids.includes(d.dashboardId));
|
||||
}
|
||||
|
||||
initDashboard(dashboard: Dashboard) {
|
||||
this.dashboardInstance = dashboard ? new Dashboard().fromJson(dashboard) : new Dashboard()
|
||||
this.selectedWidgets = []
|
||||
this.dashboardInstance = dashboard
|
||||
? new Dashboard().fromJson(dashboard)
|
||||
: new Dashboard();
|
||||
this.selectedWidgets = [];
|
||||
}
|
||||
|
||||
updateKey(key: any, value: any) {
|
||||
this[key] = value
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
resetCurrentWidget() {
|
||||
this.currentWidget = new Widget()
|
||||
this.currentWidget = new Widget();
|
||||
}
|
||||
|
||||
editWidget(widget: any) {
|
||||
this.currentWidget.update(widget)
|
||||
this.currentWidget.update(widget);
|
||||
}
|
||||
|
||||
fetchList(): Promise<any> {
|
||||
this.isLoading = true
|
||||
this.isLoading = true;
|
||||
|
||||
return dashboardService.getDashboards()
|
||||
return dashboardService
|
||||
.getDashboards()
|
||||
.then((list: any) => {
|
||||
runInAction(() => {
|
||||
this.dashboards = list.map(d => new Dashboard().fromJson(d))
|
||||
})
|
||||
}).finally(() => {
|
||||
runInAction(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
this.dashboards = list.map((d) =>
|
||||
new Dashboard().fromJson(d)
|
||||
);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetch(dashboardId: string): Promise<any> {
|
||||
this.fetchingDashboard = true
|
||||
return dashboardService.getDashboard(dashboardId).then(response => {
|
||||
// const widgets = new Dashboard().fromJson(response).widgets
|
||||
this.selectedDashboard?.update({ 'widgets' : new Dashboard().fromJson(response).widgets})
|
||||
}).finally(() => {
|
||||
this.fetchingDashboard = false
|
||||
})
|
||||
this.fetchingDashboard = true;
|
||||
return dashboardService
|
||||
.getDashboard(dashboardId)
|
||||
.then((response) => {
|
||||
// const widgets = new Dashboard().fromJson(response).widgets
|
||||
this.selectedDashboard?.update({
|
||||
widgets: new Dashboard().fromJson(response).widgets,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchingDashboard = false;
|
||||
});
|
||||
}
|
||||
|
||||
save(dashboard: IDashboard): Promise<any> {
|
||||
this.isSaving = true
|
||||
const isCreating = !dashboard.dashboardId
|
||||
this.isSaving = true;
|
||||
const isCreating = !dashboard.dashboardId;
|
||||
|
||||
dashboard.metrics = this.selectedWidgets.map(w => w.metricId)
|
||||
dashboard.metrics = this.selectedWidgets.map((w) => w.metricId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dashboardService.saveDashboard(dashboard).then(_dashboard => {
|
||||
runInAction(() => {
|
||||
if (isCreating) {
|
||||
toast.success('Dashboard created successfully')
|
||||
this.addDashboard(new Dashboard().fromJson(_dashboard))
|
||||
} else {
|
||||
toast.success('Dashboard updated successfully')
|
||||
this.updateDashboard(new Dashboard().fromJson(_dashboard))
|
||||
}
|
||||
resolve(_dashboard)
|
||||
dashboardService
|
||||
.saveDashboard(dashboard)
|
||||
.then((_dashboard) => {
|
||||
runInAction(() => {
|
||||
if (isCreating) {
|
||||
toast.success("Dashboard created successfully");
|
||||
this.addDashboard(
|
||||
new Dashboard().fromJson(_dashboard)
|
||||
);
|
||||
} else {
|
||||
toast.success("Dashboard updated successfully");
|
||||
this.updateDashboard(
|
||||
new Dashboard().fromJson(_dashboard)
|
||||
);
|
||||
}
|
||||
resolve(_dashboard);
|
||||
});
|
||||
})
|
||||
}).catch(error => {
|
||||
toast.error('Error saving dashboard')
|
||||
reject()
|
||||
}).finally(() => {
|
||||
runInAction(() => {
|
||||
this.isSaving = false
|
||||
.catch((error) => {
|
||||
toast.error("Error saving dashboard");
|
||||
reject();
|
||||
})
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
runInAction(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveMetric(metric: IWidget, dashboardId: string): Promise<any> {
|
||||
const isCreating = !metric.widgetId
|
||||
return dashboardService.saveMetric(metric, dashboardId).then(metric => {
|
||||
runInAction(() => {
|
||||
if (isCreating) {
|
||||
this.selectedDashboard?.widgets.push(metric)
|
||||
} else {
|
||||
this.selectedDashboard?.widgets.map(w => {
|
||||
if (w.widgetId === metric.widgetId) {
|
||||
w.update(metric)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
saveDashboardWidget(dashboard: Dashboard, widget: Widget) {
|
||||
widget.validate()
|
||||
if (widget.isValid) {
|
||||
this.isLoading = true
|
||||
}
|
||||
const isCreating = !metric.widgetId;
|
||||
return dashboardService
|
||||
.saveMetric(metric, dashboardId)
|
||||
.then((metric) => {
|
||||
runInAction(() => {
|
||||
if (isCreating) {
|
||||
this.selectedDashboard?.widgets.push(metric);
|
||||
} else {
|
||||
this.selectedDashboard?.widgets.map((w) => {
|
||||
if (w.widgetId === metric.widgetId) {
|
||||
w.update(metric);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboard(dashboard: Dashboard): Promise<any> {
|
||||
this.isDeleting = true
|
||||
return dashboardService.deleteDashboard(dashboard.dashboardId).then(() => {
|
||||
toast.success('Dashboard deleted successfully')
|
||||
runInAction(() => {
|
||||
this.removeDashboard(dashboard)
|
||||
this.isDeleting = true;
|
||||
return dashboardService
|
||||
.deleteDashboard(dashboard.dashboardId)
|
||||
.then(() => {
|
||||
toast.success("Dashboard deleted successfully");
|
||||
runInAction(() => {
|
||||
this.removeDashboard(dashboard);
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Dashboard could not be deleted')
|
||||
})
|
||||
.finally(() => {
|
||||
runInAction(() => {
|
||||
this.isDeleting = false
|
||||
.catch(() => {
|
||||
toast.error("Dashboard could not be deleted");
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
runInAction(() => {
|
||||
this.isDeleting = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
dashboards: this.dashboards.map(d => d.toJson())
|
||||
}
|
||||
dashboards: this.dashboards.map((d) => d.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
this.dashboards = json.dashboards.map(d => new Dashboard().fromJson(d))
|
||||
})
|
||||
return this
|
||||
this.dashboards = json.dashboards.map((d) =>
|
||||
new Dashboard().fromJson(d)
|
||||
);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addDashboard(dashboard: Dashboard) {
|
||||
this.dashboards.push(new Dashboard().fromJson(dashboard))
|
||||
this.dashboards.push(new Dashboard().fromJson(dashboard));
|
||||
}
|
||||
|
||||
removeDashboard(dashboard: Dashboard) {
|
||||
this.dashboards = this.dashboards.filter(d => d.dashboardId !== dashboard.dashboardId)
|
||||
this.dashboards = this.dashboards.filter(
|
||||
(d) => d.dashboardId !== dashboard.dashboardId
|
||||
);
|
||||
}
|
||||
|
||||
getDashboard(dashboardId: string): IDashboard|null {
|
||||
return this.dashboards.find(d => d.dashboardId === dashboardId) || null
|
||||
getDashboard(dashboardId: string): IDashboard | null {
|
||||
return (
|
||||
this.dashboards.find((d) => d.dashboardId === dashboardId) || null
|
||||
);
|
||||
}
|
||||
|
||||
getDashboardByIndex(index: number) {
|
||||
return this.dashboards[index]
|
||||
return this.dashboards[index];
|
||||
}
|
||||
|
||||
getDashboardCount() {
|
||||
return this.dashboards.length
|
||||
return this.dashboards.length;
|
||||
}
|
||||
|
||||
updateDashboard(dashboard: Dashboard) {
|
||||
const index = this.dashboards.findIndex(d => d.dashboardId === dashboard.dashboardId)
|
||||
const index = this.dashboards.findIndex(
|
||||
(d) => d.dashboardId === dashboard.dashboardId
|
||||
);
|
||||
if (index >= 0) {
|
||||
this.dashboards[index] = dashboard
|
||||
this.dashboards[index] = dashboard;
|
||||
if (this.selectedDashboard?.dashboardId === dashboard.dashboardId) {
|
||||
this.selectDashboardById(dashboard.dashboardId)
|
||||
this.selectDashboardById(dashboard.dashboardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectDashboardById = (dashboardId: any) => {
|
||||
this.selectedDashboard = this.dashboards.find(d => d.dashboardId == dashboardId) || new Dashboard();
|
||||
// if (this.selectedDashboard.dashboardId) {
|
||||
// this.fetch(this.selectedDashboard.dashboardId)
|
||||
// }
|
||||
}
|
||||
this.selectedDashboard =
|
||||
this.dashboards.find((d) => d.dashboardId == dashboardId) ||
|
||||
new Dashboard();
|
||||
};
|
||||
|
||||
setSiteId = (siteId: any) => {
|
||||
this.siteId = siteId
|
||||
}
|
||||
this.siteId = siteId;
|
||||
};
|
||||
|
||||
selectDefaultDashboard = (): Promise<Dashboard> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.dashboards.length > 0) {
|
||||
const pinnedDashboard = this.dashboards.find(d => d.isPinned)
|
||||
const pinnedDashboard = this.dashboards.find((d) => d.isPinned);
|
||||
if (pinnedDashboard) {
|
||||
this.selectedDashboard = pinnedDashboard
|
||||
this.selectedDashboard = pinnedDashboard;
|
||||
} else {
|
||||
this.selectedDashboard = this.dashboards[0]
|
||||
this.selectedDashboard = this.dashboards[0];
|
||||
}
|
||||
|
||||
resolve(this.selectedDashboard)
|
||||
resolve(this.selectedDashboard);
|
||||
}
|
||||
reject(new Error("No dashboards found"))
|
||||
})
|
||||
}
|
||||
reject(new Error("No dashboards found"));
|
||||
});
|
||||
};
|
||||
|
||||
fetchTemplates(hardRefresh): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.widgetCategories.length > 0 && !hardRefresh) {
|
||||
resolve(this.widgetCategories)
|
||||
resolve(this.widgetCategories);
|
||||
} else {
|
||||
metricService.getTemplates().then(response => {
|
||||
const categories: any[] = []
|
||||
response.forEach(category => {
|
||||
const widgets: any[] = []
|
||||
// TODO speed_location is not supported yet
|
||||
category.widgets.filter(w => w.predefinedKey !== 'speed_locations').forEach(widget => {
|
||||
const w = new Widget().fromJson(widget)
|
||||
widgets.push(w)
|
||||
})
|
||||
const c: any = {}
|
||||
c.widgets = widgets
|
||||
c.name = category.category
|
||||
c.description = category.description
|
||||
categories.push(c)
|
||||
metricService
|
||||
.getTemplates()
|
||||
.then((response) => {
|
||||
const categories: any[] = [];
|
||||
response.forEach((category: any) => {
|
||||
const widgets: any[] = [];
|
||||
// TODO speed_location is not supported yet
|
||||
category.widgets
|
||||
.filter(
|
||||
(w: any) => w.predefinedKey !== "speed_locations"
|
||||
)
|
||||
.forEach((widget: any) => {
|
||||
const w = new Widget().fromJson(widget);
|
||||
widgets.push(w);
|
||||
});
|
||||
const c: any = {};
|
||||
c.widgets = widgets;
|
||||
c.name = category.category;
|
||||
c.description = category.description;
|
||||
categories.push(c);
|
||||
});
|
||||
this.widgetCategories = categories;
|
||||
resolve(this.widgetCategories);
|
||||
})
|
||||
this.widgetCategories = categories
|
||||
resolve(this.widgetCategories)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboardWidget(dashboardId: string, widgetId: string) {
|
||||
this.isDeleting = true
|
||||
return dashboardService.deleteWidget(dashboardId, widgetId).then(() => {
|
||||
toast.success('Dashboard updated successfully')
|
||||
runInAction(() => {
|
||||
this.selectedDashboard?.removeWidget(widgetId)
|
||||
this.isDeleting = true;
|
||||
return dashboardService
|
||||
.deleteWidget(dashboardId, widgetId)
|
||||
.then(() => {
|
||||
toast.success("Dashboard updated successfully");
|
||||
runInAction(() => {
|
||||
this.selectedDashboard?.removeWidget(widgetId);
|
||||
});
|
||||
})
|
||||
}).finally(() => {
|
||||
this.isDeleting = false
|
||||
})
|
||||
.finally(() => {
|
||||
this.isDeleting = false;
|
||||
});
|
||||
}
|
||||
|
||||
addWidgetToDashboard(dashboard: IDashboard, metricIds: any) : Promise<any> {
|
||||
this.isSaving = true
|
||||
return dashboardService.addWidget(dashboard, metricIds)
|
||||
.then(response => {
|
||||
toast.success('Widget added successfully')
|
||||
}).catch(() => {
|
||||
toast.error('Widget could not be added')
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
addWidgetToDashboard(dashboard: IDashboard, metricIds: any): Promise<any> {
|
||||
this.isSaving = true;
|
||||
return dashboardService
|
||||
.addWidget(dashboard, metricIds)
|
||||
.then((response) => {
|
||||
toast.success("Widget added successfully");
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
toast.error("Widget could not be added");
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
updatePinned(dashboardId: string): Promise<any> {
|
||||
// this.isSaving = true
|
||||
return dashboardService.updatePinned(dashboardId).then(() => {
|
||||
toast.success('Dashboard pinned successfully')
|
||||
this.dashboards.forEach(d => {
|
||||
if (d.dashboardId === dashboardId) {
|
||||
d.isPinned = true
|
||||
} else {
|
||||
d.isPinned = false
|
||||
}
|
||||
return dashboardService
|
||||
.updatePinned(dashboardId)
|
||||
.then(() => {
|
||||
toast.success("Dashboard pinned successfully");
|
||||
this.dashboards.forEach((d) => {
|
||||
if (d.dashboardId === dashboardId) {
|
||||
d.isPinned = true;
|
||||
} else {
|
||||
d.isPinned = false;
|
||||
}
|
||||
});
|
||||
})
|
||||
}).catch(() => {
|
||||
toast.error('Dashboard could not be pinned')
|
||||
}).finally(() => {
|
||||
// this.isSaving = false
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Dashboard could not be pinned");
|
||||
})
|
||||
.finally(() => {
|
||||
// this.isSaving = false
|
||||
});
|
||||
}
|
||||
|
||||
setPeriod(period: any) {
|
||||
this.period = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
|
||||
this.period = Period({
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
rangeName: period.rangeName,
|
||||
});
|
||||
}
|
||||
|
||||
setDrillDownPeriod(period: any) {
|
||||
this.drillDownPeriod = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
|
||||
this.drillDownPeriod = Period({
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
rangeName: period.rangeName,
|
||||
});
|
||||
}
|
||||
|
||||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
const period = this.period.toTimestamps()
|
||||
const params = { ...period, ...data, key: metric.predefinedKey }
|
||||
|
||||
fetchMetricChartData(
|
||||
metric: IWidget,
|
||||
data: any,
|
||||
isWidget: boolean = false
|
||||
): Promise<any> {
|
||||
const period = this.period.toTimestamps();
|
||||
const params = { ...period, ...data, key: metric.predefinedKey };
|
||||
|
||||
if (metric.page && metric.limit) {
|
||||
params['page'] = metric.page
|
||||
params['limit'] = metric.limit
|
||||
params["page"] = metric.page;
|
||||
params["limit"] = metric.limit;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return metricService.getMetricChartData(metric, params, isWidget)
|
||||
return metricService
|
||||
.getMetricChartData(metric, params, isWidget)
|
||||
.then((data: any) => {
|
||||
if (metric.metricType === 'predefined' && metric.viewType === 'overview') {
|
||||
const _data = { ...data, chart: getChartFormatter(this.period)(data.chart) }
|
||||
metric.setData(_data)
|
||||
if (
|
||||
metric.metricType === "predefined" &&
|
||||
metric.viewType === "overview"
|
||||
) {
|
||||
const _data = {
|
||||
...data,
|
||||
chart: getChartFormatter(this.period)(data.chart),
|
||||
};
|
||||
metric.setData(_data);
|
||||
resolve(_data);
|
||||
} else if (metric.metricType === 'funnel') {
|
||||
const _data = { ...data }
|
||||
_data.funnel = new Funnel().fromJSON(data)
|
||||
metric.setData(_data)
|
||||
} else if (metric.metricType === "funnel") {
|
||||
const _data = { ...data };
|
||||
_data.funnel = new Funnel().fromJSON(data);
|
||||
metric.setData(_data);
|
||||
resolve(_data);
|
||||
} else {
|
||||
const _data = {
|
||||
...data,
|
||||
}
|
||||
};
|
||||
|
||||
// TODO refactor to widget class
|
||||
if (metric.metricOf === FilterKey.SESSIONS) {
|
||||
_data['sessions'] = data.sessions.map((s: any) => new Session().fromJson(s))
|
||||
_data["sessions"] = data.sessions.map((s: any) =>
|
||||
new Session().fromJson(s)
|
||||
);
|
||||
} else if (metric.metricOf === FilterKey.ERRORS) {
|
||||
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s))
|
||||
_data["errors"] = data.errors.map((s: any) =>
|
||||
new Error().fromJSON(s)
|
||||
);
|
||||
} else {
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
_data['chart'] = getChartFormatter(this.period)(data.chart)
|
||||
_data['namesMap'] = data.chart
|
||||
.map(i => Object.keys(i))
|
||||
if (data.hasOwnProperty("chart")) {
|
||||
_data["chart"] = getChartFormatter(this.period)(
|
||||
data.chart
|
||||
);
|
||||
_data["namesMap"] = data.chart
|
||||
.map((i: any) => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.filter(
|
||||
(i: any) => i !== "time" && i !== "timestamp"
|
||||
)
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, [])
|
||||
}, []);
|
||||
} else {
|
||||
_data['chart'] = getChartFormatter(this.period)(Array.isArray(data) ? data : []);
|
||||
_data['namesMap'] = Array.isArray(data) ? data.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []) : []
|
||||
_data["chart"] = getChartFormatter(this.period)(
|
||||
Array.isArray(data) ? data : []
|
||||
);
|
||||
_data["namesMap"] = Array.isArray(data)
|
||||
? data
|
||||
.map((i) => Object.keys(i))
|
||||
.flat()
|
||||
.filter(
|
||||
(i) =>
|
||||
i !== "time" &&
|
||||
i !== "timestamp"
|
||||
)
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, [])
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
metric.setData(_data)
|
||||
metric.setData(_data);
|
||||
resolve(_data);
|
||||
}
|
||||
}).catch((err: any) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export default class UserStore {
|
|||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.saving = false;
|
||||
toast.error('Error saving user');
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.saving = false;
|
||||
|
|
@ -130,6 +131,7 @@ export default class UserStore {
|
|||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.saving = false;
|
||||
toast.error('Error deleting user');
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.saving = false;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default class MessageDistributor extends StatedScreen {
|
|||
|
||||
eventList.forEach(e => {
|
||||
if (e.type === EVENT_TYPES.LOCATION) { //TODO type system
|
||||
this.locationEventManager.append(e);
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
});
|
||||
this.session.errors.forEach(e => {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export default class AssistManager {
|
|||
if (this.cleaned) { return }
|
||||
if (this.socket) { this.socket.close() } // TODO: single socket connection
|
||||
// @ts-ignore
|
||||
const urlObject = new URL(window.env.API_EDP) // does it handle ssl automatically?
|
||||
const urlObject = new URL(window.env.API_EDP || window.location.origin + '/api') // does it handle ssl automatically?
|
||||
|
||||
// @ts-ignore WTF, socket.io ???
|
||||
const socket: Socket = this.socket = io(urlObject.origin, {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import APIClient from 'App/api_client';
|
||||
import { IUser } from 'App/mstore/types/user'
|
||||
import { fetchErrorCheck } from 'App/utils'
|
||||
|
||||
export default class UserService {
|
||||
private client: APIClient;
|
||||
|
|
@ -28,11 +29,11 @@ export default class UserService {
|
|||
const data = user.toSave();
|
||||
if (user.userId) {
|
||||
return this.client.put('/client/members/' + user.userId, data)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then(fetchErrorCheck)
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
} else {
|
||||
return this.client.post('/client/members', data)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then(fetchErrorCheck)
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +46,7 @@ export default class UserService {
|
|||
|
||||
delete(userId: string) {
|
||||
return this.client.delete('/client/members/' + userId)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then(fetchErrorCheck)
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export default Member.extend({
|
|||
apiKey: undefined,
|
||||
tenantKey: undefined,
|
||||
tenantName: undefined,
|
||||
edition: undefined,
|
||||
}, {
|
||||
fromJS: ({ ...account})=> ({
|
||||
...account,
|
||||
|
|
|
|||
|
|
@ -1,132 +1,138 @@
|
|||
import origMoment from 'moment';
|
||||
import { extendMoment } from 'moment-range';
|
||||
import Record from 'Types/Record';
|
||||
import origMoment from "moment";
|
||||
import { extendMoment } from "moment-range";
|
||||
import Record from "Types/Record";
|
||||
const moment = extendMoment(origMoment);
|
||||
|
||||
export const LAST_30_MINUTES = 'LAST_30_MINUTES';
|
||||
export const TODAY = 'TODAY';
|
||||
export const LAST_24_HOURS = 'LAST_24_HOURS';
|
||||
export const YESTERDAY = 'YESTERDAY';
|
||||
export const LAST_7_DAYS = 'LAST_7_DAYS';
|
||||
export const LAST_30_DAYS = 'LAST_30_DAYS';
|
||||
export const THIS_MONTH = 'THIS_MONTH';
|
||||
export const LAST_MONTH = 'LAST_MONTH';
|
||||
export const THIS_YEAR = 'THIS_YEAR';
|
||||
export const CUSTOM_RANGE = 'CUSTOM_RANGE';
|
||||
export const LAST_30_MINUTES = "LAST_30_MINUTES";
|
||||
export const TODAY = "TODAY";
|
||||
export const LAST_24_HOURS = "LAST_24_HOURS";
|
||||
export const YESTERDAY = "YESTERDAY";
|
||||
export const LAST_7_DAYS = "LAST_7_DAYS";
|
||||
export const LAST_30_DAYS = "LAST_30_DAYS";
|
||||
export const THIS_MONTH = "THIS_MONTH";
|
||||
export const LAST_MONTH = "LAST_MONTH";
|
||||
export const THIS_YEAR = "THIS_YEAR";
|
||||
export const CUSTOM_RANGE = "CUSTOM_RANGE";
|
||||
|
||||
const RANGE_LABELS = {
|
||||
[ LAST_30_MINUTES ]: 'Last 30 Minutes',
|
||||
[ TODAY ]: 'Today',
|
||||
[ YESTERDAY ]: 'Yesterday',
|
||||
[ LAST_24_HOURS ]: 'Last 24 Hours',
|
||||
[ LAST_7_DAYS ]: 'Last 7 Days',
|
||||
[ LAST_30_DAYS ]: 'Last 30 Days',
|
||||
[ THIS_MONTH ]: 'This Month',
|
||||
[ LAST_MONTH ]: 'Last Month',
|
||||
[ THIS_YEAR ]: 'This Year',
|
||||
}
|
||||
[LAST_30_MINUTES]: "Last 30 Minutes",
|
||||
[TODAY]: "Today",
|
||||
[YESTERDAY]: "Yesterday",
|
||||
[LAST_24_HOURS]: "Last 24 Hours",
|
||||
[LAST_7_DAYS]: "Last 7 Days",
|
||||
[LAST_30_DAYS]: "Last 30 Days",
|
||||
[THIS_MONTH]: "This Month",
|
||||
[LAST_MONTH]: "Last Month",
|
||||
[THIS_YEAR]: "This Year",
|
||||
};
|
||||
|
||||
function getRange(rangeName) {
|
||||
switch (rangeName) {
|
||||
case TODAY:
|
||||
return moment.range(
|
||||
moment().startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, 'days').startOf('day'),
|
||||
moment().subtract(1, 'days').endOf('day'),
|
||||
);
|
||||
case LAST_24_HOURS:
|
||||
return moment.range(
|
||||
moment().startOf('hour').subtract(24, 'hours'),
|
||||
moment().startOf('hour'),
|
||||
);
|
||||
case LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf('hour').subtract(30, 'minutes'),
|
||||
moment().startOf('hour'),
|
||||
);
|
||||
case LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, 'days').startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, 'days').startOf('day'),
|
||||
moment().endOf('day'),
|
||||
);
|
||||
case THIS_MONTH:
|
||||
return moment().range('month');
|
||||
case LAST_MONTH:
|
||||
return moment().subtract(1, 'months').range('month');
|
||||
case THIS_YEAR:
|
||||
return moment().range('year');
|
||||
default:
|
||||
return moment.range();
|
||||
}
|
||||
switch (rangeName) {
|
||||
case TODAY:
|
||||
return moment.range(moment().startOf("day"), moment().endOf("day"));
|
||||
case YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, "days").startOf("day"),
|
||||
moment().subtract(1, "days").endOf("day")
|
||||
);
|
||||
case LAST_24_HOURS:
|
||||
return moment.range(
|
||||
// moment().startOf("hour").subtract(24, "hours"),
|
||||
// moment().startOf("hour")
|
||||
moment().subtract(24, 'hours'),
|
||||
moment(),
|
||||
);
|
||||
case LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf("hour").subtract(30, "minutes"),
|
||||
moment().startOf("hour")
|
||||
);
|
||||
case LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
);
|
||||
case LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
);
|
||||
case THIS_MONTH:
|
||||
return moment().range("month");
|
||||
case LAST_MONTH:
|
||||
return moment().subtract(1, "months").range("month");
|
||||
case THIS_YEAR:
|
||||
return moment().range("year");
|
||||
default:
|
||||
return moment.range();
|
||||
}
|
||||
}
|
||||
|
||||
export default Record({
|
||||
start: 0,
|
||||
end: 0,
|
||||
rangeName: CUSTOM_RANGE,
|
||||
range: moment.range(),
|
||||
}, {
|
||||
fromJS: period => {
|
||||
if (!period.rangeName || period.rangeName === CUSTOM_RANGE) {
|
||||
const range = moment.range(
|
||||
moment(period.start || 0),
|
||||
moment(period.end || 0),
|
||||
);
|
||||
return {
|
||||
...period,
|
||||
range,
|
||||
start: range.start.unix() * 1000,
|
||||
end: range.end.unix() * 1000,
|
||||
};
|
||||
}
|
||||
const range = getRange(period.rangeName);
|
||||
return {
|
||||
...period,
|
||||
range,
|
||||
start: range.start.unix() * 1000,
|
||||
end: range.end.unix() * 1000,
|
||||
}
|
||||
},
|
||||
// fromFilter: filter => {
|
||||
// const range = getRange(filter.rangeName);
|
||||
// return {
|
||||
// start: range.start.unix() * 1000,
|
||||
// end: range.end.unix() * 1000,
|
||||
// rangeName: filter.rangeName,
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
toJSON() {
|
||||
return {
|
||||
startDate: this.start,
|
||||
endDate: this.end,
|
||||
rangeName: this.rangeName,
|
||||
rangeValue: this.rangeName,
|
||||
}
|
||||
},
|
||||
toTimestamps() {
|
||||
return {
|
||||
startTimestamp: this.start,
|
||||
endTimestamp: this.end,
|
||||
};
|
||||
},
|
||||
rangeFormatted(format = 'MMM Do YY, hh:mm A') {
|
||||
return this.range.start.format(format) + ' - ' + this.range.end.format(format);
|
||||
},
|
||||
toTimestampstwo() {
|
||||
return {
|
||||
startTimestamp: this.start / 1000,
|
||||
endTimestamp: this.end / 1000,
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
export default Record(
|
||||
{
|
||||
start: 0,
|
||||
end: 0,
|
||||
rangeName: CUSTOM_RANGE,
|
||||
range: moment.range(),
|
||||
},
|
||||
{
|
||||
fromJS: (period) => {
|
||||
if (!period.rangeName || period.rangeName === CUSTOM_RANGE) {
|
||||
const range = moment.range(
|
||||
moment(period.start || 0),
|
||||
moment(period.end || 0)
|
||||
);
|
||||
return {
|
||||
...period,
|
||||
range,
|
||||
start: range.start.unix() * 1000,
|
||||
end: range.end.unix() * 1000,
|
||||
};
|
||||
}
|
||||
const range = getRange(period.rangeName);
|
||||
return {
|
||||
...period,
|
||||
range,
|
||||
start: range.start.unix() * 1000,
|
||||
end: range.end.unix() * 1000,
|
||||
};
|
||||
},
|
||||
// fromFilter: filter => {
|
||||
// const range = getRange(filter.rangeName);
|
||||
// return {
|
||||
// start: range.start.unix() * 1000,
|
||||
// end: range.end.unix() * 1000,
|
||||
// rangeName: filter.rangeName,
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
toJSON() {
|
||||
return {
|
||||
startDate: this.start,
|
||||
endDate: this.end,
|
||||
rangeName: this.rangeName,
|
||||
rangeValue: this.rangeName,
|
||||
};
|
||||
},
|
||||
toTimestamps() {
|
||||
return {
|
||||
startTimestamp: this.start,
|
||||
endTimestamp: this.end,
|
||||
};
|
||||
},
|
||||
rangeFormatted(format = "MMM Do YY, HH:mm") {
|
||||
return (
|
||||
this.range.start.format(format) +
|
||||
" - " +
|
||||
this.range.end.format(format)
|
||||
);
|
||||
},
|
||||
toTimestampstwo() {
|
||||
return {
|
||||
startTimestamp: this.start / 1000,
|
||||
endTimestamp: this.end / 1000,
|
||||
};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"Player": ["./app/player"],
|
||||
"HOCs/*": ["./app/components/hocs/*"],
|
||||
"Types/*": ["./app/types/*"],
|
||||
"Duck/*": ["./app/duck/*"],
|
||||
}
|
||||
},
|
||||
"include": ["app"]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if not .Values.skipMigration}}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
|
|
@ -177,3 +178,4 @@ spec:
|
|||
- name: shared
|
||||
emptyDir: {}
|
||||
restartPolicy: Never
|
||||
{{- end}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue