Compare commits
156 commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90510aa33b | ||
|
|
96a70f5d41 | ||
|
|
d4a13edcf0 | ||
|
|
51fad91a22 | ||
|
|
36abcda1e1 | ||
|
|
dd5f464f73 | ||
|
|
f9ada41272 | ||
|
|
9e24a3583e | ||
|
|
0a3129d3cd | ||
|
|
99d61db9d9 | ||
|
|
133958622e | ||
|
|
fb021f606f | ||
|
|
a2905fa8ed | ||
|
|
beec2283fd | ||
|
|
6c8b55019e | ||
|
|
e3e3e11227 | ||
|
|
c6f7de04cc | ||
|
|
2921c17cbf | ||
|
|
7eb3f5c4c8 | ||
|
|
5a9a8e588a | ||
|
|
4b14258266 | ||
|
|
744d2d4311 | ||
|
|
64242a5dc0 | ||
|
|
cae3002697 | ||
|
|
3d3c62196b | ||
|
|
e810958a5d | ||
|
|
39fa9787d1 | ||
|
|
c9c1ad4dde | ||
|
|
d9868928be | ||
|
|
a460d8c9a2 | ||
|
|
930417aab4 | ||
|
|
07bc184f4d | ||
|
|
71b7cca569 | ||
|
|
355d27eaa0 | ||
|
|
66b485cccf | ||
|
|
de33a42151 | ||
|
|
f12bdebf82 | ||
|
|
bbfa20c693 | ||
|
|
f264ba043d | ||
|
|
a05dce8125 | ||
|
|
3a1635d81f | ||
|
|
ccb332c636 | ||
|
|
80ffa15959 | ||
|
|
b2e961d621 | ||
|
|
b4d0598f23 | ||
|
|
e77f083f10 | ||
|
|
58da1d3f64 | ||
|
|
447fc26a2a | ||
|
|
9bdf6e4f92 | ||
|
|
01f403e12d | ||
|
|
39eb943b86 | ||
|
|
366b0d38b0 | ||
|
|
f4d5b3c06e | ||
|
|
93ae18133e | ||
|
|
fbe5d78270 | ||
|
|
b803eed1d4 | ||
|
|
9ed3cb1b7e | ||
|
|
5e0e5730ba | ||
|
|
d78b33dcd2 | ||
|
|
4b1ca200b4 | ||
|
|
08d930f9ff | ||
|
|
da37809bc8 | ||
|
|
d922fc7ad5 | ||
|
|
796360fdd2 | ||
|
|
13dbb60d8b | ||
|
|
9e20a49128 | ||
|
|
91f8cc1399 | ||
|
|
f8ba3f6d89 | ||
|
|
85e30b3692 | ||
|
|
0360e3726e | ||
|
|
77bbb5af36 | ||
|
|
ab0d4cfb62 | ||
|
|
3fd506a812 | ||
|
|
e8432e2dec | ||
|
|
5c76a8524c | ||
|
|
3ba40a4811 | ||
|
|
f9a3f24590 | ||
|
|
85d6d0abac | ||
|
|
b3594136ce | ||
|
|
8f67edde8d | ||
|
|
74ed29915b | ||
|
|
3ca71ec211 | ||
|
|
0e469fd056 | ||
|
|
a8cb0e1643 | ||
|
|
e171f0d8d5 | ||
|
|
68ea291444 | ||
|
|
05cbb831c7 | ||
|
|
5070ded1f4 | ||
|
|
77610a4924 | ||
|
|
7c34e4a0f6 | ||
|
|
330e21183f | ||
|
|
30ce37896c | ||
|
|
80a7817e7d | ||
|
|
1b9c568cb1 | ||
|
|
3759771ae9 | ||
|
|
f6ae5aba88 | ||
|
|
5190dc512a | ||
|
|
3fcccb51e8 | ||
|
|
26077d5689 | ||
|
|
00c57348fd | ||
|
|
1f9bc5520a | ||
|
|
aef94618f6 | ||
|
|
2a330318c7 | ||
|
|
6777d5ce2a | ||
|
|
8a6f8fe91f | ||
|
|
7b078fed4c | ||
|
|
894d4c84b3 | ||
|
|
46390a3ba9 | ||
|
|
621667f5ce | ||
|
|
a72f476f1c | ||
|
|
623946ce4e | ||
|
|
2d099214fc | ||
|
|
b0e7054f89 | ||
|
|
a9097270af | ||
|
|
5d514ddaf2 | ||
|
|
43688bb03b | ||
|
|
e050cee7bb | ||
|
|
6b35df7125 | ||
|
|
8e099b6dc3 | ||
|
|
c0a4734054 | ||
|
|
7de1efb5fe | ||
|
|
d4ff28ddbe | ||
|
|
b2256f72d0 | ||
|
|
a63bda1c79 | ||
|
|
3a0176789e | ||
|
|
f2b7271fca | ||
|
|
d50f89662b | ||
|
|
35051d201c | ||
|
|
214be95ecc | ||
|
|
dbc142c114 | ||
|
|
443f5e8f08 | ||
|
|
9f693f220d | ||
|
|
5ab30380b0 | ||
|
|
fc86555644 | ||
|
|
2a3c611a27 | ||
|
|
1d6fb0ae9e | ||
|
|
bef91a6136 | ||
|
|
1e2bd19d32 | ||
|
|
3b58cb347e | ||
|
|
ca4590501a | ||
|
|
fd12cc7585 | ||
|
|
6abded53e0 | ||
|
|
82c5e5e59d | ||
|
|
c77b0cc4de | ||
|
|
de344e62ef | ||
|
|
deb78a62c0 | ||
|
|
0724cf05f0 | ||
|
|
cc704f1bc3 | ||
|
|
4c159b2d26 | ||
|
|
42df33bc01 | ||
|
|
ae95b48760 | ||
|
|
4be3050e61 | ||
|
|
8eec6e983b | ||
|
|
5fec615044 | ||
|
|
f77568a01c | ||
|
|
618e4dc59f |
207 changed files with 5382 additions and 3073 deletions
122
.github/workflows/assist-server-ee.yaml
vendored
Normal file
122
.github/workflows/assist-server-ee.yaml
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# This action will push the assist changes to aws
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
skip_security_checks:
|
||||||
|
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- "ee/assist-server/**"
|
||||||
|
|
||||||
|
name: Build and Deploy Assist-Server EE
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# We need to diff with old commit
|
||||||
|
# to see which workers got changed.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: ./.github/composite-actions/update-keys
|
||||||
|
with:
|
||||||
|
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||||
|
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||||
|
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||||
|
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||||
|
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||||
|
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||||
|
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||||
|
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||||
|
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||||
|
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||||
|
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||||
|
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||||
|
name: Update Keys
|
||||||
|
|
||||||
|
- 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
|
||||||
|
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||||
|
id: setcontext
|
||||||
|
|
||||||
|
- name: Building and Pushing Assist-Server image
|
||||||
|
id: build-image
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||||
|
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||||
|
ENVIRONMENT: staging
|
||||||
|
run: |
|
||||||
|
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||||
|
cd assist-server
|
||||||
|
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||||
|
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||||
|
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||||
|
images=("assist-server")
|
||||||
|
for image in ${images[*]};do
|
||||||
|
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||||
|
done
|
||||||
|
err_code=$?
|
||||||
|
[[ $err_code -ne 0 ]] && {
|
||||||
|
exit $err_code
|
||||||
|
}
|
||||||
|
} && {
|
||||||
|
echo "Skipping Security Checks"
|
||||||
|
}
|
||||||
|
images=("assist-server")
|
||||||
|
for image in ${images[*]};do
|
||||||
|
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||||
|
done
|
||||||
|
- 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:
|
||||||
|
# We've to strip off the -ee, as helm will append it.
|
||||||
|
tag: `echo ${image_array[1]} | cut -d '-' -f 1`
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
- name: Deploy to kubernetes
|
||||||
|
run: |
|
||||||
|
pwd
|
||||||
|
cd scripts/helmcharts/
|
||||||
|
|
||||||
|
# Update changed image tag
|
||||||
|
sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||||
|
|
||||||
|
cat /tmp/image_override.yaml
|
||||||
|
# Deploy command
|
||||||
|
mkdir -p /tmp/charts
|
||||||
|
mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector} /tmp/charts/
|
||||||
|
rm -rf openreplay/charts/*
|
||||||
|
mv /tmp/charts/* openreplay/charts/
|
||||||
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||||
|
# We're not passing -ee flag, because helm will add that.
|
||||||
|
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||||
|
ENVIRONMENT: staging
|
||||||
189
.github/workflows/patch-build-old.yaml
vendored
Normal file
189
.github/workflows/patch-build-old.yaml
vendored
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
services:
|
||||||
|
description: 'Comma separated names of services to build(in small letters).'
|
||||||
|
required: true
|
||||||
|
default: 'chalice,frontend'
|
||||||
|
tag:
|
||||||
|
description: 'Tag to update.'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
branch:
|
||||||
|
description: 'Branch to build patches from. Make sure the branch is uptodate with tag. Else itll cause missing commits.'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
name: Build patches from tag, rewrite commit HEAD to older timestamp, and Push the tag
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build Patch from old tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||||
|
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 4
|
||||||
|
ref: ${{ github.event.inputs.tag }}
|
||||||
|
|
||||||
|
- name: Set Remote with GITHUB_TOKEN
|
||||||
|
run: |
|
||||||
|
git config --unset http.https://github.com/.extraheader
|
||||||
|
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
|
||||||
|
|
||||||
|
- name: Create backup tag with timestamp
|
||||||
|
run: |
|
||||||
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
|
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||||
|
BACKUP_TAG="${{ github.event.inputs.tag }}-backup-${TIMESTAMP}"
|
||||||
|
echo "BACKUP_TAG=${BACKUP_TAG}" >> $GITHUB_ENV
|
||||||
|
echo "INPUT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||||
|
git tag $BACKUP_TAG || { echo "Failed to create backup tag"; exit 1; }
|
||||||
|
git push origin $BACKUP_TAG || { echo "Failed to push backup tag"; exit 1; }
|
||||||
|
echo "Created backup tag: $BACKUP_TAG"
|
||||||
|
|
||||||
|
# Get the oldest commit date from the last 3 commits in raw format
|
||||||
|
OLDEST_COMMIT_TIMESTAMP=$(git log -3 --pretty=format:"%at" | tail -1)
|
||||||
|
echo "Oldest commit timestamp: $OLDEST_COMMIT_TIMESTAMP"
|
||||||
|
# Add 1 second to the timestamp
|
||||||
|
NEW_TIMESTAMP=$((OLDEST_COMMIT_TIMESTAMP + 1))
|
||||||
|
echo "NEW_TIMESTAMP=$NEW_TIMESTAMP" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup yq
|
||||||
|
uses: mikefarah/yq@master
|
||||||
|
|
||||||
|
# Configure AWS credentials for the first registry
|
||||||
|
- name: Configure AWS credentials for RELEASE_ARM_REGISTRY
|
||||||
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_DEPOT_ACCESS_KEY }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_DEPOT_SECRET_KEY }}
|
||||||
|
aws-region: ${{ secrets.AWS_DEPOT_DEFAULT_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR for RELEASE_ARM_REGISTRY
|
||||||
|
id: login-ecr-arm
|
||||||
|
run: |
|
||||||
|
aws ecr get-login-password --region ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} | docker login --username AWS --password-stdin ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||||
|
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||||
|
|
||||||
|
- uses: depot/setup-action@v1
|
||||||
|
- name: Get HEAD Commit ID
|
||||||
|
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
|
- name: Define Branch Name
|
||||||
|
run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
id: build-image
|
||||||
|
env:
|
||||||
|
DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||||
|
DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||||
|
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||||
|
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||||
|
MSAAS_REPO_FOLDER: /tmp/msaas
|
||||||
|
run: |
|
||||||
|
set -exo pipefail
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git checkout -b $BRANCH_NAME
|
||||||
|
working_dir=$(pwd)
|
||||||
|
function image_version(){
|
||||||
|
local service=$1
|
||||||
|
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||||
|
current_version=$(yq eval '.AppVersion' $chart_path)
|
||||||
|
new_version=$(echo $current_version | awk -F. '{$NF += 1 ; print $1"."$2"."$3}')
|
||||||
|
echo $new_version
|
||||||
|
# yq eval ".AppVersion = \"$new_version\"" -i $chart_path
|
||||||
|
}
|
||||||
|
function clone_msaas() {
|
||||||
|
[ -d $MSAAS_REPO_FOLDER ] || {
|
||||||
|
git clone -b $INPUT_TAG --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
|
||||||
|
cd $MSAAS_REPO_FOLDER
|
||||||
|
cd openreplay && git fetch origin && git checkout $INPUT_TAG
|
||||||
|
git log -1
|
||||||
|
cd $MSAAS_REPO_FOLDER
|
||||||
|
bash git-init.sh
|
||||||
|
git checkout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function build_managed() {
|
||||||
|
local service=$1
|
||||||
|
local version=$2
|
||||||
|
echo building managed
|
||||||
|
clone_msaas
|
||||||
|
if [[ $service == 'chalice' ]]; then
|
||||||
|
cd $MSAAS_REPO_FOLDER/openreplay/api
|
||||||
|
else
|
||||||
|
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
||||||
|
fi
|
||||||
|
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
|
||||||
|
}
|
||||||
|
# Checking for backend images
|
||||||
|
ls backend/cmd >> /tmp/backend.txt
|
||||||
|
echo Services: "${{ github.event.inputs.services }}"
|
||||||
|
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||||
|
BUILD_SCRIPT_NAME="build.sh"
|
||||||
|
# Build FOSS
|
||||||
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
|
# Check if service is backend
|
||||||
|
if grep -q $SERVICE /tmp/backend.txt; then
|
||||||
|
cd backend
|
||||||
|
foss_build_args="nil $SERVICE"
|
||||||
|
ee_build_args="ee $SERVICE"
|
||||||
|
else
|
||||||
|
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
|
||||||
|
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
|
||||||
|
ee_build_args="ee"
|
||||||
|
fi
|
||||||
|
version=$(image_version $SERVICE)
|
||||||
|
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||||
|
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||||
|
echo IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||||
|
IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||||
|
if [[ "$SERVICE" != "chalice" && "$SERVICE" != "frontend" ]]; then
|
||||||
|
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||||
|
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||||
|
else
|
||||||
|
build_managed $SERVICE $version
|
||||||
|
fi
|
||||||
|
cd $working_dir
|
||||||
|
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$SERVICE/Chart.yaml"
|
||||||
|
yq eval ".AppVersion = \"$version\"" -i $chart_path
|
||||||
|
git add $chart_path
|
||||||
|
git commit -m "Increment $SERVICE chart version"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Change commit timestamp
|
||||||
|
run: |
|
||||||
|
# Convert the timestamp to a date format git can understand
|
||||||
|
NEW_DATE=$(perl -le 'print scalar gmtime($ARGV[0])." +0000"' $NEW_TIMESTAMP)
|
||||||
|
echo "Setting commit date to: $NEW_DATE"
|
||||||
|
|
||||||
|
# Amend the commit with the new date
|
||||||
|
GIT_COMMITTER_DATE="$NEW_DATE" git commit --amend --no-edit --date="$NEW_DATE"
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
git log -1 --pretty=format:"Commit now dated: %cD"
|
||||||
|
|
||||||
|
# git tag and push
|
||||||
|
git tag $INPUT_TAG -f
|
||||||
|
git push origin $INPUT_TAG -f
|
||||||
|
|
||||||
|
|
||||||
|
# - name: Debug Job
|
||||||
|
# if: ${{ failure() }}
|
||||||
|
# uses: mxschmitt/action-tmate@v3
|
||||||
|
# env:
|
||||||
|
# DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||||
|
# DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||||
|
# MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||||
|
# MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||||
|
# MSAAS_REPO_FOLDER: /tmp/msaas
|
||||||
|
# with:
|
||||||
|
# limit-access-to-actor: true
|
||||||
246
.github/workflows/patch-build.yaml
vendored
246
.github/workflows/patch-build.yaml
vendored
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
description: 'This workflow will build for patches for latest tag, and will Always use commit from main branch.'
|
|
||||||
inputs:
|
inputs:
|
||||||
services:
|
services:
|
||||||
description: 'Comma separated names of services to build(in small letters).'
|
description: 'Comma separated names of services to build(in small letters).'
|
||||||
|
|
@ -20,12 +19,20 @@ jobs:
|
||||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Rebase with main branch, to make sure the code has latest main changes
|
- name: Rebase with main branch, to make sure the code has latest main changes
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
git pull --rebase origin main
|
git remote -v
|
||||||
|
git config --global user.email "action@github.com"
|
||||||
|
git config --global user.name "GitHub Action"
|
||||||
|
git config --global rebase.autoStash true
|
||||||
|
git fetch origin main:main
|
||||||
|
git rebase main
|
||||||
|
git log -3
|
||||||
|
|
||||||
- name: Downloading yq
|
- name: Downloading yq
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -48,6 +55,8 @@ jobs:
|
||||||
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||||
|
|
||||||
- uses: depot/setup-action@v1
|
- uses: depot/setup-action@v1
|
||||||
|
env:
|
||||||
|
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||||
- name: Get HEAD Commit ID
|
- name: Get HEAD Commit ID
|
||||||
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
- name: Define Branch Name
|
- name: Define Branch Name
|
||||||
|
|
@ -65,78 +74,168 @@ jobs:
|
||||||
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||||
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||||
MSAAS_REPO_FOLDER: /tmp/msaas
|
MSAAS_REPO_FOLDER: /tmp/msaas
|
||||||
|
SERVICES_INPUT: ${{ github.event.inputs.services }}
|
||||||
run: |
|
run: |
|
||||||
set -exo pipefail
|
#!/bin/bash
|
||||||
git config --local user.email "action@github.com"
|
set -euo pipefail
|
||||||
git config --local user.name "GitHub Action"
|
|
||||||
git checkout -b $BRANCH_NAME
|
# Configuration
|
||||||
working_dir=$(pwd)
|
readonly WORKING_DIR=$(pwd)
|
||||||
function image_version(){
|
readonly BUILD_SCRIPT_NAME="build.sh"
|
||||||
local service=$1
|
readonly BACKEND_SERVICES_FILE="/tmp/backend.txt"
|
||||||
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
|
||||||
current_version=$(yq eval '.AppVersion' $chart_path)
|
# Initialize git configuration
|
||||||
new_version=$(echo $current_version | awk -F. '{$NF += 1 ; print $1"."$2"."$3}')
|
setup_git() {
|
||||||
echo $new_version
|
git config --local user.email "action@github.com"
|
||||||
# yq eval ".AppVersion = \"$new_version\"" -i $chart_path
|
git config --local user.name "GitHub Action"
|
||||||
|
git checkout -b "$BRANCH_NAME"
|
||||||
}
|
}
|
||||||
function clone_msaas() {
|
|
||||||
[ -d $MSAAS_REPO_FOLDER ] || {
|
# Get and increment image version
|
||||||
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
|
image_version() {
|
||||||
cd $MSAAS_REPO_FOLDER
|
local service=$1
|
||||||
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
|
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||||
git log -1
|
local current_version new_version
|
||||||
cd $MSAAS_REPO_FOLDER
|
|
||||||
bash git-init.sh
|
current_version=$(yq eval '.AppVersion' "$chart_path")
|
||||||
git checkout
|
new_version=$(echo "$current_version" | awk -F. '{$NF += 1; print $1"."$2"."$3}')
|
||||||
}
|
echo "$new_version"
|
||||||
}
|
}
|
||||||
function build_managed() {
|
|
||||||
local service=$1
|
# Clone MSAAS repository if not exists
|
||||||
local version=$2
|
clone_msaas() {
|
||||||
echo building managed
|
if [[ ! -d "$MSAAS_REPO_FOLDER" ]]; then
|
||||||
clone_msaas
|
git clone -b dev --recursive "https://x-access-token:${MSAAS_REPO_CLONE_TOKEN}@${MSAAS_REPO_URL}" "$MSAAS_REPO_FOLDER"
|
||||||
if [[ $service == 'chalice' ]]; then
|
cd "$MSAAS_REPO_FOLDER"
|
||||||
cd $MSAAS_REPO_FOLDER/openreplay/api
|
cd openreplay && git fetch origin && git checkout main
|
||||||
else
|
git log -1
|
||||||
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
cd "$MSAAS_REPO_FOLDER"
|
||||||
fi
|
bash git-init.sh
|
||||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
|
git checkout
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
# Checking for backend images
|
|
||||||
ls backend/cmd >> /tmp/backend.txt
|
# Build managed services
|
||||||
echo Services: "${{ github.event.inputs.services }}"
|
build_managed() {
|
||||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
local service=$1
|
||||||
BUILD_SCRIPT_NAME="build.sh"
|
local version=$2
|
||||||
# Build FOSS
|
|
||||||
for SERVICE in "${SERVICES[@]}"; do
|
echo "Building managed service: $service"
|
||||||
# Check if service is backend
|
clone_msaas
|
||||||
if grep -q $SERVICE /tmp/backend.txt; then
|
|
||||||
cd backend
|
if [[ $service == 'chalice' ]]; then
|
||||||
foss_build_args="nil $SERVICE"
|
cd "$MSAAS_REPO_FOLDER/openreplay/api"
|
||||||
ee_build_args="ee $SERVICE"
|
else
|
||||||
else
|
cd "$MSAAS_REPO_FOLDER/openreplay/$service"
|
||||||
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
|
fi
|
||||||
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
|
|
||||||
ee_build_args="ee"
|
local build_cmd="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh"
|
||||||
fi
|
|
||||||
version=$(image_version $SERVICE)
|
echo "Executing: $build_cmd"
|
||||||
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
if ! eval "$build_cmd" 2>&1; then
|
||||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
echo "Build failed for $service"
|
||||||
echo IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
exit 1
|
||||||
IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
fi
|
||||||
if [[ "$SERVICE" != "chalice" && "$SERVICE" != "frontend" ]]; then
|
}
|
||||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
|
||||||
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
# Build service with given arguments
|
||||||
else
|
build_service() {
|
||||||
build_managed $SERVICE $version
|
local service=$1
|
||||||
fi
|
local version=$2
|
||||||
cd $working_dir
|
local build_args=$3
|
||||||
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$SERVICE/Chart.yaml"
|
local build_script=${4:-$BUILD_SCRIPT_NAME}
|
||||||
yq eval ".AppVersion = \"$version\"" -i $chart_path
|
|
||||||
git add $chart_path
|
local command="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash $build_script $build_args"
|
||||||
git commit -m "Increment $SERVICE chart version"
|
echo "Executing: $command"
|
||||||
git push --set-upstream origin $BRANCH_NAME
|
eval "$command"
|
||||||
done
|
}
|
||||||
|
|
||||||
|
# Update chart version and commit changes
|
||||||
|
update_chart_version() {
|
||||||
|
local service=$1
|
||||||
|
local version=$2
|
||||||
|
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||||
|
|
||||||
|
# Ensure we're in the original working directory/repository
|
||||||
|
cd "$WORKING_DIR"
|
||||||
|
yq eval ".AppVersion = \"$version\"" -i "$chart_path"
|
||||||
|
git add "$chart_path"
|
||||||
|
git commit -m "Increment $service chart version to $version"
|
||||||
|
git push --set-upstream origin "$BRANCH_NAME"
|
||||||
|
cd -
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
setup_git
|
||||||
|
|
||||||
|
# Get backend services list
|
||||||
|
ls backend/cmd >"$BACKEND_SERVICES_FILE"
|
||||||
|
|
||||||
|
# Parse services input (fix for GitHub Actions syntax)
|
||||||
|
echo "Services: ${SERVICES_INPUT:-$1}"
|
||||||
|
IFS=',' read -ra services <<<"${SERVICES_INPUT:-$1}"
|
||||||
|
|
||||||
|
# Process each service
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
echo "Processing service: $service"
|
||||||
|
cd "$WORKING_DIR"
|
||||||
|
|
||||||
|
local foss_build_args="" ee_build_args="" build_script="$BUILD_SCRIPT_NAME"
|
||||||
|
|
||||||
|
# Determine build configuration based on service type
|
||||||
|
if grep -q "$service" "$BACKEND_SERVICES_FILE"; then
|
||||||
|
# Backend service
|
||||||
|
cd backend
|
||||||
|
foss_build_args="nil $service"
|
||||||
|
ee_build_args="ee $service"
|
||||||
|
else
|
||||||
|
# Non-backend service
|
||||||
|
case "$service" in
|
||||||
|
chalice | alerts | crons)
|
||||||
|
cd "$WORKING_DIR/api"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cd "$service"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Special build scripts for alerts/crons
|
||||||
|
if [[ $service == 'alerts' || $service == 'crons' ]]; then
|
||||||
|
build_script="build_${service}.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ee_build_args="ee"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get version and build
|
||||||
|
local version
|
||||||
|
version=$(image_version "$service")
|
||||||
|
|
||||||
|
# Build FOSS and EE versions
|
||||||
|
build_service "$service" "$version" "$foss_build_args"
|
||||||
|
build_service "$service" "${version}-ee" "$ee_build_args"
|
||||||
|
|
||||||
|
# Build managed version for specific services
|
||||||
|
if [[ "$service" != "chalice" && "$service" != "frontend" ]]; then
|
||||||
|
echo "Nothing to build in managed for service $service"
|
||||||
|
else
|
||||||
|
build_managed "$service" "$version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update chart and commit
|
||||||
|
update_chart_version "$service" "$version"
|
||||||
|
done
|
||||||
|
cd "$WORKING_DIR"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f "$BACKEND_SERVICES_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Working directory: $WORKING_DIR"
|
||||||
|
# Run main function with all arguments
|
||||||
|
main "$SERVICES_INPUT"
|
||||||
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
|
|
@ -147,8 +246,7 @@ jobs:
|
||||||
pr_title: "Updated patch build from main ${{ env.HEAD_COMMIT_ID }}"
|
pr_title: "Updated patch build from main ${{ env.HEAD_COMMIT_ID }}"
|
||||||
pr_body: |
|
pr_body: |
|
||||||
This PR updates the Helm chart version after building the patch from $HEAD_COMMIT_ID.
|
This PR updates the Helm chart version after building the patch from $HEAD_COMMIT_ID.
|
||||||
Once this PR is merged, To update the latest tag, run the following workflow.
|
Once this PR is merged, tag update job will run automatically.
|
||||||
https://github.com/openreplay/openreplay/actions/workflows/update-tag.yaml
|
|
||||||
|
|
||||||
# - name: Debug Job
|
# - name: Debug Job
|
||||||
# if: ${{ failure() }}
|
# if: ${{ failure() }}
|
||||||
|
|
|
||||||
47
.github/workflows/update-tag.yaml
vendored
47
.github/workflows/update-tag.yaml
vendored
|
|
@ -1,35 +1,42 @@
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
pull_request:
|
||||||
description: "This workflow will build for patches for latest tag, and will Always use commit from main branch."
|
types: [closed]
|
||||||
inputs:
|
branches:
|
||||||
services:
|
- main
|
||||||
description: "This action will update the latest tag with current main branch HEAD. Should I proceed ? true/false"
|
name: Release tag update --force
|
||||||
required: true
|
|
||||||
default: "false"
|
|
||||||
|
|
||||||
name: Force Push tag with main branch HEAD
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Build Patch from main
|
name: Build Patch from main
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || github.event.inputs.services == 'true' }}
|
||||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
|
||||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get latest release tag using GitHub API
|
||||||
|
id: get-latest-tag
|
||||||
|
run: |
|
||||||
|
LATEST_TAG=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://api.github.com/repos/${{ github.repository }}/releases/latest" \
|
||||||
|
| jq -r .tag_name)
|
||||||
|
|
||||||
|
# Fallback to git command if API doesn't return a tag
|
||||||
|
if [ "$LATEST_TAG" == "null" ] || [ -z "$LATEST_TAG" ]; then
|
||||||
|
echo "Not found latest tag"
|
||||||
|
exit 100
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
|
echo "Latest tag: $LATEST_TAG"
|
||||||
|
|
||||||
- name: Set Remote with GITHUB_TOKEN
|
- name: Set Remote with GITHUB_TOKEN
|
||||||
run: |
|
run: |
|
||||||
git config --unset http.https://github.com/.extraheader
|
git config --unset http.https://github.com/.extraheader
|
||||||
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
|
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
|
||||||
- name: Push main branch to tag
|
- name: Push main branch to tag
|
||||||
run: |
|
run: |
|
||||||
git fetch --tags
|
|
||||||
git checkout main
|
git checkout main
|
||||||
git push origin HEAD:refs/tags/$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1) --force
|
echo "Updating tag ${{ env.LATEST_TAG }} to point to latest commit on main"
|
||||||
# - name: Debug Job
|
git push origin HEAD:refs/tags/${{ env.LATEST_TAG }} --force
|
||||||
# if: ${{ failure() }}
|
|
||||||
# uses: mxschmitt/action-tmate@v3
|
|
||||||
# with:
|
|
||||||
# limit-access-to-actor: true
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ def __generic_query(typename, value_length=None):
|
||||||
ORDER BY value"""
|
ORDER BY value"""
|
||||||
|
|
||||||
if value_length is None or value_length > 2:
|
if value_length is None or value_length > 2:
|
||||||
return f"""(SELECT DISTINCT value, type
|
return f"""SELECT DISTINCT ON(value,type) value, type
|
||||||
|
((SELECT DISTINCT value, type
|
||||||
FROM {TABLE}
|
FROM {TABLE}
|
||||||
WHERE
|
WHERE
|
||||||
project_id = %(project_id)s
|
project_id = %(project_id)s
|
||||||
|
|
@ -101,7 +102,7 @@ def __generic_query(typename, value_length=None):
|
||||||
AND type='{typename.upper()}'
|
AND type='{typename.upper()}'
|
||||||
AND value ILIKE %(value)s
|
AND value ILIKE %(value)s
|
||||||
ORDER BY value
|
ORDER BY value
|
||||||
LIMIT 5);"""
|
LIMIT 5)) AS raw;"""
|
||||||
return f"""SELECT DISTINCT value, type
|
return f"""SELECT DISTINCT value, type
|
||||||
FROM {TABLE}
|
FROM {TABLE}
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -326,7 +327,7 @@ def __search_metadata(project_id, value, key=None, source=None):
|
||||||
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
|
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(cur.mogrify(f"""\
|
cur.execute(cur.mogrify(f"""\
|
||||||
SELECT key, value, 'METADATA' AS TYPE
|
SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
|
||||||
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
|
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
|
||||||
LIMIT 5;""", {"project_id": project_id, "value": helper.string_to_sql_like(value),
|
LIMIT 5;""", {"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||||
"svalue": helper.string_to_sql_like("^" + value)}))
|
"svalue": helper.string_to_sql_like("^" + value)}))
|
||||||
|
|
|
||||||
|
|
@ -338,14 +338,14 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
|
||||||
SELECT details.error_id as error_id,
|
SELECT details.error_id as error_id,
|
||||||
name, message, users, total,
|
name, message, users, total,
|
||||||
sessions, last_occurrence, first_occurrence, chart
|
sessions, last_occurrence, first_occurrence, chart
|
||||||
FROM (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
|
FROM (SELECT error_id,
|
||||||
JSONExtractString(toString(`$properties`), 'name') AS name,
|
JSONExtractString(toString(`$properties`), 'name') AS name,
|
||||||
JSONExtractString(toString(`$properties`), 'message') AS message,
|
JSONExtractString(toString(`$properties`), 'message') AS message,
|
||||||
COUNT(DISTINCT user_id) AS users,
|
COUNT(DISTINCT user_id) AS users,
|
||||||
COUNT(DISTINCT events.session_id) AS sessions,
|
COUNT(DISTINCT events.session_id) AS sessions,
|
||||||
MAX(created_at) AS max_datetime,
|
MAX(created_at) AS max_datetime,
|
||||||
MIN(created_at) AS min_datetime,
|
MIN(created_at) AS min_datetime,
|
||||||
COUNT(DISTINCT JSONExtractString(toString(`$properties`), 'error_id'))
|
COUNT(DISTINCT error_id)
|
||||||
OVER() AS total
|
OVER() AS total
|
||||||
FROM {MAIN_EVENTS_TABLE} AS events
|
FROM {MAIN_EVENTS_TABLE} AS events
|
||||||
INNER JOIN (SELECT session_id, coalesce(user_id,toString(user_uuid)) AS user_id
|
INNER JOIN (SELECT session_id, coalesce(user_id,toString(user_uuid)) AS user_id
|
||||||
|
|
@ -357,7 +357,7 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
|
||||||
GROUP BY error_id, name, message
|
GROUP BY error_id, name, message
|
||||||
ORDER BY {sort} {order}
|
ORDER BY {sort} {order}
|
||||||
LIMIT %(errors_limit)s OFFSET %(errors_offset)s) AS details
|
LIMIT %(errors_limit)s OFFSET %(errors_offset)s) AS details
|
||||||
INNER JOIN (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
|
INNER JOIN (SELECT error_id,
|
||||||
toUnixTimestamp(MAX(created_at))*1000 AS last_occurrence,
|
toUnixTimestamp(MAX(created_at))*1000 AS last_occurrence,
|
||||||
toUnixTimestamp(MIN(created_at))*1000 AS first_occurrence
|
toUnixTimestamp(MIN(created_at))*1000 AS first_occurrence
|
||||||
FROM {MAIN_EVENTS_TABLE}
|
FROM {MAIN_EVENTS_TABLE}
|
||||||
|
|
@ -366,7 +366,7 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
|
||||||
GROUP BY error_id) AS time_details
|
GROUP BY error_id) AS time_details
|
||||||
ON details.error_id=time_details.error_id
|
ON details.error_id=time_details.error_id
|
||||||
INNER JOIN (SELECT error_id, groupArray([timestamp, count]) AS chart
|
INNER JOIN (SELECT error_id, groupArray([timestamp, count]) AS chart
|
||||||
FROM (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
|
FROM (SELECT error_id,
|
||||||
gs.generate_series AS timestamp,
|
gs.generate_series AS timestamp,
|
||||||
COUNT(DISTINCT session_id) AS count
|
COUNT(DISTINCT session_id) AS count
|
||||||
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS gs
|
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS gs
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
"""SELECT username, token, url
|
"""SELECT username, token, url
|
||||||
FROM public.jira_cloud
|
FROM public.jira_cloud
|
||||||
WHERE user_id=%(user_id)s;""",
|
WHERE user_id = %(user_id)s;""",
|
||||||
{"user_id": self._user_id})
|
{"user_id": self._user_id})
|
||||||
)
|
)
|
||||||
data = helper.dict_to_camel_case(cur.fetchone())
|
data = helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
@ -95,10 +95,9 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
def add(self, username, token, url, obfuscate=False):
|
def add(self, username, token, url, obfuscate=False):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify("""\
|
cur.mogrify(""" \
|
||||||
INSERT INTO public.jira_cloud(username, token, user_id,url)
|
INSERT INTO public.jira_cloud(username, token, user_id, url)
|
||||||
VALUES (%(username)s, %(token)s, %(user_id)s,%(url)s)
|
VALUES (%(username)s, %(token)s, %(user_id)s, %(url)s) RETURNING username, token, url;""",
|
||||||
RETURNING username, token, url;""",
|
|
||||||
{"user_id": self._user_id, "username": username,
|
{"user_id": self._user_id, "username": username,
|
||||||
"token": token, "url": url})
|
"token": token, "url": url})
|
||||||
)
|
)
|
||||||
|
|
@ -112,9 +111,10 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
def delete(self):
|
def delete(self):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify("""\
|
cur.mogrify(""" \
|
||||||
DELETE FROM public.jira_cloud
|
DELETE
|
||||||
WHERE user_id=%(user_id)s;""",
|
FROM public.jira_cloud
|
||||||
|
WHERE user_id = %(user_id)s;""",
|
||||||
{"user_id": self._user_id})
|
{"user_id": self._user_id})
|
||||||
)
|
)
|
||||||
return {"state": "success"}
|
return {"state": "success"}
|
||||||
|
|
@ -125,7 +125,7 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
changes={
|
changes={
|
||||||
"username": data.username,
|
"username": data.username,
|
||||||
"token": data.token if len(data.token) > 0 and data.token.find("***") == -1 \
|
"token": data.token if len(data.token) > 0 and data.token.find("***") == -1 \
|
||||||
else self.integration.token,
|
else self.integration["token"],
|
||||||
"url": str(data.url)
|
"url": str(data.url)
|
||||||
},
|
},
|
||||||
obfuscate=True
|
obfuscate=True
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,9 @@ def __complete_missing_steps(start_time, end_time, density, neutral, rows, time_
|
||||||
# compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22)
|
# compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22)
|
||||||
# if start-point is selected, the selected event is ranked n°1
|
# if start-point is selected, the selected event is ranked n°1
|
||||||
def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
||||||
|
if not data.hide_excess:
|
||||||
|
data.hide_excess = True
|
||||||
|
data.rows = 50
|
||||||
sub_events = []
|
sub_events = []
|
||||||
start_points_conditions = []
|
start_points_conditions = []
|
||||||
step_0_conditions = []
|
step_0_conditions = []
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
"operator": e.operator,
|
"operator": e.operator,
|
||||||
"filters": []
|
"filters": e.filters
|
||||||
})
|
})
|
||||||
for v in e.value:
|
for v in e.value:
|
||||||
if v not in extra_conditions[e.operator].value:
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
|
@ -178,7 +178,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
"operator": e.operator,
|
"operator": e.operator,
|
||||||
"filters": []
|
"filters": e.filters
|
||||||
})
|
})
|
||||||
for v in e.value:
|
for v in e.value:
|
||||||
if v not in extra_conditions[e.operator].value:
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
|
@ -1108,8 +1108,12 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
is_any = sh.isAny_opreator(f.operator)
|
is_any = sh.isAny_opreator(f.operator)
|
||||||
if is_any or len(f.value) == 0:
|
if is_any or len(f.value) == 0:
|
||||||
continue
|
continue
|
||||||
|
is_negative_operator = sh.is_negation_operator(f.operator)
|
||||||
f.value = helper.values_for_operator(value=f.value, op=f.operator)
|
f.value = helper.values_for_operator(value=f.value, op=f.operator)
|
||||||
op = sh.get_sql_operator(f.operator)
|
op = sh.get_sql_operator(f.operator)
|
||||||
|
r_op = ""
|
||||||
|
if is_negative_operator:
|
||||||
|
r_op = sh.reverse_sql_operator(op)
|
||||||
e_k_f = e_k + f"_fetch{j}"
|
e_k_f = e_k + f"_fetch{j}"
|
||||||
full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)}
|
full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)}
|
||||||
if f.type == schemas.FetchFilterType.FETCH_URL:
|
if f.type == schemas.FetchFilterType.FETCH_URL:
|
||||||
|
|
@ -1118,6 +1122,12 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
))
|
))
|
||||||
events_conditions[-1]["condition"].append(event_where[-1])
|
events_conditions[-1]["condition"].append(event_where[-1])
|
||||||
apply = True
|
apply = True
|
||||||
|
if is_negative_operator:
|
||||||
|
events_conditions_not.append(
|
||||||
|
{
|
||||||
|
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||||
|
events_conditions_not[-1]["condition"] = sh.multi_conditions(
|
||||||
|
f"sub.`$properties`.url_path {r_op} %({e_k_f})s", f.value, value_key=e_k_f)
|
||||||
elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE:
|
elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE:
|
||||||
event_where.append(json_condition(
|
event_where.append(json_condition(
|
||||||
"main", "$properties", 'status', op, f.value, e_k_f, True, True
|
"main", "$properties", 'status', op, f.value, e_k_f, True, True
|
||||||
|
|
@ -1130,6 +1140,13 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
))
|
))
|
||||||
events_conditions[-1]["condition"].append(event_where[-1])
|
events_conditions[-1]["condition"].append(event_where[-1])
|
||||||
apply = True
|
apply = True
|
||||||
|
if is_negative_operator:
|
||||||
|
events_conditions_not.append(
|
||||||
|
{
|
||||||
|
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||||
|
events_conditions_not[-1]["condition"] = sh.multi_conditions(
|
||||||
|
f"sub.`$properties`.method {r_op} %({e_k_f})s", f.value,
|
||||||
|
value_key=e_k_f)
|
||||||
elif f.type == schemas.FetchFilterType.FETCH_DURATION:
|
elif f.type == schemas.FetchFilterType.FETCH_DURATION:
|
||||||
event_where.append(
|
event_where.append(
|
||||||
sh.multi_conditions(f"main.`$duration_s` {f.operator} %({e_k_f})s/1000", f.value,
|
sh.multi_conditions(f"main.`$duration_s` {f.operator} %({e_k_f})s/1000", f.value,
|
||||||
|
|
@ -1142,12 +1159,26 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
))
|
))
|
||||||
events_conditions[-1]["condition"].append(event_where[-1])
|
events_conditions[-1]["condition"].append(event_where[-1])
|
||||||
apply = True
|
apply = True
|
||||||
|
if is_negative_operator:
|
||||||
|
events_conditions_not.append(
|
||||||
|
{
|
||||||
|
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||||
|
events_conditions_not[-1]["condition"] = sh.multi_conditions(
|
||||||
|
f"sub.`$properties`.request_body {r_op} %({e_k_f})s", f.value,
|
||||||
|
value_key=e_k_f)
|
||||||
elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY:
|
elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY:
|
||||||
event_where.append(json_condition(
|
event_where.append(json_condition(
|
||||||
"main", "$properties", 'response_body', op, f.value, e_k_f
|
"main", "$properties", 'response_body', op, f.value, e_k_f
|
||||||
))
|
))
|
||||||
events_conditions[-1]["condition"].append(event_where[-1])
|
events_conditions[-1]["condition"].append(event_where[-1])
|
||||||
apply = True
|
apply = True
|
||||||
|
if is_negative_operator:
|
||||||
|
events_conditions_not.append(
|
||||||
|
{
|
||||||
|
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||||
|
events_conditions_not[-1]["condition"] = sh.multi_conditions(
|
||||||
|
f"sub.`$properties`.response_body {r_op} %({e_k_f})s", f.value,
|
||||||
|
value_key=e_k_f)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"undefined FETCH filter: {f.type}")
|
logging.warning(f"undefined FETCH filter: {f.type}")
|
||||||
if not apply:
|
if not apply:
|
||||||
|
|
@ -1395,17 +1426,30 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
if extra_conditions and len(extra_conditions) > 0:
|
if extra_conditions and len(extra_conditions) > 0:
|
||||||
_extra_or_condition = []
|
_extra_or_condition = []
|
||||||
for i, c in enumerate(extra_conditions):
|
for i, c in enumerate(extra_conditions):
|
||||||
if sh.isAny_opreator(c.operator):
|
if sh.isAny_opreator(c.operator) and c.type != schemas.EventType.REQUEST_DETAILS.value:
|
||||||
continue
|
continue
|
||||||
e_k = f"ec_value{i}"
|
e_k = f"ec_value{i}"
|
||||||
op = sh.get_sql_operator(c.operator)
|
op = sh.get_sql_operator(c.operator)
|
||||||
c.value = helper.values_for_operator(value=c.value, op=c.operator)
|
c.value = helper.values_for_operator(value=c.value, op=c.operator)
|
||||||
full_args = {**full_args,
|
full_args = {**full_args,
|
||||||
**sh.multi_values(c.value, value_key=e_k)}
|
**sh.multi_values(c.value, value_key=e_k)}
|
||||||
if c.type == events.EventType.LOCATION.ui_type:
|
if c.type in (schemas.EventType.LOCATION.value, schemas.EventType.REQUEST.value):
|
||||||
_extra_or_condition.append(
|
_extra_or_condition.append(
|
||||||
sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s",
|
sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s",
|
||||||
c.value, value_key=e_k))
|
c.value, value_key=e_k))
|
||||||
|
elif c.type == schemas.EventType.REQUEST_DETAILS.value:
|
||||||
|
for j, c_f in enumerate(c.filters):
|
||||||
|
if sh.isAny_opreator(c_f.operator) or len(c_f.value) == 0:
|
||||||
|
continue
|
||||||
|
e_k += f"_{j}"
|
||||||
|
op = sh.get_sql_operator(c_f.operator)
|
||||||
|
c_f.value = helper.values_for_operator(value=c_f.value, op=c_f.operator)
|
||||||
|
full_args = {**full_args,
|
||||||
|
**sh.multi_values(c_f.value, value_key=e_k)}
|
||||||
|
if c_f.type == schemas.FetchFilterType.FETCH_URL.value:
|
||||||
|
_extra_or_condition.append(
|
||||||
|
sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s",
|
||||||
|
c_f.value, value_key=e_k))
|
||||||
else:
|
else:
|
||||||
logging.warning(f"unsupported extra_event type:${c.type}")
|
logging.warning(f"unsupported extra_event type:${c.type}")
|
||||||
if len(_extra_or_condition) > 0:
|
if len(_extra_or_condition) > 0:
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
"operator": e.operator,
|
"operator": e.operator,
|
||||||
"filters": []
|
"filters": e.filters
|
||||||
})
|
})
|
||||||
for v in e.value:
|
for v in e.value:
|
||||||
if v not in extra_conditions[e.operator].value:
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
|
@ -165,7 +165,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
"operator": e.operator,
|
"operator": e.operator,
|
||||||
"filters": []
|
"filters": e.filters
|
||||||
})
|
})
|
||||||
for v in e.value:
|
for v in e.value:
|
||||||
if v not in extra_conditions[e.operator].value:
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
|
@ -989,7 +989,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
|
||||||
sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s",
|
sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s",
|
||||||
c.value, value_key=e_k))
|
c.value, value_key=e_k))
|
||||||
else:
|
else:
|
||||||
logger.warning(f"unsupported extra_event type:${c.type}")
|
logger.warning(f"unsupported extra_event type: {c.type}")
|
||||||
if len(_extra_or_condition) > 0:
|
if len(_extra_or_condition) > 0:
|
||||||
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
|
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
|
||||||
query_part = f"""\
|
query_part = f"""\
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,10 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
||||||
sort = 'session_id'
|
sort = 'session_id'
|
||||||
if data.sort is not None and data.sort != "session_id":
|
if data.sort is not None and data.sort != "session_id":
|
||||||
# sort += " " + data.order + "," + helper.key_to_snake_case(data.sort)
|
# sort += " " + data.order + "," + helper.key_to_snake_case(data.sort)
|
||||||
sort = helper.key_to_snake_case(data.sort)
|
if data.sort == 'datetime':
|
||||||
|
sort = 'start_ts'
|
||||||
|
else:
|
||||||
|
sort = helper.key_to_snake_case(data.sort)
|
||||||
|
|
||||||
meta_keys = metadata.get(project_id=project.project_id)
|
meta_keys = metadata.get(project_id=project.project_id)
|
||||||
main_query = cur.mogrify(f"""SELECT COUNT(full_sessions) AS count,
|
main_query = cur.mogrify(f"""SELECT COUNT(full_sessions) AS count,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ if config("CH_COMPRESSION", cast=bool, default=True):
|
||||||
def transform_result(self, original_function):
|
def transform_result(self, original_function):
|
||||||
@wraps(original_function)
|
@wraps(original_function)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
logger.debug(str.encode(self.format(query=kwargs.get("query", ""), parameters=kwargs.get("parameters"))))
|
if kwargs.get("parameters"):
|
||||||
|
logger.debug(str.encode(self.format(query=kwargs.get("query", ""), parameters=kwargs.get("parameters"))))
|
||||||
|
elif len(args) > 0:
|
||||||
|
logger.debug(str.encode(args[0]))
|
||||||
result = original_function(*args, **kwargs)
|
result = original_function(*args, **kwargs)
|
||||||
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
|
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
|
||||||
column_names = result.column_names
|
column_names = result.column_names
|
||||||
|
|
@ -146,13 +149,11 @@ class ClickHouseClient:
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self.__client
|
return self.__client
|
||||||
|
|
||||||
def format(self, query, *, parameters=None):
|
def format(self, query, parameters=None):
|
||||||
if parameters is None:
|
if parameters:
|
||||||
return query
|
ctx = QueryContext(query=query, parameters=parameters)
|
||||||
return query % {
|
return ctx.final_query
|
||||||
key: f"'{value}'" if isinstance(value, str) else value
|
return query
|
||||||
for key, value in parameters.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
if config('CH_POOL', cast=bool, default=True):
|
if config('CH_POOL', cast=bool, default=True):
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,41 @@ import schemas
|
||||||
|
|
||||||
|
|
||||||
def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator, schemas.MathOperator]):
|
def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator, schemas.MathOperator]):
|
||||||
|
if isinstance(op, Enum):
|
||||||
|
op = op.value
|
||||||
return {
|
return {
|
||||||
schemas.SearchEventOperator.IS: "=",
|
schemas.SearchEventOperator.IS.value: "=",
|
||||||
schemas.SearchEventOperator.ON: "=",
|
schemas.SearchEventOperator.ON.value: "=",
|
||||||
schemas.SearchEventOperator.ON_ANY: "IN",
|
schemas.SearchEventOperator.ON_ANY.value: "IN",
|
||||||
schemas.SearchEventOperator.IS_NOT: "!=",
|
schemas.SearchEventOperator.IS_NOT.value: "!=",
|
||||||
schemas.SearchEventOperator.NOT_ON: "!=",
|
schemas.SearchEventOperator.NOT_ON.value: "!=",
|
||||||
schemas.SearchEventOperator.CONTAINS: "ILIKE",
|
schemas.SearchEventOperator.CONTAINS.value: "ILIKE",
|
||||||
schemas.SearchEventOperator.NOT_CONTAINS: "NOT ILIKE",
|
schemas.SearchEventOperator.NOT_CONTAINS.value: "NOT ILIKE",
|
||||||
schemas.SearchEventOperator.STARTS_WITH: "ILIKE",
|
schemas.SearchEventOperator.STARTS_WITH.value: "ILIKE",
|
||||||
schemas.SearchEventOperator.ENDS_WITH: "ILIKE",
|
schemas.SearchEventOperator.ENDS_WITH.value: "ILIKE",
|
||||||
# Selector operators:
|
# Selector operators:
|
||||||
schemas.ClickEventExtraOperator.IS: "=",
|
schemas.ClickEventExtraOperator.IS.value: "=",
|
||||||
schemas.ClickEventExtraOperator.IS_NOT: "!=",
|
schemas.ClickEventExtraOperator.IS_NOT.value: "!=",
|
||||||
schemas.ClickEventExtraOperator.CONTAINS: "ILIKE",
|
schemas.ClickEventExtraOperator.CONTAINS.value: "ILIKE",
|
||||||
schemas.ClickEventExtraOperator.NOT_CONTAINS: "NOT ILIKE",
|
schemas.ClickEventExtraOperator.NOT_CONTAINS.value: "NOT ILIKE",
|
||||||
schemas.ClickEventExtraOperator.STARTS_WITH: "ILIKE",
|
schemas.ClickEventExtraOperator.STARTS_WITH.value: "ILIKE",
|
||||||
schemas.ClickEventExtraOperator.ENDS_WITH: "ILIKE",
|
schemas.ClickEventExtraOperator.ENDS_WITH.value: "ILIKE",
|
||||||
|
|
||||||
schemas.MathOperator.GREATER: ">",
|
schemas.MathOperator.GREATER.value: ">",
|
||||||
schemas.MathOperator.GREATER_EQ: ">=",
|
schemas.MathOperator.GREATER_EQ.value: ">=",
|
||||||
schemas.MathOperator.LESS: "<",
|
schemas.MathOperator.LESS.value: "<",
|
||||||
schemas.MathOperator.LESS_EQ: "<=",
|
schemas.MathOperator.LESS_EQ.value: "<=",
|
||||||
}.get(op, "=")
|
}.get(op, "=")
|
||||||
|
|
||||||
|
|
||||||
def is_negation_operator(op: schemas.SearchEventOperator):
|
def is_negation_operator(op: schemas.SearchEventOperator):
|
||||||
return op in [schemas.SearchEventOperator.IS_NOT,
|
if isinstance(op, Enum):
|
||||||
schemas.SearchEventOperator.NOT_ON,
|
op = op.value
|
||||||
schemas.SearchEventOperator.NOT_CONTAINS,
|
return op in [schemas.SearchEventOperator.IS_NOT.value,
|
||||||
schemas.ClickEventExtraOperator.IS_NOT,
|
schemas.SearchEventOperator.NOT_ON.value,
|
||||||
schemas.ClickEventExtraOperator.NOT_CONTAINS]
|
schemas.SearchEventOperator.NOT_CONTAINS.value,
|
||||||
|
schemas.ClickEventExtraOperator.IS_NOT.value,
|
||||||
|
schemas.ClickEventExtraOperator.NOT_CONTAINS.value]
|
||||||
|
|
||||||
|
|
||||||
def reverse_sql_operator(op):
|
def reverse_sql_operator(op):
|
||||||
|
|
|
||||||
|
|
@ -1,591 +0,0 @@
|
||||||
-- -- Original Q3
|
|
||||||
-- WITH ranked_events AS (SELECT *
|
|
||||||
-- FROM ranked_events_1736344377403),
|
|
||||||
-- n1 AS (SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- COUNT(1) AS sessions_count
|
|
||||||
-- FROM ranked_events
|
|
||||||
-- WHERE event_number_in_session = 1
|
|
||||||
-- AND isNotNull(next_value)
|
|
||||||
-- GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
-- ORDER BY sessions_count DESC
|
|
||||||
-- LIMIT 8),
|
|
||||||
-- n2 AS (SELECT *
|
|
||||||
-- FROM (SELECT re.event_number_in_session AS event_number_in_session,
|
|
||||||
-- re.event_type AS event_type,
|
|
||||||
-- re.e_value AS e_value,
|
|
||||||
-- re.next_type AS next_type,
|
|
||||||
-- re.next_value AS next_value,
|
|
||||||
-- COUNT(1) AS sessions_count
|
|
||||||
-- FROM n1
|
|
||||||
-- INNER JOIN ranked_events AS re
|
|
||||||
-- ON (n1.next_value = re.e_value AND n1.next_type = re.event_type)
|
|
||||||
-- WHERE re.event_number_in_session = 2
|
|
||||||
-- GROUP BY re.event_number_in_session, re.event_type, re.e_value, re.next_type,
|
|
||||||
-- re.next_value) AS sub_level
|
|
||||||
-- ORDER BY sessions_count DESC
|
|
||||||
-- LIMIT 8),
|
|
||||||
-- n3 AS (SELECT *
|
|
||||||
-- FROM (SELECT re.event_number_in_session AS event_number_in_session,
|
|
||||||
-- re.event_type AS event_type,
|
|
||||||
-- re.e_value AS e_value,
|
|
||||||
-- re.next_type AS next_type,
|
|
||||||
-- re.next_value AS next_value,
|
|
||||||
-- COUNT(1) AS sessions_count
|
|
||||||
-- FROM n2
|
|
||||||
-- INNER JOIN ranked_events AS re
|
|
||||||
-- ON (n2.next_value = re.e_value AND n2.next_type = re.event_type)
|
|
||||||
-- WHERE re.event_number_in_session = 3
|
|
||||||
-- GROUP BY re.event_number_in_session, re.event_type, re.e_value, re.next_type,
|
|
||||||
-- re.next_value) AS sub_level
|
|
||||||
-- ORDER BY sessions_count DESC
|
|
||||||
-- LIMIT 8),
|
|
||||||
-- n4 AS (SELECT *
|
|
||||||
-- FROM (SELECT re.event_number_in_session AS event_number_in_session,
|
|
||||||
-- re.event_type AS event_type,
|
|
||||||
-- re.e_value AS e_value,
|
|
||||||
-- re.next_type AS next_type,
|
|
||||||
-- re.next_value AS next_value,
|
|
||||||
-- COUNT(1) AS sessions_count
|
|
||||||
-- FROM n3
|
|
||||||
-- INNER JOIN ranked_events AS re
|
|
||||||
-- ON (n3.next_value = re.e_value AND n3.next_type = re.event_type)
|
|
||||||
-- WHERE re.event_number_in_session = 4
|
|
||||||
-- GROUP BY re.event_number_in_session, re.event_type, re.e_value, re.next_type,
|
|
||||||
-- re.next_value) AS sub_level
|
|
||||||
-- ORDER BY sessions_count DESC
|
|
||||||
-- LIMIT 8),
|
|
||||||
-- n5 AS (SELECT *
|
|
||||||
-- FROM (SELECT re.event_number_in_session AS event_number_in_session,
|
|
||||||
-- re.event_type AS event_type,
|
|
||||||
-- re.e_value AS e_value,
|
|
||||||
-- re.next_type AS next_type,
|
|
||||||
-- re.next_value AS next_value,
|
|
||||||
-- COUNT(1) AS sessions_count
|
|
||||||
-- FROM n4
|
|
||||||
-- INNER JOIN ranked_events AS re
|
|
||||||
-- ON (n4.next_value = re.e_value AND n4.next_type = re.event_type)
|
|
||||||
-- WHERE re.event_number_in_session = 5
|
|
||||||
-- GROUP BY re.event_number_in_session, re.event_type, re.e_value, re.next_type,
|
|
||||||
-- re.next_value) AS sub_level
|
|
||||||
-- ORDER BY sessions_count DESC
|
|
||||||
-- LIMIT 8)
|
|
||||||
-- SELECT *
|
|
||||||
-- FROM (SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- sessions_count
|
|
||||||
-- FROM n1
|
|
||||||
-- UNION ALL
|
|
||||||
-- SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- sessions_count
|
|
||||||
-- FROM n2
|
|
||||||
-- UNION ALL
|
|
||||||
-- SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- sessions_count
|
|
||||||
-- FROM n3
|
|
||||||
-- UNION ALL
|
|
||||||
-- SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- sessions_count
|
|
||||||
-- FROM n4
|
|
||||||
-- UNION ALL
|
|
||||||
-- SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- next_type,
|
|
||||||
-- next_value,
|
|
||||||
-- sessions_count
|
|
||||||
-- FROM n5) AS chart_steps
|
|
||||||
-- ORDER BY event_number_in_session;
|
|
||||||
|
|
||||||
-- Q1
|
|
||||||
-- CREATE TEMPORARY TABLE pre_ranked_events_1736344377403 AS
|
|
||||||
CREATE TABLE pre_ranked_events_1736344377403 ENGINE = Memory AS
|
|
||||||
(WITH initial_event AS (SELECT events.session_id, MIN(datetime) AS start_event_timestamp
|
|
||||||
FROM experimental.events AS events
|
|
||||||
WHERE ((event_type = 'LOCATION' AND (url_path = '/en/deployment/')))
|
|
||||||
AND events.project_id = toUInt16(65)
|
|
||||||
AND events.datetime >= toDateTime(1735599600000 / 1000)
|
|
||||||
AND events.datetime < toDateTime(1736290799999 / 1000)
|
|
||||||
GROUP BY 1),
|
|
||||||
pre_ranked_events AS (SELECT *
|
|
||||||
FROM (SELECT session_id,
|
|
||||||
event_type,
|
|
||||||
datetime,
|
|
||||||
url_path AS e_value,
|
|
||||||
row_number() OVER (PARTITION BY session_id
|
|
||||||
ORDER BY datetime ,
|
|
||||||
message_id ) AS event_number_in_session
|
|
||||||
FROM experimental.events AS events
|
|
||||||
INNER JOIN initial_event ON (events.session_id = initial_event.session_id)
|
|
||||||
WHERE events.project_id = toUInt16(65)
|
|
||||||
AND events.datetime >= toDateTime(1735599600000 / 1000)
|
|
||||||
AND events.datetime < toDateTime(1736290799999 / 1000)
|
|
||||||
AND (events.event_type = 'LOCATION')
|
|
||||||
AND events.datetime >= initial_event.start_event_timestamp
|
|
||||||
) AS full_ranked_events
|
|
||||||
WHERE event_number_in_session <= 5)
|
|
||||||
SELECT *
|
|
||||||
FROM pre_ranked_events);
|
|
||||||
;
|
|
||||||
|
|
||||||
SELECT *
|
|
||||||
FROM pre_ranked_events_1736344377403
|
|
||||||
WHERE event_number_in_session < 3;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- ---------Q2-----------
|
|
||||||
-- CREATE TEMPORARY TABLE ranked_events_1736344377403 AS
|
|
||||||
DROP TABLE ranked_events_1736344377403;
|
|
||||||
CREATE TABLE ranked_events_1736344377403 ENGINE = Memory AS
|
|
||||||
(WITH pre_ranked_events AS (SELECT *
|
|
||||||
FROM pre_ranked_events_1736344377403),
|
|
||||||
start_points AS (SELECT DISTINCT session_id
|
|
||||||
FROM pre_ranked_events
|
|
||||||
WHERE ((event_type = 'LOCATION' AND (e_value = '/en/deployment/')))
|
|
||||||
AND pre_ranked_events.event_number_in_session = 1),
|
|
||||||
ranked_events AS (SELECT pre_ranked_events.*,
|
|
||||||
leadInFrame(e_value)
|
|
||||||
OVER (PARTITION BY session_id ORDER BY datetime
|
|
||||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_value,
|
|
||||||
leadInFrame(toNullable(event_type))
|
|
||||||
OVER (PARTITION BY session_id ORDER BY datetime
|
|
||||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_type
|
|
||||||
FROM start_points
|
|
||||||
INNER JOIN pre_ranked_events USING (session_id))
|
|
||||||
SELECT *
|
|
||||||
FROM ranked_events);
|
|
||||||
|
|
||||||
|
|
||||||
-- ranked events
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events_1736344377403
|
|
||||||
WHERE event_number_in_session = 2
|
|
||||||
-- AND e_value='/en/deployment/deploy-docker/'
|
|
||||||
-- AND next_value NOT IN ('/en/deployment/','/en/plugins/','/en/using-or/')
|
|
||||||
-- AND e_value NOT IN ('/en/deployment/deploy-docker/','/en/getting-started/','/en/deployment/deploy-ubuntu/')
|
|
||||||
AND isNotNull(next_value)
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY event_number_in_session, sessions_count DESC;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events_1736344377403
|
|
||||||
WHERE event_number_in_session = 1
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
ORDER BY event_number_in_session, sessions_count DESC;
|
|
||||||
|
|
||||||
SELECT COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events_1736344377403
|
|
||||||
WHERE event_number_in_session = 2
|
|
||||||
AND isNull(next_value)
|
|
||||||
;
|
|
||||||
|
|
||||||
-- ---------Q3 MORE -----------
|
|
||||||
WITH ranked_events AS (SELECT *
|
|
||||||
FROM ranked_events_1736344377403),
|
|
||||||
n1 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 1
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
n2 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 2
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
n3 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 3
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
drop_n AS (-- STEP 1
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
'DROP' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n1
|
|
||||||
WHERE isNull(n1.next_type)
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 2
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
'DROP' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n2
|
|
||||||
WHERE isNull(n2.next_type)),
|
|
||||||
-- TODO: make this as top_steps, where every step will go to next as top/others
|
|
||||||
top_n1 AS (-- STEP 1
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n1
|
|
||||||
WHERE isNotNull(next_type)
|
|
||||||
ORDER BY sessions_count DESC
|
|
||||||
LIMIT 3),
|
|
||||||
top_n2 AS (-- STEP 2
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n2
|
|
||||||
WHERE (event_type, e_value) IN (SELECT event_type,
|
|
||||||
e_value
|
|
||||||
FROM n2
|
|
||||||
WHERE isNotNull(next_type)
|
|
||||||
GROUP BY event_type, e_value
|
|
||||||
ORDER BY SUM(sessions_count) DESC
|
|
||||||
LIMIT 3)
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
top_n AS (SELECT *
|
|
||||||
FROM top_n1
|
|
||||||
UNION ALL
|
|
||||||
SELECT *
|
|
||||||
FROM top_n2),
|
|
||||||
u_top_n AS (SELECT DISTINCT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value
|
|
||||||
FROM top_n),
|
|
||||||
others_n AS (
|
|
||||||
-- STEP 1
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n1
|
|
||||||
WHERE isNotNull(next_type)
|
|
||||||
ORDER BY sessions_count DESC
|
|
||||||
LIMIT 1000000 OFFSET 3
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 2
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n2
|
|
||||||
WHERE isNotNull(next_type)
|
|
||||||
-- GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
ORDER BY sessions_count DESC
|
|
||||||
LIMIT 1000000 OFFSET 3)
|
|
||||||
SELECT *
|
|
||||||
FROM (
|
|
||||||
-- Top
|
|
||||||
SELECT *
|
|
||||||
FROM top_n
|
|
||||||
-- UNION ALL
|
|
||||||
-- -- Others
|
|
||||||
-- SELECT event_number_in_session,
|
|
||||||
-- event_type,
|
|
||||||
-- e_value,
|
|
||||||
-- 'OTHER' AS next_type,
|
|
||||||
-- NULL AS next_value,
|
|
||||||
-- SUM(sessions_count)
|
|
||||||
-- FROM others_n
|
|
||||||
-- GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
-- UNION ALL
|
|
||||||
-- -- Top go to Drop
|
|
||||||
-- SELECT drop_n.event_number_in_session,
|
|
||||||
-- drop_n.event_type,
|
|
||||||
-- drop_n.e_value,
|
|
||||||
-- drop_n.next_type,
|
|
||||||
-- drop_n.next_value,
|
|
||||||
-- drop_n.sessions_count
|
|
||||||
-- FROM drop_n
|
|
||||||
-- INNER JOIN u_top_n ON (drop_n.event_number_in_session = u_top_n.event_number_in_session
|
|
||||||
-- AND drop_n.event_type = u_top_n.event_type
|
|
||||||
-- AND drop_n.e_value = u_top_n.e_value)
|
|
||||||
-- ORDER BY drop_n.event_number_in_session
|
|
||||||
-- -- -- UNION ALL
|
|
||||||
-- -- -- Top go to Others
|
|
||||||
-- SELECT top_n.event_number_in_session,
|
|
||||||
-- top_n.event_type,
|
|
||||||
-- top_n.e_value,
|
|
||||||
-- 'OTHER' AS next_type,
|
|
||||||
-- NULL AS next_value,
|
|
||||||
-- SUM(top_n.sessions_count) AS sessions_count
|
|
||||||
-- FROM top_n
|
|
||||||
-- LEFT JOIN others_n ON (others_n.event_number_in_session = (top_n.event_number_in_session + 1)
|
|
||||||
-- AND top_n.next_type = others_n.event_type
|
|
||||||
-- AND top_n.next_value = others_n.e_value)
|
|
||||||
-- WHERE others_n.event_number_in_session IS NULL
|
|
||||||
-- AND top_n.next_type IS NOT NULL
|
|
||||||
-- GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
-- UNION ALL
|
|
||||||
-- -- Others got to Top
|
|
||||||
-- SELECT others_n.event_number_in_session,
|
|
||||||
-- 'OTHER' AS event_type,
|
|
||||||
-- NULL AS e_value,
|
|
||||||
-- others_n.s_next_type AS next_type,
|
|
||||||
-- others_n.s_next_value AS next_value,
|
|
||||||
-- SUM(sessions_count) AS sessions_count
|
|
||||||
-- FROM others_n
|
|
||||||
-- INNER JOIN top_n ON (others_n.event_number_in_session = top_n.event_number_in_session + 1 AND
|
|
||||||
-- others_n.s_next_type = top_n.event_type AND
|
|
||||||
-- others_n.s_next_value = top_n.event_type)
|
|
||||||
-- GROUP BY others_n.event_number_in_session, next_type, next_value
|
|
||||||
-- UNION ALL
|
|
||||||
-- -- TODO: find if this works or not
|
|
||||||
-- -- Others got to Others
|
|
||||||
-- SELECT others_n.event_number_in_session,
|
|
||||||
-- 'OTHER' AS event_type,
|
|
||||||
-- NULL AS e_value,
|
|
||||||
-- 'OTHERS' AS next_type,
|
|
||||||
-- NULL AS next_value,
|
|
||||||
-- SUM(sessions_count) AS sessions_count
|
|
||||||
-- FROM others_n
|
|
||||||
-- LEFT JOIN u_top_n ON ((others_n.event_number_in_session + 1) = u_top_n.event_number_in_session
|
|
||||||
-- AND others_n.s_next_type = u_top_n.event_type
|
|
||||||
-- AND others_n.s_next_value = u_top_n.e_value)
|
|
||||||
-- WHERE u_top_n.event_number_in_session IS NULL
|
|
||||||
-- GROUP BY others_n.event_number_in_session
|
|
||||||
)
|
|
||||||
ORDER BY event_number_in_session;
|
|
||||||
|
|
||||||
|
|
||||||
-- ---------Q3 TOP ON VALUE ONLY -----------
|
|
||||||
WITH ranked_events AS (SELECT *
|
|
||||||
FROM ranked_events_1736344377403),
|
|
||||||
n1 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 1
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
n2 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 2
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
n3 AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
next_type,
|
|
||||||
next_value,
|
|
||||||
COUNT(1) AS sessions_count
|
|
||||||
FROM ranked_events
|
|
||||||
WHERE event_number_in_session = 3
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value, next_type, next_value
|
|
||||||
ORDER BY sessions_count DESC),
|
|
||||||
|
|
||||||
drop_n AS (-- STEP 1
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
'DROP' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n1
|
|
||||||
WHERE isNull(n1.next_type)
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 2
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
'DROP' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
sessions_count
|
|
||||||
FROM n2
|
|
||||||
WHERE isNull(n2.next_type)),
|
|
||||||
top_n AS (SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
SUM(sessions_count) AS sessions_count
|
|
||||||
FROM n1
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
LIMIT 1
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 2
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
SUM(sessions_count) AS sessions_count
|
|
||||||
FROM n2
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
ORDER BY sessions_count DESC
|
|
||||||
LIMIT 3
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 3
|
|
||||||
SELECT event_number_in_session,
|
|
||||||
event_type,
|
|
||||||
e_value,
|
|
||||||
SUM(sessions_count) AS sessions_count
|
|
||||||
FROM n3
|
|
||||||
GROUP BY event_number_in_session, event_type, e_value
|
|
||||||
ORDER BY sessions_count DESC
|
|
||||||
LIMIT 3),
|
|
||||||
top_n_with_next AS (SELECT n1.*
|
|
||||||
FROM n1
|
|
||||||
UNION ALL
|
|
||||||
SELECT n2.*
|
|
||||||
FROM n2
|
|
||||||
INNER JOIN top_n ON (n2.event_number_in_session = top_n.event_number_in_session
|
|
||||||
AND n2.event_type = top_n.event_type
|
|
||||||
AND n2.e_value = top_n.e_value)),
|
|
||||||
others_n AS (
|
|
||||||
-- STEP 2
|
|
||||||
SELECT n2.*
|
|
||||||
FROM n2
|
|
||||||
WHERE (n2.event_number_in_session, n2.event_type, n2.e_value) NOT IN
|
|
||||||
(SELECT event_number_in_session, event_type, e_value
|
|
||||||
FROM top_n
|
|
||||||
WHERE top_n.event_number_in_session = 2)
|
|
||||||
UNION ALL
|
|
||||||
-- STEP 3
|
|
||||||
SELECT n3.*
|
|
||||||
FROM n3
|
|
||||||
WHERE (n3.event_number_in_session, n3.event_type, n3.e_value) NOT IN
|
|
||||||
(SELECT event_number_in_session, event_type, e_value
|
|
||||||
FROM top_n
|
|
||||||
WHERE top_n.event_number_in_session = 3))
|
|
||||||
SELECT *
|
|
||||||
FROM (
|
|
||||||
-- SELECT sum(top_n_with_next.sessions_count)
|
|
||||||
-- FROM top_n_with_next
|
|
||||||
-- WHERE event_number_in_session = 1
|
|
||||||
-- -- AND isNotNull(next_value)
|
|
||||||
-- AND (next_type, next_value) IN
|
|
||||||
-- (SELECT others_n.event_type, others_n.e_value FROM others_n WHERE others_n.event_number_in_session = 2)
|
|
||||||
-- -- SELECT * FROM others_n
|
|
||||||
-- -- SELECT * FROM n2
|
|
||||||
-- SELECT *
|
|
||||||
-- FROM top_n
|
|
||||||
-- );
|
|
||||||
-- Top to Top: valid
|
|
||||||
SELECT top_n_with_next.*
|
|
||||||
FROM top_n_with_next
|
|
||||||
INNER JOIN top_n
|
|
||||||
ON (top_n_with_next.event_number_in_session + 1 = top_n.event_number_in_session
|
|
||||||
AND top_n_with_next.next_type = top_n.event_type
|
|
||||||
AND top_n_with_next.next_value = top_n.e_value)
|
|
||||||
UNION ALL
|
|
||||||
-- Top to Others: valid
|
|
||||||
SELECT top_n_with_next.event_number_in_session,
|
|
||||||
top_n_with_next.event_type,
|
|
||||||
top_n_with_next.e_value,
|
|
||||||
'OTHER' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
SUM(top_n_with_next.sessions_count) AS sessions_count
|
|
||||||
FROM top_n_with_next
|
|
||||||
WHERE (top_n_with_next.event_number_in_session + 1, top_n_with_next.next_type, top_n_with_next.next_value) IN
|
|
||||||
(SELECT others_n.event_number_in_session, others_n.event_type, others_n.e_value FROM others_n)
|
|
||||||
GROUP BY top_n_with_next.event_number_in_session, top_n_with_next.event_type, top_n_with_next.e_value
|
|
||||||
UNION ALL
|
|
||||||
-- Top go to Drop: valid
|
|
||||||
SELECT drop_n.event_number_in_session,
|
|
||||||
drop_n.event_type,
|
|
||||||
drop_n.e_value,
|
|
||||||
drop_n.next_type,
|
|
||||||
drop_n.next_value,
|
|
||||||
drop_n.sessions_count
|
|
||||||
FROM drop_n
|
|
||||||
INNER JOIN top_n ON (drop_n.event_number_in_session = top_n.event_number_in_session
|
|
||||||
AND drop_n.event_type = top_n.event_type
|
|
||||||
AND drop_n.e_value = top_n.e_value)
|
|
||||||
ORDER BY drop_n.event_number_in_session
|
|
||||||
UNION ALL
|
|
||||||
-- Others got to Drop: valid
|
|
||||||
SELECT others_n.event_number_in_session,
|
|
||||||
'OTHER' AS event_type,
|
|
||||||
NULL AS e_value,
|
|
||||||
'DROP' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
SUM(others_n.sessions_count) AS sessions_count
|
|
||||||
FROM others_n
|
|
||||||
WHERE isNull(others_n.next_type)
|
|
||||||
AND others_n.event_number_in_session < 3
|
|
||||||
GROUP BY others_n.event_number_in_session, next_type, next_value
|
|
||||||
UNION ALL
|
|
||||||
-- Others got to Top:valid
|
|
||||||
SELECT others_n.event_number_in_session,
|
|
||||||
'OTHER' AS event_type,
|
|
||||||
NULL AS e_value,
|
|
||||||
others_n.next_type,
|
|
||||||
others_n.next_value,
|
|
||||||
SUM(others_n.sessions_count) AS sessions_count
|
|
||||||
FROM others_n
|
|
||||||
WHERE isNotNull(others_n.next_type)
|
|
||||||
AND (others_n.event_number_in_session + 1, others_n.next_type, others_n.next_value) IN
|
|
||||||
(SELECT top_n.event_number_in_session, top_n.event_type, top_n.e_value FROM top_n)
|
|
||||||
GROUP BY others_n.event_number_in_session, others_n.next_type, others_n.next_value
|
|
||||||
UNION ALL
|
|
||||||
-- Others got to Others
|
|
||||||
SELECT others_n.event_number_in_session,
|
|
||||||
'OTHER' AS event_type,
|
|
||||||
NULL AS e_value,
|
|
||||||
'OTHERS' AS next_type,
|
|
||||||
NULL AS next_value,
|
|
||||||
SUM(sessions_count) AS sessions_count
|
|
||||||
FROM others_n
|
|
||||||
WHERE isNotNull(others_n.next_type)
|
|
||||||
AND others_n.event_number_in_session < 3
|
|
||||||
AND (others_n.event_number_in_session + 1, others_n.next_type, others_n.next_value) NOT IN
|
|
||||||
(SELECT event_number_in_session, event_type, e_value FROM top_n)
|
|
||||||
GROUP BY others_n.event_number_in_session)
|
|
||||||
ORDER BY event_number_in_session, sessions_count
|
|
||||||
DESC;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -960,36 +960,6 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# We don't need this as the UI is expecting filters to override the full series' filters
|
|
||||||
# @model_validator(mode="after")
|
|
||||||
# def __merge_out_filters_with_series(self):
|
|
||||||
# for f in self.filters:
|
|
||||||
# for s in self.series:
|
|
||||||
# found = False
|
|
||||||
#
|
|
||||||
# if f.is_event:
|
|
||||||
# sub = s.filter.events
|
|
||||||
# else:
|
|
||||||
# sub = s.filter.filters
|
|
||||||
#
|
|
||||||
# for e in sub:
|
|
||||||
# if f.type == e.type and f.operator == e.operator:
|
|
||||||
# found = True
|
|
||||||
# if f.is_event:
|
|
||||||
# # If extra event: append value
|
|
||||||
# for v in f.value:
|
|
||||||
# if v not in e.value:
|
|
||||||
# e.value.append(v)
|
|
||||||
# else:
|
|
||||||
# # If extra filter: override value
|
|
||||||
# e.value = f.value
|
|
||||||
# if not found:
|
|
||||||
# sub.append(f)
|
|
||||||
#
|
|
||||||
# self.filters = []
|
|
||||||
#
|
|
||||||
# return self
|
|
||||||
|
|
||||||
# UI is expecting filters to override the full series' filters
|
# UI is expecting filters to override the full series' filters
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def __override_series_filters_with_outer_filters(self):
|
def __override_series_filters_with_outer_filters(self):
|
||||||
|
|
@ -1060,6 +1030,16 @@ class CardTable(__CardSchema):
|
||||||
values["metricValue"] = []
|
values["metricValue"] = []
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def __enforce_AND_operator(self):
|
||||||
|
self.metric_of = MetricOfTable(self.metric_of)
|
||||||
|
if self.metric_of in (MetricOfTable.VISITED_URL, MetricOfTable.FETCH, \
|
||||||
|
MetricOfTable.VISITED_URL.value, MetricOfTable.FETCH.value):
|
||||||
|
for s in self.series:
|
||||||
|
if s.filter is not None:
|
||||||
|
s.filter.events_order = SearchEventOrder.AND
|
||||||
|
return self
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def __transform(self):
|
def __transform(self):
|
||||||
self.metric_of = MetricOfTable(self.metric_of)
|
self.metric_of = MetricOfTable(self.metric_of)
|
||||||
|
|
@ -1135,7 +1115,7 @@ class CardPathAnalysis(__CardSchema):
|
||||||
view_type: MetricOtherViewType = Field(...)
|
view_type: MetricOtherViewType = Field(...)
|
||||||
metric_value: List[ProductAnalyticsSelectedEventType] = Field(default_factory=list)
|
metric_value: List[ProductAnalyticsSelectedEventType] = Field(default_factory=list)
|
||||||
density: int = Field(default=4, ge=2, le=10)
|
density: int = Field(default=4, ge=2, le=10)
|
||||||
rows: int = Field(default=3, ge=1, le=10)
|
rows: int = Field(default=5, ge=1, le=10)
|
||||||
|
|
||||||
start_type: Literal["start", "end"] = Field(default="start")
|
start_type: Literal["start", "end"] = Field(default="start")
|
||||||
start_point: List[PathAnalysisSubFilterSchema] = Field(default_factory=list)
|
start_point: List[PathAnalysisSubFilterSchema] = Field(default_factory=list)
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,16 @@ const EVENTS_DEFINITION = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
EVENTS_DEFINITION.emit = {
|
EVENTS_DEFINITION.emit = {
|
||||||
NEW_AGENT: "NEW_AGENT",
|
NEW_AGENT: "NEW_AGENT",
|
||||||
NO_AGENTS: "NO_AGENT",
|
NO_AGENTS: "NO_AGENT",
|
||||||
AGENT_DISCONNECT: "AGENT_DISCONNECTED",
|
AGENT_DISCONNECT: "AGENT_DISCONNECTED",
|
||||||
AGENTS_CONNECTED: "AGENTS_CONNECTED",
|
AGENTS_CONNECTED: "AGENTS_CONNECTED",
|
||||||
NO_SESSIONS: "SESSION_DISCONNECTED",
|
AGENTS_INFO_CONNECTED: "AGENTS_INFO_CONNECTED",
|
||||||
SESSION_ALREADY_CONNECTED: "SESSION_ALREADY_CONNECTED",
|
NO_SESSIONS: "SESSION_DISCONNECTED",
|
||||||
SESSION_RECONNECTED: "SESSION_RECONNECTED",
|
SESSION_ALREADY_CONNECTED: "SESSION_ALREADY_CONNECTED",
|
||||||
UPDATE_EVENT: EVENTS_DEFINITION.listen.UPDATE_EVENT
|
SESSION_RECONNECTED: "SESSION_RECONNECTED",
|
||||||
|
UPDATE_EVENT: EVENTS_DEFINITION.listen.UPDATE_EVENT,
|
||||||
|
WEBRTC_CONFIG: "WEBRTC_CONFIG",
|
||||||
};
|
};
|
||||||
|
|
||||||
const BASE_sessionInfo = {
|
const BASE_sessionInfo = {
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,14 @@ const respond = function (req, res, data) {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.end(JSON.stringify(result));
|
res.end(JSON.stringify(result));
|
||||||
} else {
|
} else {
|
||||||
res.cork(() => {
|
if (!res.aborted) {
|
||||||
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify(result));
|
res.cork(() => {
|
||||||
});
|
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify(result));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.debug("response aborted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const duration = performance.now() - req.startTs;
|
const duration = performance.now() - req.startTs;
|
||||||
IncreaseTotalRequests();
|
IncreaseTotalRequests();
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const findSessionSocketId = async (io, roomId, tabId) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRoomData(io, roomID) {
|
async function getRoomData(io, roomID) {
|
||||||
let tabsCount = 0, agentsCount = 0, tabIDs = [], agentIDs = [];
|
let tabsCount = 0, agentsCount = 0, tabIDs = [], agentIDs = [], config = null, agentInfos = [];
|
||||||
const connected_sockets = await io.in(roomID).fetchSockets();
|
const connected_sockets = await io.in(roomID).fetchSockets();
|
||||||
if (connected_sockets.length > 0) {
|
if (connected_sockets.length > 0) {
|
||||||
for (let socket of connected_sockets) {
|
for (let socket of connected_sockets) {
|
||||||
|
|
@ -52,13 +52,19 @@ async function getRoomData(io, roomID) {
|
||||||
} else {
|
} else {
|
||||||
agentsCount++;
|
agentsCount++;
|
||||||
agentIDs.push(socket.id);
|
agentIDs.push(socket.id);
|
||||||
|
agentInfos.push({ ...socket.handshake.query.agentInfo, socketId: socket.id });
|
||||||
|
if (socket.handshake.query.config !== undefined) {
|
||||||
|
config = socket.handshake.query.config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tabsCount = -1;
|
tabsCount = -1;
|
||||||
agentsCount = -1;
|
agentsCount = -1;
|
||||||
|
agentInfos = [];
|
||||||
|
agentIDs = [];
|
||||||
}
|
}
|
||||||
return {tabsCount, agentsCount, tabIDs, agentIDs};
|
return {tabsCount, agentsCount, tabIDs, agentIDs, config, agentInfos};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processNewSocket(socket) {
|
function processNewSocket(socket) {
|
||||||
|
|
@ -78,7 +84,7 @@ async function onConnect(socket) {
|
||||||
IncreaseOnlineConnections(socket.handshake.query.identity);
|
IncreaseOnlineConnections(socket.handshake.query.identity);
|
||||||
|
|
||||||
const io = getServer();
|
const io = getServer();
|
||||||
const {tabsCount, agentsCount, tabIDs, agentIDs} = await getRoomData(io, socket.handshake.query.roomId);
|
const {tabsCount, agentsCount, tabIDs, agentInfos, agentIDs, config} = await getRoomData(io, socket.handshake.query.roomId);
|
||||||
|
|
||||||
if (socket.handshake.query.identity === IDENTITIES.session) {
|
if (socket.handshake.query.identity === IDENTITIES.session) {
|
||||||
// Check if session with the same tabID already connected, if so, refuse new connexion
|
// Check if session with the same tabID already connected, if so, refuse new connexion
|
||||||
|
|
@ -100,7 +106,9 @@ async function onConnect(socket) {
|
||||||
// Inform all connected agents about reconnected session
|
// Inform all connected agents about reconnected session
|
||||||
if (agentsCount > 0) {
|
if (agentsCount > 0) {
|
||||||
logger.debug(`notifying new session about agent-existence`);
|
logger.debug(`notifying new session about agent-existence`);
|
||||||
|
io.to(socket.id).emit(EVENTS_DEFINITION.emit.WEBRTC_CONFIG, config);
|
||||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agentIDs);
|
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agentIDs);
|
||||||
|
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_INFO_CONNECTED, agentInfos);
|
||||||
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||||
}
|
}
|
||||||
} else if (tabsCount <= 0) {
|
} else if (tabsCount <= 0) {
|
||||||
|
|
@ -118,7 +126,8 @@ async function onConnect(socket) {
|
||||||
// Stats
|
// Stats
|
||||||
startAssist(socket, socket.handshake.query.agentID);
|
startAssist(socket, socket.handshake.query.agentID);
|
||||||
}
|
}
|
||||||
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
io.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.WEBRTC_CONFIG, socket.handshake.query.config);
|
||||||
|
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, { ...socket.handshake.query.agentInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set disconnect handler
|
// Set disconnect handler
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ package datasaver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"openreplay/backend/pkg/db/types"
|
||||||
|
|
||||||
"openreplay/backend/internal/config/db"
|
"openreplay/backend/internal/config/db"
|
||||||
"openreplay/backend/pkg/db/clickhouse"
|
"openreplay/backend/pkg/db/clickhouse"
|
||||||
"openreplay/backend/pkg/db/postgres"
|
"openreplay/backend/pkg/db/postgres"
|
||||||
"openreplay/backend/pkg/db/types"
|
|
||||||
"openreplay/backend/pkg/logger"
|
"openreplay/backend/pkg/logger"
|
||||||
. "openreplay/backend/pkg/messages"
|
. "openreplay/backend/pkg/messages"
|
||||||
queue "openreplay/backend/pkg/queue/types"
|
queue "openreplay/backend/pkg/queue/types"
|
||||||
|
|
@ -50,10 +51,6 @@ func New(log logger.Logger, cfg *db.Config, pg *postgres.Conn, ch clickhouse.Con
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *saverImpl) Handle(msg Message) {
|
func (s *saverImpl) Handle(msg Message) {
|
||||||
if msg.TypeID() == MsgCustomEvent {
|
|
||||||
defer s.Handle(types.WrapCustomEvent(msg.(*CustomEvent)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sessCtx = context.WithValue(context.Background(), "sessionID", msg.SessionID())
|
sessCtx = context.WithValue(context.Background(), "sessionID", msg.SessionID())
|
||||||
session *sessions.Session
|
session *sessions.Session
|
||||||
|
|
@ -69,6 +66,23 @@ func (s *saverImpl) Handle(msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.TypeID() == MsgCustomEvent {
|
||||||
|
m := msg.(*CustomEvent)
|
||||||
|
// Try to parse custom event payload to JSON and extract or_payload field
|
||||||
|
type CustomEventPayload struct {
|
||||||
|
CustomTimestamp uint64 `json:"or_timestamp"`
|
||||||
|
}
|
||||||
|
customPayload := &CustomEventPayload{}
|
||||||
|
if err := json.Unmarshal([]byte(m.Payload), customPayload); err == nil {
|
||||||
|
if customPayload.CustomTimestamp >= session.Timestamp {
|
||||||
|
s.log.Info(sessCtx, "custom event timestamp received: %v", m.Timestamp)
|
||||||
|
msg.Meta().Timestamp = customPayload.CustomTimestamp
|
||||||
|
s.log.Info(sessCtx, "custom event timestamp updated: %v", m.Timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer s.Handle(types.WrapCustomEvent(m))
|
||||||
|
}
|
||||||
|
|
||||||
if IsMobileType(msg.TypeID()) {
|
if IsMobileType(msg.TypeID()) {
|
||||||
if err := s.handleMobileMessage(sessCtx, session, msg); err != nil {
|
if err := s.handleMobileMessage(sessCtx, session, msg); err != nil {
|
||||||
if !postgres.IsPkeyViolation(err) {
|
if !postgres.IsPkeyViolation(err) {
|
||||||
|
|
|
||||||
3
ee/api/.gitignore
vendored
3
ee/api/.gitignore
vendored
|
|
@ -225,8 +225,7 @@ Pipfile.lock
|
||||||
/chalicelib/core/sessions/unprocessed_sessions.py
|
/chalicelib/core/sessions/unprocessed_sessions.py
|
||||||
/chalicelib/core/metrics/modules
|
/chalicelib/core/metrics/modules
|
||||||
/chalicelib/core/socket_ios.py
|
/chalicelib/core/socket_ios.py
|
||||||
/chalicelib/core/sourcemaps.py
|
/chalicelib/core/sourcemaps
|
||||||
/chalicelib/core/sourcemaps_parser.py
|
|
||||||
/chalicelib/core/tags.py
|
/chalicelib/core/tags.py
|
||||||
/chalicelib/saml
|
/chalicelib/saml
|
||||||
/chalicelib/utils/__init__.py
|
/chalicelib/utils/__init__.py
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ def __generic_query(typename, value_length=None):
|
||||||
ORDER BY value"""
|
ORDER BY value"""
|
||||||
|
|
||||||
if value_length is None or value_length > 2:
|
if value_length is None or value_length > 2:
|
||||||
return f"""(SELECT DISTINCT value, type
|
return f"""SELECT DISTINCT ON(value, type) value, type
|
||||||
|
FROM ((SELECT DISTINCT value, type
|
||||||
FROM {TABLE}
|
FROM {TABLE}
|
||||||
WHERE
|
WHERE
|
||||||
project_id = %(project_id)s
|
project_id = %(project_id)s
|
||||||
|
|
@ -102,7 +103,7 @@ def __generic_query(typename, value_length=None):
|
||||||
AND type='{typename.upper()}'
|
AND type='{typename.upper()}'
|
||||||
AND value ILIKE %(value)s
|
AND value ILIKE %(value)s
|
||||||
ORDER BY value
|
ORDER BY value
|
||||||
LIMIT 5);"""
|
LIMIT 5)) AS raw;"""
|
||||||
return f"""SELECT DISTINCT value, type
|
return f"""SELECT DISTINCT value, type
|
||||||
FROM {TABLE}
|
FROM {TABLE}
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -257,7 +258,7 @@ def __search_metadata(project_id, value, key=None, source=None):
|
||||||
WHERE project_id = %(project_id)s
|
WHERE project_id = %(project_id)s
|
||||||
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
|
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
|
||||||
with ch_client.ClickHouseClient() as cur:
|
with ch_client.ClickHouseClient() as cur:
|
||||||
query = cur.format(query=f"""SELECT key, value, 'METADATA' AS TYPE
|
query = cur.format(query=f"""SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
|
||||||
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
|
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
|
||||||
LIMIT 5;""", parameters={"project_id": project_id, "value": helper.string_to_sql_like(value),
|
LIMIT 5;""", parameters={"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||||
"svalue": helper.string_to_sql_like("^" + value)})
|
"svalue": helper.string_to_sql_like("^" + value)})
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ def get_details(project_id, error_id, user_id, **data):
|
||||||
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(0)
|
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(0)
|
||||||
|
|
||||||
ch_basic_query = errors_helper.__get_basic_constraints_ch(time_constraint=False)
|
ch_basic_query = errors_helper.__get_basic_constraints_ch(time_constraint=False)
|
||||||
ch_basic_query.append("toString(`$properties`.error_id) = %(error_id)s")
|
ch_basic_query.append("error_id = %(error_id)s")
|
||||||
|
|
||||||
with ch_client.ClickHouseClient() as ch:
|
with ch_client.ClickHouseClient() as ch:
|
||||||
data["startDate24"] = TimeUTC.now(-1)
|
data["startDate24"] = TimeUTC.now(-1)
|
||||||
|
|
@ -95,7 +95,7 @@ def get_details(project_id, error_id, user_id, **data):
|
||||||
"error_id": error_id}
|
"error_id": error_id}
|
||||||
|
|
||||||
main_ch_query = f"""\
|
main_ch_query = f"""\
|
||||||
WITH pre_processed AS (SELECT toString(`$properties`.error_id) AS error_id,
|
WITH pre_processed AS (SELECT error_id,
|
||||||
toString(`$properties`.name) AS name,
|
toString(`$properties`.name) AS name,
|
||||||
toString(`$properties`.message) AS message,
|
toString(`$properties`.message) AS message,
|
||||||
session_id,
|
session_id,
|
||||||
|
|
@ -183,7 +183,7 @@ def get_details(project_id, error_id, user_id, **data):
|
||||||
AND `$event_name` = 'ERROR'
|
AND `$event_name` = 'ERROR'
|
||||||
AND events.created_at >= toDateTime(timestamp / 1000)
|
AND events.created_at >= toDateTime(timestamp / 1000)
|
||||||
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
|
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
|
||||||
AND toString(`$properties`.error_id) = %(error_id)s
|
AND error_id = %(error_id)s
|
||||||
GROUP BY timestamp
|
GROUP BY timestamp
|
||||||
ORDER BY timestamp) AS chart_details
|
ORDER BY timestamp) AS chart_details
|
||||||
) AS chart_details24 ON TRUE
|
) AS chart_details24 ON TRUE
|
||||||
|
|
@ -196,7 +196,7 @@ def get_details(project_id, error_id, user_id, **data):
|
||||||
AND `$event_name` = 'ERROR'
|
AND `$event_name` = 'ERROR'
|
||||||
AND events.created_at >= toDateTime(timestamp / 1000)
|
AND events.created_at >= toDateTime(timestamp / 1000)
|
||||||
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
|
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
|
||||||
AND toString(`$properties`.error_id) = %(error_id)s
|
AND error_id = %(error_id)s
|
||||||
GROUP BY timestamp
|
GROUP BY timestamp
|
||||||
ORDER BY timestamp) AS chart_details
|
ORDER BY timestamp) AS chart_details
|
||||||
) AS chart_details30 ON TRUE;"""
|
) AS chart_details30 ON TRUE;"""
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
||||||
) AS users_sessions;""",
|
) AS users_sessions;""",
|
||||||
full_args)
|
full_args)
|
||||||
elif ids_only:
|
elif ids_only:
|
||||||
main_query = cur.format(query=f"""SELECT DISTINCT ON(s.session_id) s.session_id
|
main_query = cur.format(query=f"""SELECT DISTINCT ON(s.session_id) s.session_id AS session_id
|
||||||
{query_part}
|
{query_part}
|
||||||
ORDER BY s.session_id desc
|
ORDER BY s.session_id desc
|
||||||
LIMIT %(sessions_limit)s OFFSET %(sessions_limit_s)s;""",
|
LIMIT %(sessions_limit)s OFFSET %(sessions_limit_s)s;""",
|
||||||
|
|
|
||||||
|
|
@ -927,12 +927,12 @@ def authenticate_sso(email: str, internal_id: str):
|
||||||
aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti),
|
aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti),
|
||||||
"refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int),
|
"refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int),
|
||||||
"spotJwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'],
|
"spotJwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'],
|
||||||
iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE),
|
iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE, for_spot=True),
|
||||||
"spotRefreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'],
|
"spotRefreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'],
|
||||||
tenant_id=r['tenantId'],
|
tenant_id=r['tenantId'],
|
||||||
iat=j_r.spot_jwt_refresh_iat,
|
iat=j_r.spot_jwt_refresh_iat,
|
||||||
aud=spot.AUDIENCE,
|
aud=spot.AUDIENCE,
|
||||||
jwt_jti=j_r.spot_jwt_refresh_jti),
|
jwt_jti=j_r.spot_jwt_refresh_jti, for_spot=True),
|
||||||
"spotRefreshTokenMaxAge": config("JWT_SPOT_REFRESH_EXPIRATION", cast=int)
|
"spotRefreshTokenMaxAge": config("JWT_SPOT_REFRESH_EXPIRATION", cast=int)
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ rm -rf ./chalicelib/core/sessions/sessions_viewed/sessions_viewed.py
|
||||||
rm -rf ./chalicelib/core/sessions/unprocessed_sessions.py
|
rm -rf ./chalicelib/core/sessions/unprocessed_sessions.py
|
||||||
rm -rf ./chalicelib/core/metrics/modules
|
rm -rf ./chalicelib/core/metrics/modules
|
||||||
rm -rf ./chalicelib/core/socket_ios.py
|
rm -rf ./chalicelib/core/socket_ios.py
|
||||||
rm -rf ./chalicelib/core/sourcemaps.py
|
rm -rf ./chalicelib/core/sourcemaps
|
||||||
rm -rf ./chalicelib/core/sourcemaps_parser.py
|
|
||||||
rm -rf ./chalicelib/core/user_testing.py
|
rm -rf ./chalicelib/core/user_testing.py
|
||||||
rm -rf ./chalicelib/core/tags.py
|
rm -rf ./chalicelib/core/tags.py
|
||||||
rm -rf ./chalicelib/saml
|
rm -rf ./chalicelib/saml
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,11 @@ if (process.env.uws !== "true") {
|
||||||
const uWrapper = function (fn) {
|
const uWrapper = function (fn) {
|
||||||
return (res, req) => {
|
return (res, req) => {
|
||||||
res.id = 1;
|
res.id = 1;
|
||||||
|
res.aborted = false;
|
||||||
req.startTs = performance.now(); // track request's start timestamp
|
req.startTs = performance.now(); // track request's start timestamp
|
||||||
req.method = req.getMethod();
|
req.method = req.getMethod();
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
|
res.aborted = true;
|
||||||
onAbortedOrFinishedResponse(res);
|
onAbortedOrFinishedResponse(res);
|
||||||
});
|
});
|
||||||
return fn(req, res);
|
return fn(req, res);
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,50 @@ const {getCompressionConfig} = require("./helper");
|
||||||
const {logger} = require('./logger');
|
const {logger} = require('./logger');
|
||||||
|
|
||||||
let io;
|
let io;
|
||||||
|
const getServer = function () {return io;}
|
||||||
|
|
||||||
const getServer = function () {
|
const useRedis = process.env.redis === "true";
|
||||||
return io;
|
let inMemorySocketsCache = [];
|
||||||
|
let lastCacheUpdateTime = 0;
|
||||||
|
const CACHE_REFRESH_INTERVAL = parseInt(process.env.cacheRefreshInterval) || 5000;
|
||||||
|
|
||||||
|
const doFetchAllSockets = async function () {
|
||||||
|
if (useRedis) {
|
||||||
|
const now = Date.now();
|
||||||
|
logger.info(`Using in-memory cache (age: ${now - lastCacheUpdateTime}ms)`);
|
||||||
|
return inMemorySocketsCache;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return await io.fetchSockets();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching sockets:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let redisClient;
|
// Background refresher that runs independently of requests
|
||||||
const useRedis = process.env.redis === "true";
|
let cacheRefresher = null;
|
||||||
|
function startCacheRefresher() {
|
||||||
|
if (cacheRefresher) clearInterval(cacheRefresher);
|
||||||
|
|
||||||
if (useRedis) {
|
cacheRefresher = setInterval(async () => {
|
||||||
const {createClient} = require("redis");
|
const now = Date.now();
|
||||||
const REDIS_URL = (process.env.REDIS_URL || "localhost:6379").replace(/((^\w+:|^)\/\/|^)/, 'redis://');
|
// Only refresh if cache is stale
|
||||||
redisClient = createClient({url: REDIS_URL});
|
if (now - lastCacheUpdateTime >= CACHE_REFRESH_INTERVAL) {
|
||||||
redisClient.on("error", (error) => logger.error(`Redis error : ${error}`));
|
logger.debug('Background refresh triggered');
|
||||||
void redisClient.connect();
|
try {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = await io.fetchSockets();
|
||||||
|
inMemorySocketsCache = result;
|
||||||
|
lastCacheUpdateTime = now;
|
||||||
|
const duration = performance.now() - startTime;
|
||||||
|
logger.info(`Background refresh complete: ${duration}ms, ${result.length} sockets`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Background refresh error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, CACHE_REFRESH_INTERVAL / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processSocketsList = function (sockets) {
|
const processSocketsList = function (sockets) {
|
||||||
|
|
@ -28,24 +58,6 @@ const processSocketsList = function (sockets) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const doFetchAllSockets = async function () {
|
|
||||||
if (useRedis) {
|
|
||||||
try {
|
|
||||||
let cachedResult = await redisClient.get('fetchSocketsResult');
|
|
||||||
if (cachedResult) {
|
|
||||||
return JSON.parse(cachedResult);
|
|
||||||
}
|
|
||||||
let result = await io.fetchSockets();
|
|
||||||
let cachedString = JSON.stringify(processSocketsList(result));
|
|
||||||
await redisClient.set('fetchSocketsResult', cachedString, {EX: 5});
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error setting value with expiration:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await io.fetchSockets();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchSockets = async function (roomID) {
|
const fetchSockets = async function (roomID) {
|
||||||
if (!io) {
|
if (!io) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -84,6 +96,7 @@ const createSocketIOServer = function (server, prefix) {
|
||||||
});
|
});
|
||||||
io.attachApp(server);
|
io.attachApp(server);
|
||||||
}
|
}
|
||||||
|
startCacheRefresher();
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'ios', 'IOS sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'android', 'Android sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
ALTER TABLE experimental.sessions
|
||||||
|
MODIFY COLUMN platform Enum8('web'=1,'mobile'=2) DEFAULT 'web';
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
|
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
|
||||||
|
|
||||||
SET allow_experimental_json_type = 1;
|
SET allow_experimental_json_type = 1;
|
||||||
|
|
@ -151,8 +164,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
ORDER BY (project_id, "$event_name", created_at, session_id)
|
ORDER BY (project_id, "$event_name", created_at, session_id)
|
||||||
TTL _timestamp + INTERVAL 1 MONTH ,
|
TTL _deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
|
||||||
_deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
|
|
||||||
|
|
||||||
-- The list of events that should not be ingested,
|
-- The list of events that should not be ingested,
|
||||||
-- according to a specific event_name and optional properties
|
-- according to a specific event_name and optional properties
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ CREATE TABLE IF NOT EXISTS experimental.autocomplete
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(_timestamp)
|
PARTITION BY toYYYYMM(_timestamp)
|
||||||
ORDER BY (project_id, type, value)
|
ORDER BY (project_id, type, value);
|
||||||
TTL _timestamp + INTERVAL 1 MONTH;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.events
|
CREATE TABLE IF NOT EXISTS experimental.events
|
||||||
(
|
(
|
||||||
|
|
@ -87,8 +86,7 @@ CREATE TABLE IF NOT EXISTS experimental.events
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(datetime)
|
PARTITION BY toYYYYMM(datetime)
|
||||||
ORDER BY (project_id, datetime, event_type, session_id, message_id)
|
ORDER BY (project_id, datetime, event_type, session_id, message_id);
|
||||||
TTL datetime + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,7 +106,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
|
||||||
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
|
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
|
||||||
user_city LowCardinality(String),
|
user_city LowCardinality(String),
|
||||||
user_state LowCardinality(String),
|
user_state LowCardinality(String),
|
||||||
platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
|
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web',
|
||||||
datetime DateTime,
|
datetime DateTime,
|
||||||
timezone LowCardinality(Nullable(String)),
|
timezone LowCardinality(Nullable(String)),
|
||||||
duration UInt32,
|
duration UInt32,
|
||||||
|
|
@ -140,7 +138,6 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMMDD(datetime)
|
PARTITION BY toYYYYMMDD(datetime)
|
||||||
ORDER BY (project_id, datetime, session_id)
|
ORDER BY (project_id, datetime, session_id)
|
||||||
TTL datetime + INTERVAL 3 MONTH
|
|
||||||
SETTINGS index_granularity = 512;
|
SETTINGS index_granularity = 512;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
|
CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
|
||||||
|
|
@ -152,8 +149,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
|
||||||
sign Int8
|
sign Int8
|
||||||
) ENGINE = CollapsingMergeTree(sign)
|
) ENGINE = CollapsingMergeTree(sign)
|
||||||
PARTITION BY toYYYYMM(_timestamp)
|
PARTITION BY toYYYYMM(_timestamp)
|
||||||
ORDER BY (project_id, user_id, session_id)
|
ORDER BY (project_id, user_id, session_id);
|
||||||
TTL _timestamp + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
||||||
(
|
(
|
||||||
|
|
@ -163,8 +159,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(_timestamp)
|
PARTITION BY toYYYYMM(_timestamp)
|
||||||
ORDER BY (project_id, user_id, session_id)
|
ORDER BY (project_id, user_id, session_id);
|
||||||
TTL _timestamp + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
|
CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
|
||||||
(
|
(
|
||||||
|
|
@ -174,8 +169,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(_timestamp)
|
PARTITION BY toYYYYMM(_timestamp)
|
||||||
ORDER BY (project_id, user_id, error_id)
|
ORDER BY (project_id, user_id, error_id);
|
||||||
TTL _timestamp + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.issues
|
CREATE TABLE IF NOT EXISTS experimental.issues
|
||||||
(
|
(
|
||||||
|
|
@ -188,8 +182,7 @@ CREATE TABLE IF NOT EXISTS experimental.issues
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(_timestamp)
|
PARTITION BY toYYYYMM(_timestamp)
|
||||||
ORDER BY (project_id, issue_id, type)
|
ORDER BY (project_id, issue_id, type);
|
||||||
TTL _timestamp + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -292,8 +285,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions_feature_flags
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(datetime)
|
PARTITION BY toYYYYMM(datetime)
|
||||||
ORDER BY (project_id, datetime, session_id, feature_flag_id, condition_id)
|
ORDER BY (project_id, datetime, session_id, feature_flag_id, condition_id);
|
||||||
TTL datetime + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS experimental.ios_events
|
CREATE TABLE IF NOT EXISTS experimental.ios_events
|
||||||
(
|
(
|
||||||
|
|
@ -329,8 +321,7 @@ CREATE TABLE IF NOT EXISTS experimental.ios_events
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMM(datetime)
|
PARTITION BY toYYYYMM(datetime)
|
||||||
ORDER BY (project_id, datetime, event_type, session_id, message_id)
|
ORDER BY (project_id, datetime, event_type, session_id, message_id);
|
||||||
TTL datetime + INTERVAL 3 MONTH;
|
|
||||||
|
|
||||||
|
|
||||||
SET allow_experimental_json_type = 1;
|
SET allow_experimental_json_type = 1;
|
||||||
|
|
@ -484,8 +475,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
ORDER BY (project_id, "$event_name", created_at, session_id)
|
ORDER BY (project_id, "$event_name", created_at, session_id)
|
||||||
TTL _timestamp + INTERVAL 1 MONTH ,
|
TTL _deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
|
||||||
_deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
|
|
||||||
|
|
||||||
-- The list of events that should not be ingested,
|
-- The list of events that should not be ingested,
|
||||||
-- according to a specific event_name and optional properties
|
-- according to a specific event_name and optional properties
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
|
||||||
import React, { Suspense, lazy } from 'react';
|
import React, { Suspense, lazy } from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
@ -10,7 +9,7 @@ import { Loader } from 'UI';
|
||||||
|
|
||||||
import APIClient from './api_client';
|
import APIClient from './api_client';
|
||||||
import * as routes from './routes';
|
import * as routes from './routes';
|
||||||
import { debounce } from '@/utils';
|
import { debounceCall } from '@/utils';
|
||||||
|
|
||||||
const components: any = {
|
const components: any = {
|
||||||
SessionPure: lazy(() => import('Components/Session/Session')),
|
SessionPure: lazy(() => import('Components/Session/Session')),
|
||||||
|
|
@ -88,7 +87,6 @@ const ASSIST_PATH = routes.assist();
|
||||||
const LIVE_SESSION_PATH = routes.liveSession();
|
const LIVE_SESSION_PATH = routes.liveSession();
|
||||||
const MULTIVIEW_PATH = routes.multiview();
|
const MULTIVIEW_PATH = routes.multiview();
|
||||||
const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
|
const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
|
||||||
const ASSIST_STATS_PATH = routes.assistStats();
|
|
||||||
|
|
||||||
const USABILITY_TESTING_PATH = routes.usabilityTesting();
|
const USABILITY_TESTING_PATH = routes.usabilityTesting();
|
||||||
const USABILITY_TESTING_EDIT_PATH = routes.usabilityTestingEdit();
|
const USABILITY_TESTING_EDIT_PATH = routes.usabilityTestingEdit();
|
||||||
|
|
@ -99,7 +97,6 @@ const SPOT_PATH = routes.spot();
|
||||||
const SCOPE_SETUP = routes.scopeSetup();
|
const SCOPE_SETUP = routes.scopeSetup();
|
||||||
|
|
||||||
const HIGHLIGHTS_PATH = routes.highlights();
|
const HIGHLIGHTS_PATH = routes.highlights();
|
||||||
let debounceSearch: any = () => {};
|
|
||||||
|
|
||||||
function PrivateRoutes() {
|
function PrivateRoutes() {
|
||||||
const { projectsStore, userStore, integrationsStore, searchStore } = useStore();
|
const { projectsStore, userStore, integrationsStore, searchStore } = useStore();
|
||||||
|
|
@ -124,14 +121,10 @@ function PrivateRoutes() {
|
||||||
}
|
}
|
||||||
}, [siteId]);
|
}, [siteId]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
debounceSearch = debounce(() => searchStore.fetchSessions(), 500);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!searchStore.urlParsed) return;
|
if (!searchStore.urlParsed) return;
|
||||||
debounceSearch();
|
debounceCall(() => searchStore.fetchSessions(true), 250)()
|
||||||
}, [searchStore.instance.filters, searchStore.instance.eventsOrder]);
|
}, [searchStore.urlParsed, searchStore.instance.filters, searchStore.instance.eventsOrder]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loader loading className="flex-1" />}>
|
<Suspense fallback={<Loader loading className="flex-1" />}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import Counter from 'App/components/shared/SessionItem/Counter';
|
import Counter from 'App/components/shared/SessionItem/Counter';
|
||||||
import Draggable from 'react-draggable';
|
import { useDraggable } from '@neodrag/react';
|
||||||
import type { LocalStream } from 'Player';
|
import type { LocalStream } from 'Player';
|
||||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||||
import ChatControls from '../ChatControls/ChatControls';
|
import ChatControls from '../ChatControls/ChatControls';
|
||||||
|
|
@ -25,6 +25,8 @@ function ChatWindow({
|
||||||
isPrestart,
|
isPrestart,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dragRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
useDraggable(dragRef, { bounds: 'body', defaultPosition: { x: 50, y: 200 } })
|
||||||
const { player } = React.useContext(PlayerContext);
|
const { player } = React.useContext(PlayerContext);
|
||||||
|
|
||||||
const { toggleVideoLocalStream } = player.assistManager;
|
const { toggleVideoLocalStream } = player.assistManager;
|
||||||
|
|
@ -39,11 +41,7 @@ function ChatWindow({
|
||||||
}, [localVideoEnabled]);
|
}, [localVideoEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<div ref={dragRef}>
|
||||||
handle=".handle"
|
|
||||||
bounds="body"
|
|
||||||
defaultPosition={{ x: 50, y: 200 }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn(stl.wrapper, 'fixed radius bg-white shadow-xl mt-16')}
|
className={cn(stl.wrapper, 'fixed radius bg-white shadow-xl mt-16')}
|
||||||
style={{ width: '280px' }}
|
style={{ width: '280px' }}
|
||||||
|
|
@ -102,7 +100,7 @@ function ChatWindow({
|
||||||
isPrestart={isPrestart}
|
isPrestart={isPrestart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
|
||||||
{ stream: MediaStream; isAgent: boolean }[] | null
|
{ stream: MediaStream; isAgent: boolean }[] | null
|
||||||
>([]);
|
>([]);
|
||||||
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
|
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
|
||||||
const [callObject, setCallObject] = useState<{ end: () => void } | null>(
|
const [callObject, setCallObject] = useState<{ end: () => void } | null | undefined>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -135,6 +135,7 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
|
||||||
}, [peerConnectionStatus]);
|
}, [peerConnectionStatus]);
|
||||||
|
|
||||||
const addIncomeStream = (stream: MediaStream, isAgent: boolean) => {
|
const addIncomeStream = (stream: MediaStream, isAgent: boolean) => {
|
||||||
|
if (!stream.active) return;
|
||||||
setIncomeStream((oldState) => {
|
setIncomeStream((oldState) => {
|
||||||
if (oldState === null) return [{ stream, isAgent }];
|
if (oldState === null) return [{ stream, isAgent }];
|
||||||
if (
|
if (
|
||||||
|
|
@ -149,13 +150,8 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeIncomeStream = (stream: MediaStream) => {
|
const removeIncomeStream = () => {
|
||||||
setIncomeStream((prevState) => {
|
setIncomeStream([]);
|
||||||
if (!prevState) return [];
|
|
||||||
return prevState.filter(
|
|
||||||
(existingStream) => existingStream.stream.id !== stream.id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function onReject() {
|
function onReject() {
|
||||||
|
|
@ -181,7 +177,12 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
|
||||||
() => {
|
() => {
|
||||||
player.assistManager.ping(AssistActionsPing.call.end, agentId);
|
player.assistManager.ping(AssistActionsPing.call.end, agentId);
|
||||||
lStream.stop.apply(lStream);
|
lStream.stop.apply(lStream);
|
||||||
removeIncomeStream(lStream.stream);
|
removeIncomeStream();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
player.assistManager.ping(AssistActionsPing.call.end, agentId);
|
||||||
|
lStream.stop.apply(lStream);
|
||||||
|
removeIncomeStream();
|
||||||
},
|
},
|
||||||
onReject,
|
onReject,
|
||||||
onError,
|
onError,
|
||||||
|
|
|
||||||
|
|
@ -34,43 +34,40 @@ function VideoContainer({
|
||||||
}
|
}
|
||||||
const iid = setInterval(() => {
|
const iid = setInterval(() => {
|
||||||
const track = stream.getVideoTracks()[0];
|
const track = stream.getVideoTracks()[0];
|
||||||
const settings = track?.getSettings();
|
|
||||||
const isDummyVideoTrack = settings
|
|
||||||
? settings.width === 2 ||
|
|
||||||
settings.frameRate === 0 ||
|
|
||||||
(!settings.frameRate && !settings.width)
|
|
||||||
: true;
|
|
||||||
const shouldBeEnabled = track.enabled && !isDummyVideoTrack;
|
|
||||||
|
|
||||||
if (isEnabled !== shouldBeEnabled) {
|
if (track) {
|
||||||
setEnabled(shouldBeEnabled);
|
if (!track.enabled) {
|
||||||
setRemoteEnabled?.(shouldBeEnabled);
|
setEnabled(false);
|
||||||
|
setRemoteEnabled?.(false);
|
||||||
|
} else {
|
||||||
|
setEnabled(true);
|
||||||
|
setRemoteEnabled?.(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnabled(false);
|
||||||
|
setRemoteEnabled?.(false);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearInterval(iid);
|
return () => clearInterval(iid);
|
||||||
}, [stream, isEnabled]);
|
}, [stream]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
style={{
|
style={{
|
||||||
display: isEnabled ? undefined : 'none',
|
|
||||||
width: isEnabled ? undefined : '0px!important',
|
width: isEnabled ? undefined : '0px!important',
|
||||||
height: isEnabled ? undefined : '0px!important',
|
height: isEnabled ? undefined : '0px !important',
|
||||||
border: '1px solid grey',
|
border: '1px solid grey',
|
||||||
transform: local ? 'scaleX(-1)' : undefined,
|
transform: local ? 'scaleX(-1)' : undefined,
|
||||||
|
display: isEnabled ? 'block' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<video autoPlay ref={ref} muted={muted} style={{ height }} />
|
<video
|
||||||
{isAgent ? (
|
autoPlay
|
||||||
<div
|
ref={ref}
|
||||||
style={{
|
muted={muted}
|
||||||
position: 'absolute',
|
style={{ height }}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
{t('Agent')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function ModuleCard(props: Props) {
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={!module.isEnabled}
|
checked={!module.isEnabled}
|
||||||
title={module.isEnabled ? 'Enabled' : 'Disabled'}
|
title={!module.isEnabled ? 'Enabled' : 'Disabled'}
|
||||||
onChange={() => props.onToggle(module)}
|
onChange={() => props.onToggle(module)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,12 @@ function Modules() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
list(t).forEach((module) => {
|
const moduleList = list(t)
|
||||||
|
moduleList.forEach((module) => {
|
||||||
module.isEnabled = modules.includes(module.key);
|
module.isEnabled = modules.includes(module.key);
|
||||||
});
|
});
|
||||||
setModulesState(
|
setModulesState(
|
||||||
list(t).filter(
|
moduleList.filter(
|
||||||
(module) => !module.hidden && (!module.enterprise || isEnterprise),
|
(module) => !module.hidden && (!module.enterprise || isEnterprise),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying';
|
||||||
import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone';
|
import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone';
|
||||||
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
|
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
|
||||||
import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings';
|
import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings';
|
||||||
|
import VirtualModeSettings from '../shared/SessionSettings/components/VirtualMode';
|
||||||
import DebugLog from './DebugLog';
|
import DebugLog from './DebugLog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ function SessionsListingSettings() {
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<MouseTrailSettings />
|
<MouseTrailSettings />
|
||||||
<DebugLog />
|
<DebugLog />
|
||||||
|
<VirtualModeSettings />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList'
|
||||||
import { useModal } from 'Components/ModalContext';
|
import { useModal } from 'Components/ModalContext';
|
||||||
import Widget from '@/mstore/types/widget';
|
import Widget from '@/mstore/types/widget';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FilterKey } from 'Types/filter/filterType';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
metric?: any;
|
metric?: any;
|
||||||
|
|
@ -35,20 +36,20 @@ function SessionsBy(props: Props) {
|
||||||
...filtersMap[metric.metricOf],
|
...filtersMap[metric.metricOf],
|
||||||
value: [data.name],
|
value: [data.name],
|
||||||
type: filtersMap[metric.metricOf].key,
|
type: filtersMap[metric.metricOf].key,
|
||||||
filters: filtersMap[metric.metricOf].filters?.map((f: any) => {
|
filters: [],
|
||||||
const {
|
|
||||||
key,
|
|
||||||
operatorOptions,
|
|
||||||
category,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
options,
|
|
||||||
...cleaned
|
|
||||||
} = f;
|
|
||||||
return { ...cleaned, type: f.key, value: [] };
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (metric.metricOf === FilterKey.FETCH) {
|
||||||
|
baseFilter.filters = [
|
||||||
|
{
|
||||||
|
key: FilterKey.FETCH_URL,
|
||||||
|
operator: 'is',
|
||||||
|
value: [data.name],
|
||||||
|
type: FilterKey.FETCH_URL,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
operatorOptions,
|
operatorOptions,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ function BottomButtons({
|
||||||
<Button
|
<Button
|
||||||
loading={loading}
|
loading={loading}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
disabled={loading || !instance.validate()}
|
disabled={loading || !instance.validate()}
|
||||||
id="submit-button"
|
id="submit-button"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ function ClickMapRagePicker() {
|
||||||
<Checkbox onChange={onToggle} label={t('Include rage clicks')} />
|
<Checkbox onChange={onToggle} label={t('Include rage clicks')} />
|
||||||
|
|
||||||
<Button size="small" onClick={refreshHeatmapSession}>
|
<Button size="small" onClick={refreshHeatmapSession}>
|
||||||
{t('Get new session')}
|
{t('Get new image')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ function DashboardView(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
dashboardStore.resetPeriod();
|
||||||
if (queryParams.has('modal')) {
|
if (queryParams.has('modal')) {
|
||||||
onAddWidgets();
|
onAddWidgets();
|
||||||
trimQuery();
|
trimQuery();
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,6 @@ const ListView: React.FC<Props> = ({
|
||||||
if (disableSelection) {
|
if (disableSelection) {
|
||||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||||
history.push(path);
|
history.push(path);
|
||||||
} else {
|
|
||||||
toggleSelection?.(metric.metricId);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ function MetricsList({
|
||||||
}, [metricStore]);
|
}, [metricStore]);
|
||||||
|
|
||||||
|
|
||||||
const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== 'all';
|
const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== '';
|
||||||
|
|
||||||
const searchImageDimensions = { width: 60, height: 'auto' };
|
const searchImageDimensions = { width: 60, height: 'auto' };
|
||||||
const defaultImageDimensions = { width: 600, height: 'auto' };
|
const defaultImageDimensions = { width: 600, height: 'auto' };
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,10 @@ function WidgetChart(props: Props) {
|
||||||
}
|
}
|
||||||
prevMetricRef.current = _metric;
|
prevMetricRef.current = _metric;
|
||||||
const timestmaps = drillDownPeriod.toTimestamps();
|
const timestmaps = drillDownPeriod.toTimestamps();
|
||||||
|
const density = props.isPreview ? metric.density : dashboardStore.selectedDensity
|
||||||
const payload = isSaved
|
const payload = isSaved
|
||||||
? { ...metricParams }
|
? { ...metricParams, density }
|
||||||
: { ...params, ...timestmaps, ..._metric.toJson() };
|
: { ...params, ...timestmaps, ..._metric.toJson(), density };
|
||||||
debounceRequest(
|
debounceRequest(
|
||||||
_metric,
|
_metric,
|
||||||
payload,
|
payload,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
const initTableProps = [
|
const initTableProps = [
|
||||||
{
|
{
|
||||||
title: <span className="font-medium">Series</span>,
|
title: <span className="font-medium">Series</span>,
|
||||||
|
_pureTitle: 'Series',
|
||||||
dataIndex: 'seriesName',
|
dataIndex: 'seriesName',
|
||||||
key: 'seriesName',
|
key: 'seriesName',
|
||||||
sorter: (a, b) => a.seriesName.localeCompare(b.seriesName),
|
sorter: (a, b) => a.seriesName.localeCompare(b.seriesName),
|
||||||
|
|
@ -18,6 +19,7 @@ const initTableProps = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: <span className="font-medium">Avg.</span>,
|
title: <span className="font-medium">Avg.</span>,
|
||||||
|
_pureTitle: 'Avg.',
|
||||||
dataIndex: 'average',
|
dataIndex: 'average',
|
||||||
key: 'average',
|
key: 'average',
|
||||||
sorter: (a, b) => a.average - b.average,
|
sorter: (a, b) => a.average - b.average,
|
||||||
|
|
@ -94,6 +96,8 @@ function WidgetDatatable(props: Props) {
|
||||||
tableCols.push({
|
tableCols.push({
|
||||||
title: <span className="font-medium">{name}</span>,
|
title: <span className="font-medium">{name}</span>,
|
||||||
dataIndex: `${name}_${i}`,
|
dataIndex: `${name}_${i}`,
|
||||||
|
// @ts-ignore
|
||||||
|
_pureTitle: name,
|
||||||
key: `${name}_${i}`,
|
key: `${name}_${i}`,
|
||||||
sorter: (a, b) => a[`${name}_${i}`] - b[`${name}_${i}`],
|
sorter: (a, b) => a[`${name}_${i}`] - b[`${name}_${i}`],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function RangeGranularity({
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAST_24_HR_MS = 24 * 60 * 60 * 1000;
|
const PAST_24_HR_MS = 24 * 60 * 60 * 1000;
|
||||||
function calculateGranularities(periodDurationMs: number) {
|
export function calculateGranularities(periodDurationMs: number) {
|
||||||
const granularities = [
|
const granularities = [
|
||||||
{ label: 'Hourly', durationMs: 60 * 60 * 1000 },
|
{ label: 'Hourly', durationMs: 60 * 60 * 1000 },
|
||||||
{ label: 'Daily', durationMs: 24 * 60 * 60 * 1000 },
|
{ label: 'Daily', durationMs: 24 * 60 * 60 * 1000 },
|
||||||
|
|
|
||||||
|
|
@ -1,376 +1,395 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import { NoContent, Loader, Pagination } from 'UI';
|
import {NoContent, Loader, Pagination} from 'UI';
|
||||||
import { Button, Tag, Tooltip, Dropdown, message } from 'antd';
|
import {Button, Tag, Tooltip, Dropdown, message} from 'antd';
|
||||||
import { UndoOutlined, DownOutlined } from '@ant-design/icons';
|
import {UndoOutlined, DownOutlined} from '@ant-design/icons';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { useStore } from 'App/mstore';
|
import {useStore} from 'App/mstore';
|
||||||
import SessionItem from 'Shared/SessionItem';
|
import SessionItem from 'Shared/SessionItem';
|
||||||
import { observer } from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import { DateTime } from 'luxon';
|
import {DateTime} from 'luxon';
|
||||||
import { debounce, numberWithCommas } from 'App/utils';
|
import {debounce, numberWithCommas} from 'App/utils';
|
||||||
import useIsMounted from 'App/hooks/useIsMounted';
|
import useIsMounted from 'App/hooks/useIsMounted';
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
import { HEATMAP, USER_PATH, FUNNEL } from 'App/constants/card';
|
import {HEATMAP, USER_PATH, FUNNEL} from 'App/constants/card';
|
||||||
import { useTranslation } from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WidgetSessions(props: Props) {
|
function WidgetSessions(props: Props) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const listRef = React.useRef<HTMLDivElement>(null);
|
const listRef = React.useRef<HTMLDivElement>(null);
|
||||||
const { className = '' } = props;
|
const {className = ''} = props;
|
||||||
const [activeSeries, setActiveSeries] = useState('all');
|
const [activeSeries, setActiveSeries] = useState('all');
|
||||||
const [data, setData] = useState<any>([]);
|
const [data, setData] = useState<any>([]);
|
||||||
const isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
// all filtering done through series now
|
// all filtering done through series now
|
||||||
const filteredSessions = getListSessionsBySeries(data, 'all');
|
const filteredSessions = getListSessionsBySeries(data, 'all');
|
||||||
const { dashboardStore, metricStore, sessionStore, customFieldStore } =
|
const {dashboardStore, metricStore, sessionStore, customFieldStore} =
|
||||||
useStore();
|
useStore();
|
||||||
const focusedSeries = metricStore.focusedSeriesName;
|
const focusedSeries = metricStore.focusedSeriesName;
|
||||||
const filter = dashboardStore.drillDownFilter;
|
const filter = dashboardStore.drillDownFilter;
|
||||||
const widget = metricStore.instance;
|
const widget = metricStore.instance;
|
||||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat(
|
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat(
|
||||||
'LLL dd, yyyy HH:mm',
|
'LLL dd, yyyy HH:mm',
|
||||||
);
|
);
|
||||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat(
|
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat(
|
||||||
'LLL dd, yyyy HH:mm',
|
'LLL dd, yyyy HH:mm',
|
||||||
);
|
);
|
||||||
const [seriesOptions, setSeriesOptions] = useState([
|
const [seriesOptions, setSeriesOptions] = useState([
|
||||||
{ label: t('All'), value: 'all' },
|
{label: t('All'), value: 'all'},
|
||||||
]);
|
]);
|
||||||
const hasFilters =
|
const hasFilters =
|
||||||
filter.filters.length > 0 ||
|
filter.filters.length > 0 ||
|
||||||
filter.startTimestamp !== dashboardStore.drillDownPeriod.start ||
|
filter.startTimestamp !== dashboardStore.drillDownPeriod.start ||
|
||||||
filter.endTimestamp !== dashboardStore.drillDownPeriod.end;
|
filter.endTimestamp !== dashboardStore.drillDownPeriod.end;
|
||||||
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
|
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
|
||||||
const metaList = customFieldStore.list.map((i: any) => i.key);
|
const metaList = customFieldStore.list.map((i: any) => i.key);
|
||||||
|
|
||||||
const seriesDropdownItems = seriesOptions.map((option) => ({
|
const seriesDropdownItems = seriesOptions.map((option) => ({
|
||||||
key: option.value,
|
key: option.value,
|
||||||
label: (
|
label: (
|
||||||
<div onClick={() => setActiveSeries(option.value)}>{option.label}</div>
|
<div onClick={() => setActiveSeries(option.value)}>{option.label}</div>
|
||||||
),
|
),
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!widget.series) return;
|
|
||||||
const seriesOptions = widget.series.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.seriesId ?? item.name,
|
|
||||||
}));
|
}));
|
||||||
setSeriesOptions([{ label: t('All'), value: 'all' }, ...seriesOptions]);
|
|
||||||
}, [widget.series.length]);
|
|
||||||
|
|
||||||
const fetchSessions = (metricId: any, filter: any) => {
|
useEffect(() => {
|
||||||
if (!isMounted()) return;
|
if (!widget.series) return;
|
||||||
setLoading(true);
|
const seriesOptions = widget.series.map((item: any) => ({
|
||||||
delete filter.eventsOrderSupport;
|
label: item.name,
|
||||||
if (widget.metricType === FUNNEL) {
|
value: item.seriesId ?? item.name,
|
||||||
if (filter.series[0].filter.filters.length === 0) {
|
}));
|
||||||
setLoading(false);
|
setSeriesOptions([{label: t('All'), value: 'all'}, ...seriesOptions]);
|
||||||
return setData([]);
|
}, [widget.series.length]);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
widget
|
const fetchSessions = (metricId: any, filter: any) => {
|
||||||
.fetchSessions(metricId, filter)
|
if (!isMounted()) return;
|
||||||
.then((res: any) => {
|
|
||||||
setData(res);
|
if (widget.metricType === FUNNEL) {
|
||||||
if (metricStore.drillDown) {
|
if (filter.series[0].filter.filters.length === 0) {
|
||||||
setTimeout(() => {
|
setLoading(false);
|
||||||
message.info(t('Sessions Refreshed!'));
|
return setData([]);
|
||||||
listRef.current?.scrollIntoView({ behavior: 'smooth' });
|
}
|
||||||
metricStore.setDrillDown(false);
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const fetchClickmapSessions = (customFilters: Record<string, any>) => {
|
|
||||||
sessionStore.getSessions(customFilters).then((data) => {
|
|
||||||
setData([{ ...data, seriesId: 1, seriesName: 'Clicks' }]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const debounceRequest: any = React.useCallback(
|
|
||||||
debounce(fetchSessions, 1000),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const debounceClickMapSearch = React.useCallback(
|
|
||||||
debounce(fetchClickmapSessions, 1000),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const depsString = JSON.stringify(widget.series);
|
|
||||||
|
|
||||||
const loadData = () => {
|
setLoading(true);
|
||||||
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
|
const filterCopy = {...filter};
|
||||||
const clickFilter = {
|
delete filterCopy.eventsOrderSupport;
|
||||||
value: [metricStore.clickMapSearch],
|
|
||||||
type: 'CLICK',
|
try {
|
||||||
operator: 'onSelector',
|
// Handle filters properly with null checks
|
||||||
isEvent: true,
|
if (filterCopy.filters && filterCopy.filters.length > 0) {
|
||||||
// @ts-ignore
|
// Ensure the nested path exists before pushing
|
||||||
filters: [],
|
if (filterCopy.series?.[0]?.filter) {
|
||||||
};
|
if (!filterCopy.series[0].filter.filters) {
|
||||||
const timeRange = {
|
filterCopy.series[0].filter.filters = [];
|
||||||
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
|
}
|
||||||
startDate: dashboardStore.drillDownPeriod.start,
|
filterCopy.series[0].filter.filters.push(...filterCopy.filters);
|
||||||
endDate: dashboardStore.drillDownPeriod.end,
|
}
|
||||||
};
|
filterCopy.filters = [];
|
||||||
const customFilter = {
|
}
|
||||||
...filter,
|
} catch (e) {
|
||||||
...timeRange,
|
// do nothing
|
||||||
filters: [...sessionStore.userFilter.filters, clickFilter],
|
|
||||||
};
|
|
||||||
debounceClickMapSearch(customFilter);
|
|
||||||
} else {
|
|
||||||
const hasStartPoint =
|
|
||||||
!!widget.startPoint && widget.metricType === USER_PATH;
|
|
||||||
const onlyFocused = focusedSeries
|
|
||||||
? widget.series.filter((s) => s.name === focusedSeries)
|
|
||||||
: widget.series;
|
|
||||||
const activeSeries = metricStore.disabledSeries.length
|
|
||||||
? onlyFocused.filter(
|
|
||||||
(s) => !metricStore.disabledSeries.includes(s.name),
|
|
||||||
)
|
|
||||||
: onlyFocused;
|
|
||||||
const seriesJson = activeSeries.map((s) => s.toJson());
|
|
||||||
if (hasStartPoint) {
|
|
||||||
seriesJson[0].filter.filters.push(widget.startPoint.toJson());
|
|
||||||
}
|
|
||||||
if (widget.metricType === USER_PATH) {
|
|
||||||
if (
|
|
||||||
seriesJson[0].filter.filters[0].value[0] === '' &&
|
|
||||||
widget.data.nodes
|
|
||||||
) {
|
|
||||||
seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name;
|
|
||||||
} else if (
|
|
||||||
seriesJson[0].filter.filters[0].value[0] === '' &&
|
|
||||||
!widget.data.nodes?.length
|
|
||||||
) {
|
|
||||||
// no point requesting if we don't have starting point picked by api
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
widget
|
||||||
debounceRequest(widget.metricId, {
|
.fetchSessions(metricId, filterCopy)
|
||||||
...filter,
|
.then((res: any) => {
|
||||||
series: seriesJson,
|
setData(res);
|
||||||
page: metricStore.sessionsPage,
|
if (metricStore.drillDown) {
|
||||||
limit: metricStore.sessionsPageSize,
|
setTimeout(() => {
|
||||||
});
|
message.info(t('Sessions Refreshed!'));
|
||||||
}
|
listRef.current?.scrollIntoView({behavior: 'smooth'});
|
||||||
};
|
metricStore.setDrillDown(false);
|
||||||
useEffect(() => {
|
}, 0);
|
||||||
metricStore.updateKey('sessionsPage', 1);
|
}
|
||||||
loadData();
|
})
|
||||||
}, [
|
.finally(() => {
|
||||||
filter.startTimestamp,
|
setLoading(false);
|
||||||
filter.endTimestamp,
|
});
|
||||||
filter.filters,
|
};
|
||||||
depsString,
|
const fetchClickmapSessions = (customFilters: Record<string, any>) => {
|
||||||
metricStore.clickMapSearch,
|
sessionStore.getSessions(customFilters).then((data) => {
|
||||||
focusedSeries,
|
setData([{...data, seriesId: 1, seriesName: 'Clicks'}]);
|
||||||
widget.startPoint,
|
});
|
||||||
widget.data.nodes,
|
};
|
||||||
metricStore.disabledSeries.length,
|
const debounceRequest: any = React.useCallback(
|
||||||
]);
|
debounce(fetchSessions, 1000),
|
||||||
useEffect(loadData, [metricStore.sessionsPage]);
|
[],
|
||||||
useEffect(() => {
|
);
|
||||||
if (activeSeries === 'all') {
|
const debounceClickMapSearch = React.useCallback(
|
||||||
metricStore.setFocusedSeriesName(null);
|
debounce(fetchClickmapSessions, 1000),
|
||||||
} else {
|
[],
|
||||||
metricStore.setFocusedSeriesName(
|
);
|
||||||
seriesOptions.find((option) => option.value === activeSeries)?.label,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [activeSeries]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (focusedSeries) {
|
|
||||||
setActiveSeries(
|
|
||||||
seriesOptions.find((option) => option.label === focusedSeries)?.value ||
|
|
||||||
'all',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setActiveSeries('all');
|
|
||||||
}
|
|
||||||
}, [focusedSeries]);
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
const depsString = JSON.stringify(widget.series);
|
||||||
metricStore.updateKey('sessionsPage', 1);
|
|
||||||
dashboardStore.resetDrillDownFilter();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const loadData = () => {
|
||||||
<div
|
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
|
||||||
className={cn(
|
const clickFilter = {
|
||||||
className,
|
value: [metricStore.clickMapSearch],
|
||||||
'bg-white p-3 pb-0 rounded-xl shadow-sm border mt-3',
|
type: 'CLICK',
|
||||||
)}
|
operator: 'onSelector',
|
||||||
>
|
isEvent: true,
|
||||||
<div className="flex items-center justify-between">
|
// @ts-ignore
|
||||||
<div>
|
filters: [],
|
||||||
<div className="flex items-baseline gap-2">
|
};
|
||||||
<h2 className="text-xl">
|
const timeRange = {
|
||||||
{metricStore.clickMapSearch ? t('Clicks') : t('Sessions')}
|
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
|
||||||
</h2>
|
startDate: dashboardStore.drillDownPeriod.start,
|
||||||
<div className="ml-2 color-gray-medium">
|
endDate: dashboardStore.drillDownPeriod.end,
|
||||||
{metricStore.clickMapLabel
|
};
|
||||||
? `on "${metricStore.clickMapLabel}" `
|
const customFilter = {
|
||||||
: null}
|
...filter,
|
||||||
{t('between')}{' '}
|
...timeRange,
|
||||||
<span className="font-medium color-gray-darkest">
|
filters: [...sessionStore.userFilter.filters, clickFilter],
|
||||||
|
};
|
||||||
|
debounceClickMapSearch(customFilter);
|
||||||
|
} else {
|
||||||
|
const hasStartPoint =
|
||||||
|
!!widget.startPoint && widget.metricType === USER_PATH;
|
||||||
|
const onlyFocused = focusedSeries
|
||||||
|
? widget.series.filter((s) => s.name === focusedSeries)
|
||||||
|
: widget.series;
|
||||||
|
const activeSeries = metricStore.disabledSeries.length
|
||||||
|
? onlyFocused.filter(
|
||||||
|
(s) => !metricStore.disabledSeries.includes(s.name),
|
||||||
|
)
|
||||||
|
: onlyFocused;
|
||||||
|
const seriesJson = activeSeries.map((s) => s.toJson());
|
||||||
|
if (hasStartPoint) {
|
||||||
|
seriesJson[0].filter.filters.push(widget.startPoint.toJson());
|
||||||
|
}
|
||||||
|
if (widget.metricType === USER_PATH) {
|
||||||
|
if (
|
||||||
|
seriesJson[0].filter.filters[0].value[0] === '' &&
|
||||||
|
widget.data.nodes?.length
|
||||||
|
) {
|
||||||
|
seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name;
|
||||||
|
} else if (
|
||||||
|
seriesJson[0].filter.filters[0].value[0] === '' &&
|
||||||
|
!widget.data.nodes?.length
|
||||||
|
) {
|
||||||
|
// no point requesting if we don't have starting point picked by api
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debounceRequest(widget.metricId, {
|
||||||
|
...filter,
|
||||||
|
series: seriesJson,
|
||||||
|
page: metricStore.sessionsPage,
|
||||||
|
limit: metricStore.sessionsPageSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
metricStore.updateKey('sessionsPage', 1);
|
||||||
|
loadData();
|
||||||
|
}, [
|
||||||
|
filter.startTimestamp,
|
||||||
|
filter.endTimestamp,
|
||||||
|
filter.filters,
|
||||||
|
depsString,
|
||||||
|
metricStore.clickMapSearch,
|
||||||
|
focusedSeries,
|
||||||
|
widget.startPoint,
|
||||||
|
widget.data.nodes,
|
||||||
|
metricStore.disabledSeries.length,
|
||||||
|
]);
|
||||||
|
useEffect(loadData, [metricStore.sessionsPage]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSeries === 'all') {
|
||||||
|
metricStore.setFocusedSeriesName(null);
|
||||||
|
} else {
|
||||||
|
metricStore.setFocusedSeriesName(
|
||||||
|
seriesOptions.find((option) => option.value === activeSeries)?.label,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [activeSeries]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusedSeries) {
|
||||||
|
setActiveSeries(
|
||||||
|
seriesOptions.find((option) => option.label === focusedSeries)?.value ||
|
||||||
|
'all',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setActiveSeries('all');
|
||||||
|
}
|
||||||
|
}, [focusedSeries]);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
metricStore.updateKey('sessionsPage', 1);
|
||||||
|
dashboardStore.resetDrillDownFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
className,
|
||||||
|
'bg-white p-3 pb-0 rounded-xl shadow-sm border mt-3',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<h2 className="text-xl">
|
||||||
|
{metricStore.clickMapSearch ? t('Clicks') : t('Sessions')}
|
||||||
|
</h2>
|
||||||
|
<div className="ml-2 color-gray-medium">
|
||||||
|
{metricStore.clickMapLabel
|
||||||
|
? `on "${metricStore.clickMapLabel}" `
|
||||||
|
: null}
|
||||||
|
{t('between')}{' '}
|
||||||
|
<span className="font-medium color-gray-darkest">
|
||||||
{startTime}
|
{startTime}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('and')}{' '}
|
{t('and')}{' '}
|
||||||
<span className="font-medium color-gray-darkest">
|
<span className="font-medium color-gray-darkest">
|
||||||
{endTime}
|
{endTime}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
</div>
|
</div>
|
||||||
{hasFilters && (
|
{hasFilters && (
|
||||||
<Tooltip title={t('Clear Drilldown')} placement="top">
|
<Tooltip title={t('Clear Drilldown')} placement="top">
|
||||||
<Button type="text" size="small" onClick={clearFilters}>
|
<Button type="text" size="small" onClick={clearFilters}>
|
||||||
<UndoOutlined />
|
<UndoOutlined/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasFilters && widget.metricType === 'table' && (
|
{hasFilters && widget.metricType === 'table' && (
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Tag
|
<Tag
|
||||||
closable
|
closable
|
||||||
onClose={clearFilters}
|
onClose={clearFilters}
|
||||||
className="truncate max-w-44 rounded-lg"
|
className="truncate max-w-44 rounded-lg"
|
||||||
>
|
>
|
||||||
{filterText}
|
{filterText}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
||||||
<div className="flex items-center ml-6">
|
<div className="flex items-center ml-6">
|
||||||
<span className="mr-2 color-gray-medium">
|
<span className="mr-2 color-gray-medium">
|
||||||
{t('Filter by Series')}
|
{t('Filter by Series')}
|
||||||
</span>
|
</span>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: seriesDropdownItems,
|
items: seriesDropdownItems,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
selectedKeys: [activeSeries],
|
selectedKeys: [activeSeries],
|
||||||
}}
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
>
|
>
|
||||||
<Button type="text" size="small">
|
<Button type="text" size="small">
|
||||||
{seriesOptions.find((option) => option.value === activeSeries)
|
{seriesOptions.find((option) => option.value === activeSeries)
|
||||||
?.label || t('Select Series')}
|
?.label || t('Select Series')}
|
||||||
<DownOutlined />
|
<DownOutlined/>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3">
|
|
||||||
<Loader loading={loading}>
|
|
||||||
<NoContent
|
|
||||||
title={
|
|
||||||
<div className="flex items-center justify-center flex-col">
|
|
||||||
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60} />
|
|
||||||
<div className="mt-4" />
|
|
||||||
<div className="text-center">
|
|
||||||
{t('No relevant sessions found for the selected time period')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
show={filteredSessions.sessions.length === 0}
|
|
||||||
>
|
|
||||||
{filteredSessions.sessions.map((session: any) => (
|
|
||||||
<React.Fragment key={session.sessionId}>
|
|
||||||
<SessionItem
|
|
||||||
disableUser
|
|
||||||
session={session}
|
|
||||||
metaList={metaList}
|
|
||||||
/>
|
|
||||||
<div className="border-b" />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div
|
<div className="mt-3">
|
||||||
className="flex items-center justify-between p-5"
|
<Loader loading={loading}>
|
||||||
ref={listRef}
|
<NoContent
|
||||||
>
|
title={
|
||||||
<div>
|
<div className="flex items-center justify-center flex-col">
|
||||||
{t('Showing')}{' '}
|
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60}/>
|
||||||
<span className="font-medium">
|
<div className="mt-4"/>
|
||||||
|
<div className="text-center">
|
||||||
|
{t('No relevant sessions found for the selected time period')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
show={filteredSessions.sessions.length === 0}
|
||||||
|
>
|
||||||
|
{filteredSessions.sessions.map((session: any) => (
|
||||||
|
<React.Fragment key={session.sessionId}>
|
||||||
|
<SessionItem
|
||||||
|
disableUser
|
||||||
|
session={session}
|
||||||
|
metaList={metaList}
|
||||||
|
/>
|
||||||
|
<div className="border-b"/>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between p-5"
|
||||||
|
ref={listRef}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{t('Showing')}{' '}
|
||||||
|
<span className="font-medium">
|
||||||
{(metricStore.sessionsPage - 1) *
|
{(metricStore.sessionsPage - 1) *
|
||||||
metricStore.sessionsPageSize +
|
metricStore.sessionsPageSize +
|
||||||
1}
|
1}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('to')}{' '}
|
{t('to')}{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{(metricStore.sessionsPage - 1) *
|
{(metricStore.sessionsPage - 1) *
|
||||||
metricStore.sessionsPageSize +
|
metricStore.sessionsPageSize +
|
||||||
filteredSessions.sessions.length}
|
filteredSessions.sessions.length}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('of')}{' '}
|
{t('of')}{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{numberWithCommas(filteredSessions.total)}
|
{numberWithCommas(filteredSessions.total)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('sessions.')}
|
{t('sessions.')}
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
page={metricStore.sessionsPage}
|
page={metricStore.sessionsPage}
|
||||||
total={filteredSessions.total}
|
total={filteredSessions.total}
|
||||||
onPageChange={(page: any) =>
|
onPageChange={(page: any) =>
|
||||||
metricStore.updateKey('sessionsPage', page)
|
metricStore.updateKey('sessionsPage', page)
|
||||||
}
|
}
|
||||||
limit={metricStore.sessionsPageSize}
|
limit={metricStore.sessionsPageSize}
|
||||||
debounceRequest={500}
|
debounceRequest={500}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</NoContent>
|
||||||
|
</Loader>
|
||||||
</div>
|
</div>
|
||||||
</NoContent>
|
</div>
|
||||||
</Loader>
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getListSessionsBySeries = (data: any, seriesId: any) => {
|
const getListSessionsBySeries = (data: any, seriesId: any) => {
|
||||||
const arr = data.reduce(
|
const arr = data.reduce(
|
||||||
(arr: any, element: any) => {
|
(arr: any, element: any) => {
|
||||||
if (seriesId === 'all') {
|
if (seriesId === 'all') {
|
||||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||||
const sessions = element.sessions.filter(
|
const sessions = element.sessions.filter(
|
||||||
(i: any) => !sessionIds.includes(i.sessionId),
|
(i: any) => !sessionIds.includes(i.sessionId),
|
||||||
);
|
);
|
||||||
arr.sessions.push(...sessions);
|
arr.sessions.push(...sessions);
|
||||||
} else if (element.seriesId === seriesId) {
|
} else if (element.seriesId === seriesId) {
|
||||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||||
const sessions = element.sessions.filter(
|
const sessions = element.sessions.filter(
|
||||||
(i: any) => !sessionIds.includes(i.sessionId),
|
(i: any) => !sessionIds.includes(i.sessionId),
|
||||||
);
|
);
|
||||||
const duplicates = element.sessions.length - sessions.length;
|
const duplicates = element.sessions.length - sessions.length;
|
||||||
arr.sessions.push(...sessions);
|
arr.sessions.push(...sessions);
|
||||||
arr.total = element.total - duplicates;
|
arr.total = element.total - duplicates;
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
},
|
},
|
||||||
{ sessions: [] },
|
{sessions: []},
|
||||||
);
|
);
|
||||||
arr.total =
|
arr.total =
|
||||||
seriesId === 'all'
|
seriesId === 'all'
|
||||||
? Math.max(...data.map((i: any) => i.total))
|
? Math.max(...data.map((i: any) => i.total))
|
||||||
: data.find((i: any) => i.seriesId === seriesId).total;
|
: data.find((i: any) => i.seriesId === seriesId).total;
|
||||||
return arr;
|
return arr;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default observer(WidgetSessions);
|
export default observer(WidgetSessions);
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,9 @@ function WidgetView({
|
||||||
filter: { filters: selectedCard.filters },
|
filter: { filters: selectedCard.filters },
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
} else if (selectedCard.cardType === TABLE) {
|
||||||
|
cardData.series = [new FilterSeries()];
|
||||||
|
cardData.series[0].filter.eventsOrder = 'and';
|
||||||
}
|
}
|
||||||
if (selectedCard.cardType === FUNNEL) {
|
if (selectedCard.cardType === FUNNEL) {
|
||||||
cardData.series = [new FilterSeries()];
|
cardData.series = [new FilterSeries()];
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const onChartClick = () => {
|
const onChartClick = () => {
|
||||||
|
dashboardStore.setDrillDownPeriod(dashboardStore.period);
|
||||||
// if (!isWidget || isPredefined) return;
|
// if (!isWidget || isPredefined) return;
|
||||||
props.history.push(
|
props.history.push(
|
||||||
withSiteId(
|
withSiteId(
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,80 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import { Form, Input, Loader, Icon, Message } from 'UI';
|
import { Form, Input, Loader, Icon, Message } from 'UI';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { validatePassword } from 'App/validate';
|
import { validatePassword } from 'App/validate';
|
||||||
import { PASSWORD_POLICY } from 'App/constants';
|
import { PASSWORD_POLICY } from 'App/constants';
|
||||||
import stl from './forgotPassword.module.css';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import withCaptcha, { WithCaptchaProps } from 'App/withRecaptcha';
|
||||||
|
|
||||||
const recaptchaRef = React.createRef();
|
|
||||||
const ERROR_DONT_MATCH = (t) => t("Passwords don't match.");
|
const ERROR_DONT_MATCH = (t) => t("Passwords don't match.");
|
||||||
const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true';
|
|
||||||
const { CAPTCHA_SITE_KEY } = window.env;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: any;
|
params: any;
|
||||||
}
|
}
|
||||||
function CreatePassword(props: Props) {
|
|
||||||
|
function CreatePassword(props: Props & WithCaptchaProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { params } = props;
|
const { params } = props;
|
||||||
const { userStore } = useStore();
|
const { userStore } = useStore();
|
||||||
const { loading } = userStore;
|
const { loading } = userStore;
|
||||||
const { resetPassword } = userStore;
|
const { resetPassword } = userStore;
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [validationError, setValidationError] = React.useState<string | null>(
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
null,
|
const [updated, setUpdated] = useState(false);
|
||||||
);
|
const [passwordRepeat, setPasswordRepeat] = useState('');
|
||||||
const [updated, setUpdated] = React.useState(false);
|
const [password, setPassword] = useState('');
|
||||||
const [passwordRepeat, setPasswordRepeat] = React.useState('');
|
|
||||||
const [password, setPassword] = React.useState('');
|
|
||||||
const pass = params.get('pass');
|
const pass = params.get('pass');
|
||||||
const invitation = params.get('invitation');
|
const invitation = params.get('invitation');
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const { submitWithCaptcha, isVerifyingCaptcha, resetCaptcha } = props;
|
||||||
|
|
||||||
|
const handleSubmit = (token?: string) => {
|
||||||
if (!validatePassword(password)) {
|
if (!validatePassword(password)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void resetPassword({ invitation, pass, password });
|
|
||||||
|
resetPassword({
|
||||||
|
invitation,
|
||||||
|
pass,
|
||||||
|
password,
|
||||||
|
'g-recaptcha-response': token
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setUpdated(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setError(err.message);
|
||||||
|
// Reset captcha for the next attempt
|
||||||
|
resetCaptcha();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (e: any) => {
|
const onSubmit = () => {
|
||||||
e.preventDefault();
|
// Validate before attempting captcha verification
|
||||||
if (CAPTCHA_ENABLED && recaptchaRef.current) {
|
if (!validatePassword(password) || password !== passwordRepeat) {
|
||||||
recaptchaRef.current.execute();
|
setValidationError(
|
||||||
} else if (!CAPTCHA_ENABLED) {
|
password !== passwordRepeat
|
||||||
handleSubmit();
|
? ERROR_DONT_MATCH(t)
|
||||||
|
: PASSWORD_POLICY(t)
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset any previous errors
|
||||||
|
setError(null);
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
|
submitWithCaptcha({ pass, invitation, password })
|
||||||
|
.then((data) => {
|
||||||
|
handleSubmit(data['g-recaptcha-response']);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Captcha verification failed:', error);
|
||||||
|
// The component will handle showing appropriate messages
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const write = (e: any) => {
|
const write = (e: any) => {
|
||||||
|
|
@ -63,7 +91,7 @@ function CreatePassword(props: Props) {
|
||||||
} else {
|
} else {
|
||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
}
|
}
|
||||||
}, [passwordRepeat, password]);
|
}, [passwordRepeat, password, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
|
|
@ -73,19 +101,8 @@ function CreatePassword(props: Props) {
|
||||||
>
|
>
|
||||||
{!error && (
|
{!error && (
|
||||||
<>
|
<>
|
||||||
<Loader loading={loading}>
|
<Loader loading={loading || isVerifyingCaptcha}>
|
||||||
<div data-hidden={updated} className="w-full">
|
<div data-hidden={updated} className="w-full">
|
||||||
{CAPTCHA_ENABLED && (
|
|
||||||
<div className={stl.recaptcha}>
|
|
||||||
<ReCAPTCHA
|
|
||||||
ref={recaptchaRef}
|
|
||||||
size="invisible"
|
|
||||||
sitekey={CAPTCHA_SITE_KEY}
|
|
||||||
onChange={(token: any) => handleSubmit(token)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>{t('New password')}</label>
|
<label>{t('New password')}</label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -132,10 +149,15 @@ function CreatePassword(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
loading={loading}
|
loading={loading || isVerifyingCaptcha}
|
||||||
|
disabled={loading || isVerifyingCaptcha || validationError !== null}
|
||||||
className="w-full mt-4"
|
className="w-full mt-4"
|
||||||
>
|
>
|
||||||
{t('Create')}
|
{isVerifyingCaptcha
|
||||||
|
? t('Verifying...')
|
||||||
|
: loading
|
||||||
|
? t('Processing...')
|
||||||
|
: t('Create')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -153,4 +175,4 @@ function CreatePassword(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(CreatePassword);
|
export default withCaptcha(observer(CreatePassword));
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Loader, Icon } from 'UI';
|
import { Loader, Icon } from 'UI';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { Form, Input, Button, Typography } from 'antd';
|
import { Form, Input, Button, Typography } from 'antd';
|
||||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import withCaptcha, { WithCaptchaProps } from 'App/withRecaptcha';
|
||||||
|
|
||||||
function ResetPasswordRequest() {
|
interface Props {
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResetPasswordRequest(props: Props & WithCaptchaProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { userStore } = useStore();
|
const { userStore } = useStore();
|
||||||
const { loading } = userStore;
|
const { loading } = userStore;
|
||||||
const { requestResetPassword } = userStore;
|
const { requestResetPassword } = userStore;
|
||||||
const recaptchaRef = React.createRef();
|
const [requested, setRequested] = useState(false);
|
||||||
const [requested, setRequested] = React.useState(false);
|
const [email, setEmail] = useState('');
|
||||||
const [email, setEmail] = React.useState('');
|
const [error, setError] = useState(null);
|
||||||
const [error, setError] = React.useState(null);
|
const [smtpError, setSmtpError] = useState<boolean>(false);
|
||||||
const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true';
|
|
||||||
const { CAPTCHA_SITE_KEY } = window.env;
|
const { submitWithCaptcha, isVerifyingCaptcha, resetCaptcha } = props;
|
||||||
const [smtpError, setSmtpError] = React.useState<boolean>(false);
|
|
||||||
|
|
||||||
const write = (e: any) => {
|
const write = (e: any) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
@ -26,22 +28,21 @@ function ResetPasswordRequest() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
// e.preventDefault();
|
// Validation check
|
||||||
if (CAPTCHA_ENABLED && recaptchaRef.current) {
|
if (!email || email.trim() === '') {
|
||||||
recaptchaRef.current.execute();
|
return;
|
||||||
} else if (!CAPTCHA_ENABLED) {
|
|
||||||
handleSubmit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submitWithCaptcha({ email: email.trim() })
|
||||||
|
.then((data) => {
|
||||||
|
handleSubmit(data['g-recaptcha-response']);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.error('Captcha verification failed:', error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (token?: any) => {
|
const handleSubmit = (token?: string) => {
|
||||||
if (
|
|
||||||
CAPTCHA_ENABLED &&
|
|
||||||
recaptchaRef.current &&
|
|
||||||
(token === null || token === undefined)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token })
|
requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token })
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
|
|
@ -50,29 +51,21 @@ function ResetPasswordRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
// Reset captcha for the next attempt
|
||||||
|
resetCaptcha();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setRequested(true);
|
setRequested(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onFinish={onSubmit}
|
onFinish={onSubmit}
|
||||||
style={{ minWidth: '50%' }}
|
style={{ minWidth: '50%' }}
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
>
|
>
|
||||||
<Loader loading={false}>
|
<Loader loading={loading || isVerifyingCaptcha}>
|
||||||
{CAPTCHA_ENABLED && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<ReCAPTCHA
|
|
||||||
ref={recaptchaRef}
|
|
||||||
size="invisible"
|
|
||||||
data-hidden={requested}
|
|
||||||
sitekey={CAPTCHA_SITE_KEY}
|
|
||||||
onChange={(token: any) => handleSubmit(token)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!requested && (
|
{!requested && (
|
||||||
<>
|
<>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
|
@ -92,10 +85,14 @@ function ResetPasswordRequest() {
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
loading={loading}
|
loading={loading || isVerifyingCaptcha}
|
||||||
disabled={loading}
|
disabled={loading || isVerifyingCaptcha}
|
||||||
>
|
>
|
||||||
{t('Email Password Reset Link')}
|
{isVerifyingCaptcha
|
||||||
|
? t('Verifying...')
|
||||||
|
: loading
|
||||||
|
? t('Processing...')
|
||||||
|
: t('Email Password Reset Link')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -146,4 +143,4 @@ function ResetPasswordRequest() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(ResetPasswordRequest);
|
export default withCaptcha(observer(ResetPasswordRequest));
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import withPageTitle from 'HOCs/withPageTitle';
|
import withPageTitle from 'HOCs/withPageTitle';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
// Consider using a different approach for titles in functional components
|
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { ENTERPRISE_REQUEIRED } from 'App/constants';
|
|
||||||
import { forgotPassword, signup } from 'App/routes';
|
import { forgotPassword, signup } from 'App/routes';
|
||||||
import { Icon, Link, Loader, Tooltip } from 'UI';
|
import { Icon, Link, Loader } from 'UI';
|
||||||
import { Button, Form, Input } from 'antd';
|
import { Button, Form, Input } from 'antd';
|
||||||
|
|
||||||
import Copyright from 'Shared/Copyright';
|
import Copyright from 'Shared/Copyright';
|
||||||
|
|
||||||
import stl from './login.module.css';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import LanguageSwitcher from '../LanguageSwitcher';
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
|
import withCaptcha, { WithCaptchaProps } from 'App/withRecaptcha';
|
||||||
|
import SSOLogin from './SSOLogin';
|
||||||
|
|
||||||
const FORGOT_PASSWORD = forgotPassword();
|
const FORGOT_PASSWORD = forgotPassword();
|
||||||
const SIGNUP_ROUTE = signup();
|
const SIGNUP_ROUTE = signup();
|
||||||
|
|
@ -26,14 +21,15 @@ interface LoginProps {
|
||||||
location: Location;
|
location: Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true';
|
function Login({
|
||||||
|
location,
|
||||||
function Login({ location }: LoginProps) {
|
submitWithCaptcha,
|
||||||
|
isVerifyingCaptcha,
|
||||||
|
resetCaptcha,
|
||||||
|
}: LoginProps & WithCaptchaProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
// const CAPTCHA_ENABLED = useMemo(() => window.env.CAPTCHA_ENABLED === 'true', []);
|
|
||||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
|
||||||
const { loginStore, userStore } = useStore();
|
const { loginStore, userStore } = useStore();
|
||||||
const { errors } = userStore.loginRequest;
|
const { errors } = userStore.loginRequest;
|
||||||
const { loading } = loginStore;
|
const { loading } = loginStore;
|
||||||
|
|
@ -49,7 +45,6 @@ function Login({ location }: LoginProps) {
|
||||||
}, [authDetails]);
|
}, [authDetails]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// void fetchTenants();
|
|
||||||
const jwt = params.get('jwt');
|
const jwt = params.get('jwt');
|
||||||
const spotJwt = params.get('spotJwt');
|
const spotJwt = params.get('spotJwt');
|
||||||
if (spotJwt) {
|
if (spotJwt) {
|
||||||
|
|
@ -108,32 +103,36 @@ function Login({ location }: LoginProps) {
|
||||||
if (resp) {
|
if (resp) {
|
||||||
userStore.syntheticLogin(resp);
|
userStore.syntheticLogin(resp);
|
||||||
setJwt({ jwt: resp.jwt, spotJwt: resp.spotJwt ?? null });
|
setJwt({ jwt: resp.jwt, spotJwt: resp.spotJwt ?? null });
|
||||||
handleSpotLogin(resp.spotJwt);
|
if (resp.spotJwt) {
|
||||||
|
handleSpotLogin(resp.spotJwt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
userStore.syntheticLoginError(e);
|
userStore.syntheticLoginError(e);
|
||||||
|
resetCaptcha();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (CAPTCHA_ENABLED && recaptchaRef.current) {
|
if (!email || !password) {
|
||||||
recaptchaRef.current.execute();
|
return;
|
||||||
} else if (!CAPTCHA_ENABLED) {
|
|
||||||
handleSubmit();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const ssoLink =
|
submitWithCaptcha({ email: email.trim(), password })
|
||||||
window !== window.top
|
.then((data) => {
|
||||||
? `${window.location.origin}/api/sso/saml2?iFrame=true`
|
handleSubmit(data['g-recaptcha-response']);
|
||||||
: `${window.location.origin}/api/sso/saml2`;
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.error('Captcha error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="m-10 ">
|
<div className="m-10 ">
|
||||||
<img src="/assets/logo.svg" width={200} />
|
<img src="/assets/logo.svg" width={200} alt="Company Logo" />
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg bg-white shadow-sm">
|
<div className="border rounded-lg bg-white shadow-sm">
|
||||||
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">
|
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">
|
||||||
|
|
@ -145,15 +144,7 @@ function Login({ location }: LoginProps) {
|
||||||
className={cn('flex items-center justify-center flex-col')}
|
className={cn('flex items-center justify-center flex-col')}
|
||||||
style={{ width: '350px' }}
|
style={{ width: '350px' }}
|
||||||
>
|
>
|
||||||
<Loader loading={loading}>
|
<Loader loading={loading || isVerifyingCaptcha}>
|
||||||
{CAPTCHA_ENABLED && (
|
|
||||||
<ReCAPTCHA
|
|
||||||
ref={recaptchaRef}
|
|
||||||
size="invisible"
|
|
||||||
sitekey={window.env.CAPTCHA_SITE_KEY}
|
|
||||||
onChange={(token) => handleSubmit(token)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div style={{ width: '350px' }} className="px-8">
|
<div style={{ width: '350px' }} className="px-8">
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<label>{t('Email Address')}</label>
|
<label>{t('Email Address')}</label>
|
||||||
|
|
@ -186,8 +177,8 @@ function Login({ location }: LoginProps) {
|
||||||
</Loader>
|
</Loader>
|
||||||
{errors && errors.length ? (
|
{errors && errors.length ? (
|
||||||
<div className="px-8 my-2 w-full">
|
<div className="px-8 my-2 w-full">
|
||||||
{errors.map((error) => (
|
{errors.map((error, index) => (
|
||||||
<div className="flex items-center bg-red-lightest rounded p-3">
|
<div key={index} className="flex items-center bg-red-lightest rounded p-3">
|
||||||
<Icon name="info" color="red" size="20" />
|
<Icon name="info" color="red" size="20" />
|
||||||
<span className="color-red ml-2">
|
<span className="color-red ml-2">
|
||||||
{error}
|
{error}
|
||||||
|
|
@ -204,8 +195,14 @@ function Login({ location }: LoginProps) {
|
||||||
className="mt-2 w-full text-center rounded-lg"
|
className="mt-2 w-full text-center rounded-lg"
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
|
loading={loading || isVerifyingCaptcha}
|
||||||
|
disabled={loading || isVerifyingCaptcha}
|
||||||
>
|
>
|
||||||
{t('Login')}
|
{isVerifyingCaptcha
|
||||||
|
? t('Verifying...')
|
||||||
|
: loading
|
||||||
|
? t('Logging in...')
|
||||||
|
: t('Login')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="my-8 flex justify-center items-center flex-wrap">
|
<div className="my-8 flex justify-center items-center flex-wrap">
|
||||||
|
|
@ -219,63 +216,12 @@ function Login({ location }: LoginProps) {
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className={cn(stl.sso, 'py-2 flex flex-col items-center')}>
|
<SSOLogin authDetails={authDetails} />
|
||||||
{authDetails.sso ? (
|
|
||||||
<a href={ssoLink} rel="noopener noreferrer">
|
|
||||||
<Button type="text" htmlType="submit">
|
|
||||||
{`${t('Login with SSO')} ${
|
|
||||||
authDetails.ssoProvider
|
|
||||||
? `(${authDetails.ssoProvider})`
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
delay={0}
|
|
||||||
title={
|
|
||||||
<div className="text-center">
|
|
||||||
{authDetails.edition === 'ee' ? (
|
|
||||||
<span>
|
|
||||||
{t('SSO has not been configured.')}
|
|
||||||
<br />
|
|
||||||
{t('Please reach out to your admin.')}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
ENTERPRISE_REQUEIRED(t)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
htmlType="submit"
|
|
||||||
className="pointer-events-none opacity-30"
|
|
||||||
>
|
|
||||||
{`${t('Login with SSO')} ${
|
|
||||||
authDetails.ssoProvider
|
|
||||||
? `(${authDetails.ssoProvider})`
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn('flex items-center w-96 justify-center my-8', {
|
|
||||||
'!hidden': !authDetails?.enforceSSO,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a href={ssoLink} rel="noopener noreferrer">
|
|
||||||
<Button type="primary">
|
|
||||||
{`${t('Login with SSO')} ${
|
|
||||||
authDetails.ssoProvider ? `(${authDetails.ssoProvider})` : ''
|
|
||||||
}`}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{authDetails?.enforceSSO && (
|
||||||
|
<SSOLogin authDetails={authDetails} enforceSSO={true} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -287,4 +233,6 @@ function Login({ location }: LoginProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPageTitle('Login - OpenReplay')(observer(Login));
|
export default withPageTitle('Login - OpenReplay')(
|
||||||
|
withCaptcha(observer(Login))
|
||||||
|
);
|
||||||
|
|
|
||||||
78
frontend/app/components/Login/SSOLogin.tsx
Normal file
78
frontend/app/components/Login/SSOLogin.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ENTERPRISE_REQUEIRED } from 'App/constants';
|
||||||
|
import stl from './login.module.css';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
|
||||||
|
interface SSOLoginProps {
|
||||||
|
authDetails: any;
|
||||||
|
enforceSSO?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSOLogin = ({ authDetails, enforceSSO = false }: SSOLoginProps) => {
|
||||||
|
const { userStore } = useStore();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isSSOSupported } = userStore;
|
||||||
|
|
||||||
|
const getSSOLink = () =>
|
||||||
|
window !== window.top
|
||||||
|
? `${window.location.origin}/api/sso/saml2?iFrame=true`
|
||||||
|
: `${window.location.origin}/api/sso/saml2`;
|
||||||
|
|
||||||
|
const ssoLink = getSSOLink();
|
||||||
|
const ssoButtonText = `${t('Login with SSO')} ${authDetails.ssoProvider ? `(${authDetails.ssoProvider})` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
if (enforceSSO) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center w-96 justify-center my-8')}>
|
||||||
|
<a href={ssoLink} rel="noopener noreferrer">
|
||||||
|
<Button type="primary">{ssoButtonText}</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(stl.sso, 'py-2 flex flex-col items-center')}>
|
||||||
|
{authDetails.sso ? (
|
||||||
|
<a href={ssoLink} rel="noopener noreferrer">
|
||||||
|
<Button type="text" htmlType="submit">
|
||||||
|
{ssoButtonText}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div className="text-center">
|
||||||
|
{isSSOSupported ? (
|
||||||
|
<span>
|
||||||
|
{t('SSO has not been configured.')}
|
||||||
|
<br />
|
||||||
|
{t('Please reach out to your admin.')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
ENTERPRISE_REQUEIRED(t)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span className="cursor-not-allowed">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
htmlType="submit"
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
{ssoButtonText}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SSOLogin;
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router';
|
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { OB_TABS, onboarding as onboardingRoute, withSiteId } from 'App/routes';
|
import { OB_TABS, onboarding as onboardingRoute, withSiteId } from 'App/routes';
|
||||||
import { Icon } from 'UI';
|
|
||||||
|
|
||||||
import IdentifyUsersTab from './components/IdentifyUsersTab';
|
import IdentifyUsersTab from './components/IdentifyUsersTab';
|
||||||
import InstallOpenReplayTab from './components/InstallOpenReplayTab';
|
import InstallOpenReplayTab from './components/InstallOpenReplayTab';
|
||||||
import IntegrationsTab from './components/IntegrationsTab';
|
import IntegrationsTab from './components/IntegrationsTab';
|
||||||
import ManageUsersTab from './components/ManageUsersTab';
|
import ManageUsersTab from './components/ManageUsersTab';
|
||||||
import SideMenu from './components/SideMenu';
|
import SideMenu from './components/SideMenu';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Smartphone, AppWindow } from 'lucide-react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
match: {
|
match: {
|
||||||
|
|
@ -33,7 +31,7 @@ function Onboarding(props: Props) {
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div className="font-semibold flex gap-2 items-center">
|
<div className="font-semibold flex gap-2 items-center">
|
||||||
<Icon name="browser/browser" size={16} />
|
<AppWindow size={16} />
|
||||||
{t('Web')}
|
{t('Web')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -42,7 +40,7 @@ function Onboarding(props: Props) {
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div className="font-semibold flex gap-2 items-center">
|
<div className="font-semibold flex gap-2 items-center">
|
||||||
<Icon name="mobile" size={16} />
|
<Smartphone size={16} />
|
||||||
{t('Mobile')}
|
{t('Mobile')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -130,18 +130,20 @@ function IdentifyUsersTab(props: Props) {
|
||||||
'To identify users through metadata, you will have to explicitly specify your user metadata so it can be injected during sessions. Follow the below steps',
|
'To identify users through metadata, you will have to explicitly specify your user metadata so it can be injected during sessions. Follow the below steps',
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-start">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<CircleNumber text="1" />
|
<CircleNumber text="1" />
|
||||||
<MetadataList />
|
<MetadataList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-6" />
|
<div className="my-6" />
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<CircleNumber text="2" />
|
<div>
|
||||||
<div className="pt-1 w-full">
|
<CircleNumber text="2" />
|
||||||
<span className="font-bold">
|
<span className="font-bold">
|
||||||
{t('Inject metadata when recording sessions')}
|
{t('Inject metadata when recording sessions')}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-1 w-full">
|
||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
{t('Use the')}
|
{t('Use the')}
|
||||||
<span className="highlight-blue">setMetadata</span>{' '}
|
<span className="highlight-blue">setMetadata</span>{' '}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import MobileOnboardingTabs from '../OnboardingTabs/OnboardingMobileTabs';
|
||||||
import ProjectFormButton from '../ProjectFormButton';
|
import ProjectFormButton from '../ProjectFormButton';
|
||||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CircleHelp } from 'lucide-react'
|
||||||
|
|
||||||
interface Props extends WithOnboardingProps {
|
interface Props extends WithOnboardingProps {
|
||||||
platforms: Array<{
|
platforms: Array<{
|
||||||
|
|
@ -45,8 +46,8 @@ function InstallOpenReplayTab(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank">
|
<a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank">
|
||||||
<Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
|
<Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
|
||||||
<Icon name={"question-circle"} />
|
<CircleHelp size={14} />
|
||||||
<div className={"text-main"}>{t('See Documentation')}</div>
|
<div>{t('See Documentation')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,14 @@ function MetadataList() {
|
||||||
<Button type="default" onClick={() => openModal()}>
|
<Button type="default" onClick={() => openModal()}>
|
||||||
{t('Add Metadata')}
|
{t('Add Metadata')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex ml-2">
|
{fields.map((f, index) => (
|
||||||
{fields.map((f, index) => (
|
<TagBadge
|
||||||
<TagBadge
|
key={index}
|
||||||
key={index}
|
text={f.key}
|
||||||
text={f.key}
|
onRemove={() => removeMetadata(f)}
|
||||||
onRemove={() => removeMetadata(f)}
|
outline
|
||||||
outline
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react'
|
||||||
|
import DocCard from "App/components/shared/DocCard";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Mail } from 'lucide-react'
|
||||||
|
import { CopyButton } from "UI";
|
||||||
|
|
||||||
|
export function CollabCard({ showUserModal }: { showUserModal: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocCard title={t('Need help from team member?')}>
|
||||||
|
<div className={'text-main cursor-pointer flex items-center gap-2'} onClick={showUserModal}>
|
||||||
|
<Mail size={14} />
|
||||||
|
<span>
|
||||||
|
{t('Invite and Collaborate')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DocCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectKeyCard({ projectKey }: { projectKey: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<DocCard title={t('Project Key')}>
|
||||||
|
<div className="p-2 rounded bg-white flex justify-between items-center">
|
||||||
|
<div className={'font-mono'}>{projectKey}</div>
|
||||||
|
<CopyButton content={projectKey} className={'capitalize font-medium text-neutral-400'} />
|
||||||
|
</div>
|
||||||
|
</DocCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import DocCard from 'Shared/DocCard/DocCard';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||||
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
|
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
|
||||||
|
import { CollabCard, ProjectKeyCard } from "./Callouts";
|
||||||
import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
|
import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -39,18 +40,9 @@ function MobileTrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title={t('Need help from team member?')}>
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title={t('Project Key')}>
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -62,18 +54,9 @@ function MobileTrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title={t('Need help from team member?')}>
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title={t('Project Key')}>
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Tabs, Icon, CopyButton } from 'UI';
|
||||||
import DocCard from 'Shared/DocCard/DocCard';
|
import DocCard from 'Shared/DocCard/DocCard';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||||
|
import { CollabCard, ProjectKeyCard } from "./Callouts";
|
||||||
import InstallDocs from './InstallDocs';
|
import InstallDocs from './InstallDocs';
|
||||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -37,20 +38,9 @@ function TrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title="Need help from team member?">
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
<DocCard title="Project Key">
|
|
||||||
<div className="rounded bg-white px-2 py-1 flex items-center justify-between">
|
|
||||||
<span>{site.projectKey}</span>
|
|
||||||
<CopyButton
|
|
||||||
content={site.projectKey}
|
|
||||||
className="capitalize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
<DocCard title="Other ways to install">
|
<DocCard title="Other ways to install">
|
||||||
<a
|
<a
|
||||||
className="link flex items-center"
|
className="link flex items-center"
|
||||||
|
|
@ -77,18 +67,9 @@ function TrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title="Need help from team member?">
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title="Project Key">
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function SideMenu(props: Props) {
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
style={{ marginTop: '8px', border: 'none' }}
|
style={{ border: 'none' }}
|
||||||
selectedKeys={activeTab ? [activeTab] : []}
|
selectedKeys={activeTab ? [activeTab] : []}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
LikeFilled,
|
LikeFilled,
|
||||||
LikeOutlined,
|
LikeOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Tour, TourProps } from './.store/antd-virtual-7db13b4af6/package';
|
import { Tour, TourProps } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ function PlayerBlockHeader(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative border-l" style={{ minWidth: '270px' }}>
|
<div className="relative border-l" style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={TABS}
|
tabs={TABS}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ function PlayerContent({
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={
|
style={
|
||||||
activeTab && !fullscreen
|
activeTab && !fullscreen
|
||||||
? { maxWidth: 'calc(100% - 270px)' }
|
? { maxWidth: `calc(100% - ${activeTab === 'EXPORT' ? '360px' : '270px'})` }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function DropdownAudioPlayer({
|
||||||
return {
|
return {
|
||||||
url: data.url,
|
url: data.url,
|
||||||
timestamp: data.timestamp,
|
timestamp: data.timestamp,
|
||||||
start: startTs,
|
start: Math.max(0, startTs),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[audioEvents.length, sessionStart],
|
[audioEvents.length, sessionStart],
|
||||||
|
|
|
||||||
|
|
@ -114,19 +114,17 @@ function PlayerBlockHeader(props: any) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{_metaList.length > 0 && (
|
{_metaList.length > 0 && (
|
||||||
<div className="h-full flex items-center px-2 gap-1">
|
<SessionMetaList
|
||||||
<SessionMetaList
|
horizontal
|
||||||
className=""
|
metaList={_metaList}
|
||||||
metaList={_metaList}
|
maxLength={2}
|
||||||
maxLength={2}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="px-2 relative border-l border-l-gray-lighter"
|
className="px-2 relative border-l border-l-gray-lighter"
|
||||||
style={{ minWidth: '270px' }}
|
style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={TABS}
|
tabs={TABS}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function PlayerContent({
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={
|
style={
|
||||||
activeTab && !fullscreen
|
activeTab && !fullscreen
|
||||||
? { maxWidth: 'calc(100% - 270px)' }
|
? { maxWidth: `calc(100% - ${activeTab === 'EXPORT' ? '360px' : '270px'})` }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ function Player(props: IProps) {
|
||||||
setActiveTab={(tab: string) =>
|
setActiveTab={(tab: string) =>
|
||||||
activeTab === tab ? props.setActiveTab('') : props.setActiveTab(tab)
|
activeTab === tab ? props.setActiveTab('') : props.setActiveTab(tab)
|
||||||
}
|
}
|
||||||
|
activeTab={activeTab}
|
||||||
speedDown={playerContext.player.speedDown}
|
speedDown={playerContext.player.speedDown}
|
||||||
speedUp={playerContext.player.speedUp}
|
speedUp={playerContext.player.speedUp}
|
||||||
jump={playerContext.player.jump}
|
jump={playerContext.player.jump}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@ import { Icon } from 'UI';
|
||||||
function LogsButton({
|
function LogsButton({
|
||||||
integrated,
|
integrated,
|
||||||
onClick,
|
onClick,
|
||||||
|
shorten,
|
||||||
}: {
|
}: {
|
||||||
integrated: string[];
|
integrated: string[];
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
shorten?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ControlButton
|
<ControlButton
|
||||||
label="Traces"
|
label={shorten ? null : "Traces"}
|
||||||
|
customKey="traces"
|
||||||
customTags={
|
customTags={
|
||||||
<Avatar.Group>
|
<Avatar.Group>
|
||||||
{integrated.map((name) => (
|
{integrated.map((name) => (
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ function WebPlayer(props: any) {
|
||||||
uxtestingStore,
|
uxtestingStore,
|
||||||
uiPlayerStore,
|
uiPlayerStore,
|
||||||
integrationsStore,
|
integrationsStore,
|
||||||
userStore,
|
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
const devTools = sessionStore.devTools
|
||||||
const session = sessionStore.current;
|
const session = sessionStore.current;
|
||||||
const { prefetched } = sessionStore;
|
const { prefetched } = sessionStore;
|
||||||
const startedAt = sessionStore.current.startedAt || 0;
|
const startedAt = sessionStore.current.startedAt || 0;
|
||||||
|
|
@ -57,14 +57,17 @@ function WebPlayer(props: any) {
|
||||||
const [fullView, setFullView] = useState(false);
|
const [fullView, setFullView] = useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (windowActive) {
|
const handleActivation = () => {
|
||||||
const handleActivation = () => {
|
if (!document.hidden) {
|
||||||
if (!document.hidden) {
|
setWindowActive(true);
|
||||||
setWindowActive(true);
|
document.removeEventListener('visibilitychange', handleActivation);
|
||||||
document.removeEventListener('visibilitychange', handleActivation);
|
}
|
||||||
}
|
};
|
||||||
};
|
document.addEventListener('visibilitychange', handleActivation);
|
||||||
document.addEventListener('visibilitychange', handleActivation);
|
|
||||||
|
return () => {
|
||||||
|
devTools.update('network', { activeTab: 'ALL' });
|
||||||
|
document.removeEventListener('visibilitychange', handleActivation);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,6 @@ function TabChange({ from, to, activeUrl, onClick }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default observer(EventGroupWrapper);
|
export default observer(EventGroupWrapper);
|
||||||
|
|
@ -4,17 +4,17 @@ import cn from 'classnames';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { VList, VListHandle } from 'virtua';
|
import { VList, VListHandle } from 'virtua';
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd';
|
||||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react';
|
||||||
import EventGroupWrapper from './EventGroupWrapper';
|
import EventGroupWrapper from './EventGroupWrapper';
|
||||||
import EventSearch from './EventSearch/EventSearch';
|
import EventSearch from './EventSearch/EventSearch';
|
||||||
import styles from './eventsBlock.module.css';
|
import styles from './eventsBlock.module.css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CloseOutlined } from ".store/@ant-design-icons-virtual-42686020c5/package";
|
import { CloseOutlined } from "@ant-design/icons";
|
||||||
import { Tooltip } from ".store/antd-virtual-9dbfadb7f6/package";
|
import { Tooltip } from "antd";
|
||||||
import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal";
|
import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
@ -25,7 +25,7 @@ const MODES = {
|
||||||
SELECT: 'select',
|
SELECT: 'select',
|
||||||
SEARCH: 'search',
|
SEARCH: 'search',
|
||||||
EXPORT: 'export',
|
EXPORT: 'export',
|
||||||
}
|
};
|
||||||
|
|
||||||
function EventsBlock(props: IProps) {
|
function EventsBlock(props: IProps) {
|
||||||
const defaultFramework = getDefaultFramework();
|
const defaultFramework = getDefaultFramework();
|
||||||
|
|
@ -95,7 +95,7 @@ function EventsBlock(props: IProps) {
|
||||||
? e.time >= zoomStartTs && e.time <= zoomEndTs
|
? e.time >= zoomStartTs && e.time <= zoomEndTs
|
||||||
: false
|
: false
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
filteredLength,
|
filteredLength,
|
||||||
notesWithEvtsLength,
|
notesWithEvtsLength,
|
||||||
|
|
@ -126,6 +126,7 @@ function EventsBlock(props: IProps) {
|
||||||
},
|
},
|
||||||
[usedEvents, time, endTime],
|
[usedEvents, time, endTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentTimeEventIndex = findLastFitting(time);
|
const currentTimeEventIndex = findLastFitting(time);
|
||||||
|
|
||||||
const write = ({
|
const write = ({
|
||||||
|
|
@ -182,6 +183,7 @@ function EventsBlock(props: IProps) {
|
||||||
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
||||||
const isCurrent = index === currentTimeEventIndex;
|
const isCurrent = index === currentTimeEventIndex;
|
||||||
const isPrev = index < currentTimeEventIndex;
|
const isPrev = index < currentTimeEventIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventGroupWrapper
|
<EventGroupWrapper
|
||||||
query={query}
|
query={query}
|
||||||
|
|
@ -249,12 +251,14 @@ function EventsBlock(props: IProps) {
|
||||||
onClick={() => setMode(MODES.SEARCH)}
|
onClick={() => setMode(MODES.SEARCH)}
|
||||||
>
|
>
|
||||||
<Search size={14} />
|
<Search size={14} />
|
||||||
<div>{t('Search')} {usedEvents.length} {t('events')}</div>
|
<div>
|
||||||
|
{t('Search')} {usedEvents.length} {t('events')}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={t('Close Panel')} placement='bottom' >
|
<Tooltip title={t('Close Panel')} placement="bottom">
|
||||||
<Button
|
<Button
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
type='text'
|
type="text"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('');
|
setActiveTab('');
|
||||||
}}
|
}}
|
||||||
|
|
@ -263,19 +267,23 @@ function EventsBlock(props: IProps) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{mode === MODES.SEARCH ?
|
{mode === MODES.SEARCH ? (
|
||||||
<div className={'flex items-center gap-2'}>
|
<div className={'flex items-center gap-2'}>
|
||||||
<EventSearch
|
<EventSearch
|
||||||
onChange={write}
|
onChange={write}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
value={query}
|
value={query}
|
||||||
eventsText={
|
eventsText={
|
||||||
usedEvents.length ? `${usedEvents.length} ${t('Events')}` : `0 ${t('Events')}`
|
usedEvents.length
|
||||||
|
? `${usedEvents.length} ${t('Events')}`
|
||||||
|
: `0 ${t('Events')}`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button type={'text'} onClick={() => setMode(MODES.SELECT)}>{t('Cancel')}</Button>
|
<Button type={'text'} onClick={() => setMode(MODES.SELECT)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn('flex-1 pb-4', styles.eventsList)}
|
className={cn('flex-1 pb-4', styles.eventsList)}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ function GraphQL({ panelHeight }: { panelHeight: number }) {
|
||||||
|
|
||||||
const filterList = (list: any, value: string) => {
|
const filterList = (list: any, value: string) => {
|
||||||
const filterRE = getRE(value, 'i');
|
const filterRE = getRE(value, 'i');
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
? list.filter(
|
? list.filter(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Popover, Button } from 'antd';
|
||||||
import stl from './controlButton.module.css';
|
import stl from './controlButton.module.css';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
label: string;
|
label: React.ReactNode;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
|
@ -18,6 +18,7 @@ interface IProps {
|
||||||
noIcon?: boolean;
|
noIcon?: boolean;
|
||||||
popover?: React.ReactNode;
|
popover?: React.ReactNode;
|
||||||
customTags?: React.ReactNode;
|
customTags?: React.ReactNode;
|
||||||
|
customKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ControlButton({
|
function ControlButton({
|
||||||
|
|
@ -28,29 +29,28 @@ function ControlButton({
|
||||||
active = false,
|
active = false,
|
||||||
popover = undefined,
|
popover = undefined,
|
||||||
customTags,
|
customTags,
|
||||||
|
customKey,
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
return (
|
return (
|
||||||
<Popover content={popover} open={popover ? undefined : false}>
|
<Popover content={popover} open={popover ? undefined : false}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
id={`control-button-${label.toLowerCase()}`}
|
id={`control-button-${customKey ? customKey.toLowerCase() : label!.toString().toLowerCase()}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{customTags}
|
{customTags}
|
||||||
{hasErrors && (
|
{hasErrors && (
|
||||||
<div className={stl.labels}>
|
<div className="w-2 h-2 rounded-full bg-red" />
|
||||||
<div className={stl.errorSymbol} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<span
|
{label && <span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-semibold hover:text-main',
|
'font-semibold hover:text-main',
|
||||||
active ? 'color-main' : 'color-gray-darkest',
|
active ? 'color-main' : 'color-gray-darkest',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ import {
|
||||||
} from 'App/mstore/uiPlayerStore';
|
} from 'App/mstore/uiPlayerStore';
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
|
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
|
||||||
|
import { CodeOutlined, DashboardOutlined, ClusterOutlined } from '@ant-design/icons';
|
||||||
|
import { ArrowDownUp, ListCollapse, Merge, Waypoints } from 'lucide-react'
|
||||||
|
|
||||||
import ControlButton from './ControlButton';
|
import ControlButton from './ControlButton';
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
|
|
@ -52,23 +54,23 @@ export const SKIP_INTERVALS = {
|
||||||
function getStorageName(type: any) {
|
function getStorageName(type: any) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case STORAGE_TYPES.REDUX:
|
case STORAGE_TYPES.REDUX:
|
||||||
return 'Redux';
|
return { name: 'Redux', icon: <Icon name='integrations/redux' size={14} /> };
|
||||||
case STORAGE_TYPES.MOBX:
|
case STORAGE_TYPES.MOBX:
|
||||||
return 'Mobx';
|
return { name: 'Mobx', icon: <Icon name='integrations/mobx' size={14} /> };
|
||||||
case STORAGE_TYPES.VUEX:
|
case STORAGE_TYPES.VUEX:
|
||||||
return 'Vuex';
|
return { name: 'Vuex', icon: <Icon name='integrations/vuejs' size={14} /> };
|
||||||
case STORAGE_TYPES.NGRX:
|
case STORAGE_TYPES.NGRX:
|
||||||
return 'NgRx';
|
return { name: 'NgRx', icon: <Icon name='integrations/ngrx' size={14} /> };
|
||||||
case STORAGE_TYPES.ZUSTAND:
|
case STORAGE_TYPES.ZUSTAND:
|
||||||
return 'Zustand';
|
return { name: 'Zustand', icon: <Icon name='integrations/zustand' size={14} /> };
|
||||||
case STORAGE_TYPES.NONE:
|
case STORAGE_TYPES.NONE:
|
||||||
return 'State';
|
return { name: 'State', icon: <ClusterOutlined size={14} /> };
|
||||||
default:
|
default:
|
||||||
return 'State';
|
return { name: 'State', icon: <ClusterOutlined size={14} /> };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Controls({ setActiveTab }: any) {
|
function Controls({ setActiveTab, activeTab }: any) {
|
||||||
const { player, store } = React.useContext(PlayerContext);
|
const { player, store } = React.useContext(PlayerContext);
|
||||||
const {
|
const {
|
||||||
uxtestingStore,
|
uxtestingStore,
|
||||||
|
|
@ -191,6 +193,7 @@ function Controls({ setActiveTab }: any) {
|
||||||
bottomBlock={bottomBlock}
|
bottomBlock={bottomBlock}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
events={events}
|
events={events}
|
||||||
|
activeTab={activeTab}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -212,6 +215,7 @@ interface IDevtoolsButtons {
|
||||||
bottomBlock: number;
|
bottomBlock: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
events: any[];
|
events: any[];
|
||||||
|
activeTab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DevtoolsButtons = observer(
|
const DevtoolsButtons = observer(
|
||||||
|
|
@ -221,6 +225,7 @@ const DevtoolsButtons = observer(
|
||||||
bottomBlock,
|
bottomBlock,
|
||||||
disabled,
|
disabled,
|
||||||
events,
|
events,
|
||||||
|
activeTab,
|
||||||
}: IDevtoolsButtons) => {
|
}: IDevtoolsButtons) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { aiSummaryStore, integrationsStore } = useStore();
|
const { aiSummaryStore, integrationsStore } = useStore();
|
||||||
|
|
@ -262,6 +267,36 @@ const DevtoolsButtons = observer(
|
||||||
const possibleAudio = events.filter((e) => e.name.includes('media/audio'));
|
const possibleAudio = events.filter((e) => e.name.includes('media/audio'));
|
||||||
const integratedServices =
|
const integratedServices =
|
||||||
integrationsStore.integrations.backendLogIntegrations;
|
integrationsStore.integrations.backendLogIntegrations;
|
||||||
|
|
||||||
|
const showIcons = activeTab === 'EXPORT'
|
||||||
|
const labels = {
|
||||||
|
console: {
|
||||||
|
icon: <CodeOutlined size={14} />,
|
||||||
|
label: t('Console'),
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
icon: <DashboardOutlined size={14} />,
|
||||||
|
label: t('Performance'),
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
icon: <ArrowDownUp size={14} strokeWidth={2} />,
|
||||||
|
label: t('Network'),
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
icon: <ListCollapse size={14} strokeWidth={2} />,
|
||||||
|
label: t('Events'),
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
icon: getStorageName(storageType).icon,
|
||||||
|
label: getStorageName(storageType).name,
|
||||||
|
},
|
||||||
|
graphql: {
|
||||||
|
icon: <Merge size={14} strokeWidth={2} />,
|
||||||
|
label: 'Graphql',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const getLabel = (block: string) => labels[block][showIcons ? 'icon' : 'label']
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isSaas ? <SummaryButton onClick={showSummary} /> : null}
|
{isSaas ? <SummaryButton onClick={showSummary} /> : null}
|
||||||
|
|
@ -274,6 +309,7 @@ const DevtoolsButtons = observer(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="xray"
|
||||||
label="X-Ray"
|
label="X-Ray"
|
||||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||||
active={bottomBlock === OVERVIEW && !inspectorMode}
|
active={bottomBlock === OVERVIEW && !inspectorMode}
|
||||||
|
|
@ -286,10 +322,11 @@ const DevtoolsButtons = observer(
|
||||||
<div>{t('Launch Console')}</div>
|
<div>{t('Launch Console')}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="console"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(CONSOLE)}
|
onClick={() => toggleBottomTools(CONSOLE)}
|
||||||
active={bottomBlock === CONSOLE && !inspectorMode}
|
active={bottomBlock === CONSOLE && !inspectorMode}
|
||||||
label={t('Console')}
|
label={getLabel('console')}
|
||||||
hasErrors={logRedCount > 0 || showExceptions}
|
hasErrors={logRedCount > 0 || showExceptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -300,10 +337,11 @@ const DevtoolsButtons = observer(
|
||||||
<div>{t('Launch Network')}</div>
|
<div>{t('Launch Network')}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="network"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(NETWORK)}
|
onClick={() => toggleBottomTools(NETWORK)}
|
||||||
active={bottomBlock === NETWORK && !inspectorMode}
|
active={bottomBlock === NETWORK && !inspectorMode}
|
||||||
label={t('Network')}
|
label={getLabel('network')}
|
||||||
hasErrors={resourceRedCount > 0}
|
hasErrors={resourceRedCount > 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -314,10 +352,11 @@ const DevtoolsButtons = observer(
|
||||||
<div>{t('Launch Performance')}</div>
|
<div>{t('Launch Performance')}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="performance"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||||
active={bottomBlock === PERFORMANCE && !inspectorMode}
|
active={bottomBlock === PERFORMANCE && !inspectorMode}
|
||||||
label="Performance"
|
label={getLabel('performance')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showGraphql && (
|
{showGraphql && (
|
||||||
|
|
@ -325,7 +364,8 @@ const DevtoolsButtons = observer(
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(GRAPHQL)}
|
onClick={() => toggleBottomTools(GRAPHQL)}
|
||||||
active={bottomBlock === GRAPHQL && !inspectorMode}
|
active={bottomBlock === GRAPHQL && !inspectorMode}
|
||||||
label="Graphql"
|
label={getLabel('graphql')}
|
||||||
|
customKey="graphql"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -337,10 +377,11 @@ const DevtoolsButtons = observer(
|
||||||
<div>{t('Launch State')}</div>
|
<div>{t('Launch State')}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="state"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(STORAGE)}
|
onClick={() => toggleBottomTools(STORAGE)}
|
||||||
active={bottomBlock === STORAGE && !inspectorMode}
|
active={bottomBlock === STORAGE && !inspectorMode}
|
||||||
label={getStorageName(storageType) as string}
|
label={getLabel('state')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ControlButton
|
<ControlButton
|
||||||
|
|
@ -350,14 +391,16 @@ const DevtoolsButtons = observer(
|
||||||
<div>{t('Launch Events')}</div>
|
<div>{t('Launch Events')}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
customKey="events"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||||
active={bottomBlock === STACKEVENTS && !inspectorMode}
|
active={bottomBlock === STACKEVENTS && !inspectorMode}
|
||||||
label={t('Events')}
|
label={getLabel('events')}
|
||||||
hasErrors={stackRedCount > 0}
|
hasErrors={stackRedCount > 0}
|
||||||
/>
|
/>
|
||||||
{showProfiler && (
|
{showProfiler && (
|
||||||
<ControlButton
|
<ControlButton
|
||||||
|
customKey="profiler"
|
||||||
disabled={disableButtons}
|
disabled={disableButtons}
|
||||||
onClick={() => toggleBottomTools(PROFILER)}
|
onClick={() => toggleBottomTools(PROFILER)}
|
||||||
active={bottomBlock === PROFILER && !inspectorMode}
|
active={bottomBlock === PROFILER && !inspectorMode}
|
||||||
|
|
@ -368,6 +411,7 @@ const DevtoolsButtons = observer(
|
||||||
<LogsButton
|
<LogsButton
|
||||||
integrated={integratedServices.map((service) => service.name)}
|
integrated={integratedServices.map((service) => service.name)}
|
||||||
onClick={() => toggleBottomTools(BACKENDLOGS)}
|
onClick={() => toggleBottomTools(BACKENDLOGS)}
|
||||||
|
shorten={showIcons}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{possibleAudio.length ? (
|
{possibleAudio.length ? (
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import {
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import stl from './timeline.module.css';
|
import stl from './timeline.module.css';
|
||||||
import { getTimelinePosition } from './getTimelinePosition';
|
import { getTimelinePosition } from './getTimelinePosition';
|
||||||
|
import { useStore } from '@/mstore';
|
||||||
|
|
||||||
function EventsList() {
|
function EventsList() {
|
||||||
const { store } = useContext(PlayerContext);
|
const { store } = useContext(PlayerContext);
|
||||||
|
const { uiPlayerStore } = useStore();
|
||||||
|
|
||||||
const { eventCount, endTime } = store.get();
|
const { eventCount, endTime } = store.get();
|
||||||
const { tabStates } = store.get();
|
const { tabStates } = store.get();
|
||||||
|
|
@ -17,7 +19,6 @@ function EventsList() {
|
||||||
() => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [],
|
() => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [],
|
||||||
[eventCount],
|
[eventCount],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasDuplicates = events.some(
|
const hasDuplicates = events.some(
|
||||||
(e, i) =>
|
(e, i) =>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ function SubHeader(props) {
|
||||||
projectsStore,
|
projectsStore,
|
||||||
userStore,
|
userStore,
|
||||||
issueReportingStore,
|
issueReportingStore,
|
||||||
|
settingsStore
|
||||||
} = useStore();
|
} = useStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { favorite } = sessionStore.current;
|
const { favorite } = sessionStore.current;
|
||||||
|
|
@ -45,7 +46,7 @@ function SubHeader(props) {
|
||||||
const currentSession = sessionStore.current;
|
const currentSession = sessionStore.current;
|
||||||
const projectId = projectsStore.siteId;
|
const projectId = projectsStore.siteId;
|
||||||
const integrations = integrationsStore.issues.list;
|
const integrations = integrationsStore.issues.list;
|
||||||
const { store } = React.useContext(PlayerContext);
|
const { player, store } = React.useContext(PlayerContext);
|
||||||
const { location: currentLocation = 'loading...' } = store.get();
|
const { location: currentLocation = 'loading...' } = store.get();
|
||||||
const hasIframe = localStorage.getItem(IFRAME) === 'true';
|
const hasIframe = localStorage.getItem(IFRAME) === 'true';
|
||||||
const [hideTools, setHideTools] = React.useState(false);
|
const [hideTools, setHideTools] = React.useState(false);
|
||||||
|
|
@ -127,6 +128,13 @@ function SubHeader(props) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showVModeBadge = store.get().vModeBadge;
|
||||||
|
const onVMode = () => {
|
||||||
|
settingsStore.sessionSettings.updateKey('virtualMode', true);
|
||||||
|
player.enableVMode?.();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
@ -143,6 +151,8 @@ function SubHeader(props) {
|
||||||
siteId={projectId!}
|
siteId={projectId!}
|
||||||
currentLocation={currentLocation}
|
currentLocation={currentLocation}
|
||||||
version={currentSession?.trackerVersion ?? ''}
|
version={currentSession?.trackerVersion ?? ''}
|
||||||
|
virtualElsFailed={showVModeBadge}
|
||||||
|
onVMode={onVMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SessionTabs />
|
<SessionTabs />
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ function UnitStepsModal({ onClose }: Props) {
|
||||||
<div className={'w-full'}>
|
<div className={'w-full'}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
width={340}
|
width={340}
|
||||||
height={'calc(100vh - 146px)'}
|
height={'calc(100vh - 174px)'}
|
||||||
extra={`${events.length} Events`}
|
extra={`${events.length} Events`}
|
||||||
copy
|
copy
|
||||||
code={eventStr}
|
code={eventStr}
|
||||||
|
|
|
||||||
|
|
@ -34,38 +34,46 @@ const WarnBadge = React.memo(
|
||||||
currentLocation,
|
currentLocation,
|
||||||
version,
|
version,
|
||||||
siteId,
|
siteId,
|
||||||
|
virtualElsFailed,
|
||||||
|
onVMode,
|
||||||
}: {
|
}: {
|
||||||
currentLocation: string;
|
currentLocation: string;
|
||||||
version: string;
|
version: string;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
|
virtualElsFailed: boolean;
|
||||||
|
onVMode: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const localhostWarnSiteKey = localhostWarn(siteId);
|
const localhostWarnSiteKey = localhostWarn(siteId);
|
||||||
const defaultLocalhostWarn =
|
const defaultLocalhostWarn =
|
||||||
localStorage.getItem(localhostWarnSiteKey) !== '1';
|
localStorage.getItem(localhostWarnSiteKey) !== '1';
|
||||||
const localhostWarnActive =
|
const localhostWarnActive = Boolean(
|
||||||
currentLocation &&
|
currentLocation &&
|
||||||
defaultLocalhostWarn &&
|
defaultLocalhostWarn &&
|
||||||
/(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation);
|
/(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation)
|
||||||
|
)
|
||||||
const trackerVersion = window.env.TRACKER_VERSION ?? undefined;
|
const trackerVersion = window.env.TRACKER_VERSION ?? undefined;
|
||||||
const trackerVerDiff = compareVersions(version, trackerVersion);
|
const trackerVerDiff = compareVersions(version, trackerVersion);
|
||||||
const trackerWarnActive = trackerVerDiff !== VersionComparison.Same;
|
const trackerWarnActive = trackerVerDiff !== VersionComparison.Same;
|
||||||
|
|
||||||
const [showLocalhostWarn, setLocalhostWarn] =
|
const [warnings, setWarnings] = React.useState<[localhostWarn: boolean, trackerWarn: boolean, virtualElsFailWarn: boolean]>([localhostWarnActive, trackerWarnActive, virtualElsFailed])
|
||||||
React.useState(localhostWarnActive);
|
|
||||||
const [showTrackerWarn, setTrackerWarn] = React.useState(trackerWarnActive);
|
|
||||||
|
|
||||||
const closeWarning = (type: 1 | 2) => {
|
React.useEffect(() => {
|
||||||
|
setWarnings([localhostWarnActive, trackerWarnActive, virtualElsFailed])
|
||||||
|
}, [localhostWarnActive, trackerWarnActive, virtualElsFailed])
|
||||||
|
|
||||||
|
const closeWarning = (type: 0 | 1 | 2) => {
|
||||||
if (type === 1) {
|
if (type === 1) {
|
||||||
localStorage.setItem(localhostWarnSiteKey, '1');
|
localStorage.setItem(localhostWarnSiteKey, '1');
|
||||||
setLocalhostWarn(false);
|
|
||||||
}
|
|
||||||
if (type === 2) {
|
|
||||||
setTrackerWarn(false);
|
|
||||||
}
|
}
|
||||||
|
setWarnings((prev) => {
|
||||||
|
const newWarnings = [...prev];
|
||||||
|
newWarnings[type] = false;
|
||||||
|
return newWarnings;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!showLocalhostWarn && !showTrackerWarn) return null;
|
if (!warnings.some(el => el === true)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -79,7 +87,7 @@ const WarnBadge = React.memo(
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showLocalhostWarn ? (
|
{warnings[0] ? (
|
||||||
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
|
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span>{t('Some assets may load incorrectly on localhost.')}</span>
|
<span>{t('Some assets may load incorrectly on localhost.')}</span>
|
||||||
|
|
@ -101,7 +109,7 @@ const WarnBadge = React.memo(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showTrackerWarn ? (
|
{warnings[1] ? (
|
||||||
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
|
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -125,6 +133,21 @@ const WarnBadge = React.memo(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="py-1 ml-3 cursor-pointer"
|
||||||
|
onClick={() => closeWarning(1)}
|
||||||
|
>
|
||||||
|
<Icon name="close" size={16} color="black" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{warnings[2] ? (
|
||||||
|
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div>{t('If you have issues displaying custom HTML elements (i.e when using LWC), consider turning on Virtual Mode.')}</div>
|
||||||
|
<div className='link' onClick={onVMode}>{t('Enable')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="py-1 ml-3 cursor-pointer"
|
className="py-1 ml-3 cursor-pointer"
|
||||||
onClick={() => closeWarning(2)}
|
onClick={() => closeWarning(2)}
|
||||||
|
|
|
||||||
|
|
@ -12,60 +12,123 @@ import {
|
||||||
getDateRangeFromValue,
|
getDateRangeFromValue,
|
||||||
getDateRangeLabel,
|
getDateRangeLabel,
|
||||||
} from 'App/dateRange';
|
} from 'App/dateRange';
|
||||||
import { DateTime, Interval } from 'luxon';
|
import { DateTime, Interval, Settings } from 'luxon';
|
||||||
|
|
||||||
import styles from './dateRangePopup.module.css';
|
import styles from './dateRangePopup.module.css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function DateRangePopup(props: any) {
|
function DateRangePopup(props: any) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [displayDates, setDisplayDates] = React.useState<[Date, Date]>([new Date(), new Date()]);
|
||||||
const [range, setRange] = React.useState(
|
const [range, setRange] = React.useState(
|
||||||
props.selectedDateRange ||
|
props.selectedDateRange ||
|
||||||
Interval.fromDateTimes(DateTime.now(), DateTime.now()),
|
Interval.fromDateTimes(DateTime.now(), DateTime.now()),
|
||||||
);
|
);
|
||||||
const [value, setValue] = React.useState<string | null>(null);
|
const [value, setValue] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const selectCustomRange = (range) => {
|
React.useEffect(() => {
|
||||||
let newRange;
|
if (props.selectedDateRange) {
|
||||||
if (props.singleDay) {
|
const start = new Date(
|
||||||
newRange = Interval.fromDateTimes(
|
props.selectedDateRange.start.year,
|
||||||
DateTime.fromJSDate(range),
|
props.selectedDateRange.start.month - 1, // JS months are 0-based
|
||||||
DateTime.fromJSDate(range),
|
props.selectedDateRange.start.day
|
||||||
);
|
);
|
||||||
} else {
|
const end = new Date(
|
||||||
newRange = Interval.fromDateTimes(
|
props.selectedDateRange.end.year,
|
||||||
DateTime.fromJSDate(range[0]),
|
props.selectedDateRange.end.month - 1,
|
||||||
DateTime.fromJSDate(range[1]),
|
props.selectedDateRange.end.day
|
||||||
);
|
);
|
||||||
}
|
setDisplayDates([start, end]);
|
||||||
setRange(newRange);
|
}
|
||||||
|
}, [props.selectedDateRange]);
|
||||||
|
|
||||||
|
const createNaiveTime = (dateTime: DateTime) => {
|
||||||
|
if (!dateTime) return null;
|
||||||
|
return DateTime.fromObject({
|
||||||
|
hour: dateTime.hour,
|
||||||
|
minute: dateTime.minute
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const selectCustomRange = (newDates: [Date, Date]) => {
|
||||||
|
if (!newDates || !newDates[0] || !newDates[1]) return;
|
||||||
|
|
||||||
|
setDisplayDates(newDates);
|
||||||
|
|
||||||
|
const selectedTzStart = DateTime.fromObject({
|
||||||
|
year: newDates[0].getFullYear(),
|
||||||
|
month: newDates[0].getMonth() + 1,
|
||||||
|
day: newDates[0].getDate(),
|
||||||
|
hour: 0,
|
||||||
|
minute: 0
|
||||||
|
}).setZone(Settings.defaultZone);
|
||||||
|
|
||||||
|
const selectedTzEnd = DateTime.fromObject({
|
||||||
|
year: newDates[1].getFullYear(),
|
||||||
|
month: newDates[1].getMonth() + 1,
|
||||||
|
day: newDates[1].getDate(),
|
||||||
|
hour: 23,
|
||||||
|
minute: 59
|
||||||
|
}).setZone(Settings.defaultZone);
|
||||||
|
|
||||||
|
const updatedRange = Interval.fromDateTimes(selectedTzStart, selectedTzEnd);
|
||||||
|
setRange(updatedRange);
|
||||||
setValue(CUSTOM_RANGE);
|
setValue(CUSTOM_RANGE);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRangeTimeStart = (value: DateTime) => {
|
const setRangeTimeStart = (naiveTime: DateTime) => {
|
||||||
if (!range.end || value > range.end) {
|
if (!range.end || !naiveTime) return;
|
||||||
return;
|
|
||||||
}
|
const newStart = range.start.set({
|
||||||
const newRange = range.start.set({
|
hour: naiveTime.hour,
|
||||||
hour: value.hour,
|
minute: naiveTime.minute
|
||||||
minute: value.minute,
|
|
||||||
});
|
});
|
||||||
setRange(Interval.fromDateTimes(newRange, range.end));
|
|
||||||
|
if (newStart > range.end) return;
|
||||||
|
|
||||||
|
setRange(Interval.fromDateTimes(newStart, range.end));
|
||||||
setValue(CUSTOM_RANGE);
|
setValue(CUSTOM_RANGE);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRangeTimeEnd = (value: DateTime) => {
|
const setRangeTimeEnd = (naiveTime: DateTime) => {
|
||||||
if (!range.start || (value && value < range.start)) {
|
if (!range.start || !naiveTime) return;
|
||||||
return;
|
|
||||||
}
|
const newEnd = range.end.set({
|
||||||
const newRange = range.end.set({ hour: value.hour, minute: value.minute });
|
hour: naiveTime.hour,
|
||||||
setRange(Interval.fromDateTimes(range.start, newRange));
|
minute: naiveTime.minute
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newEnd < range.start) return;
|
||||||
|
|
||||||
|
setRange(Interval.fromDateTimes(range.start, newEnd));
|
||||||
setValue(CUSTOM_RANGE);
|
setValue(CUSTOM_RANGE);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectValue = (value: string) => {
|
const selectValue = (value: string) => {
|
||||||
const range = getDateRangeFromValue(value);
|
const newRange = getDateRangeFromValue(value);
|
||||||
setRange(range);
|
|
||||||
|
if (!newRange.start || !newRange.end) {
|
||||||
|
setRange(Interval.fromDateTimes(DateTime.now(), DateTime.now()));
|
||||||
|
setDisplayDates([new Date(), new Date()]);
|
||||||
|
setValue(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const zonedStart = newRange.start.setZone(Settings.defaultZone);
|
||||||
|
const zonedEnd = newRange.end.setZone(Settings.defaultZone);
|
||||||
|
setRange(Interval.fromDateTimes(zonedStart, zonedEnd));
|
||||||
|
|
||||||
|
const start = new Date(
|
||||||
|
zonedStart.year,
|
||||||
|
zonedStart.month - 1,
|
||||||
|
zonedStart.day
|
||||||
|
);
|
||||||
|
const end = new Date(
|
||||||
|
zonedEnd.year,
|
||||||
|
zonedEnd.month - 1,
|
||||||
|
zonedEnd.day
|
||||||
|
);
|
||||||
|
setDisplayDates([start, end]);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -77,9 +140,9 @@ function DateRangePopup(props: any) {
|
||||||
const isUSLocale =
|
const isUSLocale =
|
||||||
navigator.language === 'en-US' || navigator.language.startsWith('en-US');
|
navigator.language === 'en-US' || navigator.language.startsWith('en-US');
|
||||||
|
|
||||||
const rangeForDisplay = props.singleDay
|
const naiveStartTime = createNaiveTime(range.start);
|
||||||
? range.start.ts
|
const naiveEndTime = createNaiveTime(range.end);
|
||||||
: [range.start!.startOf('day').ts, range.end!.startOf('day').ts];
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={`${styles.body} h-fit`}>
|
<div className={`${styles.body} h-fit`}>
|
||||||
|
|
@ -103,7 +166,7 @@ function DateRangePopup(props: any) {
|
||||||
shouldCloseCalendar={() => false}
|
shouldCloseCalendar={() => false}
|
||||||
isOpen
|
isOpen
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
value={rangeForDisplay}
|
value={displayDates}
|
||||||
calendarProps={{
|
calendarProps={{
|
||||||
tileDisabled: props.isTileDisabled,
|
tileDisabled: props.isTileDisabled,
|
||||||
selectRange: !props.singleDay,
|
selectRange: !props.singleDay,
|
||||||
|
|
@ -122,7 +185,7 @@ function DateRangePopup(props: any) {
|
||||||
<span>{range.start.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
|
<span>{range.start.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
|
format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
|
||||||
value={range.start}
|
value={naiveStartTime}
|
||||||
onChange={setRangeTimeStart}
|
onChange={setRangeTimeStart}
|
||||||
needConfirm={false}
|
needConfirm={false}
|
||||||
showNow={false}
|
showNow={false}
|
||||||
|
|
@ -132,7 +195,7 @@ function DateRangePopup(props: any) {
|
||||||
<span>{range.end.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
|
<span>{range.end.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
|
format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
|
||||||
value={range.end}
|
value={naiveEndTime}
|
||||||
onChange={setRangeTimeEnd}
|
onChange={setRangeTimeEnd}
|
||||||
needConfirm={false}
|
needConfirm={false}
|
||||||
showNow={false}
|
showNow={false}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { ResourceType, Timed } from 'Player';
|
import { ResourceType, Timed } from 'Player';
|
||||||
|
import { WsChannel } from 'Player/web/messages';
|
||||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||||
import WebPlayer from 'Player/web/WebPlayer';
|
import WebPlayer from 'Player/web/WebPlayer';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import i18n from 'App/i18n'
|
||||||
|
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,25 +20,27 @@ import {
|
||||||
} from 'App/components/Session/playerContext';
|
} from 'App/components/Session/playerContext';
|
||||||
import { formatMs } from 'App/date';
|
import { formatMs } from 'App/date';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { formatBytes } from 'App/utils';
|
import { formatBytes, debounceCall } from 'App/utils';
|
||||||
import { Icon, NoContent, Tabs } from 'UI';
|
import { Icon, NoContent, Tabs } from 'UI';
|
||||||
import { Tooltip, Input, Switch, Form } from 'antd';
|
import { Tooltip, Input, Switch, Form } from 'antd';
|
||||||
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||||
import { WsChannel } from 'App/player/web/messages';
|
|
||||||
|
|
||||||
import BottomBlock from '../BottomBlock';
|
import BottomBlock from '../BottomBlock';
|
||||||
import InfoLine from '../BottomBlock/InfoLine';
|
import InfoLine from '../BottomBlock/InfoLine';
|
||||||
import TabSelector from '../TabSelector';
|
import TabSelector from '../TabSelector';
|
||||||
import TimeTable from '../TimeTable';
|
import TimeTable from '../TimeTable';
|
||||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
|
||||||
import WSPanel from './WSPanel';
|
import WSPanel from './WSPanel';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { mergeListsWithZoom, processInChunks } from './utils'
|
||||||
|
|
||||||
|
// Constants remain the same
|
||||||
const INDEX_KEY = 'network';
|
const INDEX_KEY = 'network';
|
||||||
|
|
||||||
const ALL = 'ALL';
|
const ALL = 'ALL';
|
||||||
const XHR = 'xhr';
|
const XHR = 'xhr';
|
||||||
const JS = 'js';
|
const JS = 'js';
|
||||||
|
|
@ -62,6 +72,9 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
|
||||||
const DOM_LOADED_TIME_COLOR = 'teal';
|
const DOM_LOADED_TIME_COLOR = 'teal';
|
||||||
const LOAD_TIME_COLOR = 'red';
|
const LOAD_TIME_COLOR = 'red';
|
||||||
|
|
||||||
|
const BATCH_SIZE = 2500;
|
||||||
|
const INITIAL_LOAD_SIZE = 5000;
|
||||||
|
|
||||||
export function renderType(r: any) {
|
export function renderType(r: any) {
|
||||||
return (
|
return (
|
||||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||||
|
|
@ -79,13 +92,17 @@ export function renderName(r: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSize(r: any) {
|
function renderSize(r: any) {
|
||||||
const { t } = useTranslation();
|
const t = i18n.t;
|
||||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
const notCaptured = t('Not captured');
|
||||||
|
const resSizeStr = t('Resource size')
|
||||||
let triggerText;
|
let triggerText;
|
||||||
let content;
|
let content;
|
||||||
if (r.decodedBodySize == null || r.decodedBodySize === 0) {
|
if (r.responseBodySize) {
|
||||||
|
triggerText = formatBytes(r.responseBodySize);
|
||||||
|
content = undefined;
|
||||||
|
} else if (r.decodedBodySize == null || r.decodedBodySize === 0) {
|
||||||
triggerText = 'x';
|
triggerText = 'x';
|
||||||
content = t('Not captured');
|
content = notCaptured;
|
||||||
} else {
|
} else {
|
||||||
const headerSize = r.headerSize || 0;
|
const headerSize = r.headerSize || 0;
|
||||||
const showTransferred = r.headerSize != null;
|
const showTransferred = r.headerSize != null;
|
||||||
|
|
@ -100,7 +117,7 @@ function renderSize(r: any) {
|
||||||
)} transferred over network`}
|
)} transferred over network`}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
<li>{`${t('Resource size')}: ${formatBytes(r.decodedBodySize)} `}</li>
|
<li>{`${resSizeStr}: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +185,8 @@ function renderStatus({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main component for Network Panel
|
||||||
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
const { player, store } = React.useContext(PlayerContext);
|
const { player, store } = React.useContext(PlayerContext);
|
||||||
const { sessionStore, uiPlayerStore } = useStore();
|
const { sessionStore, uiPlayerStore } = useStore();
|
||||||
|
|
@ -216,6 +235,7 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
|
|
||||||
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
||||||
const getTabName = (tabId: string) => tabNames[tabId];
|
const getTabName = (tabId: string) => tabNames[tabId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkPanelComp
|
<NetworkPanelComp
|
||||||
loadTime={loadTime}
|
loadTime={loadTime}
|
||||||
|
|
@ -228,8 +248,8 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
resourceListNow={resourceListNow}
|
resourceListNow={resourceListNow}
|
||||||
player={player}
|
player={player}
|
||||||
startedAt={startedAt}
|
startedAt={startedAt}
|
||||||
websocketList={websocketList as WSMessage[]}
|
websocketList={websocketList}
|
||||||
websocketListNow={websocketListNow as WSMessage[]}
|
websocketListNow={websocketListNow}
|
||||||
getTabNum={getTabNum}
|
getTabNum={getTabNum}
|
||||||
getTabName={getTabName}
|
getTabName={getTabName}
|
||||||
showSingleTab={showSingleTab}
|
showSingleTab={showSingleTab}
|
||||||
|
|
@ -269,9 +289,7 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
resourceListNow={resourceListNow}
|
resourceListNow={resourceListNow}
|
||||||
player={player}
|
player={player}
|
||||||
startedAt={startedAt}
|
startedAt={startedAt}
|
||||||
// @ts-ignore
|
|
||||||
websocketList={websocketList}
|
websocketList={websocketList}
|
||||||
// @ts-ignore
|
|
||||||
websocketListNow={websocketListNow}
|
websocketListNow={websocketListNow}
|
||||||
zoomEnabled={zoomEnabled}
|
zoomEnabled={zoomEnabled}
|
||||||
zoomStartTs={zoomStartTs}
|
zoomStartTs={zoomStartTs}
|
||||||
|
|
@ -280,12 +298,35 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSMessage = Timed & {
|
const useInfiniteScroll = (loadMoreCallback: () => void, hasMore: boolean) => {
|
||||||
channelName: string;
|
const observerRef = useRef<IntersectionObserver>(null);
|
||||||
data: string;
|
const loadingRef = useRef<HTMLDivElement>(null);
|
||||||
timestamp: number;
|
|
||||||
dir: 'up' | 'down';
|
useEffect(() => {
|
||||||
messageType: string;
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting && hasMore) {
|
||||||
|
loadMoreCallback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadingRef.current) {
|
||||||
|
observer.observe(loadingRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
observerRef.current = observer;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loadMoreCallback, hasMore, loadingRef]);
|
||||||
|
|
||||||
|
return loadingRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -302,8 +343,8 @@ interface Props {
|
||||||
resourceList: Timed[];
|
resourceList: Timed[];
|
||||||
fetchListNow: Timed[];
|
fetchListNow: Timed[];
|
||||||
resourceListNow: Timed[];
|
resourceListNow: Timed[];
|
||||||
websocketList: Array<WSMessage>;
|
websocketList: Array<WsChannel>;
|
||||||
websocketListNow: Array<WSMessage>;
|
websocketListNow: Array<WsChannel>;
|
||||||
player: WebPlayer | MobilePlayer;
|
player: WebPlayer | MobilePlayer;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
|
@ -349,107 +390,189 @@ export const NetworkPanelComp = observer(
|
||||||
>(null);
|
>(null);
|
||||||
const { showModal } = useModal();
|
const { showModal } = useModal();
|
||||||
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
||||||
|
|
||||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [displayedItems, setDisplayedItems] = useState([]);
|
||||||
|
const [totalItems, setTotalItems] = useState(0);
|
||||||
|
const [summaryStats, setSummaryStats] = useState({
|
||||||
|
resourcesSize: 0,
|
||||||
|
transferredSize: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalListRef = useRef([]);
|
||||||
|
const socketListRef = useRef([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessionStore: { devTools },
|
sessionStore: { devTools },
|
||||||
} = useStore();
|
} = useStore();
|
||||||
const { filter } = devTools[INDEX_KEY];
|
const { filter } = devTools[INDEX_KEY];
|
||||||
const { activeTab } = devTools[INDEX_KEY];
|
const { activeTab } = devTools[INDEX_KEY];
|
||||||
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
|
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
|
||||||
|
const [inputFilterValue, setInputFilterValue] = useState(filter);
|
||||||
|
|
||||||
const socketList = useMemo(
|
const debouncedFilter = useCallback(
|
||||||
() =>
|
debounceCall((filterValue) => {
|
||||||
websocketList.filter(
|
devTools.update(INDEX_KEY, { filter: filterValue });
|
||||||
(ws, i, arr) =>
|
}, 300),
|
||||||
arr.findIndex((it) => it.channelName === ws.channelName) === i,
|
[],
|
||||||
),
|
|
||||||
[websocketList],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = useMemo(
|
// Process socket lists once
|
||||||
() =>
|
useEffect(() => {
|
||||||
// TODO: better merge (with body size info) - do it in player
|
const uniqueSocketList = websocketList.filter(
|
||||||
resourceList
|
(ws, i, arr) =>
|
||||||
.filter(
|
arr.findIndex((it) => it.channelName === ws.channelName) === i,
|
||||||
(res) =>
|
|
||||||
!fetchList.some((ft) => {
|
|
||||||
// res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player)
|
|
||||||
if (res.name === ft.name) {
|
|
||||||
if (res.time === ft.time) return true;
|
|
||||||
if (res.url.includes(ft.url)) {
|
|
||||||
return (
|
|
||||||
Math.abs(res.time - ft.time) < 350 ||
|
|
||||||
Math.abs(res.timestamp - ft.timestamp) < 350
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.name !== ft.name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Math.abs(res.time - ft.time) > 250) {
|
|
||||||
return false;
|
|
||||||
} // TODO: find good epsilons
|
|
||||||
if (Math.abs(res.duration - ft.duration) > 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.concat(fetchList)
|
|
||||||
.concat(
|
|
||||||
socketList.map((ws) => ({
|
|
||||||
...ws,
|
|
||||||
type: 'websocket',
|
|
||||||
method: 'ws',
|
|
||||||
url: ws.channelName,
|
|
||||||
name: ws.channelName,
|
|
||||||
status: '101',
|
|
||||||
duration: 0,
|
|
||||||
transferredBodySize: 0,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.filter((req) =>
|
|
||||||
zoomEnabled
|
|
||||||
? req.time >= zoomStartTs! && req.time <= zoomEndTs!
|
|
||||||
: true,
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.time - b.time),
|
|
||||||
[resourceList.length, fetchList.length, socketList.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
let filteredList = useMemo(() => {
|
|
||||||
if (!showOnlyErrors) {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
return list.filter(
|
|
||||||
(it) => parseInt(it.status) >= 400 || !it.success || it.error,
|
|
||||||
);
|
);
|
||||||
}, [showOnlyErrors, list]);
|
socketListRef.current = uniqueSocketList;
|
||||||
filteredList = useRegExListFilterMemo(
|
}, [websocketList.length]);
|
||||||
filteredList,
|
|
||||||
(it) => [it.status, it.name, it.type, it.method],
|
|
||||||
filter,
|
|
||||||
);
|
|
||||||
filteredList = useTabListFilterMemo(
|
|
||||||
filteredList,
|
|
||||||
(it) => TYPE_TO_TAB[it.type],
|
|
||||||
ALL,
|
|
||||||
activeTab,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) =>
|
// Initial data processing - do this only once when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Heaviest operation here, will create a final merged network list
|
||||||
|
const processData = async () => {
|
||||||
|
const fetchUrls = new Set(
|
||||||
|
fetchList.map((ft) => {
|
||||||
|
return `${ft.name}-${Math.floor(ft.time / 100)}-${Math.floor(ft.duration / 100)}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want to get resources that aren't in fetch list
|
||||||
|
const filteredResources = await processInChunks(resourceList, (chunk) =>
|
||||||
|
chunk.filter((res: any) => {
|
||||||
|
const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`;
|
||||||
|
return !fetchUrls.has(key);
|
||||||
|
}),
|
||||||
|
BATCH_SIZE,
|
||||||
|
25,
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedSockets = socketListRef.current.map((ws: any) => ({
|
||||||
|
...ws,
|
||||||
|
type: 'websocket',
|
||||||
|
method: 'ws',
|
||||||
|
url: ws.channelName,
|
||||||
|
name: ws.channelName,
|
||||||
|
status: '101',
|
||||||
|
duration: 0,
|
||||||
|
transferredBodySize: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mergedList: Timed[] = mergeListsWithZoom(
|
||||||
|
filteredResources as Timed[],
|
||||||
|
fetchList,
|
||||||
|
processedSockets as Timed[],
|
||||||
|
{ enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 }
|
||||||
|
)
|
||||||
|
|
||||||
|
originalListRef.current = mergedList;
|
||||||
|
setTotalItems(mergedList.length);
|
||||||
|
|
||||||
|
calculateResourceStats(resourceList);
|
||||||
|
|
||||||
|
// Only display initial chunk
|
||||||
|
setDisplayedItems(mergedList.slice(0, INITIAL_LOAD_SIZE));
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
void processData();
|
||||||
|
}, [
|
||||||
|
resourceList.length,
|
||||||
|
fetchList.length,
|
||||||
|
socketListRef.current.length,
|
||||||
|
zoomEnabled,
|
||||||
|
zoomStartTs,
|
||||||
|
zoomEndTs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const calculateResourceStats = (resourceList: Record<string, any>) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let resourcesSize = 0
|
||||||
|
let transferredSize = 0
|
||||||
|
resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => {
|
||||||
|
resourcesSize += decodedBodySize || 0
|
||||||
|
transferredSize += (headerSize || 0) + (encodedBodySize || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
setSummaryStats({
|
||||||
|
resourcesSize,
|
||||||
|
transferredSize,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (originalListRef.current.length === 0) return;
|
||||||
|
setIsProcessing(true);
|
||||||
|
const applyFilters = async () => {
|
||||||
|
let filteredItems: any[] = originalListRef.current;
|
||||||
|
|
||||||
|
filteredItems = await processInChunks(filteredItems, (chunk) =>
|
||||||
|
chunk.filter(
|
||||||
|
(it) => {
|
||||||
|
let valid = true;
|
||||||
|
if (showOnlyErrors) {
|
||||||
|
valid = parseInt(it.status) >= 400 || !it.success || it.error
|
||||||
|
}
|
||||||
|
if (filter) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(filter, 'i');
|
||||||
|
valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method);
|
||||||
|
} catch (e) {
|
||||||
|
valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeTab !== ALL) {
|
||||||
|
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update displayed items
|
||||||
|
setDisplayedItems(filteredItems.slice(0, INITIAL_LOAD_SIZE));
|
||||||
|
setTotalItems(filteredItems.length);
|
||||||
|
setIsProcessing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
void applyFilters();
|
||||||
|
}, [filter, activeTab, showOnlyErrors]);
|
||||||
|
|
||||||
|
const loadMoreItems = useCallback(() => {
|
||||||
|
if (isProcessing) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplayedItems((prevItems) => {
|
||||||
|
const currentLength = prevItems.length;
|
||||||
|
const newItems = originalListRef.current.slice(
|
||||||
|
currentLength,
|
||||||
|
currentLength + BATCH_SIZE,
|
||||||
|
);
|
||||||
|
return [...prevItems, ...newItems];
|
||||||
|
});
|
||||||
|
setIsProcessing(false);
|
||||||
|
}, 10);
|
||||||
|
}, [isProcessing]);
|
||||||
|
|
||||||
|
const hasMoreItems = displayedItems.length < totalItems;
|
||||||
|
const loadingRef = useInfiniteScroll(loadMoreItems, hasMoreItems);
|
||||||
|
|
||||||
|
const onTabClick = (activeTab) => {
|
||||||
devTools.update(INDEX_KEY, { activeTab });
|
devTools.update(INDEX_KEY, { activeTab });
|
||||||
const onFilterChange = ({
|
};
|
||||||
target: { value },
|
|
||||||
}: React.ChangeEvent<HTMLInputElement>) =>
|
const onFilterChange = ({ target: { value } }) => {
|
||||||
devTools.update(INDEX_KEY, { filter: value });
|
setInputFilterValue(value)
|
||||||
|
debouncedFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
// AutoScroll
|
|
||||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||||
filteredList,
|
displayedItems,
|
||||||
getLastItemTime(fetchListNow, resourceListNow),
|
getLastItemTime(fetchListNow, resourceListNow),
|
||||||
activeIndex,
|
activeIndex,
|
||||||
(index) => devTools.update(INDEX_KEY, { index }),
|
(index) => devTools.update(INDEX_KEY, { index }),
|
||||||
|
|
@ -462,24 +585,6 @@ export const NetworkPanelComp = observer(
|
||||||
timeoutStartAutoscroll();
|
timeoutStartAutoscroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourcesSize = useMemo(
|
|
||||||
() =>
|
|
||||||
resourceList.reduce(
|
|
||||||
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
[resourceList.length],
|
|
||||||
);
|
|
||||||
const transferredSize = useMemo(
|
|
||||||
() =>
|
|
||||||
resourceList.reduce(
|
|
||||||
(sum, { headerSize, encodedBodySize }) =>
|
|
||||||
sum + (headerSize || 0) + (encodedBodySize || 0),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
[resourceList.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
const referenceLines = useMemo(() => {
|
const referenceLines = useMemo(() => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
|
|
||||||
|
|
@ -513,7 +618,7 @@ export const NetworkPanelComp = observer(
|
||||||
isSpot={isSpot}
|
isSpot={isSpot}
|
||||||
time={item.time + startedAt}
|
time={item.time + startedAt}
|
||||||
resource={item}
|
resource={item}
|
||||||
rows={filteredList}
|
rows={displayedItems}
|
||||||
fetchPresented={fetchList.length > 0}
|
fetchPresented={fetchList.length > 0}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
|
|
@ -525,12 +630,10 @@ export const NetworkPanelComp = observer(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
|
||||||
stopAutoscroll();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableCols = React.useMemo(() => {
|
const tableCols = useMemo(() => {
|
||||||
const cols: any[] = [
|
const cols = [
|
||||||
{
|
{
|
||||||
label: t('Status'),
|
label: t('Status'),
|
||||||
dataKey: 'status',
|
dataKey: 'status',
|
||||||
|
|
@ -585,7 +688,7 @@ export const NetworkPanelComp = observer(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cols;
|
return cols;
|
||||||
}, [showSingleTab]);
|
}, [showSingleTab, activeTab, t, getTabName, getTabNum, isSpot]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomBlock
|
<BottomBlock
|
||||||
|
|
@ -617,7 +720,7 @@ export const NetworkPanelComp = observer(
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={onFilterChange}
|
onChange={onFilterChange}
|
||||||
width={280}
|
width={280}
|
||||||
value={filter}
|
value={inputFilterValue}
|
||||||
size="small"
|
size="small"
|
||||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||||
/>
|
/>
|
||||||
|
|
@ -625,7 +728,7 @@ export const NetworkPanelComp = observer(
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
||||||
<div>
|
<div className="flex items-center">
|
||||||
<Form.Item name="show-errors-only" className="mb-0">
|
<Form.Item name="show-errors-only" className="mb-0">
|
||||||
<label
|
<label
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -642,21 +745,29 @@ export const NetworkPanelComp = observer(
|
||||||
<span className="text-sm ms-2">4xx-5xx Only</span>
|
<span className="text-sm ms-2">4xx-5xx Only</span>
|
||||||
</label>
|
</label>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{isProcessing && (
|
||||||
|
<span className="text-xs text-gray-500 ml-4">
|
||||||
|
Processing data...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<InfoLine>
|
<InfoLine>
|
||||||
|
<InfoLine.Point label={`${totalItems}`} value="requests" />
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={`${filteredList.length}`}
|
label={`${displayedItems.length}/${totalItems}`}
|
||||||
value=" requests"
|
value="displayed"
|
||||||
|
display={displayedItems.length < totalItems}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatBytes(transferredSize)}
|
label={formatBytes(summaryStats.transferredSize)}
|
||||||
value="transferred"
|
value="transferred"
|
||||||
display={transferredSize > 0}
|
display={summaryStats.transferredSize > 0}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatBytes(resourcesSize)}
|
label={formatBytes(summaryStats.resourcesSize)}
|
||||||
value="resources"
|
value="resources"
|
||||||
display={resourcesSize > 0}
|
display={summaryStats.resourcesSize > 0}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatMs(domBuildingTime)}
|
label={formatMs(domBuildingTime)}
|
||||||
|
|
@ -679,42 +790,67 @@ export const NetworkPanelComp = observer(
|
||||||
/>
|
/>
|
||||||
</InfoLine>
|
</InfoLine>
|
||||||
</div>
|
</div>
|
||||||
<NoContent
|
|
||||||
title={
|
{isLoading ? (
|
||||||
<div className="capitalize flex items-center gap-2">
|
<div className="flex items-center justify-center h-full">
|
||||||
<InfoCircleOutlined size={18} />
|
<div className="text-center">
|
||||||
{t('No Data')}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
|
||||||
|
<p>Processing initial network data...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
size="small"
|
) : (
|
||||||
show={filteredList.length === 0}
|
<NoContent
|
||||||
>
|
title={
|
||||||
{/* @ts-ignore */}
|
<div className="capitalize flex items-center gap-2">
|
||||||
<TimeTable
|
<InfoCircleOutlined size={18} />
|
||||||
rows={filteredList}
|
{t('No Data')}
|
||||||
tableHeight={panelHeight - 102}
|
</div>
|
||||||
referenceLines={referenceLines}
|
}
|
||||||
renderPopup
|
size="small"
|
||||||
onRowClick={showDetailsModal}
|
show={displayedItems.length === 0}
|
||||||
sortBy="time"
|
|
||||||
sortAscending
|
|
||||||
onJump={(row: any) => {
|
|
||||||
devTools.update(INDEX_KEY, {
|
|
||||||
index: filteredList.indexOf(row),
|
|
||||||
});
|
|
||||||
player.jump(row.time);
|
|
||||||
}}
|
|
||||||
activeIndex={activeIndex}
|
|
||||||
>
|
>
|
||||||
{tableCols}
|
<div>
|
||||||
</TimeTable>
|
<TimeTable
|
||||||
{selectedWsChannel ? (
|
rows={displayedItems}
|
||||||
<WSPanel
|
tableHeight={panelHeight - 102 - (hasMoreItems ? 30 : 0)}
|
||||||
socketMsgList={selectedWsChannel}
|
referenceLines={referenceLines}
|
||||||
onClose={() => setSelectedWsChannel(null)}
|
renderPopup
|
||||||
/>
|
onRowClick={showDetailsModal}
|
||||||
) : null}
|
sortBy="time"
|
||||||
</NoContent>
|
sortAscending
|
||||||
|
onJump={(row) => {
|
||||||
|
devTools.update(INDEX_KEY, {
|
||||||
|
index: displayedItems.indexOf(row),
|
||||||
|
});
|
||||||
|
player.jump(row.time);
|
||||||
|
}}
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
>
|
||||||
|
{tableCols}
|
||||||
|
</TimeTable>
|
||||||
|
|
||||||
|
{hasMoreItems && (
|
||||||
|
<div
|
||||||
|
ref={loadingRef}
|
||||||
|
className="flex justify-center items-center text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
|
||||||
|
Loading more data ({totalItems - displayedItems.length}{' '}
|
||||||
|
remaining)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedWsChannel ? (
|
||||||
|
<WSPanel
|
||||||
|
socketMsgList={selectedWsChannel}
|
||||||
|
onClose={() => setSelectedWsChannel(null)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</NoContent>
|
||||||
|
)}
|
||||||
</BottomBlock.Content>
|
</BottomBlock.Content>
|
||||||
</BottomBlock>
|
</BottomBlock>
|
||||||
);
|
);
|
||||||
|
|
@ -722,7 +858,6 @@ export const NetworkPanelComp = observer(
|
||||||
);
|
);
|
||||||
|
|
||||||
const WebNetworkPanel = observer(NetworkPanelCont);
|
const WebNetworkPanel = observer(NetworkPanelCont);
|
||||||
|
|
||||||
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
||||||
|
|
||||||
export { WebNetworkPanel, MobileNetworkPanel };
|
export { WebNetworkPanel, MobileNetworkPanel };
|
||||||
|
|
|
||||||
178
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
178
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
export function mergeListsWithZoom<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(
|
||||||
|
arr1: T[],
|
||||||
|
arr2: Y[],
|
||||||
|
arr3: Z[],
|
||||||
|
zoom?: { enabled: boolean; start: number; end: number },
|
||||||
|
): Array<T | Y | Z> {
|
||||||
|
// Early return for empty arrays
|
||||||
|
if (arr1.length === 0 && arr2.length === 0 && arr3.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized for common case - no zoom
|
||||||
|
if (!zoom?.enabled) {
|
||||||
|
return mergeThreeSortedArrays(arr1, arr2, arr3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search for start indexes (faster than linear search for large arrays)
|
||||||
|
const index1 = binarySearchStartIndex(arr1, zoom.start);
|
||||||
|
const index2 = binarySearchStartIndex(arr2, zoom.start);
|
||||||
|
const index3 = binarySearchStartIndex(arr3, zoom.start);
|
||||||
|
|
||||||
|
// Merge arrays within zoom range
|
||||||
|
return mergeThreeSortedArraysWithinRange(
|
||||||
|
arr1,
|
||||||
|
arr2,
|
||||||
|
arr3,
|
||||||
|
index1,
|
||||||
|
index2,
|
||||||
|
index3,
|
||||||
|
zoom.start,
|
||||||
|
zoom.end,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function binarySearchStartIndex<T extends Record<string, any>>(
|
||||||
|
arr: T[],
|
||||||
|
threshold: number,
|
||||||
|
): number {
|
||||||
|
if (arr.length === 0) return 0;
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = arr.length - 1;
|
||||||
|
|
||||||
|
// Handle edge cases first for better performance
|
||||||
|
if (arr[high].time < threshold) return arr.length;
|
||||||
|
if (arr[low].time >= threshold) return 0;
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
const mid = Math.floor((low + high) / 2);
|
||||||
|
|
||||||
|
if (arr[mid].time < threshold) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeThreeSortedArrays<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(arr1: T[], arr2: Y[], arr3: Z[]): Array<T | Y | Z> {
|
||||||
|
const totalLength = arr1.length + arr2.length + arr3.length;
|
||||||
|
// prealloc array size
|
||||||
|
const result = new Array(totalLength);
|
||||||
|
|
||||||
|
let i = 0,
|
||||||
|
j = 0,
|
||||||
|
k = 0,
|
||||||
|
index = 0;
|
||||||
|
|
||||||
|
while (i < arr1.length || j < arr2.length || k < arr3.length) {
|
||||||
|
const val1 = i < arr1.length ? arr1[i].time : Infinity;
|
||||||
|
const val2 = j < arr2.length ? arr2[j].time : Infinity;
|
||||||
|
const val3 = k < arr3.length ? arr3[k].time : Infinity;
|
||||||
|
|
||||||
|
if (val1 <= val2 && val1 <= val3) {
|
||||||
|
result[index++] = arr1[i++];
|
||||||
|
} else if (val2 <= val1 && val2 <= val3) {
|
||||||
|
result[index++] = arr2[j++];
|
||||||
|
} else {
|
||||||
|
result[index++] = arr3[k++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same as above, just with zoom stuff
|
||||||
|
function mergeThreeSortedArraysWithinRange<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(
|
||||||
|
arr1: T[],
|
||||||
|
arr2: Y[],
|
||||||
|
arr3: Z[],
|
||||||
|
startIdx1: number,
|
||||||
|
startIdx2: number,
|
||||||
|
startIdx3: number,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
): Array<T | Y | Z> {
|
||||||
|
// we don't know beforehand how many items will be there
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let i = startIdx1;
|
||||||
|
let j = startIdx2;
|
||||||
|
let k = startIdx3;
|
||||||
|
|
||||||
|
while (i < arr1.length || j < arr2.length || k < arr3.length) {
|
||||||
|
const val1 = i < arr1.length ? arr1[i].time : Infinity;
|
||||||
|
const val2 = j < arr2.length ? arr2[j].time : Infinity;
|
||||||
|
const val3 = k < arr3.length ? arr3[k].time : Infinity;
|
||||||
|
|
||||||
|
// Early termination: if all remaining values exceed end time
|
||||||
|
if (Math.min(val1, val2, val3) > end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val1 <= val2 && val1 <= val3) {
|
||||||
|
if (val1 <= end) {
|
||||||
|
result.push(arr1[i]);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (val2 <= val1 && val2 <= val3) {
|
||||||
|
if (val2 <= end) {
|
||||||
|
result.push(arr2[j]);
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
if (val3 <= end) {
|
||||||
|
result.push(arr3[k]);
|
||||||
|
}
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processInChunks(
|
||||||
|
items: any[],
|
||||||
|
processFn: (item: any) => any,
|
||||||
|
chunkSize = 1000,
|
||||||
|
overscan = 0,
|
||||||
|
) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: any[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const processNextChunk = () => {
|
||||||
|
const chunk = items.slice(index, index + chunkSize + overscan);
|
||||||
|
result = result.concat(processFn(chunk));
|
||||||
|
index += chunkSize;
|
||||||
|
|
||||||
|
if (index < items.length) {
|
||||||
|
setTimeout(processNextChunk, 0);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processNextChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ function DocCard(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('p-5 bg-gray-lightest mb-4 rounded', className)}>
|
<div className={cn('p-5 bg-gray-lightest mb-4 rounded-lg', className)}>
|
||||||
<div className="font-medium mb-2 flex items-center">
|
<div className="font-medium mb-2 flex items-center">
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import cn from 'classnames';
|
||||||
import { Loader } from 'UI';
|
import { Loader } from 'UI';
|
||||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { VList } from 'virtua';
|
||||||
|
|
||||||
function TruncatedText({
|
function TruncatedText({
|
||||||
text,
|
text,
|
||||||
|
|
@ -124,7 +125,7 @@ export function AutocompleteModal({
|
||||||
if (index === blocksAmount - 1 && blocksAmount > 1) {
|
if (index === blocksAmount - 1 && blocksAmount > 1) {
|
||||||
str += ' and ';
|
str += ' and ';
|
||||||
}
|
}
|
||||||
str += `"${block.trim()}"`;
|
str += block.trim();
|
||||||
if (index < blocksAmount - 2) {
|
if (index < blocksAmount - 2) {
|
||||||
str += ', ';
|
str += ', ';
|
||||||
}
|
}
|
||||||
|
|
@ -170,25 +171,27 @@ export function AutocompleteModal({
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis"
|
className="flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis"
|
||||||
style={{ maxHeight: 200 }}
|
style={{ height: Math.min(sortedOptions.length * 32, 240) }}
|
||||||
>
|
>
|
||||||
{sortedOptions.map((item) => (
|
<VList count={sortedOptions.length} itemSize={18}>
|
||||||
<div
|
{sortedOptions.map((item) => (
|
||||||
key={item.value}
|
<div
|
||||||
onClick={() => onSelectOption(item)}
|
key={item.value}
|
||||||
className="cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2"
|
onClick={() => onSelectOption(item)}
|
||||||
>
|
className="cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2"
|
||||||
<Checkbox checked={isSelected(item)} /> {item.label}
|
>
|
||||||
</div>
|
<Checkbox checked={isSelected(item)} /> {item.label}
|
||||||
))}
|
</div>
|
||||||
|
))}
|
||||||
|
</VList>
|
||||||
</div>
|
</div>
|
||||||
{query.length ? (
|
{query.length ? (
|
||||||
<div className="border-y border-y-gray-light py-2">
|
<div className="border-y border-y-gray-light py-2">
|
||||||
<div
|
<div
|
||||||
className="whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
|
className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
|
||||||
onClick={applyQuery}
|
onClick={applyQuery}
|
||||||
>
|
>
|
||||||
{t('Apply')} {queryStr}
|
{t('Apply')} <span className='font-semibold'>{queryStr}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,10 @@ const FilterAutoComplete = observer(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
|
if (!initialFocus) {
|
||||||
|
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
|
||||||
|
}
|
||||||
setInitialFocus(true);
|
setInitialFocus(true);
|
||||||
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ function LiveSessionSearch() {
|
||||||
const appliedFilter = searchStoreLive.instance;
|
const appliedFilter = searchStoreLive.instance;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void searchStoreLive.fetchSessions();
|
if (projectsStore.activeSiteId) {
|
||||||
}, []);
|
void searchStoreLive.fetchSessions(true);
|
||||||
|
}
|
||||||
|
}, [projectsStore.activeSiteId])
|
||||||
|
|
||||||
const onAddFilter = (filter: any) => {
|
const onAddFilter = (filter: any) => {
|
||||||
filter.autoOpen = true;
|
filter.autoOpen = true;
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,6 @@ function SessionFilters() {
|
||||||
onBeforeLoad: async () => {
|
onBeforeLoad: async () => {
|
||||||
await reloadTags();
|
await reloadTags();
|
||||||
},
|
},
|
||||||
onLoaded: () => {
|
|
||||||
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onAddFilter = (filter: any) => {
|
const onAddFilter = (filter: any) => {
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ export default function MetaItem(props: Props) {
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={label}
|
text={label}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
|
maxWidth={'300px'}
|
||||||
popupProps={{ size: 'small', disabled: true }}
|
popupProps={{ size: 'small', disabled: true }}
|
||||||
/>
|
/>
|
||||||
<span className="bg-neutral-200 inline-block w-[1px] min-h-[17px]"></span>
|
<span className="bg-neutral-200 inline-block w-[1px] min-h-[17px]"></span>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={value}
|
text={value}
|
||||||
|
maxWidth={'350px'}
|
||||||
className="p-0 text-neutral-500"
|
className="p-0 text-neutral-500"
|
||||||
popupProps={{ size: 'small', disabled: true }}
|
popupProps={{ size: 'small', disabled: true }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,15 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
metaList: any[];
|
metaList: any[];
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
onMetaClick?: (meta: { name: string, value: string }) => void;
|
||||||
|
horizontal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SessionMetaList(props: Props) {
|
export default function SessionMetaList(props: Props) {
|
||||||
const { className = '', metaList, maxLength = 14 } = props;
|
const { className = '', metaList, maxLength = 14, horizontal = false } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center flex-wrap gap-1', className)}>
|
<div className={cn('flex items-center gap-1', horizontal ? '' : 'flex-wrap', className)}>
|
||||||
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
|
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<MetaItem label={label} value={`${value}`} />
|
<MetaItem label={label} value={`${value}`} />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import ListingVisibility from './components/ListingVisibility';
|
||||||
import DefaultPlaying from './components/DefaultPlaying';
|
import DefaultPlaying from './components/DefaultPlaying';
|
||||||
import DefaultTimezone from './components/DefaultTimezone';
|
import DefaultTimezone from './components/DefaultTimezone';
|
||||||
import CaptureRate from './components/CaptureRate';
|
import CaptureRate from './components/CaptureRate';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function SessionSettings() {
|
function SessionSettings() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { Switch } from 'UI';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function VirtualModeSettings() {
|
||||||
|
const { settingsStore } = useStore();
|
||||||
|
const { sessionSettings } = settingsStore;
|
||||||
|
const { virtualMode } = sessionSettings;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const updateSettings = (checked: boolean) => {
|
||||||
|
settingsStore.sessionSettings.updateKey('virtualMode', !virtualMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg">{t('Virtual Mode')}</h3>
|
||||||
|
<div className="my-1">
|
||||||
|
{t('Change this setting if you have issues with recordings containing Lightning Web Components (or similar custom HTML Element libraries).')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Switch onChange={updateSettings} checked={virtualMode} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(VirtualModeSettings);
|
||||||
|
|
@ -5,6 +5,15 @@ import { Tooltip } from 'antd';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code?: string;
|
||||||
|
extra?: string;
|
||||||
|
language?: string;
|
||||||
|
copy?: boolean;
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CodeBlock({
|
export default function CodeBlock({
|
||||||
code = '',
|
code = '',
|
||||||
extra = '',
|
extra = '',
|
||||||
|
|
@ -12,7 +21,7 @@ export default function CodeBlock({
|
||||||
copy = false,
|
copy = false,
|
||||||
width = undefined,
|
width = undefined,
|
||||||
height = undefined,
|
height = undefined,
|
||||||
}) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
|
||||||
{icon && (
|
{icon && (
|
||||||
<Icon
|
<Icon
|
||||||
name={icon}
|
name={icon}
|
||||||
className="absolute top-0 bottom-0 my-auto ml-4"
|
className="absolute top-0 bottom-0 my-auto ml-4 z-10"
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const GLOBAL_HAS_NO_RECORDINGS = '__$global-hasNoRecordings$__';
|
||||||
export const SITE_ID_STORAGE_KEY = '__$user-siteId$__';
|
export const SITE_ID_STORAGE_KEY = '__$user-siteId$__';
|
||||||
export const GETTING_STARTED = '__$user-gettingStarted$__';
|
export const GETTING_STARTED = '__$user-gettingStarted$__';
|
||||||
export const MOUSE_TRAIL = '__$session-mouseTrail$__';
|
export const MOUSE_TRAIL = '__$session-mouseTrail$__';
|
||||||
|
export const VIRTUAL_MODE_KEY = '__$session-virtualMode$__'
|
||||||
export const IFRAME = '__$session-iframe$__';
|
export const IFRAME = '__$session-iframe$__';
|
||||||
export const JWT_PARAM = '__$session-jwt-param$__';
|
export const JWT_PARAM = '__$session-jwt-param$__';
|
||||||
export const MENU_COLLAPSED = '__$global-menuCollapsed$__';
|
export const MENU_COLLAPSED = '__$global-menuCollapsed$__';
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,8 @@ const useSessionSearchQueryHandler = ({
|
||||||
searchStore.applyFilter(filter, true);
|
searchStore.applyFilter(filter, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important: Mark URL as parsed BEFORE fetching
|
|
||||||
// This prevents the initial fetch when the URL is parsed
|
|
||||||
searchStore.setUrlParsed();
|
searchStore.setUrlParsed();
|
||||||
|
onLoaded?.();
|
||||||
// Then fetch sessions - this is the only place that should fetch initially
|
|
||||||
await searchStore.fetchSessions();
|
|
||||||
onLoaded();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error applying filter from query:', error);
|
console.error('Error applying filter from query:', error);
|
||||||
searchStore.setUrlParsed();
|
searchStore.setUrlParsed();
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ function SideMenu(props: Props) {
|
||||||
<Tag
|
<Tag
|
||||||
color="cyan"
|
color="cyan"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
className="text-xs"
|
className="text-xs ml-2"
|
||||||
>
|
>
|
||||||
{t('Beta')}
|
{t('Beta')}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,7 @@
|
||||||
"Returning users between": "Returning users between",
|
"Returning users between": "Returning users between",
|
||||||
"Sessions": "Sessions",
|
"Sessions": "Sessions",
|
||||||
"No recordings found.": "No recordings found.",
|
"No recordings found.": "No recordings found.",
|
||||||
"Get new session": "Get new session",
|
"Get new image": "Get new image",
|
||||||
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.",
|
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.",
|
||||||
"Add Card": "Add Card",
|
"Add Card": "Add Card",
|
||||||
"Create Dashboard": "Create Dashboard",
|
"Create Dashboard": "Create Dashboard",
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,7 @@
|
||||||
"Returning users between": "Usuarios recurrentes entre",
|
"Returning users between": "Usuarios recurrentes entre",
|
||||||
"Sessions": "Sesiones",
|
"Sessions": "Sesiones",
|
||||||
"No recordings found.": "No se encontraron grabaciones.",
|
"No recordings found.": "No se encontraron grabaciones.",
|
||||||
"Get new session": "Obtener nueva sesión",
|
"Get new image": "Obtener nueva sesión",
|
||||||
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.",
|
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.",
|
||||||
"Add Card": "Agregar tarjeta",
|
"Add Card": "Agregar tarjeta",
|
||||||
"Create Dashboard": "Crear panel",
|
"Create Dashboard": "Crear panel",
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,7 @@
|
||||||
"Returning users between": "Utilisateurs récurrents entre",
|
"Returning users between": "Utilisateurs récurrents entre",
|
||||||
"Sessions": "Sessions",
|
"Sessions": "Sessions",
|
||||||
"No recordings found.": "Aucun enregistrement trouvé.",
|
"No recordings found.": "Aucun enregistrement trouvé.",
|
||||||
"Get new session": "Obtenir une nouvelle session",
|
"Get new image": "Obtenir une nouvelle session",
|
||||||
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.",
|
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.",
|
||||||
"Add Card": "Ajouter une carte",
|
"Add Card": "Ajouter une carte",
|
||||||
"Create Dashboard": "Créer un tableau de bord",
|
"Create Dashboard": "Créer un tableau de bord",
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,7 @@
|
||||||
"Returning users between": "Возвращающиеся пользователи за период",
|
"Returning users between": "Возвращающиеся пользователи за период",
|
||||||
"Sessions": "Сессии",
|
"Sessions": "Сессии",
|
||||||
"No recordings found.": "Записей не найдено.",
|
"No recordings found.": "Записей не найдено.",
|
||||||
"Get new session": "Получить новую сессию",
|
"Get new image": "Получить новую сессию",
|
||||||
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
|
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
|
||||||
"Add Card": "Добавить карточку",
|
"Add Card": "Добавить карточку",
|
||||||
"Create Dashboard": "Создать дашборд",
|
"Create Dashboard": "Создать дашборд",
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attribute": "Еще атрибут",
|
"More attribute": "Еще атрибут",
|
||||||
"More attributes": "Еще атрибуты",
|
"More attributes": "Еще атрибуты",
|
||||||
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
|
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
|
||||||
"Include rage clicks": "Включить невыносимые клики"
|
"Include rage clicks": "Включить невыносимые клики",
|
||||||
}
|
"Interface Language": "Язык интерфейса",
|
||||||
|
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
|
||||||
|
"Language": "Язык"
|
||||||
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue