Merge remote-tracking branch 'origin/dev' into api-v1.7.0
1
.github/workflows/api-ee.yaml
vendored
|
|
@ -6,6 +6,7 @@ on:
|
|||
- dev
|
||||
paths:
|
||||
- ee/api/**
|
||||
- api/**
|
||||
|
||||
name: Build and Deploy Chalice EE
|
||||
|
||||
|
|
|
|||
70
.github/workflows/frontend.yaml
vendored
|
|
@ -3,9 +3,13 @@ on:
|
|||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- api-v1.5.5
|
||||
- dev
|
||||
paths:
|
||||
- frontend/**
|
||||
# Disable previous workflows for this action.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }} #-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -23,6 +27,10 @@ jobs:
|
|||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
|
|
@ -31,16 +39,60 @@ jobs:
|
|||
# - name: Install
|
||||
# run: npm install
|
||||
|
||||
- name: Build and deploy
|
||||
- name: Building and Pushing frontend image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
cd frontend
|
||||
bash build.sh
|
||||
cp -arl public frontend
|
||||
minio_pod=$(kubectl get po -n db -l app.kubernetes.io/name=minio -n db --output custom-columns=name:.metadata.name | tail -n+2)
|
||||
echo $minio_pod
|
||||
echo copying frontend to container.
|
||||
kubectl -n db cp frontend $minio_pod:/data/
|
||||
rm -rf frontend
|
||||
mv .env.sample .env
|
||||
docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:14-stretch-slim /bin/bash -c "yarn && yarn build"
|
||||
# https://github.com/docker/cli/issues/1134#issuecomment-613516912
|
||||
DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} .
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}
|
||||
- name: Creating old image input
|
||||
run: |
|
||||
#
|
||||
# Create yaml with existing image tags
|
||||
#
|
||||
kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\
|
||||
tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt
|
||||
|
||||
echo > /tmp/image_override.yaml
|
||||
|
||||
for line in `cat /tmp/image_tag.txt`;
|
||||
do
|
||||
image_array=($(echo "$line" | tr ':' '\n'))
|
||||
cat <<EOF >> /tmp/image_override.yaml
|
||||
${image_array[0]}:
|
||||
image:
|
||||
tag: ${image_array[1]}
|
||||
EOF
|
||||
done
|
||||
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
## Update secerts
|
||||
sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.OSS_PG_PASSWORD }}\"/g" vars.yaml
|
||||
sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\"/g" vars.yaml
|
||||
sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\"/g" vars.yaml
|
||||
sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.OSS_JWT_SECRET }}\"/g" vars.yaml
|
||||
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.OSS_DOMAIN_NAME }}\"/g" vars.yaml
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
|
|
|
|||
3
.github/workflows/workers-ee.yaml
vendored
|
|
@ -7,6 +7,7 @@ on:
|
|||
- dev
|
||||
paths:
|
||||
- ee/backend/**
|
||||
- backend/**
|
||||
|
||||
name: Build and deploy workers EE
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ jobs:
|
|||
## Update images
|
||||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
sed -i "/${image}/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
sed -i "/${image}/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
done
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
|
|
|
|||
9
.github/workflows/workers.yaml
vendored
|
|
@ -33,11 +33,12 @@ jobs:
|
|||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
|
||||
# Caching docker images
|
||||
- uses: satackey/action-docker-layer-caching@v0.0.11
|
||||
# Ignore the failure of a step and avoid terminating the job.
|
||||
continue-on-error: true
|
||||
# - uses: satackey/action-docker-layer-caching@v0.0.11
|
||||
# # Ignore the failure of a step and avoid terminating the job.
|
||||
# continue-on-error: true
|
||||
|
||||
|
||||
- name: Build, tag
|
||||
id: build-image
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ WORKDIR /root
|
|||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
|
||||
FROM prepare AS build
|
||||
COPY cmd cmd
|
||||
COPY pkg pkg
|
||||
COPY internal internal
|
||||
RUN go mod tidy
|
||||
|
||||
ARG SERVICE_NAME
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openreplay/backend/cmd/$SERVICE_NAME
|
||||
|
|
@ -27,7 +28,7 @@ ENV TZ=UTC \
|
|||
MAXMINDDB_FILE=/root/geoip.mmdb \
|
||||
UAPARSER_FILE=/root/regexes.yaml \
|
||||
HTTP_PORT=80 \
|
||||
BEACON_SIZE_LIMIT=7000000 \
|
||||
BEACON_SIZE_LIMIT=1000000 \
|
||||
KAFKA_USE_SSL=true \
|
||||
KAFKA_MAX_POLL_INTERVAL_MS=400000 \
|
||||
REDIS_STREAMS_MAX_LEN=10000 \
|
||||
|
|
@ -50,8 +51,8 @@ ENV TZ=UTC \
|
|||
FS_CLEAN_HRS=72 \
|
||||
FILE_SPLIT_SIZE=300000 \
|
||||
LOG_QUEUE_STATS_INTERVAL_SEC=60 \
|
||||
BATCH_QUEUE_LIMIT=20 \
|
||||
BATCH_SIZE_LIMIT=10000000 \
|
||||
DB_BATCH_QUEUE_LIMIT=20 \
|
||||
DB_BATCH_SIZE_LIMIT=10000000 \
|
||||
PARTITIONS_NUMBER=1
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func main() {
|
|||
cfg := db.New()
|
||||
|
||||
// Init database
|
||||
pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres), cfg.ProjectExpirationTimeoutMs)
|
||||
pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres, cfg.BatchQueueLimit, cfg.BatchSizeLimit), cfg.ProjectExpirationTimeoutMs)
|
||||
defer pg.Close()
|
||||
|
||||
// HandlersFabric returns the list of message handlers we want to be applied to each incoming message.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"log"
|
||||
"openreplay/backend/internal/config/ender"
|
||||
"openreplay/backend/internal/sessionender"
|
||||
"openreplay/backend/pkg/db/cache"
|
||||
"openreplay/backend/pkg/db/postgres"
|
||||
"openreplay/backend/pkg/monitoring"
|
||||
"time"
|
||||
|
||||
|
|
@ -30,6 +32,9 @@ func main() {
|
|||
// Load service configuration
|
||||
cfg := ender.New()
|
||||
|
||||
pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres, 0, 0), cfg.ProjectExpirationTimeoutMs)
|
||||
defer pg.Close()
|
||||
|
||||
// Init all modules
|
||||
statsLogger := logger.NewQueueStats(cfg.LoggerTimeout)
|
||||
sessions, err := sessionender.New(metrics, intervals.EVENTS_SESSION_END_TIMEOUT, cfg.PartitionsNumber)
|
||||
|
|
@ -44,8 +49,17 @@ func main() {
|
|||
cfg.TopicRawWeb,
|
||||
},
|
||||
func(sessionID uint64, msg messages.Message, meta *types.Meta) {
|
||||
switch msg.(type) {
|
||||
case *messages.SessionStart, *messages.SessionEnd:
|
||||
// Skip several message types
|
||||
return
|
||||
}
|
||||
// Test debug
|
||||
if msg.Meta().Timestamp == 0 {
|
||||
log.Printf("ZERO TS, sessID: %d, msgType: %d", sessionID, msg.TypeID())
|
||||
}
|
||||
statsLogger.Collect(sessionID, meta)
|
||||
sessions.UpdateSession(sessionID, meta.Timestamp)
|
||||
sessions.UpdateSession(sessionID, meta.Timestamp, msg.Meta().Timestamp)
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
|
@ -70,8 +84,12 @@ func main() {
|
|||
// Find ended sessions and send notification to other services
|
||||
sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool {
|
||||
msg := &messages.SessionEnd{Timestamp: uint64(timestamp)}
|
||||
if err := pg.InsertSessionEnd(sessionID, msg.Timestamp); err != nil {
|
||||
log.Printf("can't save sessionEnd to database, sessID: %d", sessionID)
|
||||
return false
|
||||
}
|
||||
if err := producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(msg)); err != nil {
|
||||
log.Printf("can't send SessionEnd to trigger topic: %s; sessID: %d", err, sessionID)
|
||||
log.Printf("can't send sessionEnd to topic: %s; sessID: %d", err, sessionID)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import (
|
|||
"log"
|
||||
"openreplay/backend/internal/config/heuristics"
|
||||
"openreplay/backend/pkg/handlers"
|
||||
"openreplay/backend/pkg/handlers/custom"
|
||||
ios2 "openreplay/backend/pkg/handlers/ios"
|
||||
web2 "openreplay/backend/pkg/handlers/web"
|
||||
"openreplay/backend/pkg/intervals"
|
||||
logger "openreplay/backend/pkg/log"
|
||||
|
|
@ -39,12 +37,12 @@ func main() {
|
|||
&web2.MemoryIssueDetector{},
|
||||
&web2.NetworkIssueDetector{},
|
||||
&web2.PerformanceAggregator{},
|
||||
// iOS handlers
|
||||
&ios2.AppNotResponding{},
|
||||
&ios2.ClickRageDetector{},
|
||||
&ios2.PerformanceAggregator{},
|
||||
// iOS's handlers
|
||||
//&ios2.AppNotResponding{},
|
||||
//&ios2.ClickRageDetector{},
|
||||
//&ios2.PerformanceAggregator{},
|
||||
// Other handlers (you can add your custom handlers here)
|
||||
&custom.CustomHandler{},
|
||||
//&custom.CustomHandler{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func main() {
|
|||
defer producer.Close(15000)
|
||||
|
||||
// Connect to database
|
||||
dbConn := cache.NewPGCache(postgres.NewConn(cfg.Postgres), 1000*60*20)
|
||||
dbConn := cache.NewPGCache(postgres.NewConn(cfg.Postgres, 0, 0), 1000*60*20)
|
||||
defer dbConn.Close()
|
||||
|
||||
// Build all services
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func main() {
|
|||
|
||||
cfg := config.New()
|
||||
|
||||
pg := postgres.NewConn(cfg.PostgresURI)
|
||||
pg := postgres.NewConn(cfg.PostgresURI, 0, 0)
|
||||
defer pg.Close()
|
||||
|
||||
tokenizer := token.NewTokenizer(cfg.TokenSecret)
|
||||
|
|
@ -74,7 +74,7 @@ func main() {
|
|||
log.Printf("Requesting all...\n")
|
||||
manager.RequestAll()
|
||||
case event := <-manager.Events:
|
||||
log.Printf("New integration event: %+v\n", *event.RawErrorEvent)
|
||||
log.Printf("New integration event: %+v\n", *event.IntegrationEvent)
|
||||
sessionID := event.SessionID
|
||||
if sessionID == 0 {
|
||||
sessData, err := tokenizer.Parse(event.Token)
|
||||
|
|
@ -84,8 +84,7 @@ func main() {
|
|||
}
|
||||
sessionID = sessData.ID
|
||||
}
|
||||
// TODO: send to ready-events topic. Otherwise it have to go through the events worker.
|
||||
producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(event.RawErrorEvent))
|
||||
producer.Produce(cfg.TopicAnalytics, sessionID, messages.Encode(event.IntegrationEvent))
|
||||
case err := <-manager.Errors:
|
||||
log.Printf("Integration error: %v\n", err)
|
||||
case i := <-manager.RequestDataUpdates:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func New() *Config {
|
|||
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
|
||||
TopicAnalytics: env.String("TOPIC_ANALYTICS"),
|
||||
CommitBatchTimeout: 15 * time.Second,
|
||||
BatchQueueLimit: env.Int("BATCH_QUEUE_LIMIT"),
|
||||
BatchSizeLimit: env.Int("BATCH_SIZE_LIMIT"),
|
||||
BatchQueueLimit: env.Int("DB_BATCH_QUEUE_LIMIT"),
|
||||
BatchSizeLimit: env.Int("DB_BATCH_SIZE_LIMIT"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,23 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
GroupEnder string
|
||||
LoggerTimeout int
|
||||
TopicRawWeb string
|
||||
ProducerTimeout int
|
||||
PartitionsNumber int
|
||||
Postgres string
|
||||
ProjectExpirationTimeoutMs int64
|
||||
GroupEnder string
|
||||
LoggerTimeout int
|
||||
TopicRawWeb string
|
||||
ProducerTimeout int
|
||||
PartitionsNumber int
|
||||
}
|
||||
|
||||
func New() *Config {
|
||||
return &Config{
|
||||
GroupEnder: env.String("GROUP_ENDER"),
|
||||
LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"),
|
||||
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
|
||||
ProducerTimeout: 2000,
|
||||
PartitionsNumber: env.Int("PARTITIONS_NUMBER"),
|
||||
Postgres: env.String("POSTGRES_STRING"),
|
||||
ProjectExpirationTimeoutMs: 1000 * 60 * 20,
|
||||
GroupEnder: env.String("GROUP_ENDER"),
|
||||
LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"),
|
||||
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
|
||||
ProducerTimeout: 2000,
|
||||
PartitionsNumber: env.Int("PARTITIONS_NUMBER"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ type Config struct {
|
|||
|
||||
func New() *Config {
|
||||
return &Config{
|
||||
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
|
||||
PostgresURI: env.String("POSTGRES_STRING"),
|
||||
TokenSecret: env.String("TOKEN_SECRET"),
|
||||
TopicAnalytics: env.String("TOPIC_ANALYTICS"),
|
||||
PostgresURI: env.String("POSTGRES_STRING"),
|
||||
TokenSecret: env.String("TOKEN_SECRET"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error {
|
|||
|
||||
// Web
|
||||
case *SessionStart:
|
||||
return mi.pg.InsertWebSessionStart(sessionID, m)
|
||||
return mi.pg.HandleWebSessionStart(sessionID, m)
|
||||
case *SessionEnd:
|
||||
return mi.pg.InsertWebSessionEnd(sessionID, m)
|
||||
return mi.pg.HandleWebSessionEnd(sessionID, m)
|
||||
case *UserID:
|
||||
return mi.pg.InsertWebUserID(sessionID, m)
|
||||
case *UserAnonymousID:
|
||||
|
|
@ -42,6 +42,15 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error {
|
|||
return mi.pg.InsertWebFetchEvent(sessionID, m)
|
||||
case *GraphQLEvent:
|
||||
return mi.pg.InsertWebGraphQLEvent(sessionID, m)
|
||||
case *IntegrationEvent:
|
||||
return mi.pg.InsertWebErrorEvent(sessionID, &ErrorEvent{
|
||||
MessageID: m.Meta().Index,
|
||||
Timestamp: m.Timestamp,
|
||||
Source: m.Source,
|
||||
Name: m.Name,
|
||||
Message: m.Message,
|
||||
Payload: m.Payload,
|
||||
})
|
||||
|
||||
// IOS
|
||||
case *IOSSessionStart:
|
||||
|
|
@ -66,15 +75,6 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error {
|
|||
case *IOSCrash:
|
||||
return mi.pg.InsertIOSCrash(sessionID, m)
|
||||
|
||||
case *RawErrorEvent:
|
||||
return mi.pg.InsertWebErrorEvent(sessionID, &ErrorEvent{
|
||||
MessageID: m.Meta().Index, // TODO: is it possible to catch panic here???
|
||||
Timestamp: m.Timestamp,
|
||||
Source: m.Source,
|
||||
Name: m.Name,
|
||||
Message: m.Message,
|
||||
Payload: m.Payload,
|
||||
})
|
||||
}
|
||||
return nil // "Not implemented"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
expTime := startTime.Add(time.Duration(p.MaxSessionDuration) * time.Millisecond)
|
||||
tokenData = &token.TokenData{ID: sessionID, ExpTime: expTime.UnixMilli()}
|
||||
|
||||
e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(&SessionStart{
|
||||
sessionStart := &SessionStart{
|
||||
Timestamp: req.Timestamp,
|
||||
ProjectID: uint64(p.ProjectID),
|
||||
TrackerVersion: req.TrackerVersion,
|
||||
|
|
@ -115,7 +115,13 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
UserDeviceMemorySize: req.DeviceMemory,
|
||||
UserDeviceHeapSize: req.JsHeapSizeLimit,
|
||||
UserID: req.UserID,
|
||||
}))
|
||||
}
|
||||
|
||||
// Save sessionStart to db
|
||||
e.services.Database.InsertWebSessionStart(sessionID, sessionStart)
|
||||
|
||||
// Send sessionStart message to kafka
|
||||
e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(sessionStart))
|
||||
}
|
||||
|
||||
ResponseWithJSON(w, &StartSessionResponse{
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ func (b *bugsnag) Request(c *client) error {
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "bugsnag",
|
||||
Timestamp: timestamp,
|
||||
Name: e.Exceptions[0].Message,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ type client struct {
|
|||
type SessionErrorEvent struct {
|
||||
SessionID uint64
|
||||
Token string
|
||||
*messages.RawErrorEvent
|
||||
*messages.IntegrationEvent
|
||||
}
|
||||
|
||||
type ClientMap map[string]*client
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func (cw *cloudwatch) Request(c *client) error {
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
//SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "cloudwatch",
|
||||
Timestamp: timestamp, // e.IngestionTime ??
|
||||
Name: name,
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ func (d *datadog) Request(c *client) error {
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
//SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "datadog",
|
||||
Timestamp: timestamp,
|
||||
Name: ddLog.Content.Attributes.Error.Message,
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func (es *elasticsearch) Request(c *client) error {
|
|||
//SessionID: sessionID,
|
||||
SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "elasticsearch",
|
||||
Timestamp: timestamp,
|
||||
Name: fmt.Sprintf("%v", docID),
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func (nr *newrelic) Request(c *client) error {
|
|||
c.setLastMessageTimestamp(e.Timestamp)
|
||||
c.evChan <- &SessionErrorEvent{
|
||||
Token: e.OpenReplaySessionToken,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "newrelic",
|
||||
Timestamp: e.Timestamp,
|
||||
Name: e.ErrorClass,
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ func (rb *rollbar) Request(c *client) error {
|
|||
c.setLastMessageTimestamp(timestamp)
|
||||
c.evChan <- &SessionErrorEvent{
|
||||
Token: e["body.message.openReplaySessionToken"],
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "rollbar",
|
||||
Timestamp: timestamp,
|
||||
Name: e["item.title"],
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ PageLoop:
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "sentry",
|
||||
Timestamp: timestamp,
|
||||
Name: e.Title,
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func (sd *stackdriver) Request(c *client) error {
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
//SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "stackdriver",
|
||||
Timestamp: timestamp,
|
||||
Name: e.InsertID, // not sure about that
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ func (sl *sumologic) Request(c *client) error {
|
|||
c.evChan <- &SessionErrorEvent{
|
||||
//SessionID: sessionID,
|
||||
Token: token,
|
||||
RawErrorEvent: &messages.RawErrorEvent{
|
||||
IntegrationEvent: &messages.IntegrationEvent{
|
||||
Source: "sumologic",
|
||||
Timestamp: e.Timestamp,
|
||||
Name: name,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type EndedSessionHandler func(sessionID uint64, timestamp int64) bool
|
|||
type session struct {
|
||||
lastTimestamp int64
|
||||
lastUpdate int64
|
||||
lastUserTime int64
|
||||
isEnded bool
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ func New(metrics *monitoring.Metrics, timeout int64, parts int) (*SessionEnder,
|
|||
}
|
||||
|
||||
// UpdateSession save timestamp for new sessions and update for existing sessions
|
||||
func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) {
|
||||
func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp, msgTimestamp int64) {
|
||||
localTS := time.Now().UnixMilli()
|
||||
currTS := timestamp
|
||||
if currTS == 0 {
|
||||
|
|
@ -62,11 +63,12 @@ func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) {
|
|||
sess, ok := se.sessions[sessionID]
|
||||
if !ok {
|
||||
se.sessions[sessionID] = &session{
|
||||
lastTimestamp: currTS, // timestamp from message broker
|
||||
lastUpdate: localTS, // local timestamp
|
||||
lastTimestamp: currTS, // timestamp from message broker
|
||||
lastUpdate: localTS, // local timestamp
|
||||
lastUserTime: msgTimestamp, // last timestamp from user's machine
|
||||
isEnded: false,
|
||||
}
|
||||
log.Printf("added new session: %d", sessionID)
|
||||
//log.Printf("added new session: %d", sessionID)
|
||||
se.activeSessions.Add(context.Background(), 1)
|
||||
se.totalSessions.Add(context.Background(), 1)
|
||||
return
|
||||
|
|
@ -74,6 +76,7 @@ func (se *SessionEnder) UpdateSession(sessionID uint64, timestamp int64) {
|
|||
if currTS > sess.lastTimestamp {
|
||||
sess.lastTimestamp = currTS
|
||||
sess.lastUpdate = localTS
|
||||
sess.lastUserTime = msgTimestamp
|
||||
sess.isEnded = false
|
||||
}
|
||||
}
|
||||
|
|
@ -86,10 +89,12 @@ func (se *SessionEnder) HandleEndedSessions(handler EndedSessionHandler) {
|
|||
if sess.isEnded || (se.timeCtrl.LastTimestamp(sessID)-sess.lastTimestamp > se.timeout) ||
|
||||
(currTime-sess.lastUpdate > se.timeout) {
|
||||
sess.isEnded = true
|
||||
if handler(sessID, sess.lastTimestamp) {
|
||||
if handler(sessID, sess.lastUserTime) {
|
||||
delete(se.sessions, sessID)
|
||||
se.activeSessions.Add(context.Background(), -1)
|
||||
removedSessions++
|
||||
} else {
|
||||
log.Printf("sessID: %d, userTime: %d", sessID, sess.lastUserTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
backend/pkg/db/cache/messages-common.go
vendored
|
|
@ -1,22 +1,25 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"log"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"time"
|
||||
// . "openreplay/backend/pkg/db/types"
|
||||
)
|
||||
|
||||
func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64) error {
|
||||
//duration, err := c.Conn.InsertSessionEnd(sessionID, timestamp)
|
||||
func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) error {
|
||||
_, err := c.Conn.InsertSessionEnd(sessionID, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PGCache) HandleSessionEnd(sessionID uint64) error {
|
||||
if err := c.Conn.HandleSessionEnd(sessionID); err != nil {
|
||||
log.Printf("can't handle session end: %s", err)
|
||||
}
|
||||
c.DeleteSession(sessionID)
|
||||
// session, err := c.GetSession(sessionID)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// session.Duration = &duration
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +48,12 @@ func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error {
|
|||
return nil
|
||||
}
|
||||
if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil {
|
||||
// Try to insert metadata after one minute
|
||||
time.AfterFunc(time.Minute, func() {
|
||||
if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil {
|
||||
log.Printf("metadata retry err: %s", err)
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
session.SetMetadata(keyNo, metadata.Value)
|
||||
|
|
|
|||
2
backend/pkg/db/cache/messages-ios.go
vendored
|
|
@ -32,7 +32,7 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er
|
|||
}
|
||||
|
||||
func (c *PGCache) InsertIOSSessionEnd(sessionID uint64, e *IOSSessionEnd) error {
|
||||
return c.insertSessionEnd(sessionID, e.Timestamp)
|
||||
return c.InsertSessionEnd(sessionID, e.Timestamp)
|
||||
}
|
||||
|
||||
func (c *PGCache) InsertIOSScreenEnter(sessionID uint64, screenEnter *IOSScreenEnter) error {
|
||||
|
|
|
|||
32
backend/pkg/db/cache/messages-web.go
vendored
|
|
@ -7,6 +7,30 @@ import (
|
|||
)
|
||||
|
||||
func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error {
|
||||
return c.Conn.InsertSessionStart(sessionID, &Session{
|
||||
SessionID: sessionID,
|
||||
Platform: "web",
|
||||
Timestamp: s.Timestamp,
|
||||
ProjectID: uint32(s.ProjectID),
|
||||
TrackerVersion: s.TrackerVersion,
|
||||
RevID: s.RevID,
|
||||
UserUUID: s.UserUUID,
|
||||
UserOS: s.UserOS,
|
||||
UserOSVersion: s.UserOSVersion,
|
||||
UserDevice: s.UserDevice,
|
||||
UserCountry: s.UserCountry,
|
||||
// web properties (TODO: unite different platform types)
|
||||
UserAgent: s.UserAgent,
|
||||
UserBrowser: s.UserBrowser,
|
||||
UserBrowserVersion: s.UserBrowserVersion,
|
||||
UserDeviceType: s.UserDeviceType,
|
||||
UserDeviceMemorySize: s.UserDeviceMemorySize,
|
||||
UserDeviceHeapSize: s.UserDeviceHeapSize,
|
||||
UserID: &s.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error {
|
||||
if c.sessions[sessionID] != nil {
|
||||
return errors.New("This session already in cache!")
|
||||
}
|
||||
|
|
@ -31,7 +55,7 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error
|
|||
UserDeviceHeapSize: s.UserDeviceHeapSize,
|
||||
UserID: &s.UserID,
|
||||
}
|
||||
if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil {
|
||||
if err := c.Conn.HandleSessionStart(sessionID, c.sessions[sessionID]); err != nil {
|
||||
c.sessions[sessionID] = nil
|
||||
return err
|
||||
}
|
||||
|
|
@ -39,7 +63,11 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error
|
|||
}
|
||||
|
||||
func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error {
|
||||
return c.insertSessionEnd(sessionID, e.Timestamp)
|
||||
return c.InsertSessionEnd(sessionID, e.Timestamp)
|
||||
}
|
||||
|
||||
func (c *PGCache) HandleWebSessionEnd(sessionID uint64, e *SessionEnd) error {
|
||||
return c.HandleSessionEnd(sessionID)
|
||||
}
|
||||
|
||||
func (c *PGCache) InsertWebErrorEvent(sessionID uint64, e *ErrorEvent) error {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package postgres
|
|||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
|
|
@ -14,24 +15,33 @@ func getTimeoutContext() context.Context {
|
|||
return ctx
|
||||
}
|
||||
|
||||
type batchItem struct {
|
||||
query string
|
||||
arguments []interface{}
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?)
|
||||
batches map[uint64]*pgx.Batch
|
||||
batchSizes map[uint64]int
|
||||
rawBatches map[uint64][]*batchItem
|
||||
batchQueueLimit int
|
||||
batchSizeLimit int
|
||||
}
|
||||
|
||||
func NewConn(url string) *Conn {
|
||||
func NewConn(url string, queueLimit, sizeLimit int) *Conn {
|
||||
c, err := pgxpool.Connect(context.Background(), url)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalln("pgxpool.Connect Error")
|
||||
}
|
||||
return &Conn{
|
||||
c: c,
|
||||
batches: make(map[uint64]*pgx.Batch),
|
||||
batchSizes: make(map[uint64]int),
|
||||
c: c,
|
||||
batches: make(map[uint64]*pgx.Batch),
|
||||
batchSizes: make(map[uint64]int),
|
||||
rawBatches: make(map[uint64][]*batchItem),
|
||||
batchQueueLimit: queueLimit,
|
||||
batchSizeLimit: sizeLimit,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +54,17 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{})
|
|||
batch, ok := conn.batches[sessionID]
|
||||
if !ok {
|
||||
conn.batches[sessionID] = &pgx.Batch{}
|
||||
conn.rawBatches[sessionID] = make([]*batchItem, 0)
|
||||
batch = conn.batches[sessionID]
|
||||
}
|
||||
batch.Queue(sql, args...)
|
||||
// Temp raw batch store
|
||||
raw := conn.rawBatches[sessionID]
|
||||
raw = append(raw, &batchItem{
|
||||
query: sql,
|
||||
arguments: args,
|
||||
})
|
||||
conn.rawBatches[sessionID] = raw
|
||||
}
|
||||
|
||||
func (conn *Conn) CommitBatches() {
|
||||
|
|
@ -56,12 +74,16 @@ func (conn *Conn) CommitBatches() {
|
|||
for i := 0; i < l; i++ {
|
||||
if ct, err := br.Exec(); err != nil {
|
||||
log.Printf("Error in PG batch (command tag %s, session: %d): %v \n", ct.String(), sessID, err)
|
||||
failedSql := conn.rawBatches[sessID][i]
|
||||
query := strings.ReplaceAll(failedSql.query, "\n", " ")
|
||||
log.Println("failed sql req:", query, failedSql.arguments)
|
||||
}
|
||||
}
|
||||
br.Close() // returns err
|
||||
}
|
||||
conn.batches = make(map[uint64]*pgx.Batch)
|
||||
conn.batchSizes = make(map[uint64]int)
|
||||
conn.rawBatches = make(map[uint64][]*batchItem)
|
||||
}
|
||||
|
||||
func (conn *Conn) updateBatchSize(sessionID uint64, reqSize int) {
|
||||
|
|
@ -83,6 +105,9 @@ func (conn *Conn) commitBatch(sessionID uint64) {
|
|||
for i := 0; i < l; i++ {
|
||||
if ct, err := br.Exec(); err != nil {
|
||||
log.Printf("Error in PG batch (command tag %s, session: %d): %v \n", ct.String(), sessionID, err)
|
||||
failedSql := conn.rawBatches[sessionID][i]
|
||||
query := strings.ReplaceAll(failedSql.query, "\n", " ")
|
||||
log.Println("failed sql req:", query, failedSql.arguments)
|
||||
}
|
||||
}
|
||||
br.Close()
|
||||
|
|
@ -90,6 +115,7 @@ func (conn *Conn) commitBatch(sessionID uint64) {
|
|||
// Clean batch info
|
||||
delete(conn.batches, sessionID)
|
||||
delete(conn.batchSizes, sessionID)
|
||||
delete(conn.rawBatches, sessionID)
|
||||
}
|
||||
|
||||
func (conn *Conn) query(sql string, args ...interface{}) (pgx.Rows, error) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package postgres
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"openreplay/backend/pkg/db/types"
|
||||
|
|
@ -31,14 +32,17 @@ func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value str
|
|||
FROM sessions
|
||||
WHERE session_id = $3
|
||||
) ON CONFLICT DO NOTHING`
|
||||
conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID)
|
||||
if err := conn.exec(sqlRequest, value, tp, sessionID); err != nil {
|
||||
log.Printf("can't insert autocomplete: %s", err)
|
||||
}
|
||||
//conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID)
|
||||
|
||||
// Record approximate message size
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8)
|
||||
//conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8)
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error {
|
||||
if err := conn.exec(`
|
||||
return conn.exec(`
|
||||
INSERT INTO sessions (
|
||||
session_id, project_id, start_ts,
|
||||
user_uuid, user_device, user_device_type, user_country,
|
||||
|
|
@ -66,9 +70,10 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error {
|
|||
s.Platform,
|
||||
s.UserAgent, s.UserBrowser, s.UserBrowserVersion, s.UserDeviceMemorySize, s.UserDeviceHeapSize,
|
||||
s.UserID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error {
|
||||
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS)
|
||||
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice)
|
||||
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry)
|
||||
|
|
@ -79,6 +84,20 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error {
|
|||
}
|
||||
|
||||
func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) {
|
||||
var dur uint64
|
||||
if err := conn.queryRow(`
|
||||
UPDATE sessions SET duration=$2 - start_ts
|
||||
WHERE session_id=$1
|
||||
RETURNING duration
|
||||
`,
|
||||
sessionID, timestamp,
|
||||
).Scan(&dur); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dur, nil
|
||||
}
|
||||
|
||||
func (conn *Conn) HandleSessionEnd(sessionID uint64) error {
|
||||
// TODO: search acceleration?
|
||||
sqlRequest := `
|
||||
UPDATE sessions
|
||||
|
|
@ -96,18 +115,7 @@ func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64,
|
|||
|
||||
// Record approximate message size
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+8)
|
||||
|
||||
var dur uint64
|
||||
if err := conn.queryRow(`
|
||||
UPDATE sessions SET duration=$2 - start_ts
|
||||
WHERE session_id=$1
|
||||
RETURNING duration
|
||||
`,
|
||||
sessionID, timestamp,
|
||||
).Scan(&dur); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dur, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error {
|
||||
|
|
@ -115,7 +123,7 @@ func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64
|
|||
INSERT INTO events_common.requests (
|
||||
session_id, timestamp, seq_index, url, duration, success
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6
|
||||
$1, $2, $3, left($4, 2700), $5, $6
|
||||
)`
|
||||
conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), url, duration, success)
|
||||
|
||||
|
|
@ -129,7 +137,7 @@ func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index ui
|
|||
INSERT INTO events_common.customs (
|
||||
session_id, timestamp, seq_index, name, payload
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
$1, $2, $3, left($4, 2700), $5
|
||||
)`
|
||||
conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), name, payload)
|
||||
|
||||
|
|
@ -222,7 +230,7 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag
|
|||
INSERT INTO events_common.customs
|
||||
(session_id, seq_index, timestamp, name, payload, level)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, 'error')
|
||||
($1, $2, $3, left($4, 2700), $5, 'error')
|
||||
`,
|
||||
sessionID, getSqIdx(e.MessageID), e.Timestamp, e.ContextString, e.Payload,
|
||||
); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"log"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/url"
|
||||
)
|
||||
|
|
@ -27,16 +28,25 @@ func (conn *Conn) InsertWebStatsPerformance(sessionID uint64, p *PerformanceTrac
|
|||
$10, $11, $12,
|
||||
$13, $14, $15
|
||||
)`
|
||||
conn.batchQueue(sessionID, sqlRequest,
|
||||
//conn.batchQueue(sessionID, sqlRequest,
|
||||
// sessionID, timestamp, timestamp, // ??? TODO: primary key by timestamp+session_id
|
||||
// p.MinFPS, p.AvgFPS, p.MaxFPS,
|
||||
// p.MinCPU, p.AvgCPU, p.MinCPU,
|
||||
// p.MinTotalJSHeapSize, p.AvgTotalJSHeapSize, p.MaxTotalJSHeapSize,
|
||||
// p.MinUsedJSHeapSize, p.AvgUsedJSHeapSize, p.MaxUsedJSHeapSize,
|
||||
//)
|
||||
if err := conn.exec(sqlRequest,
|
||||
sessionID, timestamp, timestamp, // ??? TODO: primary key by timestamp+session_id
|
||||
p.MinFPS, p.AvgFPS, p.MaxFPS,
|
||||
p.MinCPU, p.AvgCPU, p.MinCPU,
|
||||
p.MinTotalJSHeapSize, p.AvgTotalJSHeapSize, p.MaxTotalJSHeapSize,
|
||||
p.MinUsedJSHeapSize, p.AvgUsedJSHeapSize, p.MaxUsedJSHeapSize,
|
||||
)
|
||||
); err != nil {
|
||||
log.Printf("can't insert perf: %s", err)
|
||||
}
|
||||
|
||||
// Record approximate message size
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+8*15)
|
||||
//conn.updateBatchSize(sessionID, len(sqlRequest)+8*15)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +67,7 @@ func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent
|
|||
) VALUES (
|
||||
$1, $2, $3,
|
||||
$4,
|
||||
$5, $6, $7,
|
||||
left($5, 2700), $6, $7,
|
||||
$8, $9,
|
||||
NULLIF($10, '')::events.resource_method,
|
||||
NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), NULLIF($14, 0), NULLIF($15, 0)
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet
|
|||
duration, success
|
||||
) VALUES (
|
||||
$1, $2, $3,
|
||||
$4, $5, $6, $7,
|
||||
left($4, 2700), $5, $6, $7,
|
||||
$8, $9, $10::smallint, NULLIF($11, '')::http_method,
|
||||
$12, $13
|
||||
) ON CONFLICT DO NOTHING`
|
||||
|
|
@ -242,7 +242,7 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet
|
|||
|
||||
// Record approximate message size
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.URL)+len(host)+len(path)+len(query)+
|
||||
len(*request)+len(*response)+len(url.EnsureMethod(e.Method))+8*5+1)
|
||||
len(e.Request)+len(e.Response)+len(url.EnsureMethod(e.Method))+8*5+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *G
|
|||
request_body, response_body
|
||||
) VALUES (
|
||||
$1, $2, $3,
|
||||
$4,
|
||||
left($4, 2700),
|
||||
$5, $6
|
||||
) ON CONFLICT DO NOTHING`
|
||||
conn.batchQueue(sessionID, sqlRequest, sessionID, e.Timestamp, e.MessageID,
|
||||
|
|
@ -269,6 +269,6 @@ func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *G
|
|||
)
|
||||
|
||||
// Record approximate message size
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(*request)+len(*response)+8*3)
|
||||
conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(e.Variables)+len(e.Response)+8*3)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ReadBatchReader(reader io.Reader, messageHandler func(Message)) error {
|
||||
|
|
@ -16,15 +13,7 @@ func ReadBatchReader(reader io.Reader, messageHandler func(Message)) error {
|
|||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "Unknown message code:") {
|
||||
code := strings.TrimPrefix(err.Error(), "Unknown message code: ")
|
||||
msg, err = DecodeExtraMessage(code, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't decode msg: %s", err)
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, "Batch Message decoding error on message with index %v", index)
|
||||
}
|
||||
return errors.Wrapf(err, "Batch Message decoding error on message with index %v", index)
|
||||
}
|
||||
msg = transformDeprecated(msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,5 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
//"io"
|
||||
)
|
||||
|
||||
func Encode(msg Message) []byte {
|
||||
return msg.Encode()
|
||||
}
|
||||
|
||||
//
|
||||
// func EncodeList(msgs []Message) []byte {
|
||||
|
||||
// }
|
||||
//
|
||||
|
||||
// func Decode(b []byte) (Message, error) {
|
||||
// return ReadMessage(bytes.NewReader(b))
|
||||
// }
|
||||
|
||||
// func DecodeEach(b []byte, callback func(Message)) error {
|
||||
// var err error
|
||||
// reader := bytes.NewReader(b)
|
||||
// for {
|
||||
// msg, err := ReadMessage(reader)
|
||||
// if err != nil {
|
||||
// break
|
||||
// }
|
||||
// callback(msg)
|
||||
// }
|
||||
// if err == io.EOF {
|
||||
// return nil
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
|
||||
func GetMessageTypeID(b []byte) (uint64, error) {
|
||||
reader := bytes.NewReader(b)
|
||||
return ReadUint(reader)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ func (msg *JSException) TypeID() int {
|
|||
return 25
|
||||
}
|
||||
|
||||
type RawErrorEvent struct {
|
||||
type IntegrationEvent struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
Source string
|
||||
|
|
@ -580,7 +580,7 @@ type RawErrorEvent struct {
|
|||
Payload string
|
||||
}
|
||||
|
||||
func (msg *RawErrorEvent) Encode() []byte {
|
||||
func (msg *IntegrationEvent) Encode() []byte {
|
||||
buf := make([]byte, 51+len(msg.Source)+len(msg.Name)+len(msg.Message)+len(msg.Payload))
|
||||
buf[0] = 26
|
||||
p := 1
|
||||
|
|
@ -592,7 +592,7 @@ func (msg *RawErrorEvent) Encode() []byte {
|
|||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *RawErrorEvent) TypeID() int {
|
||||
func (msg *IntegrationEvent) TypeID() int {
|
||||
return 26
|
||||
}
|
||||
|
||||
|
|
@ -1396,7 +1396,7 @@ type IssueEvent struct {
|
|||
Type string
|
||||
ContextString string
|
||||
Context string
|
||||
Payload string // TODO: check, maybe it's better to use empty interface here
|
||||
Payload string
|
||||
}
|
||||
|
||||
func (msg *IssueEvent) Encode() []byte {
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
|
|||
return msg, nil
|
||||
|
||||
case 26:
|
||||
msg := &RawErrorEvent{}
|
||||
msg := &IntegrationEvent{}
|
||||
if msg.Timestamp, err = ReadUint(reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type SessionFinished struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
}
|
||||
|
||||
func (msg *SessionFinished) Encode() []byte {
|
||||
buf := make([]byte, 11)
|
||||
buf[0] = 127
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *SessionFinished) TypeID() int {
|
||||
return 127
|
||||
}
|
||||
|
||||
func DecodeExtraMessage(code string, reader io.Reader) (Message, error) {
|
||||
var err error
|
||||
if code != "127" {
|
||||
return nil, fmt.Errorf("unknown message code: %s", code)
|
||||
}
|
||||
trigger := &SessionFinished{}
|
||||
if trigger.Timestamp, err = ReadUint(reader); err != nil {
|
||||
return nil, fmt.Errorf("can't read message timestamp: %s", err)
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
|
@ -49,11 +49,16 @@ func (b *builder) handleMessage(message Message, messageID uint64) {
|
|||
}
|
||||
timestamp := GetTimestamp(message)
|
||||
if timestamp == 0 {
|
||||
log.Printf("skip message with empty timestamp, sessID: %d, msgID: %d, msgType: %d", b.sessionID, messageID, message.TypeID())
|
||||
switch message.(type) {
|
||||
case *SessionEnd, *IssueEvent, *PerformanceTrackAggr:
|
||||
break
|
||||
default:
|
||||
log.Printf("skip message with empty timestamp, sessID: %d, msgID: %d, msgType: %d", b.sessionID, messageID, message.TypeID())
|
||||
}
|
||||
return
|
||||
}
|
||||
if timestamp < b.timestamp {
|
||||
log.Printf("skip message with wrong timestamp, sessID: %d, msgID: %d, type: %d, msgTS: %d, lastTS: %d", b.sessionID, messageID, message.TypeID(), timestamp, b.timestamp)
|
||||
//log.Printf("skip message with wrong timestamp, sessID: %d, msgID: %d, type: %d, msgTS: %d, lastTS: %d", b.sessionID, messageID, message.TypeID(), timestamp, b.timestamp)
|
||||
} else {
|
||||
b.timestamp = timestamp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ func DiscardURLQuery(url string) string {
|
|||
}
|
||||
|
||||
func GetURLParts(rawURL string) (string, string, string, error) {
|
||||
rawURL = strings.Replace(rawURL, "\t", "", -1) // Other chars?
|
||||
u, err := _url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
|
|
|
|||
4
ee/api/.gitignore
vendored
|
|
@ -247,7 +247,6 @@ Pipfile
|
|||
/db_changes.sql
|
||||
/Dockerfile.bundle
|
||||
/entrypoint.bundle.sh
|
||||
#/entrypoint.sh
|
||||
/chalicelib/core/heatmaps.py
|
||||
/routers/subs/insights.py
|
||||
/schemas.py
|
||||
|
|
@ -258,5 +257,4 @@ Pipfile
|
|||
/build_alerts.sh
|
||||
/routers/subs/metrics.py
|
||||
/routers/subs/v1_api.py
|
||||
/chalicelib/core/dashboards.py
|
||||
entrypoint.sh
|
||||
/chalicelib/core/dashboards.py
|
||||
6
ee/api/entrypoint.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
bash env_vars.sh
|
||||
cd sourcemap-reader
|
||||
nohup npm start &> /tmp/sourcemap-reader.log &
|
||||
cd ..
|
||||
uvicorn app:app --host 0.0.0.0 --reload --proxy-headers
|
||||
27
ee/api/env_vars.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ -z "${ENV_CONFIG_OVERRIDE_PATH}" ]]; then
|
||||
echo 'no env-override'
|
||||
else
|
||||
override=$ENV_CONFIG_OVERRIDE_PATH
|
||||
if [ -f "$override" ]; then
|
||||
# to remove endOfLine form sed result
|
||||
echo "" >> $override
|
||||
sed 's/=.*//;/^$/d' $override > .replacements
|
||||
|
||||
# to remove all defined os-env-vars
|
||||
cat .replacements | while read line
|
||||
do
|
||||
unset $line
|
||||
done
|
||||
rm .replacements
|
||||
|
||||
# to merge predefined .env with the override.env
|
||||
cp .env .env.d
|
||||
sort -u -t '=' -k 1,1 $override .env.d > .env
|
||||
rm .env.d
|
||||
else
|
||||
echo "$override does not exist."
|
||||
fi
|
||||
|
||||
fi
|
||||
7
frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.cache
|
||||
**/build.sh
|
||||
**/build_*.sh
|
||||
**/*deploy.sh
|
||||
18
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from node:14-stretch-slim AS builder
|
||||
workdir /work
|
||||
COPY . .
|
||||
RUN cp .env.sample .env
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:alpine as cicd
|
||||
LABEL maintainer=Rajesh<rajesh@openreplay.com>
|
||||
COPY public /var/www/openreplay
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
|
||||
# Default step in docker build
|
||||
FROM nginx:alpine
|
||||
LABEL maintainer=Rajesh<rajesh@openreplay.com>
|
||||
COPY --from=builder /work/public /var/www/openreplay
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
OpenReplay prototype UI
|
||||
|
||||
On new icon addition:
|
||||
`npm run generate:icons`
|
||||
`yarn gen:icons`
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
const webpack = require('webpack');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
// const CircularDependencyPlugin = require('circular-dependency-plugin')
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); //TODO: replace Moment with date-fns ??
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const alias = require('./path-alias');
|
||||
const environments = require('./env');
|
||||
|
||||
|
||||
const DIST_DIR = 'public';
|
||||
|
||||
const GLOBAL_STYLES_DIR = 'app/styles';
|
||||
|
||||
const cssEntrypoints = [
|
||||
'codemirror/lib/codemirror.css',
|
||||
'codemirror/theme/yeti.css',
|
||||
'codemirror/addon/lint/lint.css',
|
||||
'react-daterange-picker/dist/css/react-calendar.css',
|
||||
'react-datepicker/dist/react-datepicker.css',
|
||||
'rc-time-picker/assets/index.css',
|
||||
];
|
||||
|
||||
const babelLoader = {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[ '@babel/preset-env', { // probably, use dynamic imports for polifills in future
|
||||
"targets": "> 4%, not dead",
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-flow", //TODO: remove, use ts
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-syntax-bigint",
|
||||
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
|
||||
[ '@babel/plugin-proposal-decorators', { legacy: true } ],
|
||||
[ '@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
[ '@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
// 'recharts'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const cssFiles = fs.readdirSync(GLOBAL_STYLES_DIR, { withFileTypes: true });
|
||||
cssFiles.forEach(file => {
|
||||
if (/.css$/.test(file.name)) {
|
||||
const pathFullName = path.join(__dirname, GLOBAL_STYLES_DIR, file.name);
|
||||
cssEntrypoints.push(pathFullName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function prepareEnv(env) {
|
||||
const pEnv = {};
|
||||
Object.keys(env).forEach(key => {
|
||||
pEnv[ `window.ENV.${ key }` ] = typeof env[ key ] === 'function' ? env[ key ]() : JSON.stringify(env[ key ]);
|
||||
});
|
||||
return pEnv;
|
||||
}
|
||||
|
||||
module.exports = (envName = 'local') => {
|
||||
const env = environments[ envName ];
|
||||
const cssFileLoader = {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
hmr: !env.PRODUCTION,
|
||||
},
|
||||
}
|
||||
return {
|
||||
// Polyfill only for async (TODO)
|
||||
entry: [ './app/initialize.js' ].concat(cssEntrypoints),
|
||||
output: {
|
||||
path: path.join(__dirname, DIST_DIR),
|
||||
filename: 'app-[contenthash:7].js',
|
||||
publicPath: '/',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
'React': 'react' // back code compatability
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
path: path.join(__dirname, DIST_DIR),
|
||||
filename: 'app-[contenthash:7].css'
|
||||
}),
|
||||
new CopyWebpackPlugin([ 'app/assets' ]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'app/assets/index.html'
|
||||
}),
|
||||
new MomentLocalesPlugin(),
|
||||
new webpack.DefinePlugin(prepareEnv(env)),
|
||||
// new BundleAnalyzerPlugin({ analyzerMode: 'static'}),
|
||||
// new CircularDependencyPlugin({
|
||||
// // exclude detection of files based on a RegExp
|
||||
// exclude: /node_modules/,
|
||||
// // add errors to webpack instead of warnings
|
||||
// failOnError: true,
|
||||
// // allow import cycles that include an asyncronous import,
|
||||
// // e.g. via import(/* webpackMode: "weak" */ './file.js')
|
||||
// allowAsyncCycles: false,
|
||||
// // set the current working directory for displaying module paths
|
||||
// cwd: process.cwd(),
|
||||
// })
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
// global and module css separation. TODO more beautyfull
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: [ path.join(__dirname, "app/components"), path.join(__dirname, "app/player") ],
|
||||
use: [
|
||||
cssFileLoader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]_[local]_[hash:base64:7]'
|
||||
},
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: [ path.join(__dirname, "node_modules"), path.join(__dirname, "app/styles") ],
|
||||
use: [
|
||||
cssFileLoader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: [ path.join(__dirname, "app"), path.join(__dirname, ".storybook") ],
|
||||
use: babelLoader,
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
include: path.join(__dirname, "app"),
|
||||
use: [ 'ts-loader' ]
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias,
|
||||
extensions: ['.js', '.json', '.ts', '.tsx' ],
|
||||
},
|
||||
mode: env.PRODUCTION ? 'production' : 'development',
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, DIST_DIR),
|
||||
//compress: true,
|
||||
port: 3333,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
stats: 'errors-only',
|
||||
devtool: env.SOURCEMAP && 'source-map'
|
||||
};
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ class Router extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.props.setSessionPath(prevProps.location.pathname)
|
||||
this.props.setSessionPath(prevProps.location)
|
||||
if (prevProps.email !== this.props.email && !this.props.email) {
|
||||
this.props.fetchTenants();
|
||||
}
|
||||
|
|
|
|||
1
frontend/app/assets/integrations/assist.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#d7e2e2;}.cls-3{fill:#9da0a0;}.cls-4{fill:#fab29a;}.cls-5{fill:#222;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M114,0H6A6,6,0,0,0,0,6V114a6,6,0,0,0,6,6H114a6,6,0,0,0,6-6V6A6,6,0,0,0,114,0Z"/><path class="cls-2" d="M28,108.75H91a8,8,0,0,0,8-8v-64a8,8,0,0,0-8-8H55.67L35,12V28.81H28a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8Z"/><path class="cls-3" d="M54.11,79.63H64.89A13.25,13.25,0,0,1,78.15,92.88v7.2H40.85v-7.2A13.25,13.25,0,0,1,54.11,79.63Z"/><path class="cls-4" d="M46.18,53.82H72.82V66.3a13.32,13.32,0,1,1-26.64,0Z"/><path class="cls-5" d="M76.15,55v6.93a65,65,0,0,1-22.58-4.94,14.93,14.93,0,0,1-10.72,5V55a16.67,16.67,0,0,1,33.33,0Z"/><path d="M59.67,41.83A13.55,13.55,0,0,0,46.11,55.39V58.1h2.71a2.72,2.72,0,0,1,1.92.8,2.75,2.75,0,0,1,.79,1.91V69a2.75,2.75,0,0,1-.79,1.92,2.71,2.71,0,0,1-1.92.79H46.11A2.71,2.71,0,0,1,43.39,69V55.39a16.23,16.23,0,0,1,4.77-11.5,16.26,16.26,0,0,1,23,0,16.23,16.23,0,0,1,4.77,11.5V71.66a6.78,6.78,0,0,1-6.78,6.78H63.37A2.68,2.68,0,0,1,61,79.8H58.31a2.72,2.72,0,0,1-1.92-.8,2.67,2.67,0,0,1-.79-1.91,2.71,2.71,0,0,1,2.71-2.72H61a2.7,2.7,0,0,1,1.36.37,2.76,2.76,0,0,1,1,1h5.79a4.08,4.08,0,0,0,4.07-4.07H70.51a2.67,2.67,0,0,1-1.91-.79A2.72,2.72,0,0,1,67.8,69V60.81a2.71,2.71,0,0,1,.8-1.91,2.68,2.68,0,0,1,1.91-.8h2.72V55.39a13.61,13.61,0,0,0-4-9.59,13.44,13.44,0,0,0-4.39-2.94,13.61,13.61,0,0,0-5.19-1Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
frontend/app/assets/integrations/bugsnag.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="2500" height="1719" viewBox="0 0 256 176" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M57.838 170.017c.151 1.663-.051 3.789-.14 5.436h56.864c.053-1.654.091-3.311.091-4.974 0-39.942-15.768-76.266-44.011-104.51C56.704 52.032 40.885 41.31 23.246 33.898L0 86.328c33.989 15.82 54.211 43.783 57.838 83.689zm69.197-1.644c.108 2.371-.062 4.732-.167 7.08h58.177c.077-2.355.13-4.714.13-7.08 0-28.826-5.66-56.82-16.82-83.207-10.767-25.456-26.169-48.306-45.778-67.915a216.421 216.421 0 0 0-15.686-14.218l-37.68 44.315c37.293 33.313 55.304 65.858 57.824 121.025zM235.263 64.39C226.595 41.785 213.935 19.521 198.727 0l-46.95 34.442c27.495 35.099 44.442 79.71 46.058 127.612.152 4.502-.164 8.969-.457 13.399h58.252c.226-4.448.447-8.916.344-13.399-.805-34.945-8.23-65.12-20.71-97.665z" fill="#3676A1"/></svg>
|
||||
|
After Width: | Height: | Size: 835 B |
1
frontend/app/assets/integrations/cloudwatch.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="3.62 8.78 64 64"><path d="M35.605 72.78L63.9 52.803l-28.266-4.13-28.296 4.16z" fill="#b7ca9d"/><path d="M12.986 21.167v31.18l4.797 1.427L29.017 36.56 17.783 19.315z" fill="#4c622c"/><path d="M34.694 23.14v27.72l-16.9 2.975v-34.52z" fill="#759c3f"/><path d="M46.686 33.98l-19.52 22.68-6.62-2.004V12.1l6.62-3.3z" fill="#4c622c"/><path d="M49.753 17.585v33.822L27.165 56.7V8.78z" fill="#759c3f"/><path d="M35.605 72.78v-9.928L7.34 52.833v5.8z" fill="#4c622c"/><path d="M63.9 52.803v5.83L35.605 72.78v-9.928z" fill="#759c3f"/><path d="M35.605 59.178l20.342-16h-6.86l-19.522 1.336z" fill="#b7ca9d"/><path d="M29.563 44.514v12.842l6.042 1.822V45.152z" fill="#4c622c"/><path d="M55.946 53.076v-9.898L35.604 45.15v14.027z" fill="#759c3f"/></svg>
|
||||
|
After Width: | Height: | Size: 786 B |
39
frontend/app/assets/integrations/datadog.svg
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve">
|
||||
<path id="Bits" fill-rule="evenodd" clip-rule="evenodd" fill="#774AA4" d="M350.2,268.3l-27.3-18.1l-22.8,38.1l-26.5-7.8
|
||||
l-23.3,35.7l1.2,11.2l126.7-23.4l-7.4-79.4L350.2,268.3z M232,234l20.3-2.8c3.3,1.5,5.6,2,9.5,3.1c6.1,1.6,13.3,3.1,23.8-2.2
|
||||
c2.5-1.2,7.6-5.9,9.6-8.6l83.3-15.2l8.5,103.2l-142.7,25.8L232,234z M386.7,196.8l-8.2,1.6L362.7,34.7L93.5,66l33.2,270l31.5-4.6
|
||||
c-2.5-3.6-6.4-8-13.1-13.5c-9.3-7.7-6-20.9-0.5-29.2c7.2-14,44.6-31.8,42.4-54.2c-0.8-8.1-2-18.7-9.6-26c-0.3,3,0.2,5.9,0.2,5.9
|
||||
s-3.1-4-4.6-9.4c-1.5-2.1-2.7-2.7-4.4-5.5c-1.2,3.2-1,6.9-1,6.9s-2.5-6-2.9-11.1c-1.5,2.3-1.9,6.6-1.9,6.6s-3.3-9.5-2.5-14.6
|
||||
c-1.5-4.4-6-13.2-4.7-33.2c8.2,5.8,26.3,4.4,33.4-6c2.3-3.5,3.9-12.9-1.2-31.4c-3.3-11.9-11.4-29.6-14.6-36.4l-0.4,0.3
|
||||
c1.7,5.4,5.1,16.8,6.4,22.3c4,16.7,5.1,22.5,3.2,30.2c-1.6,6.7-5.4,11.1-15.1,16c-9.7,4.9-22.6-7-23.4-7.7
|
||||
c-9.4-7.5-16.7-19.8-17.5-25.8c-0.8-6.5,3.8-10.5,6.1-15.8c-3.3,1-7,2.6-7,2.6s4.4-4.6,9.9-8.6c2.3-1.5,3.6-2.5,6-4.4
|
||||
c-3.4-0.1-6.2,0-6.2,0s5.7-3.1,11.7-5.4c-4.4-0.2-8.5,0-8.5,0s12.8-5.7,22.9-10c7-2.9,13.8-2,17.6,3.5c5,7.3,10.3,11.2,21.4,13.6
|
||||
c6.9-3,8.9-4.6,17.5-7c7.6-8.4,13.5-9.4,13.5-9.4s-3,2.7-3.7,7c4.3-3.4,9-6.2,9-6.2s-1.8,2.3-3.5,5.8l0.4,0.6c5-3,10.9-5.4,10.9-5.4
|
||||
s-1.7,2.1-3.7,4.9c3.8,0,11.5,0.2,14.4,0.5c17.6,0.4,21.2-18.8,28-21.2c8.4-3,12.2-4.9,26.6,9.3c12.3,12.2,22,33.9,17.2,38.8
|
||||
c-4,4-11.9-1.6-20.7-12.6c-4.6-5.8-8.1-12.7-9.8-21.4c-1.4-7.4-6.8-11.6-6.8-11.6s3.1,7,3.1,13.2c0,3.4,0.4,16,5.8,23
|
||||
c-0.5,1-0.8,5.1-1.4,5.9c-6.3-7.6-19.7-13-21.9-14.6c7.4,6.1,24.5,20.1,31.1,33.6c6.2,12.7,2.5,24.4,5.7,27.4
|
||||
c0.9,0.9,13.3,16.4,15.7,24.2c4.2,13.6,0.2,27.9-5.2,36.8l-15.3,2.4c-2.2-0.6-3.7-0.9-5.7-2.1c1.1-2,3.3-6.8,3.3-7.9l-0.9-1.5
|
||||
c-4.7,6.7-12.7,13.3-19.3,17.1c-8.7,4.9-18.6,4.2-25.1,2.1c-18.4-5.7-35.9-18.2-40.1-21.5c0,0-0.1,2.6,0.7,3.2
|
||||
c4.6,5.3,15.3,14.8,25.6,21.4l-21.9,2.4l10.4,81c-4.6,0.7-5.3,1-10.3,1.7c-4.4-15.7-12.9-26-22.2-32c-8.2-5.3-19.5-6.5-30.3-4.3
|
||||
l-0.7,0.8c7.5-0.8,16.4,0.3,25.5,6.1c8.9,5.7,16.1,20.3,18.8,29.1c3.4,11.3,5.7,23.3-3.4,36.1c-6.5,9.1-25.5,14.1-40.8,3.2
|
||||
c4.1,6.6,9.6,12,17.1,13c11.1,1.5,21.6-0.4,28.8-7.9c6.2-6.4,9.4-19.7,8.6-33.7l9.8-1.4l3.5,25.2l161.6-19.5L386.7,196.8z
|
||||
M288.4,128.5c-0.5,1-1.2,1.7-0.1,5.1l0.1,0.2l0.2,0.4l0.4,1c1.9,3.9,4,7.6,7.5,9.5c0.9-0.2,1.9-0.3,2.8-0.3
|
||||
c3.3-0.1,5.4,0.4,6.7,1.1c0.1-0.7,0.1-1.6,0.1-3.1c-0.3-5,1-13.5-8.6-17.9c-3.6-1.7-8.7-1.2-10.3,0.9c0.3,0,0.6,0.1,0.8,0.2
|
||||
C290.6,126.6,288.8,127.5,288.4,128.5 M315.2,175c-1.3-0.7-7.1-0.4-11.2,0.1c-7.8,0.9-16.3,3.7-18.2,5.1c-3.4,2.6-1.8,7.2,0.7,9
|
||||
c7,5.2,13.1,8.7,19.6,7.9c4-0.5,7.5-6.8,9.9-12.5C317.6,180.7,317.6,176.4,315.2,175 M245.8,134.7c2.2-2.1-11-4.9-21.3,2.1
|
||||
c-7.6,5.2-7.8,16.3-0.6,22.6c0.7,0.6,1.3,1.1,1.9,1.4c2.1-1,4.5-2,7.3-2.9c4.7-1.5,8.6-2.3,11.8-2.7c1.5-1.7,3.3-4.7,2.9-10.2
|
||||
C247.2,137.6,241.6,138.7,245.8,134.7"/>
|
||||
<path id="Text" fill-rule="evenodd" clip-rule="evenodd" fill="#774AA4" d="M69.9,435.7H43.7v-60.4h26.2c18.9,0,28.4,9.5,28.4,28.6
|
||||
C98.3,425.1,88.9,435.7,69.9,435.7 M54.9,426h13.3c12.6,0,18.8-7.4,18.8-22.1c0-12.6-6.3-18.9-18.8-18.9H54.9V426z M110.1,435.7
|
||||
H98.6l25.7-60.4h12.1l26.3,60.4h-12.1l-7.6-16.5h-19.4l3.9-9.7h12.6l-9.9-22.7L110.1,435.7z M156.2,375.3h45.9v9.7h-17.4v50.7h-11.2
|
||||
V385h-17.4V375.3z M207.9,435.7h-11.5l25.7-60.4h12.1l26.3,60.4h-12.1l-7.6-16.5h-19.4l3.8-9.7h12.6l-9.9-22.7L207.9,435.7z
|
||||
M294.1,435.7h-26.2v-60.4h26.2c18.9,0,28.4,9.5,28.4,28.6C322.5,425.1,313.1,435.7,294.1,435.7 M279.1,426h13.3
|
||||
c12.6,0,18.8-7.4,18.8-22.1c0-12.6-6.3-18.9-18.8-18.9h-13.3V426z M330.1,405.6c0-20.5,10.1-30.7,30.4-30.7
|
||||
c20,0,29.9,10.2,29.9,30.7c0,20.4-10,30.6-29.9,30.6C341.2,436.1,331,426,330.1,405.6 M360.5,426.4c12.2,0,18.3-7,18.3-21.1
|
||||
c0-13.8-6.1-20.8-18.3-20.8c-12.5,0-18.8,6.9-18.8,20.8C341.8,419.4,348,426.4,360.5,426.4 M437.3,411.3v14.1c-2.6,0.7-4.9,1-6.9,1
|
||||
c-13.7,0-20.6-7.2-20.6-21.8c0-13.4,7.3-20.1,21.8-20.1c6.1,0,11.7,1.1,16.9,3.4v-10.1c-5.2-2-11.1-3-17.8-3
|
||||
c-21.7,0-32.6,9.9-32.6,29.8c0,21,10.7,31.5,32.1,31.5c7.4,0,13.5-1.1,18.3-3.2v-31.6h-18.1l-3.8,9.9H437.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/app/assets/integrations/elasticsearch.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M255.96 134.393c0-21.521-13.373-40.117-33.223-47.43a75.239 75.239 0 0 0 1.253-13.791c0-39.909-32.386-72.295-72.295-72.295-23.193 0-44.923 11.074-58.505 30.088-6.686-5.224-14.835-7.94-23.402-7.94-21.104 0-38.446 17.133-38.446 38.446 0 4.597.836 9.194 2.298 13.373C13.582 81.739 0 100.962 0 122.274c0 21.522 13.373 40.327 33.431 47.64-.835 4.388-1.253 8.985-1.253 13.79 0 39.7 32.386 72.087 72.086 72.087 23.402 0 44.924-11.283 58.505-30.088 6.686 5.223 15.044 8.149 23.611 8.149 21.104 0 38.446-17.134 38.446-38.446 0-4.597-.836-9.194-2.298-13.373 19.64-7.104 33.431-26.327 33.431-47.64z" fill="#FFF"/><path d="M100.085 110.364l57.043 26.119 57.669-50.565a64.312 64.312 0 0 0 1.253-12.746c0-35.52-28.834-64.355-64.355-64.355-21.313 0-41.162 10.447-53.072 27.998l-9.612 49.73 11.074 23.82z" fill="#F4BD19"/><path d="M40.953 170.75c-.835 4.179-1.253 8.567-1.253 12.955 0 35.52 29.043 64.564 64.564 64.564 21.522 0 41.372-10.656 53.49-28.208l9.403-49.729-12.746-24.238-57.251-26.118-56.207 50.774z" fill="#3CBEB1"/><path d="M40.536 71.918l39.073 9.194 8.775-44.506c-5.432-4.179-11.91-6.268-18.805-6.268-16.925 0-30.924 13.79-30.924 30.924 0 3.552.627 7.313 1.88 10.656z" fill="#E9478C"/><path d="M37.192 81.32c-17.551 5.642-29.67 22.567-29.67 40.954 0 17.97 11.074 34.059 27.79 40.327l54.953-49.73-10.03-21.52-43.043-10.03z" fill="#2C458F"/><path d="M167.784 219.852c5.432 4.18 11.91 6.478 18.596 6.478 16.925 0 30.924-13.79 30.924-30.924 0-3.761-.627-7.314-1.88-10.657l-39.073-9.193-8.567 44.296z" fill="#95C63D"/><path d="M175.724 165.317l43.043 10.03c17.551-5.85 29.67-22.566 29.67-40.954 0-17.97-11.074-33.849-27.79-40.326l-56.415 49.311 11.492 21.94z" fill="#176655"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/app/assets/integrations/github.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/app/assets/integrations/graphql.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" height="120" rx="6.01" width="120"/><path d="m66.84 29.17 16.44 9.5a7 7 0 0 1 1.62-1.28 7.13 7.13 0 1 1 7.1 12.35 7 7 0 0 1 -1.9.76v19a7.13 7.13 0 1 1 -5.25 13.11 6.85 6.85 0 0 1 -1.76-1.43l-16.32 9.44a7.26 7.26 0 0 1 .36 2.25 7.13 7.13 0 1 1 -14-2l-16.42-9.51a7 7 0 0 1 -1.59 1.25 7.13 7.13 0 1 1 -5.25-13.11v-19a7 7 0 0 1 -1.9-.76 7.13 7.13 0 1 1 7.13-12.35 7 7 0 0 1 1.61 1.27l16.45-9.5a7.22 7.22 0 0 1 -.29-2 7.13 7.13 0 1 1 14.26 0 7.24 7.24 0 0 1 -.29 2.01zm-1.72 2.92 16.49 9.52a7.12 7.12 0 0 0 5.13 8.87v19l-.27.07-21.54-37.27zm-10.05.2-21.53 37.3-.28-.08v-19a7.06 7.06 0 0 0 4.45-3.35 7.15 7.15 0 0 0 .68-5.53l16.48-9.51a1.89 1.89 0 0 1 .2.17zm6.93 1.71 21.52 37.27a7.09 7.09 0 0 0 -1.25 1.6 6.84 6.84 0 0 0 -.75 1.88h-43a6.84 6.84 0 0 0 -.75-1.88 7.26 7.26 0 0 0 -1.25-1.59l21.48-37.28a7.21 7.21 0 0 0 4 0zm3.28 54.09 16.38-9.46c0-.16-.09-.31-.13-.47h-43c0 .09 0 .19-.08.28l16.46 9.5a7.14 7.14 0 0 1 10.41.15z" fill="#e535ab" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
1
frontend/app/assets/integrations/jira-text.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg id="Logos" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 254.6 150"><defs><style>.cls-1{fill:#253858;}.cls-2{fill:#2684ff;}.cls-3{fill:url(#New_Gradient_Swatch_8);}.cls-4{fill:url(#New_Gradient_Swatch_8-2);}</style><linearGradient id="New_Gradient_Swatch_8" x1="55.29" y1="50.91" x2="40.58" y2="66.08" gradientUnits="userSpaceOnUse"><stop offset="0.18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="New_Gradient_Swatch_8-2" x1="39.15" y1="68.24" x2="22.14" y2="84.79" xlink:href="#New_Gradient_Swatch_8"/></defs><title>jira-logo-gradient-blue</title><path class="cls-1" d="M109.87,42.62h7.92V82.84c0,10.64-4.66,18-15.57,18a28.19,28.19,0,0,1-9.51-1.5V91.64a22.4,22.4,0,0,0,8.19,1.49c6.69,0,9-4,9-9.77Z"/><path class="cls-1" d="M132.23,39.54a4.94,4.94,0,0,1,5.27,5.28,5.28,5.28,0,1,1-10.55,0A4.94,4.94,0,0,1,132.23,39.54Zm-3.88,16.9h7.57v44h-7.57Z"/><path class="cls-1" d="M154.75,100.44h-7.39v-44h7.39v7.74c2.55-5.19,6.95-8.89,15.58-8.36v7.39c-9.68-1-15.58,1.94-15.58,11.27Z"/><path class="cls-1" d="M205.79,92.52c-2.81,5.8-8.09,8.8-14.87,8.8-11.7,0-17.6-9.95-17.6-22.88,0-12.41,6.16-22.88,18.48-22.88,6.42,0,11.35,2.9,14,8.62V56.44h7.57v44h-7.57Zm-12.85,1.76c6.78,0,12.85-4.32,12.85-14.08V76.68c0-9.77-5.54-14.08-12-14.08-8.53,0-12.93,5.63-12.93,15.84C180.89,89,185.11,94.28,192.94,94.28Z"/><path class="cls-2" d="M70.14,33.62H35.56A15.61,15.61,0,0,0,51.17,49.23h6.36v6.15A15.6,15.6,0,0,0,73.14,71V36.62A3,3,0,0,0,70.14,33.62Z"/><path class="cls-3" d="M53,50.85H18.44A15.61,15.61,0,0,0,34.05,66.46h6.37V72.6A15.61,15.61,0,0,0,56,88.21V53.85A3,3,0,0,0,53,50.85Z"/><path class="cls-4" d="M35.91,68.08H1.33a15.6,15.6,0,0,0,15.6,15.61H23.3v6.14a15.61,15.61,0,0,0,15.61,15.61V71.08A3,3,0,0,0,35.91,68.08Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
23
frontend/app/assets/integrations/jira.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 74 76" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>Jira Software-blue</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="67.6800148%" y1="40.3276956%" x2="40.820818%" y2="81.6596195%" id="linearGradient-1">
|
||||
<stop stop-color="#777" offset="18%"></stop>
|
||||
<stop stop-color="#999" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="32.6559851%" y1="59.1664468%" x2="59.3430302%" y2="17.9899763%" id="linearGradient-2">
|
||||
<stop stop-color="#777" offset="18%"></stop>
|
||||
<stop stop-color="#999" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Jira-Software-blue" fill-rule="nonzero">
|
||||
<path d="M72.4,35.76 L39.8,3.16 L36.64,0 L36.64,0 L12.1,24.54 L12.1,24.54 L0.88,35.76 C-0.289813512,36.9312702 -0.289813512,38.8287298 0.88,40 L23.3,62.42 L36.64,75.76 L61.18,51.22 L61.56,50.84 L72.4,40 C73.5698135,38.8287298 73.5698135,36.9312702 72.4,35.76 Z M36.64,49.08 L25.44,37.88 L36.64,26.68 L47.84,37.88 L36.64,49.08 Z" id="Shape" fill="#999"></path>
|
||||
<path d="M36.64,26.68 C29.3070783,19.346212 29.2713402,7.46777926 36.56,0.09 L12.05,24.59 L25.39,37.93 L36.64,26.68 Z" id="Path" fill="url(#linearGradient-1)"></path>
|
||||
<path d="M47.87,37.85 L36.64,49.08 C40.179363,52.6172581 42.1679334,57.4160731 42.1679334,62.42 C42.1679334,67.4239269 40.179363,72.2227419 36.64,75.76 L36.64,75.76 L61.21,51.19 L47.87,37.85 Z" id="Path" fill="url(#linearGradient-2)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/app/assets/integrations/mobx.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M256 236.394V19.607c0-8.894-5.923-16.4-14.037-18.8l-9.215 5.514-102.265 109.037-3.206 10.021-1.873 9.62 31.89 119.18 4.933 1.82h74.167c10.828 0 19.606-8.777 19.606-19.605" fill="#EA6618"/><path d="M0 19.606v216.787c0 6.705 3.367 12.62 8.5 16.155l6.287-3.01 108.246-115.894 4.244-8.265.159-7.99L97.976 5.306 93.513 0H19.606C8.778 0 0 8.778 0 19.606" fill="#d65813"/><path d="M127.277 125.38L241.963.806a19.595 19.595 0 0 0-5.57-.807H93.515l33.763 125.38z" fill="#e05e11"/><path d="M19.606 256h142.622l-34.951-130.621L8.499 252.549A19.511 19.511 0 0 0 19.606 256" fill="#de5c16"/><path d="M94.918 97.03h14.225c5.668 21.386 12.119 40.152 19.316 57.085 8.152-19.05 14.127-37.83 19.185-57.086h13.442c-6.02 23.926-15.868 48.04-27.132 72.93h-11.89c-10.82-23.586-20.03-47.837-27.146-72.93zm-46.92-37.055h31.63v135.637h-31.77v-10.456H67.33V70.152H47.998V59.975zm160.169 10.177h-19.332v115.004h19.47v10.456h-31.769V59.975h31.63v10.177z" fill="#FFF"/></svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
1
frontend/app/assets/integrations/newrelic.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg id="CMYK_-_square" data-name="CMYK - square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 681.02 551.55"><defs><style>.cls-1{fill:#0097a0;}.cls-2{fill:#5bc6cc;}.cls-3{fill:#231f20;}</style></defs><title>NewRelic-logo-square</title><g id="outlines"><path class="cls-1" d="M692.8,220.54C660.86,73.7,484.77-12.68,299.47,27.61s-309.63,192-277.7,338.83,208,233.22,393.32,192.93S724.72,367.37,692.8,220.54ZM344.87,476.79c-103.41,0-187.2-83.82-187.2-187.22s83.8-187.19,187.2-187.19,187.2,83.81,187.2,187.19S448.25,476.79,344.87,476.79Z" transform="translate(-16.78 -17.71)"/><path class="cls-2" d="M391.53,57.56c-132.32,0-239.61,107.28-239.61,239.6S259.21,536.78,391.53,536.78,631.15,429.49,631.15,297.16,523.85,57.56,391.53,57.56ZM344.87,473.78c-101.75,0-184.19-82.47-184.19-184.21S243.12,105.4,344.87,105.4,529,187.85,529,289.57,446.58,473.78,344.87,473.78Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M278.93,271.2l-20.19-42.33c-4.82-10-9.77-21.36-11.46-26.7l-.39.39c.65,7.55.78,17.06.91,25l.52,43.63H233.61V181.08h16.93l21.88,44a164.17,164.17,0,0,1,9.25,23.18l.39-.39c-.39-4.56-1.3-17.45-1.3-25.66l-.26-41.15h14.2V271.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M321.51,242.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.73a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.27,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H346C346,220.93,341.31,214.55,334.15,214.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M437,271.46H423.61l-8.07-30.34c-2.08-7.81-4.3-18-4.3-18H411s-1,6.51-4.3,18.62l-7.94,29.69H385.32l-18-65.25,14.2-2,7.16,31.91c1.82,8.2,3.39,17.32,3.39,17.32h.39a178.91,178.91,0,0,1,3.78-17.71l8.47-30.47h14.07L426.22,235c2.74,10.68,4.17,18.75,4.17,18.75h.39s1.56-10,3.26-17.71l6.77-30.74h14.85Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M267.62,387.2l-7.81-13.94c-6.25-11.07-10.42-17.32-15.37-22.27a7.64,7.64,0,0,0-5.86-2.73V387.2H223.86V297.08h27.48c20.19,0,29.3,11.72,29.3,25.79,0,12.89-8.33,24.75-22.4,24.75,3.26,1.69,9.25,10.42,13.93,18l13.28,21.62Zm-20.84-78h-8.21v28.52h7.68c7.81,0,12-1,14.72-3.78,2.47-2.47,4-6.25,4-10.94C265,313.88,260.06,309.19,246.78,309.19Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M305.12,358.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.72a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.28,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H329.6C329.6,336.93,324.92,330.55,317.75,330.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M371.28,388.63c-14.46,0-14.46-13-14.46-18.62V313.88a106.72,106.72,0,0,0-1.3-19.27l14.72-3.26c1,4,1.17,9.51,1.17,18.1v55.87c0,8.86.39,10.29,1.43,11.85a4,4,0,0,0,4.69,1l2.34,8.86A22.44,22.44,0,0,1,371.28,388.63Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M396.15,311.53A9.34,9.34,0,0,1,386.9,302a9.44,9.44,0,1,1,9.25,9.51ZM389,387.2V322.34l14.46-2.6V387.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M444.46,388.89c-18,0-28-12.63-28-33.86,0-24,14.33-35.42,29-35.42,7.16,0,12.37,1.69,18.23,7.16l-7.16,9.51c-3.91-3.52-7.29-5.08-11.07-5.08a11.2,11.2,0,0,0-10.42,6.64c-2,4-2.73,10.16-2.73,18.36,0,9,1.43,14.72,4.43,18A11.58,11.58,0,0,0,445.5,378c4.56,0,9-2.21,13.28-6.51l6.77,8.72C459.57,386.16,453.32,388.89,444.46,388.89Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M477.78,388.64A9.67,9.67,0,1,1,487.4,379,9.63,9.63,0,0,1,477.78,388.64Zm0-17.42a7.78,7.78,0,1,0,7.44,7.75A7.55,7.55,0,0,0,477.78,371.22Zm1.9,13.1c-.42-.73-.6-1-1-1.79-1.07-2-1.4-2.5-1.79-2.65a.72.72,0,0,0-.34-.08v4.52H474.4V373.48h4a3,3,0,0,1,3.2,3.17,2.78,2.78,0,0,1-2.42,3,2.47,2.47,0,0,1,.44.47c.62.78,2.6,4.21,2.6,4.21Zm-1.14-8.94a4.35,4.35,0,0,0-1.22-.16h-.78v2.94h.73c.94,0,1.35-.1,1.64-.36a1.53,1.53,0,0,0,.42-1.09A1.28,1.28,0,0,0,478.53,375.38Z" transform="translate(-16.78 -17.71)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
1
frontend/app/assets/integrations/ngrx.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" height="120" rx="6.01" width="120"/><g fill-rule="evenodd"><path d="m60.08 20.79-37.37 12.97 5.35 49.58 32.02 17.45z" fill="#412846"/><path d="m59.92 20.79 37.37 12.97-5.35 49.58-32.02 17.45z" fill="#4b314f"/><path d="m78.63 48.16a10.08 10.08 0 0 1 2.63 6.77 15 15 0 0 1 -2.65 8.25c1.36-1.06 2.93-3.34 4.71-6.82q1.16 10.59-8.58 16.08c2.07-.19 4.83-1.55 8.24-4.1q-5.46 13.17-20.1 13.89a24.42 24.42 0 0 1 -15.53-5.67 22.92 22.92 0 0 1 -8-11.39c-2.35-2.55-2.35-2.82-2.59-3.83s.15-1.3.83-2.29a3.7 3.7 0 0 0 .33-2.83 7.12 7.12 0 0 1 -1-3.76 3.68 3.68 0 0 1 1.65-2.61 8.47 8.47 0 0 0 2-2.11 10.37 10.37 0 0 0 .21-3.43c0-2 1.1-3.08 3.32-3.26 3.33-.26 5.22-2.77 6.26-3.91a4 4 0 0 1 3-1.13 6.34 6.34 0 0 1 4.94 2.07 20.12 20.12 0 0 1 11 2.87q7.97 4.71 8.7 10.18-.85 7.2-19.29-.37-9.65 2.73-9.49 11.84 0 8.35 8.07 12.14c-2.62-2.58-3.74-4.74-3.36-6.53q8.18 9.69 18.62 7.24a8.78 8.78 0 0 1 -7.32-3c4.7-.12 9.14-2.3 13.32-6.58a9.29 9.29 0 0 1 -7.61 2.19q10.86-8.51 7.69-19.9zm-13.15-.87a1.07 1.07 0 1 0 -1.06-1.07 1.06 1.06 0 0 0 1.06 1.07z" fill="#ba2bd2"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
12
frontend/app/assets/integrations/openreplay.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 52 59" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Group</title>
|
||||
<g id="logos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="1" transform="translate(-107.000000, -142.000000)" fill-rule="nonzero">
|
||||
<g id="Group" transform="translate(107.000000, 142.000000)">
|
||||
<path d="M44.2286654,29.5 L6.50039175,7.42000842 L6.50039175,51.5799916 L44.2286654,29.5 Z M49.3769757,24.9357962 C50.9991976,25.8727671 52,27.6142173 52,29.5 C52,31.3857827 50.9991976,33.1272329 49.3769757,34.0642038 L8.01498302,58.2754687 C4.63477932,60.2559134 0,57.9934848 0,53.7112649 L0,5.2887351 C0,1.00651517 4.63477932,-1.25591343 8.01498302,0.724531317 L49.3769757,24.9357962 Z" id="outline" fill="#394EFF"></path>
|
||||
<path d="M29.4155818,28.4568548 L14.7929806,20.1454193 C14.2168086,19.8179252 13.4842425,20.0195184 13.1567483,20.5956904 C13.0540138,20.7764349 13,20.9807697 13,21.188671 L13,37.8115419 C13,38.4742836 13.5372583,39.0115419 14.2,39.0115419 C14.4079013,39.0115419 14.6122361,38.9575281 14.7929806,38.8547936 L29.4155818,30.5433581 C29.9917538,30.215864 30.193347,29.4832978 29.8658528,28.9071259 C29.7590506,28.7192249 29.6034827,28.563657 29.4155818,28.4568548 Z" id="inner-play" fill="#27A2A8"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/app/assets/integrations/redux.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" height="120" rx="6.01" width="120"/><g fill="#764abc"><path d="m76.26 75.87a6 6 0 0 0 -.65-12h-.21a6 6 0 0 0 -5.79 6.22 6.16 6.16 0 0 0 1.71 4c-3.64 7.18-9.22 12.44-17.58 16.83a29 29 0 0 1 -17.48 3.33c-4.83-.64-8.58-2.79-10.94-6.33a15.74 15.74 0 0 1 -.86-16.62 25.18 25.18 0 0 1 7.29-8.58c-.43-1.39-1.07-3.75-1.39-5.47-15.55 11.22-13.94 26.45-9.23 33.63 3.54 5.37 10.73 8.69 18.66 8.69a26.22 26.22 0 0 0 6.44-.75 41.15 41.15 0 0 0 30.03-22.95z"/><path d="m95.13 62.57c-8.13-9.57-20.13-14.8-33.89-14.8h-1.71a5.9 5.9 0 0 0 -5.26-3.21h-.21a6 6 0 0 0 .21 12h.22a6 6 0 0 0 5.25-3.65h1.93a40.88 40.88 0 0 1 22.84 7 28.71 28.71 0 0 1 11.37 13.71 14.87 14.87 0 0 1 -.21 12.65 15.76 15.76 0 0 1 -14.67 8.8 27.55 27.55 0 0 1 -10.51-2.25c-1.18 1.07-3.32 2.78-4.82 3.86a33.16 33.16 0 0 0 13.8 3.32c10.3 0 17.91-5.68 20.81-11.37 3.11-6.22 2.89-16.94-5.15-26.06z"/><path d="m40.65 77.69a6 6 0 0 0 6 5.8h.21a6 6 0 0 0 -.21-12h-.22a1.8 1.8 0 0 0 -.75.11 39.29 39.29 0 0 1 -5.57-23.81 28.73 28.73 0 0 1 6.32-16.62c3.11-4 9.12-5.9 13.19-6 11.38-.24 16.21 13.92 16.53 19.6 1.39.32 3.75 1.07 5.36 1.61-1.29-17.38-12.01-26.38-22.31-26.38-9.65 0-18.55 7-22.09 17.27-4.93 13.73-1.71 26.91 4.29 37.31a4.85 4.85 0 0 0 -.75 3.11z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
20
frontend/app/assets/integrations/rollbar.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="rollbar-mark-color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 304 240" style="enable-background:new 0 0 304 240;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#3A4757;}
|
||||
.st1{fill:#F7941D;}
|
||||
.st2{fill:#BFD730;}
|
||||
.st3{fill:#00BAD9;}
|
||||
</style>
|
||||
<title>rollbar-logo-color-vertical</title>
|
||||
<g id="icon">
|
||||
<path class="st0" d="M303.8,239.1V25.7c-0.5-13.6-0.9-34.3-31.4-21.9C221.7,22.4,170.6,40.2,120.3,60C82.2,75,40.5,91.4,19,171.6
|
||||
c-5.6,21-13.4,46.4-19,67.5h49.4c4.6-17,10.2-38.4,14.8-55.4c15.4-57.4,45.6-69.3,73.2-80.1C176.9,88,217,73.8,257,59.1v179.9
|
||||
H303.8z"/>
|
||||
<path class="st1" d="M119,124.5c-5,2.8-9.8,6.1-14.1,9.9c-14.9,13.3-23,32.2-28,51.1l-14.1,51.9H119V124.5z"/>
|
||||
<path class="st2" d="M180.1,99.7c-12.7,4.7-25.3,9.6-38,14.5c-3.4,1.3-6.7,2.6-10,4v119.2H180L180.1,99.7z"/>
|
||||
<path class="st3" d="M243.8,237.4v-161c-16.8,6.1-33.7,12.2-50.5,18.4v142.6H243.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
7
frontend/app/assets/integrations/segment.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 256 238" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M159.580504,162.376777 L12.2929172,162.376777 C5.50108043,162.376777 0,156.875696 0,150.083859 C0,143.292023 5.50108043,137.790942 12.2929172,137.790942 L159.580504,137.790942 C166.372341,137.790942 171.873421,143.292023 171.873421,150.083859 C171.873421,156.875696 166.372341,162.376777 159.580504,162.376777 Z M243.707083,99.922611 L96.4256423,99.922611 C89.6338055,99.922611 84.1327251,94.4215306 84.1327251,87.6296938 C84.1327251,80.8378571 89.6338055,75.3367767 96.4256423,75.3367767 L243.707083,75.3367767 C250.49892,75.3367767 256,80.8378571 256,87.6296938 C256,94.4215306 250.49892,99.922611 243.707083,99.922611 Z M213.257527,40.7844597 C213.257527,47.5736504 207.753801,53.0773769 200.96461,53.0773769 C194.175419,53.0773769 188.671693,47.5736504 188.671693,40.7844597 C188.671693,33.995269 194.175419,28.4915426 200.96461,28.4915426 C207.753801,28.4915426 213.257527,33.995269 213.257527,40.7844597 Z M67.3252341,196.947533 C67.3252341,203.736724 61.8215076,209.24045 55.0323169,209.24045 C48.2431262,209.24045 42.7393998,203.736724 42.7393998,196.947533 C42.7393998,190.158342 48.2431262,184.654616 55.0323169,184.654616 C61.8215076,184.654616 67.3252341,190.158342 67.3252341,196.947533 Z" fill="#93C8A2"></path>
|
||||
<path d="M127.932808,237.521926 C115.941219,237.521926 104.097068,235.740388 92.7320885,232.217245 C86.2514224,230.208999 82.6252498,223.327934 84.632237,216.846878 C86.6410739,210.335052 93.5399235,206.710545 100.005675,208.737812 C109.011654,211.532982 118.404655,212.945926 127.932808,212.945926 C169.623853,212.945926 205.822848,186.19215 218.014092,146.35329 C218.966942,143.237747 221.119645,140.629013 223.997689,139.102122 C226.875733,137.575232 230.242842,137.255537 233.356814,138.213507 C239.844067,140.179342 243.493147,147.059762 241.508883,153.540871 C226.138517,203.764865 180.494302,237.521926 127.932808,237.521926 Z M26.1087364,99.839808 C22.2118385,99.8403441 18.5457396,97.9921972 16.2285897,94.8590471 C13.9114398,91.7258969 13.2180139,87.6792824 14.359805,83.9534098 C29.7332433,33.7324874 75.3774578,-2.13162821e-14 127.932808,-2.13162821e-14 C139.933612,-2.13162821e-14 151.777764,1.78153731 163.133528,5.30775253 C166.246394,6.27212311 168.84862,8.43366553 170.367664,11.3167969 C171.886709,14.1999282 172.198119,17.5684397 171.23338,20.6811909 C170.271899,23.7958935 168.110641,26.4000467 165.226474,27.9190603 C162.342308,29.4380738 158.972271,29.747106 155.859942,28.7779708 C146.860106,26.0135163 137.460961,24.6005729 127.932808,24.6005729 C86.2509786,24.6005729 50.0458403,51.3543487 37.8545962,91.1932088 C36.2728154,96.3542766 31.5098278,99.8801084 26.111808,99.8858823 L26.1087364,99.839808 Z" fill="#43AF79"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/app/assets/integrations/sentry-text.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 717.11 249.68"><title>sentry-logo-black</title><path d="M430.56,143.76,386.07,86.33H375v77h11.22v-59l45.74,59h9.82v-77H430.56Zm-112-14.27H358.4v-10H318.52V96.31h45v-10H307.07v77h57v-10H318.52Zm-46.84-9.78c-15.57-3.72-19.83-6.69-19.83-13.84,0-6.46,5.71-10.81,14.22-10.81,7.09,0,14.07,2.51,21.3,7.67l6.06-8.54c-8-6.13-16.65-9-27.13-9-15.25,0-25.89,9-25.89,21.92,0,13.84,9,18.63,25.5,22.63,14.51,3.35,18.93,6.5,18.93,13.5s-6,11.38-15.35,11.38c-9.07,0-16.81-3-25-9.82l-6.79,8.08a47.82,47.82,0,0,0,31.41,11.6c16.49,0,27.14-8.87,27.14-22.6C296.27,130.23,289.38,124,271.68,119.71Zm373.9-33.37-23.19,36.31-23-36.31H586l30.51,46.54v30.47h11.56V132.53l30.5-46.19ZM450.87,96.76H476.1v66.58h11.57V96.76h25.23V86.33h-62ZM566.4,133.28c11.64-3.21,18-11.37,18-23,0-14.78-10.84-24-28.28-24H522v77h11.45V135.62h19.42l19.54,27.72h13.37l-21.1-29.58Zm-33-7.52V96.53H555c11.27,0,17.74,5.31,17.74,14.56,0,8.91-6.92,14.67-17.62,14.67ZM144.9,65.43a13.75,13.75,0,0,0-23.81,0l-19.6,33.95,5,2.87a96.14,96.14,0,0,1,47.83,77.4H140.56a82.4,82.4,0,0,0-41-65.54l-5-2.86L76.3,143l5,2.87a46.35,46.35,0,0,1,22.46,33.78H72.33a2.27,2.27,0,0,1-2-3.41l8.76-15.17a31.87,31.87,0,0,0-10-5.71L60.42,170.5a13.75,13.75,0,0,0,11.91,20.62h43.25v-5.73A57.16,57.16,0,0,0,91.84,139l6.88-11.92a70.93,70.93,0,0,1,30.56,58.26v5.74h36.65v-5.73A107.62,107.62,0,0,0,117.09,95.3L131,71.17a2.27,2.27,0,0,1,3.93,0l60.66,105.07a2.27,2.27,0,0,1-2,3.41H179.4c.18,3.83.2,7.66,0,11.48h14.24a13.75,13.75,0,0,0,11.91-20.62Z" style="fill:#221f20"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
6
frontend/app/assets/integrations/sentry.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg viewBox="0 0 150 134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Untitled</title>
|
||||
<g id="sentry" fill="#000000" fill-rule="nonzero">
|
||||
<path style="fill:#221f20" d="M86.9,7.43 C84.4430551,3.1785152 79.9053667,0.559932776 74.995,0.559932776 C70.0846333,0.559932776 65.5469449,3.1785152 63.09,7.43 L43.49,41.38 L48.49,44.25 C76.4054109,60.3953737 94.3667995,89.4610537 96.32,121.65 L82.56,121.65 C80.6256171,94.350123 65.2628284,69.7921433 41.56,56.11 L36.56,53.25 L18.3,85 L23.3,87.87 C35.5781336,95.0821178 43.8597493,107.537727 45.76,121.65 L14.33,121.65 C13.5113725,121.663415 12.7489511,121.23496 12.3347402,120.52873 C11.9205293,119.8225 11.918718,118.947939 12.33,118.24 L21.09,103.07 C18.1310309,100.575254 14.7423935,98.6403422 11.09,97.36 L2.42,112.5 C-0.033833822,116.754289 -0.0322538222,121.994373 2.42414508,126.247181 C4.88054397,130.49999 9.41876057,133.119738 14.33,133.12 L57.58,133.12 L57.58,127.39 C57.585764,109.009083 48.7518498,91.7468551 33.84,81 L40.72,69.08 C59.8414525,82.3159451 71.2600253,104.084468 71.28,127.34 L71.28,133.08 L107.93,133.08 L107.93,127.35 C107.896313,91.0129684 89.5283621,57.1465894 59.09,37.3 L73,13.17 C73.4057253,12.4685156 74.1546338,12.0365188 74.965,12.0365188 C75.7753662,12.0365188 76.5242747,12.4685156 76.93,13.17 L137.59,118.24 C138.001282,118.947939 137.999471,119.8225 137.58526,120.52873 C137.171049,121.23496 136.408627,121.663415 135.59,121.65 L121.4,121.65 C121.58,125.48 121.6,129.31 121.4,133.13 L135.64,133.13 C140.551239,133.13 145.089456,130.50999 147.545855,126.257181 C150.002254,122.004373 150.003834,116.764289 147.55,112.51 L86.9,7.43 Z" id="Path"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
frontend/app/assets/integrations/slack-bw.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-slack" viewBox="0 0 16 16">
|
||||
<path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
8
frontend/app/assets/integrations/slack.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 51 51">
|
||||
<g fill="none">
|
||||
<path fill="#E01E5A" d="M10.9228346 5.62204724C10.9228346 8.55354331 8.55354331 10.9228346 5.62204724 10.9228346 2.69055118 10.9228346.321259843 8.55354331.321259843 5.62204724.321259843 2.69055118 2.69055118.321259843 5.62204724.321259843L10.9228346.321259843 10.9228346 5.62204724zM13.5732283 5.62204724C13.5732283 2.69055118 15.9425197.321259843 18.8740157.321259843 21.8055118.321259843 24.1748031 2.69055118 24.1748031 5.62204724L24.1748031 18.8740157C24.1748031 21.8055118 21.8055118 24.1748031 18.8740157 24.1748031 15.9425197 24.1748031 13.5732283 21.8055118 13.5732283 18.8740157L13.5732283 5.62204724z" transform="translate(0 26.504)"/>
|
||||
<path fill="#36C5F0" d="M18.8740157 10.8425197C15.9425197 10.8425197 13.5732283 8.47322835 13.5732283 5.54173228 13.5732283 2.61023622 15.9425197.240944882 18.8740157.240944882 21.8055118.240944882 24.1748031 2.61023622 24.1748031 5.54173228L24.1748031 10.8425197 18.8740157 10.8425197zM18.8740157 13.5330709C21.8055118 13.5330709 24.1748031 15.9023622 24.1748031 18.8338583 24.1748031 21.7653543 21.8055118 24.1346457 18.8740157 24.1346457L5.58188976 24.1346457C2.6503937 24.1346457.281102362 21.7653543.281102362 18.8338583.281102362 15.9023622 2.6503937 13.5330709 5.58188976 13.5330709L18.8740157 13.5330709z"/>
|
||||
<path fill="#2EB67D" d="M13.6133858 18.8338583C13.6133858 15.9023622 15.9826772 13.5330709 18.9141732 13.5330709 21.8456693 13.5330709 24.2149606 15.9023622 24.2149606 18.8338583 24.2149606 21.7653543 21.8456693 24.1346457 18.9141732 24.1346457L13.6133858 24.1346457 13.6133858 18.8338583zM10.9629921 18.8338583C10.9629921 21.7653543 8.59370079 24.1346457 5.66220472 24.1346457 2.73070866 24.1346457.361417323 21.7653543.361417323 18.8338583L.361417323 5.54173228C.361417323 2.61023622 2.73070866.240944882 5.66220472.240944882 8.59370079.240944882 10.9629921 2.61023622 10.9629921 5.54173228L10.9629921 18.8338583z" transform="translate(26.504)"/>
|
||||
<path fill="#ECB22E" d="M5.66220472 13.5732283C8.59370079 13.5732283 10.9629921 15.9425197 10.9629921 18.8740157 10.9629921 21.8055118 8.59370079 24.1748031 5.66220472 24.1748031 2.73070866 24.1748031.361417323 21.8055118.361417323 18.8740157L.361417323 13.5732283 5.66220472 13.5732283zM5.66220472 10.9228346C2.73070866 10.9228346.361417323 8.55354331.361417323 5.62204724.361417323 2.69055118 2.73070866.321259843 5.66220472.321259843L18.9543307.321259843C21.8858268.321259843 24.2551181 2.69055118 24.2551181 5.62204724 24.2551181 8.55354331 21.8858268 10.9228346 18.9543307 10.9228346L5.66220472 10.9228346z" transform="translate(26.504 26.504)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
7
frontend/app/assets/integrations/stackdriver.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 65 57" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" font-family="Roboto" font-size="14px" text-anchor="middle">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<use xlink:href="#A" x=".5" y=".5"></use>
|
||||
<symbol id="A" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M47.83 55.172l-16-29.834-15.984 29.84z" fill="#009245"></path><path d="M31.83 27.617l-16 27.565L0 27.617z" fill="#006837"></path><path d="M0 27.627L15.97.01h31.942L31.905 27.627z" fill="#39b54a"></path><path d="M47.83 55.172L31.904 27.618 47.91 0 64 27.585z" fill="#8cc63f"></path></g></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 750 B |
4
frontend/app/assets/integrations/sumologic.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#009"/>
|
||||
<path d="M352.9 361.4c-12.7-2.7-21.8-5.1-26.9-6.3-6.3-1.8-11.2-4.2-13.6-7.6v-13.9c4.2-6.6 14.5-10.9 28.7-10.9 19.6 0 33.2 4.5 54.7 21.5l25.1-30.2c-25.4-21.2-47.1-28.7-78.6-28.7-36.3 0-61.3 14.8-71.3 36v45.9c7.9 15.4 26 24.2 62.2 32 13 3 21.8 5.1 26.9 6 6.6 2.1 12.7 4.8 16 9.7v16c-4.5 7.3-15.4 11.2-29.6 11.2-10.3.3-20.2-1.8-29.6-5.7-9.1-3.6-19.6-10.6-32.9-21.5l-26.9 29.9c29 25.7 53.5 34.4 88.2 34.4 37.2 0 63.5-14.2 73.4-36.9v-46.2c-8.4-17.2-28.7-26.2-65.8-34.7zm401.5-70.1v182.5h-42.6v-19.3c-9.7 15.7-27.5 24.8-53.5 24.8-43.2 0-64.7-22.4-64.7-58v-130h46.5v117.5c0 19 10.3 30.2 30.2 30.2 23.3 0 37.5-13.3 37.5-37.8v-110h46.6zM531.8 606.9v127.5h-46.5V620.8c0-21.8-8.8-35.1-29-35.1s-32 15.1-32 36.3v112.4h-46.5V620.8c0-23.3-9.7-35.1-29-35.1-20.5 0-32 15.1-32 36.3v112.4h-46.5V551.9h43.2v20.2c10.6-17.5 28.1-26.3 52-26.3 23.3 0 40.8 9.7 50.8 26.9 12.4-17.8 30.8-26.9 54.7-26.9 38.8.1 60.8 23.4 60.8 61.1zm139.8-62.8c-42 0-74.9 22.7-88.5 58.9v76.8c13.3 36.3 46.5 58.9 88.5 58.9s74.6-22.7 87.9-58.6v-77.4c-13.5-35.9-46.5-58.6-87.9-58.6zm43 124.2c-7.3 19.3-22.7 30.8-43.2 30.8-20.2 0-36.3-11.5-43.5-30.8v-53.8c7.3-19.3 22.7-31.1 43.5-31.1 20.5 0 35.7 11.8 42.9 31.1l.3 53.8z" style="fill:#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/app/assets/integrations/vuejs.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" height="120" rx="6.01" width="120"/><path d="m87.83 20h18.55l-46.38 80-46.38-80h35.48l10.9 18.55 10.67-18.55z" fill="#41b883"/><path d="m13.62 20 46.38 80 46.38-80h-18.55l-27.83 48-28.06-48z" fill="#41b883"/><path d="m31.94 20 28.06 48.23 27.83-48.23h-17.16l-10.67 18.55-10.9-18.55z" fill="#35495e"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
|
|
@ -50,7 +50,7 @@ const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
|
|||
const AlertForm = props => {
|
||||
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId, style={ width: '580px', height: '100vh' } } = props;
|
||||
const write = ({ target: { value, name } }) => props.edit({ [ name ]: value })
|
||||
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value });
|
||||
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name }}) => props.edit({ [ name ]: checked })
|
||||
// const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked })
|
||||
// const onChangeCheck = (e) => { console.log(e) }
|
||||
|
|
@ -96,7 +96,7 @@ const AlertForm = props => {
|
|||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={ writeOption }
|
||||
onSelect={ (e, { name, value }) => props.edit({ [ name ]: value }) }
|
||||
value={{ value: instance.detectionMethod }}
|
||||
list={ [
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
|
|
@ -144,7 +144,7 @@ const AlertForm = props => {
|
|||
name="left"
|
||||
value={ triggerOptions.find(i => i.value === instance.query.left) }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value }) }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value: value.value }) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -157,29 +157,32 @@ const AlertForm = props => {
|
|||
name="operator"
|
||||
defaultValue={ instance.query.operator }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value }) }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value: value.value }) }
|
||||
/>
|
||||
{ unit && (
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px'}}
|
||||
label={{ basic: true, content: unit }}
|
||||
labelPosition='right'
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px'}}
|
||||
// label={{ basic: true, content: unit }}
|
||||
// labelPosition='right'
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">{'test'}</span>
|
||||
</>
|
||||
)}
|
||||
{ !unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -309,7 +312,7 @@ const AlertForm = props => {
|
|||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1" />
|
||||
<Button basic onClick={props.onClose}>Cancel</Button>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</div>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const AlertsList = props => {
|
|||
<div className="mb-3 w-full px-3">
|
||||
<Input
|
||||
name="searchQuery"
|
||||
fluid
|
||||
placeholder="Search by Name or Metric"
|
||||
onChange={({ target: { value } }) => setQuery(value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,14 +16,11 @@ interface Props {
|
|||
fetchList: any;
|
||||
}
|
||||
function Notifications(props: Props) {
|
||||
// const { notifications } = props;
|
||||
const { showModal } = useModal();
|
||||
// const unReadNotificationsCount = notifications.filter(({viewed}: any) => !viewed).size
|
||||
const { notificationStore } = useStore();
|
||||
const count = useObserver(() => notificationStore.notificationsCount);
|
||||
|
||||
useEffect(() => {
|
||||
notificationStore.fetchNotificationsCount();
|
||||
const interval = setInterval(() => {
|
||||
notificationStore.fetchNotificationsCount()
|
||||
}, AUTOREFRESH_INTERVAL);
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props
|
|||
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
|
||||
<div className="flex items-center">
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: audioEnabled})}>
|
||||
<Button varient="text" onClick={toggleAudio} hover>
|
||||
<Button variant="text" onClick={toggleAudio} hover>
|
||||
<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : audioEnabled })}>{audioEnabled ? 'Mute' : 'Unmute'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: videoEnabled})}>
|
||||
<Button varient="text" onClick={toggleVideo} hover>
|
||||
<Button variant="text" onClick={toggleVideo} hover>
|
||||
<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="16" />
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : videoEnabled })}>{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
//@ts-nocheck
|
||||
import React, { useState, FC, useEffect } from 'react'
|
||||
import VideoContainer from '../components/VideoContainer'
|
||||
import { Icon, Popup, Button } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import Counter from 'App/components/shared/SessionItem/Counter'
|
||||
import stl from './chatWindow.module.css'
|
||||
|
|
|
|||
|
|
@ -6,19 +6,14 @@ import {
|
|||
fetchFavoriteList as fetchFavoriteSessionList
|
||||
} from 'Duck/sessions';
|
||||
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import SessionList from './SessionList';
|
||||
import stl from './bugFinder.module.css';
|
||||
import withLocationHandlers from "HOCs/withLocationHandlers";
|
||||
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
||||
import { fetchSources } from 'Duck/customField';
|
||||
import { setFunnelPage } from 'Duck/sessions';
|
||||
import { setActiveTab } from 'Duck/search';
|
||||
import SessionsMenu from './SessionsMenu/SessionsMenu';
|
||||
import { LAST_7_DAYS } from 'Types/app/period';
|
||||
import { resetFunnel } from 'Duck/funnels';
|
||||
import { resetFunnelFilters } from 'Duck/funnelFilters'
|
||||
import NoSessionsMessage from 'Shared/NoSessionsMessage';
|
||||
import SessionSearch from 'Shared/SessionSearch';
|
||||
import MainSearchBar from 'Shared/MainSearchBar';
|
||||
|
|
@ -65,10 +60,6 @@ const allowedQueryKeys = [
|
|||
fetchSources,
|
||||
clearEvents,
|
||||
setActiveTab,
|
||||
fetchFunnelsList,
|
||||
resetFunnel,
|
||||
resetFunnelFilters,
|
||||
setFunnelPage,
|
||||
clearSearch,
|
||||
fetchSessions,
|
||||
})
|
||||
|
|
@ -94,9 +85,6 @@ export default class BugFinder extends React.PureComponent {
|
|||
if (props.sessions.size === 0) {
|
||||
props.fetchSessions();
|
||||
}
|
||||
props.resetFunnel();
|
||||
props.resetFunnelFilters();
|
||||
props.fetchFunnelsList(LAST_7_DAYS)
|
||||
|
||||
const queryFilter = this.props.query.all(allowedQueryKeys);
|
||||
if (queryFilter.hasOwnProperty('userId')) {
|
||||
|
|
@ -104,10 +92,6 @@ export default class BugFinder extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setFunnelPage(false);
|
||||
}
|
||||
|
||||
toggleRehydratePanel = () => {
|
||||
this.setState({ showRehydratePanel: !this.state.showRehydratePanel })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,42 +95,48 @@ export default class SessionList extends React.PureComponent {
|
|||
const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title={<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
|
||||
{this.getNoContentMessage(activeTab)}
|
||||
</div>}
|
||||
// subtext="Please try changing your search parameters."
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && list.size === 0}
|
||||
subtext={
|
||||
<div>
|
||||
<div>Please try changing your search parameters.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Loader loading={ loading }>
|
||||
{ list.map(session => (
|
||||
<SessionItem
|
||||
key={ session.sessionId }
|
||||
session={ session }
|
||||
hasUserFilter={hasUserFilter}
|
||||
onUserClick={this.onUserClick}
|
||||
metaList={metaList}
|
||||
lastPlayedSessionId={lastPlayedSessionId}
|
||||
/>
|
||||
))}
|
||||
</Loader>
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
totalPages={Math.ceil(total / PER_PAGE)}
|
||||
onPageChange={(page) => this.props.updateCurrentPage(page)}
|
||||
limit={PER_PAGE}
|
||||
debounceRequest={1000}
|
||||
/>
|
||||
<div className="bg-white p-3 rounded border">
|
||||
<NoContent
|
||||
title={<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
|
||||
{this.getNoContentMessage(activeTab)}
|
||||
</div>}
|
||||
// subtext="Please try changing your search parameters."
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && list.size === 0}
|
||||
subtext={
|
||||
<div>
|
||||
<div>Please try changing your search parameters.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
<Loader loading={ loading }>
|
||||
{ list.map(session => (
|
||||
<>
|
||||
<SessionItem
|
||||
key={ session.sessionId }
|
||||
session={ session }
|
||||
hasUserFilter={hasUserFilter}
|
||||
onUserClick={this.onUserClick}
|
||||
metaList={metaList}
|
||||
lastPlayedSessionId={lastPlayedSessionId}
|
||||
/>
|
||||
<div className="border-b" />
|
||||
</>
|
||||
))}
|
||||
</Loader>
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
totalPages={Math.ceil(total / PER_PAGE)}
|
||||
onPageChange={(page) => this.props.updateCurrentPage(page)}
|
||||
limit={PER_PAGE}
|
||||
debounceRequest={1000}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { tokenRE } from 'Types/integrations/bugsnagConfig';
|
||||
import { edit } from 'Duck/integrations/actions';
|
||||
import { Dropdown } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { withRequest } from 'HOCs';
|
||||
|
||||
@connect(state => ({
|
||||
|
|
@ -50,7 +50,7 @@ export default class ProjectListDropdown extends React.PureComponent {
|
|||
this.fetchProjectList();
|
||||
}
|
||||
}
|
||||
onChange = (e, target) => {
|
||||
onChange = (target) => {
|
||||
if (typeof this.props.onChange === 'function') {
|
||||
this.props.onChange({ target });
|
||||
}
|
||||
|
|
@ -65,11 +65,11 @@ export default class ProjectListDropdown extends React.PureComponent {
|
|||
} = this.props;
|
||||
const options = projects.map(({ name, id }) => ({ text: name, value: id }));
|
||||
return (
|
||||
<Dropdown
|
||||
selection
|
||||
<Select
|
||||
// selection
|
||||
options={ options }
|
||||
name={ name }
|
||||
value={ value }
|
||||
value={ options.find(o => o.value === value) }
|
||||
placeholder={ placeholder }
|
||||
onChange={ this.onChange }
|
||||
loading={ loading }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig';
|
||||
import { edit } from 'Duck/integrations/actions';
|
||||
import { Dropdown } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { withRequest } from 'HOCs';
|
||||
|
||||
@connect(state => ({
|
||||
|
|
@ -48,7 +48,7 @@ export default class LogGroupDropdown extends React.PureComponent {
|
|||
this.fetchLogGroups();
|
||||
}
|
||||
}
|
||||
onChange = (e, target) => {
|
||||
onChange = (target) => {
|
||||
if (typeof this.props.onChange === 'function') {
|
||||
this.props.onChange({ target });
|
||||
}
|
||||
|
|
@ -63,11 +63,11 @@ export default class LogGroupDropdown extends React.PureComponent {
|
|||
} = this.props;
|
||||
const options = values.map(g => ({ text: g, value: g }));
|
||||
return (
|
||||
<Dropdown
|
||||
selection
|
||||
<Select
|
||||
// selection
|
||||
options={ options }
|
||||
name={ name }
|
||||
value={ value }
|
||||
value={ options.find(o => o.value === value) }
|
||||
placeholder={ placeholder }
|
||||
onChange={ this.onChange }
|
||||
loading={ loading }
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const IntegrationItem = ({
|
|||
<Icon name="check" size="14" color="white" />
|
||||
</div>
|
||||
)}
|
||||
<Icon name={ icon } size="40" />
|
||||
<img className="h-12 w-12" src={'/assets/' + icon + '.svg'} alt="integration" />
|
||||
<h4 className="my-2">{ title }</h4>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import cn from 'classnames';
|
|||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import {
|
||||
Form, IconButton, SlideModal, Input, Button, Loader,
|
||||
NoContent, Popup, CopyButton, Dropdown } from 'UI';
|
||||
NoContent, Popup, CopyButton } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member';
|
||||
import { fetchList as fetchRoles } from 'Duck/roles';
|
||||
import styles from './manageUsers.module.css';
|
||||
|
|
@ -39,7 +40,7 @@ class ManageUsers extends React.PureComponent {
|
|||
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }
|
||||
|
||||
// writeOption = (e, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
onChange = (e, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
onChange = ({ name, value }) => this.props.edit({ [ name ]: value.value });
|
||||
onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked });
|
||||
setFocus = () => this.focusElement && this.focusElement.focus();
|
||||
closeModal = () => this.setState({ showModal: false });
|
||||
|
|
@ -138,12 +139,12 @@ class ManageUsers extends React.PureComponent {
|
|||
{ isEnterprise && (
|
||||
<Form.Field>
|
||||
<label htmlFor="role">{ 'Role' }</label>
|
||||
<Dropdown
|
||||
<Select
|
||||
placeholder="Role"
|
||||
selection
|
||||
options={ roles }
|
||||
name="roleId"
|
||||
value={ member.roleId }
|
||||
value={ roles.find(r => r.value === member.roleId) }
|
||||
onChange={ this.onChange }
|
||||
/>
|
||||
</Form.Field>
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ const RoleForm = (props: Props) => {
|
|||
isSearchable
|
||||
name="permissions"
|
||||
options={ permissions }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'permissions', value }) }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'permissions', value: value.value }) }
|
||||
value={null}
|
||||
/>
|
||||
{ role.permissions.size > 0 && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { Popup, IconButton } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached site limit.';
|
||||
|
||||
function AddProjectButton({ isAdmin = false, onClick }: any ) {
|
||||
const { userStore } = useStore();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0));
|
||||
return (
|
||||
<Popup
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : (!canAddProject ? LIMIT_WARNING : 'Add a Project') }` }
|
||||
>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
disabled={ !canAddProject || !isAdmin }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ onClick }
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddProjectButton;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AddUserButton';
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Loader, SlideModal, IconButton, Icon, Button, Popup, TextLink } from 'UI';
|
||||
import { Loader, SlideModal, Icon, Button, Popup, TextLink } from 'UI';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site';
|
||||
import stl from './sites.module.css';
|
||||
|
|
@ -10,8 +10,9 @@ import NewSiteForm from './NewSiteForm';
|
|||
import GDPRForm from './GDPRForm';
|
||||
import TrackingCodeModal from 'Shared/TrackingCodeModal';
|
||||
import BlockedIps from './BlockedIps';
|
||||
import { confirm } from 'UI';
|
||||
import { confirm, PageTitle } from 'UI';
|
||||
import SiteSearch from './SiteSearch';
|
||||
import AddProjectButton from './AddProjectButton';
|
||||
|
||||
const STATUS_MESSAGE_MAP = {
|
||||
[ RED ]: ' There seems to be an issue (please verify your installation)',
|
||||
|
|
@ -19,9 +20,6 @@ const STATUS_MESSAGE_MAP = {
|
|||
[ GREEN ]: 'All good!',
|
||||
};
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached site limit.';
|
||||
|
||||
const BLOCKED_IPS = 'BLOCKED_IPS';
|
||||
const NONE = 'NONE';
|
||||
|
||||
|
|
@ -143,21 +141,14 @@ class Sites extends React.PureComponent {
|
|||
/>
|
||||
<div className={ stl.wrapper }>
|
||||
<div className={ stl.tabHeader }>
|
||||
<h3 className={ cn(stl.tabTitle, "text-2xl") }>{ 'Projects' }</h3>
|
||||
<Popup
|
||||
disabled={ canAddSites }
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : LIMIT_WARNING }` }
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={ !canAddSites }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ this.showNewSiteForm }
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
{/* <h3 className={ cn(stl.tabTitle, "text-2xl") }>{ 'Projects' }</h3> */}
|
||||
<PageTitle
|
||||
title={<div className="mr-4">Projects</div>}
|
||||
actionButton={(
|
||||
<AddProjectButton isAdmin={isAdmin} onClick={this.showNewSiteForm} />
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex ml-auto items-center">
|
||||
<TextLink
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import UserList from './components/UserList';
|
||||
import { PageTitle, Popup, IconButton } from 'UI';
|
||||
import { PageTitle } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import UserSearch from './components/UserSearch';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserForm from './components/UserForm';
|
||||
import { connect } from 'react-redux';
|
||||
import AddUserButton from './components/AddUserButton';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached users limit.';
|
||||
interface Props {
|
||||
account: any;
|
||||
isEnterprise: boolean;
|
||||
|
|
@ -43,22 +42,7 @@ function UsersView(props: Props) {
|
|||
<PageTitle
|
||||
title={<div>Team <span className="color-gray-medium">{userCount}</span></div>}
|
||||
actionButton={(
|
||||
<Popup
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : (reachedLimit ? LIMIT_WARNING : 'Add team member') }` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top left"
|
||||
>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
disabled={ reachedLimit || !isAdmin }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
className="ml-3"
|
||||
onClick={ () => editHandler(null) }
|
||||
/>
|
||||
</Popup>
|
||||
<AddUserButton isAdmin={isAdmin} onClick={() => editHandler(null)} />
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { Popup, IconButton } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached users limit.';
|
||||
|
||||
function AddUserButton({ isAdmin = false, onClick }: any ) {
|
||||
const { userStore } = useStore();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const cannAddUser = useObserver(() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0));
|
||||
return (
|
||||
<Popup
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : (!cannAddUser ? LIMIT_WARNING : 'Add team member') }` }
|
||||
>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
disabled={ !cannAddUser || !isAdmin }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ onClick }
|
||||
className="ml-4"
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddUserButton;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AddUserButton';
|
||||
|
|
@ -27,6 +27,7 @@ function UserForm(props: Props) {
|
|||
const onSave = () => {
|
||||
userStore.saveUser(user).then(() => {
|
||||
hideModal();
|
||||
userStore.fetchLimits();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ function UserForm(props: Props) {
|
|||
})) {
|
||||
userStore.deleteUser(user.userId).then(() => {
|
||||
hideModal();
|
||||
userStore.fetchLimits();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,29 +47,32 @@ function UserListItem(props: Props) {
|
|||
|
||||
<div className="col-span-2 justify-self-end invisible group-hover:visible">
|
||||
<div className="grid grid-cols-2 gap-3 items-center justify-end">
|
||||
{!user.isJoined && user.invitationLink ? (
|
||||
<Popup
|
||||
delay={500}
|
||||
content="Copy Invite Code"
|
||||
hideOnClick={true}
|
||||
>
|
||||
<button className='' onClick={copyInviteCode}>
|
||||
<Icon name="link-45deg" size="16" color="teal"/>
|
||||
</button>
|
||||
</Popup>
|
||||
) : <div/>}
|
||||
{!user.isJoined && user.isExpiredInvite && (
|
||||
<Popup
|
||||
delay={500}
|
||||
arrow
|
||||
content="Generate Invite"
|
||||
hideOnClick={true}
|
||||
>
|
||||
<button className='' onClick={generateInvite}>
|
||||
<Icon name="link-45deg" size="16" color="red"/>
|
||||
</button>
|
||||
</Popup>
|
||||
)}
|
||||
<div>
|
||||
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (
|
||||
<Popup
|
||||
delay={500}
|
||||
content="Copy Invite Code"
|
||||
hideOnClick={true}
|
||||
>
|
||||
<button className='' onClick={copyInviteCode}>
|
||||
<Icon name="link-45deg" size="16" color="teal"/>
|
||||
</button>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{!user.isJoined && user.isExpiredInvite && (
|
||||
<Popup
|
||||
delay={500}
|
||||
arrow
|
||||
content="Generate Invite"
|
||||
hideOnClick={true}
|
||||
>
|
||||
<button className='' onClick={generateInvite}>
|
||||
<Icon name="link-45deg" size="16" color="red"/>
|
||||
</button>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
<button className='' onClick={editHandler}>
|
||||
<Icon name="pencil" color="teal" size="16" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
import cn from 'classnames';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip } from 'recharts';
|
||||
import CountBadge from '../../common/CountBadge';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
// onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetricOverviewChart(props: Props) {
|
||||
const { data } = props;
|
||||
|
|
@ -33,7 +30,7 @@ function CustomMetricOverviewChart(props: Props) {
|
|||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ {
|
||||
top: 50, right: 0, left: 0, bottom: 1,
|
||||
top: 50, right: 0, left: 0, bottom: 0,
|
||||
} }
|
||||
>
|
||||
{gradientDef}
|
||||
|
|
@ -60,7 +57,7 @@ function CustomMetricOverviewChart(props: Props) {
|
|||
export default CustomMetricOverviewChart
|
||||
|
||||
|
||||
const countView = (avg, unit) => {
|
||||
const countView = (avg: any, unit: any) => {
|
||||
if (unit === 'mb') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg / 1024 / 1024);
|
||||
|
|
@ -72,4 +69,4 @@ const countView = (avg, unit) => {
|
|||
return numberWithCommas(count > 1000 ? count +'k' : count);
|
||||
}
|
||||
return avg ? numberWithCommas(avg): 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ function CustomMetriPercentage(props: Props) {
|
|||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||
<div className="text-6xl">{numberWithCommas(data.count)}</div>
|
||||
<div className="text-lg mt-6">{`${parseInt(data.previousCount || 0)} ( ${parseInt(data.countProgress || 0).toFixed(1)}% )`}</div>
|
||||
<div className="text-lg mt-6">{`${parseInt(data.previousCount || 0)} ( ${Math.floor(parseInt(data.countProgress || 0))}% )`}</div>
|
||||
<div className="color-gray-medium">from previous period.</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,80 @@
|
|||
import React from 'react';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Pagination, NoContent } from 'UI';
|
||||
import ErrorListItem from '../../../components/Errors/ErrorListItem';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import ErrorDetailsModal from '../../../components/Errors/ErrorDetailsModal';
|
||||
|
||||
const PER_PAGE = 5;
|
||||
interface Props {
|
||||
|
||||
metric: any;
|
||||
isTemplate?: boolean;
|
||||
isEdit?: boolean;
|
||||
history: any,
|
||||
location: any,
|
||||
}
|
||||
function CustomMetricTableErrors(props) {
|
||||
function CustomMetricTableErrors(props: RouteComponentProps<Props>) {
|
||||
const { metric, isEdit = false } = props;
|
||||
const errorId = new URLSearchParams(props.location.search).get("errorId");
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const onErrorClick = (e: any, error: any) => {
|
||||
e.stopPropagation();
|
||||
props.history.replace({search: (new URLSearchParams({errorId : error.errorId})).toString()});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!errorId) return;
|
||||
|
||||
showModal(<ErrorDetailsModal errorId={errorId} />, { right: true, onClose: () => {
|
||||
if (props.history.location.pathname.includes("/dashboard")) {
|
||||
props.history.replace({search: ""});
|
||||
}
|
||||
}});
|
||||
|
||||
return () => {
|
||||
hideModal();
|
||||
}
|
||||
}, [errorId])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<NoContent
|
||||
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)} />
|
||||
))}
|
||||
|
||||
{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)}
|
||||
limit={metric.limit}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEdit && (
|
||||
<ViewMore total={metric.data.total} limit={metric.limit} />
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricTableErrors;
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,48 +1,54 @@
|
|||
import React from 'react';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import { Pagination } from 'UI';
|
||||
import { Pagination, NoContent } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
metric: any;
|
||||
isTemplate?: boolean;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricTableSessions(props: Props) {
|
||||
const { data = { sessions: [], total: 0 }, isEdit = false, metric = {}, isTemplate } = props;
|
||||
const currentPage = 1;
|
||||
const { isEdit = false, metric } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.sessions && data.sessions.map((session: any, index: any) => (
|
||||
<SessionItem session={session} />
|
||||
))}
|
||||
|
||||
{isEdit && (
|
||||
<div className="my-6 flex items-center justify-center">
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
totalPages={Math.ceil(data.total / PER_PAGE)}
|
||||
onPageChange={(page: any) => this.props.updateCurrentPage(page)}
|
||||
limit={PER_PAGE}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
return useObserver(() => (
|
||||
<NoContent
|
||||
show={!metric || !metric.data || !metric.data.sessions || metric.data.sessions.length === 0}
|
||||
size="small"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
|
||||
{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)}
|
||||
limit={metric.data.total}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEdit && (
|
||||
<ViewMore total={data.total} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{!isEdit && (
|
||||
<ViewMore total={metric.data.total} limit={metric.limit} />
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
));
|
||||
}
|
||||
|
||||
export default CustomMetricTableSessions;
|
||||
|
||||
const ViewMore = ({ total }: any) => total > PER_PAGE && (
|
||||
<div className="my-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
.wrapper {
|
||||
background-color: $gray-light;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.innerWapper {
|
||||
border-radius: 3px;
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, SegmentSelection } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import Period from 'Types/app/period';
|
||||
import stl from './CustomMetricWidgetPreview.module.css';
|
||||
import { remove } from 'Duck/customMetrics';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { edit } from 'Duck/customMetrics';
|
||||
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||
import CustomMetricPercentage from '../CustomMetricPercentage';
|
||||
import CustomMetricTable from '../CustomMetricTable';
|
||||
import CustomMetricPieChart from '../CustomMetricPieChart';
|
||||
|
||||
const customParams = (rangeName: string) => {
|
||||
const params = { density: 70 }
|
||||
|
||||
// if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
// if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
// if (rangeName === YESTERDAY) params.density = 70
|
||||
// if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
data?: any;
|
||||
onClickEdit?: (e) => void;
|
||||
remove: (id) => void;
|
||||
edit: (metric) => void;
|
||||
}
|
||||
function CustomMetricWidget(props: Props) {
|
||||
const { metric } = props;
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<any>({ chart: [{}] })
|
||||
const [period, setPeriod] = useState(Period({ rangeName: metric.rangeName, startDate: metric.startDate, endDate: metric.endDate }));
|
||||
|
||||
const colors = Styles.customMetricColors;
|
||||
const params = customParams(period.rangeName)
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
const isTable = metric.metricType === 'table';
|
||||
|
||||
useEffect(() => {
|
||||
// Check for title change
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return
|
||||
};
|
||||
prevMetricRef.current = metric;
|
||||
setLoading(true);
|
||||
}, [metric])
|
||||
|
||||
const onDateChange = (changedDates) => {
|
||||
setPeriod({ ...changedDates, rangeName: changedDates.rangeValue })
|
||||
props.edit({ ...changedDates, rangeName: changedDates.rangeValue });
|
||||
}
|
||||
|
||||
const chagneViewType = (e, { name, value }) => {
|
||||
props.edit({ [ name ]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-auto font-medium">Preview</div>
|
||||
<div className="flex items-center">
|
||||
{isTimeSeries && (
|
||||
<>
|
||||
<span className="color-gray-medium mr-4">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary
|
||||
icons={true}
|
||||
onSelect={ chagneViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={ [
|
||||
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
|
||||
{ value: 'progress', name: 'Progress', icon: 'hash' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isTable && (
|
||||
<>
|
||||
<span className="mr-1 color-gray-medium">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary={true}
|
||||
icons={true}
|
||||
onSelect={ chagneViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={[
|
||||
{ value: 'table', name: 'Table', icon: 'table' },
|
||||
{ value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-4" />
|
||||
<span className="mr-1 color-gray-medium">Time Range</span>
|
||||
<DateRange
|
||||
rangeValue={metric.rangeName}
|
||||
startDate={metric.startDate}
|
||||
endDate={metric.endDate}
|
||||
onDateChange={onDateChange}
|
||||
customRangeRight
|
||||
direction="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={stl.wrapper}>
|
||||
<div className={stl.innerWapper}>
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.length === 0 }
|
||||
>
|
||||
<div className="p-4 font-medium">
|
||||
{metric.name}
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
{ isTimeSeries && (
|
||||
<>
|
||||
{ metric.viewType === 'progress' && (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)}
|
||||
{ metric.viewType === 'lineChart' && (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
// seriesMap={seriesMap}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{ isTable && (
|
||||
<>
|
||||
{ metric.viewType === 'table' ? (
|
||||
<CustomMetricTable metric={metric} data={data[0]} />
|
||||
) : (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, { remove, edit })(CustomMetricWidget);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CustomMetricWidgetPreview';
|
||||
|
|
@ -7,7 +7,7 @@ import Chart from './Chart';
|
|||
import ResourceInfo from './ResourceInfo';
|
||||
import CopyPath from './CopyPath';
|
||||
|
||||
const cols = [
|
||||
const cols: Array<Object> = [
|
||||
{
|
||||
key: 'resource',
|
||||
title: 'Resource',
|
||||
|
|
@ -17,7 +17,7 @@ const cols = [
|
|||
{
|
||||
key: 'sessions',
|
||||
title: 'Sessions',
|
||||
toText: count => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`,
|
||||
toText: (count: number) => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
|
|
@ -25,16 +25,17 @@ const cols = [
|
|||
title: 'Trend',
|
||||
Component: Chart,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
key: 'copy-path',
|
||||
title: '',
|
||||
Component: CopyPath,
|
||||
cellClass: 'invisible group-hover:visible text-right',
|
||||
width: '20%',
|
||||
}
|
||||
];
|
||||
|
||||
const copyPathCol = {
|
||||
key: 'copy-path',
|
||||
title: '',
|
||||
Component: CopyPath,
|
||||
cellClass: 'invisible group-hover:visible text-right',
|
||||
width: '20%',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
|
|
@ -43,6 +44,10 @@ interface Props {
|
|||
function MissingResources(props: Props) {
|
||||
const { data, metric, isTemplate } = props;
|
||||
|
||||
if (!isTemplate) {
|
||||
cols.push(copyPathCol);
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title="No resources missing."
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function ResourceLoadedVsResponseEnd(props: Props) {
|
|||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ResponsiveContainer height={ 246 } width="100%">
|
||||
<ComposedChart
|
||||
data={metric.data.chart}
|
||||
margin={ Styles.chartMargins}
|
||||
|
|
@ -67,4 +67,4 @@ function ResourceLoadedVsResponseEnd(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsResponseEnd;
|
||||
export default ResourceLoadedVsResponseEnd;
|
||||
|
|
|
|||