Merge remote-tracking branch 'origin/dev' into api-v1.7.0

This commit is contained in:
Taha Yassine Kraiem 2022-06-27 19:50:32 +02:00
commit 747487cc4c
59 changed files with 1963 additions and 1575 deletions

View file

@ -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.

View file

@ -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 }}

View file

@ -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

View file

@ -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() }}

View file

@ -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() }}

View file

@ -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 {

View file

@ -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{

View file

@ -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
}

View file

@ -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

View file

@ -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())

View file

@ -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
}

View file

@ -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
}

View file

@ -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
View file

@ -0,0 +1,4 @@
{
"tabWidth": 4,
"useTabs": false
}

View file

@ -25,7 +25,6 @@ const siteIdRequiredPaths = [
'/custom_metrics',
'/dashboards',
'/metrics',
'/trails',
// '/custom_metrics/sessions',
];

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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,

View file

@ -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

View file

@ -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')

View file

@ -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>
);
}

View file

@ -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

View file

@ -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}
/>

View file

@ -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));

View file

@ -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>
);
);

View file

@ -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>
);
);

View file

@ -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

View file

@ -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

View file

@ -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))))
);

View file

@ -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}
>

View file

@ -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>

View file

@ -146,7 +146,7 @@ function WidgetChart(props: Props) {
return (
<CustomMetricTableErrors
metric={metric}
isTemplate={isTemplate}
// isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
)

View file

@ -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)
}
}, [])

View file

@ -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);

View file

@ -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

View file

@ -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>
))
}

View file

@ -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));

View file

@ -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));

View file

@ -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>
);
}
};

View file

@ -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>
);
}

View file

@ -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

View file

@ -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>
)}

View file

@ -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>
);
}
}
);

View file

@ -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

View file

@ -22,6 +22,7 @@ const Confirmation = ({
return (
<Modal
open={show}
onClose={() => proceed(false)}
>
<Modal.Header>{header}</Modal.Header>
<Modal.Content>

View file

@ -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;

View file

@ -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 dont have the necessary permissions to access this feature. Please check with your admin.</span> :
<span>You dont 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 dont have the necessary
permissions to access this feature. Please check with your
admin.
</span>
) : (
<span>
You dont 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)
);

View file

@ -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];
};

View file

@ -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);

View file

@ -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);
});
});
}
}

View file

@ -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;

View file

@ -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 => {

View file

@ -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, {

View file

@ -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 || {});
}

View file

@ -18,6 +18,7 @@ export default Member.extend({
apiKey: undefined,
tenantKey: undefined,
tenantName: undefined,
edition: undefined,
}, {
fromJS: ({ ...account})=> ({
...account,

View file

@ -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,
};
},
},
}
);

View file

@ -25,6 +25,7 @@
"Player": ["./app/player"],
"HOCs/*": ["./app/components/hocs/*"],
"Types/*": ["./app/types/*"],
"Duck/*": ["./app/duck/*"],
}
},
"include": ["app"]

View file

@ -1,3 +1,4 @@
{{- if not .Values.skipMigration}}
---
apiVersion: v1
kind: ConfigMap
@ -177,3 +178,4 @@ spec:
- name: shared
emptyDir: {}
restartPolicy: Never
{{- end}}