Compare commits
128 commits
main
...
v1.21.0-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019c6c2bb8 | ||
|
|
1324c26429 | ||
|
|
9612fa2024 | ||
|
|
6cbe9632c9 | ||
|
|
46e0903eef | ||
|
|
dadb1bc01f | ||
|
|
2cf3243d52 | ||
|
|
ec27fa2250 | ||
|
|
502e1135f1 | ||
|
|
fc542f4671 | ||
|
|
1ac90a6059 | ||
|
|
41b9e38085 | ||
|
|
6e7a3b22ab | ||
|
|
a319146519 | ||
|
|
075beaf5dc | ||
|
|
7385ed957f | ||
|
|
87085ee191 | ||
|
|
e20043a639 | ||
|
|
26630b7454 | ||
|
|
c499114c78 | ||
|
|
83897cb89c | ||
|
|
47411e7f2b | ||
|
|
6bf370b25f | ||
|
|
5ec3855491 | ||
|
|
d95cfd9ff6 | ||
|
|
f0d6d8ea12 | ||
|
|
9c57f3db32 | ||
|
|
fa59621482 | ||
|
|
e4ae3c8ba4 | ||
|
|
af8cfacd98 | ||
|
|
a2ad342a8f | ||
|
|
d108e1db82 | ||
|
|
51496ae5e2 | ||
|
|
6b26f85d6e | ||
|
|
0ca9327c7b | ||
|
|
8914399cad | ||
|
|
885c20827e | ||
|
|
8888399359 | ||
|
|
eca379feed | ||
|
|
f9ec7bce68 | ||
|
|
3975962f70 | ||
|
|
0780b17a1a | ||
|
|
4b34eceb15 | ||
|
|
917dad581d | ||
|
|
72e42dfd5f | ||
|
|
a9a5829000 | ||
|
|
1af02eb2c3 | ||
|
|
ac519a0a10 | ||
|
|
14f72dbbd7 | ||
|
|
8326b913f1 | ||
|
|
fdbed5139c | ||
|
|
76b0ae7b0d | ||
|
|
1a4893bde8 | ||
|
|
9e0f309453 | ||
|
|
262392f32a | ||
|
|
f8a18006d3 | ||
|
|
ddaaa1be71 | ||
|
|
0417f2c756 | ||
|
|
af6dc4b839 | ||
|
|
cceb4f0c25 | ||
|
|
1f16816252 | ||
|
|
77c084cac0 | ||
|
|
c8775f3c15 | ||
|
|
d52a47e2cc | ||
|
|
82854d014d | ||
|
|
65d83e91c5 | ||
|
|
df67acc78f | ||
|
|
e5997c662f | ||
|
|
9ae88e62fd | ||
|
|
e73bd5fef5 | ||
|
|
93dc49bf41 | ||
|
|
c2a3853bd9 | ||
|
|
5cca953a16 | ||
|
|
9367c977ca | ||
|
|
30dba23530 | ||
|
|
21c30af4f0 | ||
|
|
da617747dc | ||
|
|
ac2d12d95f | ||
|
|
aa7b3fd617 | ||
|
|
94b541c758 | ||
|
|
45ef98b163 | ||
|
|
35eb7d4152 | ||
|
|
7c23521cb8 | ||
|
|
8a8df0a8cb | ||
|
|
e54f62a0e6 | ||
|
|
0bdb416594 | ||
|
|
3484da2f60 | ||
|
|
2f164708e7 | ||
|
|
c66296a050 | ||
|
|
a1cf508cb3 | ||
|
|
38594319f0 | ||
|
|
c963ec5e91 | ||
|
|
c04090a778 | ||
|
|
fc48ba4149 | ||
|
|
04db322e54 | ||
|
|
c9ea3651db | ||
|
|
1dc63bf88b | ||
|
|
2fb7b3d542 | ||
|
|
57a21eb31d | ||
|
|
e9a1a8c4eb | ||
|
|
14191c1de4 | ||
|
|
7e52c97d62 | ||
|
|
1cdb9bd06d | ||
|
|
e7ad4c8bd0 | ||
|
|
29d69e5b24 | ||
|
|
2e5517509b | ||
|
|
c95a4f6770 | ||
|
|
8af7d1a263 | ||
|
|
332cbb3516 | ||
|
|
1b564f53d5 | ||
|
|
1aa3b4b4e5 | ||
|
|
d531b5da7e | ||
|
|
e173591d88 | ||
|
|
359ecc85af | ||
|
|
f0e8100283 | ||
|
|
251d727375 | ||
|
|
b00a90484e | ||
|
|
ce0686eec3 | ||
|
|
34232ed23c | ||
|
|
954bfbf8f7 | ||
|
|
c0197cdfeb | ||
|
|
12ab110e0e | ||
|
|
f48808f42e | ||
|
|
b080a98764 | ||
|
|
dd885c65ac | ||
|
|
0ad2836650 | ||
|
|
20b76a0ed9 | ||
|
|
884f3499ef |
158 changed files with 3259 additions and 1778 deletions
2
.github/workflows/api-ee.yaml
vendored
2
.github/workflows/api-ee.yaml
vendored
|
|
@ -10,8 +10,6 @@ on:
|
|||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
- v1.11.0-patch
|
||||
- actions_test
|
||||
paths:
|
||||
- "ee/api/**"
|
||||
- "api/**"
|
||||
|
|
|
|||
1
.github/workflows/api.yaml
vendored
1
.github/workflows/api.yaml
vendored
|
|
@ -10,7 +10,6 @@ on:
|
|||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
- v1.11.0-patch
|
||||
paths:
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
|
|
|
|||
1
.github/workflows/assist-ee.yaml
vendored
1
.github/workflows/assist-ee.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "ee/assist/**"
|
||||
- "assist/**"
|
||||
|
|
|
|||
1
.github/workflows/assist.yaml
vendored
1
.github/workflows/assist.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "assist/**"
|
||||
- "!assist/.gitignore"
|
||||
|
|
|
|||
1
.github/workflows/crons-ee.yaml
vendored
1
.github/workflows/crons-ee.yaml
vendored
|
|
@ -10,7 +10,6 @@ on:
|
|||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
- v1.11.0-patch
|
||||
paths:
|
||||
- "ee/api/**"
|
||||
- "api/**"
|
||||
|
|
|
|||
1
.github/workflows/patch-build.yaml
vendored
1
.github/workflows/patch-build.yaml
vendored
|
|
@ -83,6 +83,7 @@ jobs:
|
|||
[ -d $MSAAS_REPO_FOLDER ] || {
|
||||
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
git log -1
|
||||
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
|
||||
git log -1
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
|
|
|
|||
1
.github/workflows/peers-ee.yaml
vendored
1
.github/workflows/peers-ee.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "ee/peers/**"
|
||||
- "peers/**"
|
||||
|
|
|
|||
1
.github/workflows/peers.yaml
vendored
1
.github/workflows/peers.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "peers/**"
|
||||
- "!peers/.gitignore"
|
||||
|
|
|
|||
92
.github/workflows/release-deployment.yaml
vendored
Normal file
92
.github/workflows/release-deployment.yaml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
name: Release Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
services:
|
||||
description: 'Comma-separated list of services to deploy'
|
||||
required: true
|
||||
branch:
|
||||
description: 'Branch to deploy (defaults to dev)'
|
||||
required: false
|
||||
default: 'dev'
|
||||
|
||||
env:
|
||||
IMAGE_REGISTRY_URL: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||
DOCKER_REPO_OSS: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Set image tag with branch info
|
||||
run: |
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}-${{ github.event.inputs.branch }}-${SHORT_SHA}" >> $GITHUB_ENV
|
||||
echo "Using image tag: ${IMAGE_TAG}-${{ github.event.inputs.branch }}-${SHORT_SHA}"
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
|
||||
- name: Build and push Docker images
|
||||
run: |
|
||||
# Parse the comma-separated services list into an array
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
|
||||
# Define backend services (consider moving this to workflow inputs or repo config)
|
||||
ls backend/cmd >> /tmp/backend.txt
|
||||
|
||||
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
|
||||
echo IMAGE_TAG=$IMAGE_TAG 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=$IMAGE_TAG DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
done
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
name: Using ee release cluster
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_RELEASE_KUBECONFIG }}
|
||||
|
||||
- name: Deploy to ee release Kubernetes
|
||||
run: |
|
||||
echo "Deploying services to EE cluster: ${{ github.event.inputs.services }}"
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
|
||||
echo "Deploying $SERVICE to EE cluster with image tag: ${IMAGE_TAG}"
|
||||
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${{ secrets.RELEASE_OSS_REGISTRY }}/$SERVICE:${IMAGE_TAG}
|
||||
done
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
name: Using foss release cluster
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.FOSS_RELEASE_KUBECONFIG }}
|
||||
|
||||
- name: Deploy to FOSS release Kubernetes
|
||||
run: |
|
||||
echo "Deploying services to FOSS cluster: ${{ github.event.inputs.services }}"
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
|
||||
echo "Deploying $SERVICE to FOSS cluster with image tag: ${IMAGE_TAG}"
|
||||
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${{ secrets.RELEASE_OSS_REGISTRY }}/$SERVICE:${IMAGE_TAG}
|
||||
done
|
||||
1
.github/workflows/sourcemaps-reader-ee.yaml
vendored
1
.github/workflows/sourcemaps-reader-ee.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "sourcemap-reader/**"
|
||||
- "!sourcemap-reader/.gitignore"
|
||||
|
|
|
|||
1
.github/workflows/sourcemaps-reader.yaml
vendored
1
.github/workflows/sourcemaps-reader.yaml
vendored
|
|
@ -9,7 +9,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
- api-*
|
||||
paths:
|
||||
- "sourcemap-reader/**"
|
||||
- "!sourcemap-reader/.gitignore"
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
"isEvent": True,
|
||||
"value": [],
|
||||
"operator": e.operator,
|
||||
"filters": []
|
||||
"filters": e.filters
|
||||
})
|
||||
for v in e.value:
|
||||
if v not in extra_conditions[e.operator].value:
|
||||
|
|
@ -330,7 +330,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
"isEvent": True,
|
||||
"value": [],
|
||||
"operator": e.operator,
|
||||
"filters": []
|
||||
"filters": e.filters
|
||||
})
|
||||
for v in e.value:
|
||||
if v not in extra_conditions[e.operator].value:
|
||||
|
|
@ -1156,7 +1156,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
|
|||
sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s",
|
||||
c.value, value_key=e_k))
|
||||
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:
|
||||
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
|
||||
query_part = f"""\
|
||||
|
|
|
|||
|
|
@ -457,12 +457,6 @@ def set_password_invitation(user_id, new_password):
|
|||
user = update(tenant_id=-1, user_id=user_id, changes=changes)
|
||||
r = authenticate(user['email'], new_password)
|
||||
|
||||
tenant_id = r.pop("tenantId")
|
||||
r["limits"] = {
|
||||
"teamMember": -1,
|
||||
"projects": -1,
|
||||
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
|
||||
|
||||
return {
|
||||
"jwt": r.pop("jwt"),
|
||||
"refreshToken": r.pop("refreshToken"),
|
||||
|
|
@ -470,10 +464,7 @@ def set_password_invitation(user_id, new_password):
|
|||
"spotJwt": r.pop("spotJwt"),
|
||||
"spotRefreshToken": r.pop("spotRefreshToken"),
|
||||
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
|
||||
'data': {
|
||||
"scopeState": scope.get_scope(-1),
|
||||
"user": r
|
||||
}
|
||||
**r
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -129,13 +129,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
|
|||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
|
||||
if data.webhook_id is not None:
|
||||
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
|
||||
changes={"endpoint": data.endpoint.unicode_string(),
|
||||
changes={"endpoint": data.endpoint,
|
||||
"authHeader": data.auth_header,
|
||||
"name": data.name},
|
||||
replace_none=replace_none)
|
||||
else:
|
||||
return add(tenant_id=tenant_id,
|
||||
endpoint=data.endpoint.unicode_string(),
|
||||
endpoint=data.endpoint,
|
||||
auth_header=data.auth_header,
|
||||
name=data.name,
|
||||
replace_none=replace_none)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,41 @@
|
|||
from typing import Union
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import schemas
|
||||
|
||||
|
||||
def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator]):
|
||||
if isinstance(op, Enum):
|
||||
op = op.value
|
||||
return {
|
||||
schemas.SearchEventOperator.IS: "=",
|
||||
schemas.SearchEventOperator.ON: "=",
|
||||
schemas.SearchEventOperator.ON_ANY: "IN",
|
||||
schemas.SearchEventOperator.IS_NOT: "!=",
|
||||
schemas.SearchEventOperator.NOT_ON: "!=",
|
||||
schemas.SearchEventOperator.CONTAINS: "ILIKE",
|
||||
schemas.SearchEventOperator.NOT_CONTAINS: "NOT ILIKE",
|
||||
schemas.SearchEventOperator.STARTS_WITH: "ILIKE",
|
||||
schemas.SearchEventOperator.ENDS_WITH: "ILIKE",
|
||||
schemas.SearchEventOperator.IS.value: "=",
|
||||
schemas.SearchEventOperator.ON.value: "=",
|
||||
schemas.SearchEventOperator.ON_ANY.value: "IN",
|
||||
schemas.SearchEventOperator.IS_NOT.value: "!=",
|
||||
schemas.SearchEventOperator.NOT_ON.value: "!=",
|
||||
schemas.SearchEventOperator.CONTAINS.value: "ILIKE",
|
||||
schemas.SearchEventOperator.NOT_CONTAINS.value: "NOT ILIKE",
|
||||
schemas.SearchEventOperator.STARTS_WITH.value: "ILIKE",
|
||||
schemas.SearchEventOperator.ENDS_WITH.value: "ILIKE",
|
||||
# Selector operators:
|
||||
schemas.ClickEventExtraOperator.IS: "=",
|
||||
schemas.ClickEventExtraOperator.IS_NOT: "!=",
|
||||
schemas.ClickEventExtraOperator.CONTAINS: "ILIKE",
|
||||
schemas.ClickEventExtraOperator.NOT_CONTAINS: "NOT ILIKE",
|
||||
schemas.ClickEventExtraOperator.STARTS_WITH: "ILIKE",
|
||||
schemas.ClickEventExtraOperator.ENDS_WITH: "ILIKE",
|
||||
schemas.ClickEventExtraOperator.IS.value: "=",
|
||||
schemas.ClickEventExtraOperator.IS_NOT.value: "!=",
|
||||
schemas.ClickEventExtraOperator.CONTAINS.value: "ILIKE",
|
||||
schemas.ClickEventExtraOperator.NOT_CONTAINS.value: "NOT ILIKE",
|
||||
schemas.ClickEventExtraOperator.STARTS_WITH.value: "ILIKE",
|
||||
schemas.ClickEventExtraOperator.ENDS_WITH.value: "ILIKE",
|
||||
}.get(op, "=")
|
||||
|
||||
|
||||
def is_negation_operator(op: schemas.SearchEventOperator):
|
||||
return op in [schemas.SearchEventOperator.IS_NOT,
|
||||
schemas.SearchEventOperator.NOT_ON,
|
||||
schemas.SearchEventOperator.NOT_CONTAINS,
|
||||
schemas.ClickEventExtraOperator.IS_NOT,
|
||||
schemas.ClickEventExtraOperator.NOT_CONTAINS]
|
||||
if isinstance(op, Enum):
|
||||
op = op.value
|
||||
return op in [schemas.SearchEventOperator.IS_NOT.value,
|
||||
schemas.SearchEventOperator.NOT_ON.value,
|
||||
schemas.SearchEventOperator.NOT_CONTAINS.value,
|
||||
schemas.ClickEventExtraOperator.IS_NOT.value,
|
||||
schemas.ClickEventExtraOperator.NOT_CONTAINS.value]
|
||||
|
||||
|
||||
def reverse_sql_operator(op):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh
|
||||
export TZ=UTC
|
||||
|
||||
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers --log-level ${S_LOGLEVEL:-warning}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh
|
||||
export TZ=UTC
|
||||
export ASSIST_KEY=ignore
|
||||
uvicorn app:app --host 0.0.0.0 --port 8888 --log-level ${S_LOGLEVEL:-warning}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/zsh
|
||||
export TZ=UTC
|
||||
|
||||
uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/zsh
|
||||
export TZ=UTC
|
||||
|
||||
uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning}
|
||||
|
|
@ -211,7 +211,8 @@ class IssueTrackingJiraSchema(IssueTrackingIntegration):
|
|||
|
||||
class WebhookSchema(BaseModel):
|
||||
webhook_id: Optional[int] = Field(default=None)
|
||||
endpoint: AnyHttpUrl = Field(...)
|
||||
processed_endpoint: AnyHttpUrl = Field(..., alias="endpoint")
|
||||
endpoint: Optional[str] = Field(default=None, doc_hidden=True)
|
||||
auth_header: Optional[str] = Field(default=None)
|
||||
name: str = Field(default="", max_length=100, pattern=NAME_PATTERN)
|
||||
|
||||
|
|
@ -754,6 +755,8 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
|
|||
for f in values.get("filters", []):
|
||||
vals = []
|
||||
for v in f.get("value", []):
|
||||
if f.get("type", "") == FilterType.DURATION.value and v is None:
|
||||
v = 0
|
||||
if v is not None and (f.get("type", "") != FilterType.DURATION.value
|
||||
or str(v).isnumeric()):
|
||||
vals.append(v)
|
||||
|
|
@ -1020,33 +1023,51 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
|||
|
||||
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
|
||||
@model_validator(mode="after")
|
||||
def __merge_out_filters_with_series(self):
|
||||
for f in self.filters:
|
||||
for s in self.series:
|
||||
found = False
|
||||
|
||||
def __override_series_filters_with_outer_filters(self):
|
||||
if len(self.filters) > 0:
|
||||
events = []
|
||||
filters = []
|
||||
for f in self.filters:
|
||||
if f.is_event:
|
||||
sub = s.filter.events
|
||||
events.append(f)
|
||||
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)
|
||||
|
||||
filters.append(f)
|
||||
for s in self.series:
|
||||
s.filter.events = events
|
||||
s.filter.filters = filters
|
||||
self.filters = []
|
||||
|
||||
return self
|
||||
|
||||
|
||||
|
|
@ -1357,6 +1378,7 @@ class LiveFilterType(str, Enum):
|
|||
USER_BROWSER = FilterType.USER_BROWSER.value
|
||||
USER_DEVICE = FilterType.USER_DEVICE.value
|
||||
USER_COUNTRY = FilterType.USER_COUNTRY.value
|
||||
USER_CITY = FilterType.USER_CITY.value
|
||||
USER_STATE = FilterType.USER_STATE.value
|
||||
USER_ID = FilterType.USER_ID.value
|
||||
USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ func (c *cacher) cacheURL(t *Task) {
|
|||
if contentType == "" {
|
||||
contentType = mime.TypeByExtension(filepath.Ext(res.Request.URL.Path))
|
||||
}
|
||||
contentEncoding := res.Header.Get("Content-Encoding")
|
||||
|
||||
// Skip html file (usually it's a CDN mock for 404 error)
|
||||
if strings.HasPrefix(contentType, "text/html") {
|
||||
|
|
@ -159,7 +160,7 @@ func (c *cacher) cacheURL(t *Task) {
|
|||
|
||||
// TODO: implement in streams
|
||||
start = time.Now()
|
||||
err = c.objStorage.Upload(strings.NewReader(strData), t.cachePath, contentType, objectstorage.NoCompression)
|
||||
err = c.objStorage.Upload(strings.NewReader(strData), t.cachePath, contentType, contentEncoding, objectstorage.NoCompression)
|
||||
if err != nil {
|
||||
metrics.RecordUploadDuration(float64(time.Now().Sub(start).Milliseconds()), true)
|
||||
c.Errors <- errors.Wrap(err, t.urlContext)
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ func (s *Storage) uploadSession(payload interface{}) {
|
|||
metrics.RecordSessionCompressionRatio(task.domsRawSize/float64(task.doms.Len()), DOM.String())
|
||||
// Upload session to s3
|
||||
start := time.Now()
|
||||
if err := s.objStorage.Upload(task.doms, task.id+string(DOM)+"s", "application/octet-stream", task.compression); err != nil {
|
||||
if err := s.objStorage.Upload(task.doms, task.id+string(DOM)+"s", "application/octet-stream", "", task.compression); err != nil {
|
||||
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
|
||||
}
|
||||
uploadDoms = time.Now().Sub(start).Milliseconds()
|
||||
|
|
@ -398,7 +398,7 @@ func (s *Storage) uploadSession(payload interface{}) {
|
|||
metrics.RecordSessionCompressionRatio(task.domeRawSize/float64(task.dome.Len()), DOM.String())
|
||||
// Upload session to s3
|
||||
start := time.Now()
|
||||
if err := s.objStorage.Upload(task.dome, task.id+string(DOM)+"e", "application/octet-stream", task.compression); err != nil {
|
||||
if err := s.objStorage.Upload(task.dome, task.id+string(DOM)+"e", "application/octet-stream", "", task.compression); err != nil {
|
||||
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
|
||||
}
|
||||
uploadDome = time.Now().Sub(start).Milliseconds()
|
||||
|
|
@ -411,7 +411,7 @@ func (s *Storage) uploadSession(payload interface{}) {
|
|||
metrics.RecordSessionCompressionRatio(task.devRawSize/float64(task.dev.Len()), DEV.String())
|
||||
// Upload session to s3
|
||||
start := time.Now()
|
||||
if err := s.objStorage.Upload(task.dev, task.id+string(DEV), "application/octet-stream", task.compression); err != nil {
|
||||
if err := s.objStorage.Upload(task.dev, task.id+string(DEV), "application/octet-stream", "", task.compression); err != nil {
|
||||
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
|
||||
}
|
||||
uploadDev = time.Now().Sub(start).Milliseconds()
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
|
|||
cfg.Token = val
|
||||
}
|
||||
}
|
||||
requestUrl := fmt.Sprintf("https://sentry.io/api/0/projects/%s/%s/events/", cfg.OrganizationSlug, cfg.ProjectSlug)
|
||||
requestUrl := fmt.Sprintf("https://sentry.io/api/0/projects/%s/%s/issues/", cfg.OrganizationSlug, cfg.ProjectSlug)
|
||||
|
||||
testCallLimit := 1
|
||||
params := url.Values{}
|
||||
if sessionID != 0 {
|
||||
params.Add("query", fmt.Sprintf("openReplaySession.id=%d", sessionID))
|
||||
params.Add("query", fmt.Sprintf("openReplaySession.id:%d", sessionID))
|
||||
} else {
|
||||
params.Add("per_page", fmt.Sprintf("%d", testCallLimit))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const (
|
|||
)
|
||||
|
||||
type ObjectStorage interface {
|
||||
Upload(reader io.Reader, key string, contentType string, compression CompressionType) error
|
||||
Upload(reader io.Reader, key string, contentType, contentEncoding string, compression CompressionType) error
|
||||
Get(key string) (io.ReadCloser, error)
|
||||
Exists(key string) bool
|
||||
GetCreationTime(key string) *time.Time
|
||||
|
|
|
|||
|
|
@ -67,19 +67,22 @@ func NewS3(cfg *objConfig.ObjectsConfig) (objectstorage.ObjectStorage, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, compression objectstorage.CompressionType) error {
|
||||
func (s *storageImpl) Upload(reader io.Reader, key string, contentType, contentEncoding string, compression objectstorage.CompressionType) error {
|
||||
cacheControl := "max-age=2628000, immutable, private"
|
||||
var contentEncoding *string
|
||||
var encoding *string
|
||||
switch compression {
|
||||
case objectstorage.Gzip:
|
||||
encodeStr := "gzip"
|
||||
contentEncoding = &encodeStr
|
||||
encoding = &encodeStr
|
||||
case objectstorage.Brotli:
|
||||
encodeStr := "br"
|
||||
contentEncoding = &encodeStr
|
||||
encoding = &encodeStr
|
||||
case objectstorage.Zstd:
|
||||
// Have to ignore contentEncoding for Zstd (otherwise will be an error in browser)
|
||||
}
|
||||
if contentEncoding != "" {
|
||||
encoding = &contentEncoding
|
||||
}
|
||||
|
||||
_, err := s.uploader.Upload(&s3manager.UploadInput{
|
||||
Body: reader,
|
||||
|
|
@ -87,7 +90,7 @@ func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, c
|
|||
Key: &key,
|
||||
ContentType: &contentType,
|
||||
CacheControl: &cacheControl,
|
||||
ContentEncoding: contentEncoding,
|
||||
ContentEncoding: encoding,
|
||||
Tagging: s.fileTag,
|
||||
})
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
FROM python:3.11-alpine
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec tini
|
||||
FROM python:3.12-alpine
|
||||
LABEL maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN mv env.default .env
|
||||
RUN apk add --no-cache tini xmlsec && \
|
||||
export UV_SYSTEM_PYTHON=true && \
|
||||
pip install uv && \
|
||||
apk add --no-cache --virtual .build-deps \
|
||||
build-base \
|
||||
libressl \
|
||||
libffi-dev \
|
||||
libressl-dev \
|
||||
libxslt-dev \
|
||||
libxml2-dev \
|
||||
xmlsec-dev && \
|
||||
uv pip install --no-cache-dir --upgrade -r requirements.txt && \
|
||||
# Solve the libxml2 library version mismatch by reinstalling lxml with matching libxml2
|
||||
uv pip uninstall lxml && \
|
||||
uv pip install --no-cache-dir --no-binary lxml lxml --force-reinstall && \
|
||||
# Create non-root user
|
||||
adduser -u 1001 openreplay -D && \
|
||||
# Cleanup build dependencies
|
||||
apk del .build-deps
|
||||
|
||||
ARG envarg
|
||||
ARG GIT_SHA
|
||||
ENV SOURCE_MAP_VERSION=0.7.4 \
|
||||
APP_NAME=chalice \
|
||||
LISTEN_PORT=8000 \
|
||||
|
|
@ -10,19 +33,12 @@ ENV SOURCE_MAP_VERSION=0.7.4 \
|
|||
ENTERPRISE_BUILD=${envarg} \
|
||||
GIT_SHA=$GIT_SHA
|
||||
|
||||
WORKDIR /work
|
||||
COPY requirements.txt ./requirements.txt
|
||||
# Caching the source build
|
||||
RUN pip install --no-cache-dir --upgrade uv
|
||||
RUN uv pip install --no-cache-dir --upgrade pip setuptools wheel --system
|
||||
RUN uv pip install --no-cache-dir --upgrade python3-saml==1.16.0 --no-binary=lxml --system
|
||||
RUN uv pip install --no-cache-dir --upgrade -r requirements.txt --system
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN mv env.default .env
|
||||
|
||||
RUN adduser -u 1001 openreplay -D
|
||||
USER 1001
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ./entrypoint.sh
|
||||
CMD ["./entrypoint.sh"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base tini
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base tini
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ python-decouple = "==3.8"
|
|||
pydantic = {extras = ["email"], version = "==2.9.2"}
|
||||
apscheduler = "==3.10.4"
|
||||
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
|
||||
lxml = "!=4.7.0,<=5.2.1,>=4.6.5"
|
||||
python3-saml = "==1.16.0"
|
||||
python-multipart = "==0.0.17"
|
||||
redis = "==5.2.0"
|
||||
|
|
|
|||
|
|
@ -182,3 +182,20 @@ def delete(tenant_id, user_id, role_id):
|
|||
{"tenant_id": tenant_id, "role_id": role_id})
|
||||
cur.execute(query=query)
|
||||
return get_roles(tenant_id=tenant_id)
|
||||
|
||||
|
||||
def get_role(tenant_id, role_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify("""SELECT roles.*
|
||||
FROM public.roles
|
||||
WHERE tenant_id =%(tenant_id)s
|
||||
AND deleted_at IS NULL
|
||||
AND not service_role
|
||||
AND role_id = %(role_id)s
|
||||
LIMIT 1;""",
|
||||
{"tenant_id": tenant_id, "role_id": role_id})
|
||||
cur.execute(query=query)
|
||||
row = cur.fetchone()
|
||||
if row is not None:
|
||||
row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"])
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
"isEvent": True,
|
||||
"value": [],
|
||||
"operator": e.operator,
|
||||
"filters": []
|
||||
"filters": e.filters
|
||||
})
|
||||
for v in e.value:
|
||||
if v not in extra_conditions[e.operator].value:
|
||||
|
|
@ -386,7 +386,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
"isEvent": True,
|
||||
"value": [],
|
||||
"operator": e.operator,
|
||||
"filters": []
|
||||
"filters": e.filters
|
||||
})
|
||||
for v in e.value:
|
||||
if v not in extra_conditions[e.operator].value:
|
||||
|
|
@ -506,7 +506,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
data.filters.append(
|
||||
schemas.SessionSearchFilterSchema(value=[issue['type']],
|
||||
type=schemas.FilterType.ISSUE.value,
|
||||
operator='is')
|
||||
operator=schemas.SearchEventOperator.IS)
|
||||
)
|
||||
ss_constraints = []
|
||||
full_args = {"project_id": project_id, "startDate": data.startTimestamp, "endDate": data.endTimestamp,
|
||||
|
|
@ -1200,8 +1200,12 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
is_any = _isAny_opreator(f.operator)
|
||||
if is_any or len(f.value) == 0:
|
||||
continue
|
||||
is_negative_operator = sh.is_negation_operator(f.operator)
|
||||
f.value = helper.values_for_operator(value=f.value, op=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}"
|
||||
full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)}
|
||||
if f.type == schemas.FetchFilterType.FETCH_URL:
|
||||
|
|
@ -1210,6 +1214,13 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
value_key=e_k_f))
|
||||
events_conditions[-1]["condition"].append(event_where[-1])
|
||||
apply = True
|
||||
if is_negative_operator:
|
||||
events_conditions_not.append(
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = _multiple_conditions(
|
||||
f"sub.url_path {r_op} %({e_k_f})s", f.value,
|
||||
value_key=e_k_f)
|
||||
elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.status {f.operator} %({e_k_f})s", f.value,
|
||||
|
|
@ -1221,6 +1232,13 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
_multiple_conditions(f"main.method {op} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
events_conditions[-1]["condition"].append(event_where[-1])
|
||||
apply = True
|
||||
if is_negative_operator:
|
||||
events_conditions_not.append(
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = _multiple_conditions(
|
||||
f"sub.method {r_op} %({e_k_f})s", f.value,
|
||||
value_key=e_k_f)
|
||||
elif f.type == schemas.FetchFilterType.FETCH_DURATION:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s", f.value,
|
||||
|
|
@ -1232,11 +1250,25 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
_multiple_conditions(f"main.request_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
events_conditions[-1]["condition"].append(event_where[-1])
|
||||
apply = True
|
||||
if is_negative_operator:
|
||||
events_conditions_not.append(
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = _multiple_conditions(
|
||||
f"sub.request_body {r_op} %({e_k_f})s", f.value,
|
||||
value_key=e_k_f)
|
||||
elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.response_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
events_conditions[-1]["condition"].append(event_where[-1])
|
||||
apply = True
|
||||
if is_negative_operator:
|
||||
events_conditions_not.append(
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = _multiple_conditions(
|
||||
f"sub.response_body {r_op} %({e_k_f})s", f.value,
|
||||
value_key=e_k_f)
|
||||
else:
|
||||
logging.warning(f"undefined FETCH filter: {f.type}")
|
||||
if not apply:
|
||||
|
|
@ -1481,19 +1513,32 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
if extra_conditions and len(extra_conditions) > 0:
|
||||
_extra_or_condition = []
|
||||
for i, c in enumerate(extra_conditions):
|
||||
if _isAny_opreator(c.operator):
|
||||
if _isAny_opreator(c.operator) and c.type != schemas.EventType.REQUEST_DETAILS.value:
|
||||
continue
|
||||
e_k = f"ec_value{i}"
|
||||
op = sh.get_sql_operator(c.operator)
|
||||
c.value = helper.values_for_operator(value=c.value, op=c.operator)
|
||||
full_args = {**full_args,
|
||||
**_multiple_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(
|
||||
_multiple_conditions(f"extra_event.url_path {op} %({e_k})s",
|
||||
c.value, value_key=e_k))
|
||||
elif c.type == schemas.EventType.REQUEST_DETAILS.value:
|
||||
for j, c_f in enumerate(c.filters):
|
||||
if _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,
|
||||
**_multiple_values(c_f.value, value_key=e_k)}
|
||||
if c_f.type == schemas.FetchFilterType.FETCH_URL.value:
|
||||
_extra_or_condition.append(
|
||||
_multiple_conditions(f"extra_event.url_path {op} %({e_k})s",
|
||||
c_f.value, value_key=e_k))
|
||||
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:
|
||||
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,23 @@ def __group_metadata(session, project_metadata):
|
|||
|
||||
|
||||
def get_pre_replay(project_id, session_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""\
|
||||
SELECT encode(file_key,'hex') AS file_key
|
||||
FROM public.sessions
|
||||
WHERE project_id = %(project_id)s
|
||||
AND session_id = %(session_id)s;""",
|
||||
{"project_id": project_id, "session_id": session_id}
|
||||
)
|
||||
cur.execute(query=query)
|
||||
|
||||
data = cur.fetchone()
|
||||
file_key = None
|
||||
if data is not None:
|
||||
file_key = data['file_key']
|
||||
return {
|
||||
'fileKey': file_key,
|
||||
'domURL': [sessions_mobs.get_first_url(project_id=project_id, session_id=session_id, check_existence=False)]}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,12 @@ def create_member(tenant_id, user_id, data: schemas.CreateMemberSchema, backgrou
|
|||
role_id = data.roleId
|
||||
if role_id is None:
|
||||
role_id = roles.get_role_by_name(tenant_id=tenant_id, name="member").get("roleId")
|
||||
else:
|
||||
role = roles.get_role(tenant_id=tenant_id, role_id=role_id)
|
||||
if role is None:
|
||||
return {"errors": ["role not found"]}
|
||||
if role["name"].lower() == "owner" and role["protected"]:
|
||||
return {"errors": ["invalid role"]}
|
||||
invitation_token = __generate_invitation_token()
|
||||
user = get_deleted_user_by_email(email=data.email)
|
||||
if user is not None and user["tenantId"] == tenant_id:
|
||||
|
|
@ -333,7 +339,7 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
|
|||
if editor_id != user_id_to_update:
|
||||
admin = get_user_role(tenant_id=tenant_id, user_id=editor_id)
|
||||
if not admin["superAdmin"] and not admin["admin"]:
|
||||
return {"errors": ["unauthorized"]}
|
||||
return {"errors": ["unauthorized, you must have admin privileges"]}
|
||||
if admin["admin"] and user["superAdmin"]:
|
||||
return {"errors": ["only the owner can edit his own details"]}
|
||||
else:
|
||||
|
|
@ -343,10 +349,10 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
|
|||
return {"errors": ["cannot change your own admin privileges"]}
|
||||
if changes.roleId:
|
||||
if user["superAdmin"] and changes.roleId != user["roleId"]:
|
||||
changes.roleId = None
|
||||
return {"errors": ["owner's role cannot be changed"]}
|
||||
|
||||
if changes.roleId != user["roleId"]:
|
||||
elif user["superAdmin"]:
|
||||
changes.roleId = None
|
||||
elif changes.roleId != user["roleId"]:
|
||||
return {"errors": ["cannot change your own role"]}
|
||||
|
||||
if changes.name and len(changes.name) > 0:
|
||||
|
|
@ -357,6 +363,12 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
|
|||
|
||||
if changes.roleId is not None:
|
||||
_changes["roleId"] = changes.roleId
|
||||
role = roles.get_role(tenant_id=tenant_id, role_id=changes.roleId)
|
||||
if role is None:
|
||||
return {"errors": ["role not found"]}
|
||||
else:
|
||||
if role["name"].lower() == "owner" and role["protected"]:
|
||||
return {"errors": ["invalid role"]}
|
||||
|
||||
if len(_changes.keys()) > 0:
|
||||
update(tenant_id=tenant_id, user_id=user_id_to_update, changes=_changes, output=False)
|
||||
|
|
@ -540,12 +552,6 @@ def set_password_invitation(tenant_id, user_id, new_password):
|
|||
user = update(tenant_id=tenant_id, user_id=user_id, changes=changes)
|
||||
r = authenticate(user['email'], new_password)
|
||||
|
||||
tenant_id = r.pop("tenantId")
|
||||
r["limits"] = {
|
||||
"teamMember": -1,
|
||||
"projects": -1,
|
||||
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
|
||||
|
||||
return {
|
||||
"jwt": r.pop("jwt"),
|
||||
"refreshToken": r.pop("refreshToken"),
|
||||
|
|
@ -554,10 +560,7 @@ def set_password_invitation(tenant_id, user_id, new_password):
|
|||
"spotRefreshToken": r.pop("spotRefreshToken"),
|
||||
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
|
||||
"tenantId": tenant_id,
|
||||
'data': {
|
||||
"scopeState": scope.get_scope(tenant_id),
|
||||
"user": r
|
||||
}
|
||||
**r
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -899,12 +902,12 @@ def authenticate_sso(email: str, internal_id: str):
|
|||
aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti),
|
||||
"refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int),
|
||||
"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'],
|
||||
tenant_id=r['tenantId'],
|
||||
iat=j_r.spot_jwt_refresh_iat,
|
||||
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)
|
||||
}
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -136,13 +136,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
|
|||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
|
||||
if data.webhook_id is not None:
|
||||
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
|
||||
changes={"endpoint": data.endpoint.unicode_string(),
|
||||
changes={"endpoint": data.endpoint,
|
||||
"authHeader": data.auth_header,
|
||||
"name": data.name},
|
||||
replace_none=replace_none)
|
||||
else:
|
||||
return add(tenant_id=tenant_id,
|
||||
endpoint=data.endpoint.unicode_string(),
|
||||
endpoint=data.endpoint,
|
||||
auth_header=data.auth_header,
|
||||
name=data.name,
|
||||
replace_none=replace_none)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent
|
|||
schemas.EventType.REQUEST: "REQUEST",
|
||||
schemas.EventType.REQUEST_DETAILS: "REQUEST",
|
||||
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
|
||||
schemas.GraphqlFilterType.GRAPHQL_NAME: "GRAPHQL",
|
||||
schemas.EventType.STATE_ACTION: "STATEACTION",
|
||||
schemas.EventType.ERROR: "ERROR",
|
||||
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/bin/sh
|
||||
export TZ=UTC
|
||||
sh env_vars.sh
|
||||
source /tmp/.env.override
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/bin/sh
|
||||
export TZ=UTC
|
||||
export ASSIST_KEY=ignore
|
||||
sh env_vars.sh
|
||||
source /tmp/.env.override
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/bin/sh
|
||||
export TZ=UTC
|
||||
export ASSIST_KEY=ignore
|
||||
sh env_vars.sh
|
||||
source /tmp/.env.override
|
||||
|
|
|
|||
|
|
@ -50,16 +50,19 @@ func NewStorage(cfg *config.ObjectsConfig) (objectstorage.ObjectStorage, error)
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, compression objectstorage.CompressionType) error {
|
||||
func (s *storageImpl) Upload(reader io.Reader, key string, contentType, contentEncoding string, compression objectstorage.CompressionType) error {
|
||||
cacheControl := "max-age=2628000, immutable, private"
|
||||
var contentEncoding *string
|
||||
var encoding *string
|
||||
switch compression {
|
||||
case objectstorage.Gzip:
|
||||
gzipStr := "gzip"
|
||||
contentEncoding = &gzipStr
|
||||
encoding = &gzipStr
|
||||
case objectstorage.Brotli:
|
||||
gzipStr := "br"
|
||||
contentEncoding = &gzipStr
|
||||
encoding = &gzipStr
|
||||
}
|
||||
if contentEncoding != "" {
|
||||
encoding = &contentEncoding
|
||||
}
|
||||
// Remove leading slash to avoid empty folder creation
|
||||
if strings.HasPrefix(key, "/") {
|
||||
|
|
@ -68,7 +71,7 @@ func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, c
|
|||
_, err := s.client.UploadStream(context.Background(), s.container, key, reader, &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobCacheControl: &cacheControl,
|
||||
BlobContentEncoding: contentEncoding,
|
||||
BlobContentEncoding: encoding,
|
||||
BlobContentType: &contentType,
|
||||
},
|
||||
Tags: s.tags,
|
||||
|
|
|
|||
|
|
@ -93,6 +93,15 @@ DROP TYPE IF EXISTS events.resource_method;
|
|||
|
||||
ALTER TYPE integration_provider ADD VALUE IF NOT EXISTS 'dynatrace';
|
||||
|
||||
UPDATE users SET settings=COALESCE(settings, '{}'::jsonb) || '{
|
||||
"modules": [
|
||||
"usability-tests",
|
||||
"feature-flags"
|
||||
]
|
||||
}'::jsonb
|
||||
WHERE settings IS NULL
|
||||
OR settings -> 'modules' IS NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
\elif :is_next
|
||||
|
|
|
|||
|
|
@ -198,6 +198,11 @@ export default class APIClient {
|
|||
}
|
||||
|
||||
return fetch(edp + _path, init).then((response) => {
|
||||
if (response.status === 403) {
|
||||
console.warn('API returned 403. Clearing JWT token.');
|
||||
this.onUpdateJwt({ jwt: undefined }); // Clear the token
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ function AssistSearchField() {
|
|||
const showRecords = () => {
|
||||
showModal(<Recordings />, { right: true, width: 960 });
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
searchStoreLive.clearSearch();
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center w-full gap-2">
|
||||
<div style={{ width: '60%' }}>
|
||||
|
|
@ -42,7 +46,7 @@ function AssistSearchField() {
|
|||
type="link"
|
||||
className="ml-auto font-medium"
|
||||
disabled={!hasFilters && !hasEvents}
|
||||
onClick={() => searchStoreLive.clearSearch()}
|
||||
onClick={onClear}
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import React from 'react';
|
|||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import AssistSearchField from './AssistSearchField';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
|
||||
function AssistView() {
|
||||
usePageTitle('Co-Browse - OpenReplay');
|
||||
return (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
|
||||
<AssistSearchField />
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function UserForm() {
|
|||
const isSaving = userStore.saving;
|
||||
const user: any = userStore.instance || userStore.initUser();
|
||||
const roles = roleStore.list
|
||||
.filter((r) => (r.isProtected ? user.isSuperAdmin : true))
|
||||
.filter((r) => (r.protected ? user.isSuperAdmin : true))
|
||||
.map((r) => ({ label: r.name, value: r.roleId }));
|
||||
|
||||
const onChangeCheckbox = (e: any) => {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function CardSessionsByList({ list, selected, paginated, onClickHandler = () =>
|
|||
<div className="m-0">
|
||||
<div className="flex justify-between m-0 p-0">
|
||||
<Typography.Text ellipsis={true}>{row.displayName}</Typography.Text>
|
||||
<Typography.Text type="secondary"> {row.sessionCount}</Typography.Text>
|
||||
<Typography.Text type="secondary" className="flex-shrink-0"> {row.sessionCount}</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ArrowRight } from 'lucide-react';
|
|||
import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList';
|
||||
import { useModal } from 'Components/ModalContext';
|
||||
import Widget from '@/mstore/types/widget';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
|
||||
interface Props {
|
||||
metric?: any;
|
||||
|
|
@ -18,7 +19,7 @@ interface Props {
|
|||
function SessionsBy(props: Props) {
|
||||
const { metric = {}, data = { values: [] }, onClick = () => null, isTemplate } = props;
|
||||
const [selected, setSelected] = React.useState<any>(null);
|
||||
const total = data.count;
|
||||
const total = data.total;
|
||||
const { openModal, closeModal } = useModal();
|
||||
const modalMetric = React.useMemo(() => Object.assign(new Widget(), metric), [metric]);
|
||||
|
||||
|
|
@ -27,12 +28,22 @@ function SessionsBy(props: Props) {
|
|||
...filtersMap[metric.metricOf],
|
||||
value: [data.name],
|
||||
type: filtersMap[metric.metricOf].key,
|
||||
filters: filtersMap[metric.metricOf].filters?.map((f: any) => {
|
||||
const { key, operatorOptions, category, icon, label, options, ...cleaned } = f;
|
||||
return { ...cleaned, type: f.key, value: [] };
|
||||
})
|
||||
filters: [],
|
||||
};
|
||||
|
||||
if (metric.metricOf === FilterKey.FETCH) {
|
||||
baseFilter.filters = [
|
||||
{
|
||||
key: FilterKey.FETCH_URL,
|
||||
operator: 'is',
|
||||
value: [data.name],
|
||||
type: FilterKey.FETCH_URL,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { key, operatorOptions, category, icon, label, options, ...finalFilter } = baseFilter;
|
||||
|
||||
setSelected(data.name);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
import React from 'react';
|
||||
|
||||
import ExCard from './ExCard';
|
||||
import { size } from '@floating-ui/react-dom-interactions';
|
||||
|
||||
const TYPES = {
|
||||
Frustrations: 'frustrations',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards';
|
|||
import { useStore } from 'App/mstore';
|
||||
import Option from './Option';
|
||||
import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary';
|
||||
import { FUNNEL } from 'App/constants/card';
|
||||
import { FUNNEL, TABLE } from 'App/constants/card';
|
||||
import { useHistory } from 'react-router';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import FilterSeries from '@/mstore/types/filterSeries';
|
||||
|
|
@ -72,9 +72,18 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
|
|||
name: "Series 1",
|
||||
filter: {
|
||||
filters: selectedCard.filters,
|
||||
eventsOrder: selectedCard.cardType === TABLE ? 'and' : 'then'
|
||||
}
|
||||
})
|
||||
];
|
||||
} else if (selectedCard.cardType === TABLE) {
|
||||
cardData.series = [new FilterSeries().fromJson({
|
||||
name: "Series 1",
|
||||
filter: {
|
||||
filters: [],
|
||||
eventsOrder: 'and'
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
if (selectedCard.cardType === FUNNEL) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function ExcludeFilters(props: Props) {
|
|||
onRemoveFilter={() => onRemoveFilter(index)}
|
||||
// saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={false}
|
||||
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
|
||||
// excludeFilterKeys={excludeFilterKeys}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -136,9 +136,10 @@ function FilterSeries(props: Props) {
|
|||
toggleExpand={() => setExpanded(!expanded)}/>
|
||||
)}
|
||||
|
||||
{expandable && (
|
||||
{expandable && !expanded && (
|
||||
<Space className="justify-between w-full px-5 py-2 cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>
|
||||
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
|
||||
{/*<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>*/}
|
||||
<Button size="small"
|
||||
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
</Space>
|
||||
|
|
@ -156,13 +157,13 @@ function FilterSeries(props: Props) {
|
|||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
// actions={[
|
||||
// expandable && (
|
||||
// <Button onClick={() => setExpanded(!expanded)}
|
||||
// size="small"
|
||||
// icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
// )
|
||||
// ]}
|
||||
actions={[
|
||||
expandable && (
|
||||
<Button onClick={() => setExpanded(!expanded)}
|
||||
size="small"
|
||||
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
)
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div className="color-gray-medium">{emptyMessage}</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useStore } from 'App/mstore';
|
||||
import React from 'react';
|
||||
// import Select from 'Shared/Select';
|
||||
import { Select } from 'antd';
|
||||
|
||||
const sortOptions = [
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
|
|||
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
|
||||
// const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
|
||||
|
||||
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention;
|
||||
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention || isPathAnalysis;
|
||||
|
||||
// const onAddFilter = (filter: any) => {
|
||||
// metric.series[0].filter.addFilter(filter);
|
||||
|
|
|
|||
|
|
@ -1,221 +1,246 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { NoContent, Loader, Pagination, Button } from 'UI';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {NoContent, Loader, Pagination, Button} from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import {useStore} from 'App/mstore';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { DateTime } from 'luxon';
|
||||
import { debounce } from 'App/utils';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {DateTime} from 'luxon';
|
||||
import {debounce} from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import { HEATMAP } from 'App/constants/card';
|
||||
import { Tag } from 'antd';
|
||||
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import {numberWithCommas} from 'App/utils';
|
||||
import {HEATMAP} from 'App/constants/card';
|
||||
import { Tag, Typography } from 'antd';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function WidgetSessions(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const [activeSeries, setActiveSeries] = useState('all');
|
||||
const [data, setData] = useState<any>([]);
|
||||
const isMounted = useIsMounted();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const filteredSessions = getListSessionsBySeries(data, activeSeries);
|
||||
const { dashboardStore, metricStore, sessionStore, customFieldStore } = useStore();
|
||||
const filter = dashboardStore.drillDownFilter;
|
||||
const widget = metricStore.instance;
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
const [seriesOptions, setSeriesOptions] = useState([{ label: 'All', value: 'all' }]);
|
||||
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
|
||||
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
|
||||
const metaList = customFieldStore.list.map((i: any) => i.key);
|
||||
const {className = ''} = props;
|
||||
const [activeSeries, setActiveSeries] = useState('all');
|
||||
const [data, setData] = useState<any>([]);
|
||||
const isMounted = useIsMounted();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const filteredSessions = getListSessionsBySeries(data, activeSeries);
|
||||
const {dashboardStore, metricStore, sessionStore, customFieldStore} = useStore();
|
||||
const filter = dashboardStore.drillDownFilter;
|
||||
const widget = metricStore.instance;
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm');
|
||||
const [seriesOptions, setSeriesOptions] = useState([{label: 'All', value: 'all'}]);
|
||||
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
|
||||
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
|
||||
const metaList = customFieldStore.list.map((i: any) => i.key);
|
||||
|
||||
const writeOption = ({ value }: any) => setActiveSeries(value.value);
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const seriesOptions = data.map((item: any) => ({
|
||||
label: item.seriesName,
|
||||
value: item.seriesId
|
||||
}));
|
||||
setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]);
|
||||
}, [data]);
|
||||
const writeOption = ({value}: any) => setActiveSeries(value.value);
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const seriesOptions = data.map((item: any) => ({
|
||||
label: item.seriesName,
|
||||
value: item.seriesId
|
||||
}));
|
||||
setSeriesOptions([{label: 'All', value: 'all'}, ...seriesOptions]);
|
||||
}, [data]);
|
||||
|
||||
const fetchSessions = (metricId: any, filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
delete filter.eventsOrderSupport;
|
||||
widget
|
||||
.fetchSessions(metricId, filter)
|
||||
.then((res: any) => {
|
||||
setData(res);
|
||||
})
|
||||
.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 fetchSessions = (metricId: any, filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
|
||||
const depsString = JSON.stringify(widget.series);
|
||||
const filterCopy = {...filter};
|
||||
delete filterCopy.eventsOrderSupport;
|
||||
|
||||
const loadData = () => {
|
||||
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
|
||||
const clickFilter = {
|
||||
value: [metricStore.clickMapSearch],
|
||||
type: 'CLICK',
|
||||
operator: 'onSelector',
|
||||
isEvent: true,
|
||||
// @ts-ignore
|
||||
filters: []
|
||||
};
|
||||
const timeRange = {
|
||||
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
|
||||
startDate: dashboardStore.drillDownPeriod.start,
|
||||
endDate: dashboardStore.drillDownPeriod.end
|
||||
};
|
||||
const customFilter = {
|
||||
...filter,
|
||||
...timeRange,
|
||||
filters: [...sessionStore.userFilter.filters, clickFilter]
|
||||
};
|
||||
debounceClickMapSearch(customFilter);
|
||||
} else {
|
||||
debounceRequest(widget.metricId, {
|
||||
...filter,
|
||||
series: widget.series.map((s) => s.toJson()),
|
||||
page: metricStore.sessionsPage,
|
||||
limit: metricStore.sessionsPageSize
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
metricStore.updateKey('sessionsPage', 1);
|
||||
loadData();
|
||||
}, [
|
||||
filter.startTimestamp,
|
||||
filter.endTimestamp,
|
||||
filter.filters,
|
||||
depsString,
|
||||
metricStore.clickMapSearch,
|
||||
activeSeries
|
||||
]);
|
||||
useEffect(loadData, [metricStore.sessionsPage]);
|
||||
|
||||
const clearFilters = () => {
|
||||
metricStore.updateKey('sessionsPage', 1);
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
|
||||
<div className="ml-2 color-gray-medium">
|
||||
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
|
||||
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
|
||||
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasFilters && widget.metricType === 'table' &&
|
||||
<div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
|
||||
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Filter by Series</span>
|
||||
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain />
|
||||
</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">
|
||||
No relevant sessions found for the selected time period
|
||||
</div>
|
||||
</div>
|
||||
try {
|
||||
// Handle filters properly with null checks
|
||||
if (filterCopy.filters && filterCopy.filters.length > 0) {
|
||||
// Ensure the nested path exists before pushing
|
||||
if (filterCopy.series?.[0]?.filter) {
|
||||
if (!filterCopy.series[0].filter.filters) {
|
||||
filterCopy.series[0].filter.filters = [];
|
||||
}
|
||||
filterCopy.series[0].filter.filters.push(...filterCopy.filters);
|
||||
}
|
||||
filterCopy.filters = [];
|
||||
}
|
||||
show={filteredSessions.sessions.length === 0}
|
||||
>
|
||||
{filteredSessions.sessions.map((session: any) => (
|
||||
<React.Fragment key={session.sessionId}>
|
||||
<SessionItem session={session} metaList={metaList} />
|
||||
<div className="border-b" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
widget
|
||||
.fetchSessions(metricId, filterCopy)
|
||||
.then((res: any) => {
|
||||
setData(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div>
|
||||
Showing{' '}
|
||||
<span className="font-medium">
|
||||
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 = () => {
|
||||
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
|
||||
const clickFilter = {
|
||||
value: [metricStore.clickMapSearch],
|
||||
type: 'CLICK',
|
||||
operator: 'onSelector',
|
||||
isEvent: true,
|
||||
// @ts-ignore
|
||||
filters: []
|
||||
};
|
||||
const timeRange = {
|
||||
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
|
||||
startDate: dashboardStore.drillDownPeriod.start,
|
||||
endDate: dashboardStore.drillDownPeriod.end
|
||||
};
|
||||
const customFilter = {
|
||||
...filter,
|
||||
...timeRange,
|
||||
filters: [...sessionStore.userFilter.filters, clickFilter]
|
||||
};
|
||||
debounceClickMapSearch(customFilter);
|
||||
} else {
|
||||
debounceRequest(widget.metricId, {
|
||||
...filter,
|
||||
series: widget.series.map((s) => s.toJson()),
|
||||
page: metricStore.sessionsPage,
|
||||
limit: metricStore.sessionsPageSize
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
metricStore.updateKey('sessionsPage', 1);
|
||||
loadData();
|
||||
}, [
|
||||
filter.startTimestamp,
|
||||
filter.endTimestamp,
|
||||
filter.filters,
|
||||
depsString,
|
||||
metricStore.clickMapSearch,
|
||||
activeSeries
|
||||
]);
|
||||
useEffect(loadData, [metricStore.sessionsPage]);
|
||||
|
||||
const clearFilters = () => {
|
||||
metricStore.updateKey('sessionsPage', 1);
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
|
||||
<div className="ml-2 color-gray-medium">
|
||||
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
|
||||
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
|
||||
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasFilters && widget.metricType === 'table' &&
|
||||
<div className="py-2">
|
||||
<Tag closable onClose={clearFilters} className="inline-flex">
|
||||
<Typography.Text className="max-w-80 overflow-hidden text-ellipsis inline-block">
|
||||
{filterText}
|
||||
</Typography.Text>
|
||||
</Tag>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
|
||||
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Filter by Series</span>
|
||||
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain/>
|
||||
</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">
|
||||
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 session={session} metaList={metaList}/>
|
||||
<div className="border-b"/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div>
|
||||
Showing{' '}
|
||||
<span className="font-medium">
|
||||
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize + 1}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="font-medium">
|
||||
to{' '}
|
||||
<span className="font-medium">
|
||||
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize +
|
||||
filteredSessions.sessions.length}
|
||||
filteredSessions.sessions.length}
|
||||
</span>{' '}
|
||||
of <span className="font-medium">{numberWithCommas(filteredSessions.total)}</span>{' '}
|
||||
sessions.
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
total={filteredSessions.total}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
of <span className="font-medium">{numberWithCommas(filteredSessions.total)}</span>{' '}
|
||||
sessions.
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
total={filteredSessions.total}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getListSessionsBySeries = (data: any, seriesId: any) => {
|
||||
const arr = data.reduce(
|
||||
(arr: any, element: any) => {
|
||||
if (seriesId === 'all') {
|
||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
|
||||
arr.sessions.push(...sessions);
|
||||
} else if (element.seriesId === seriesId) {
|
||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
|
||||
const duplicates = element.sessions.length - sessions.length;
|
||||
arr.sessions.push(...sessions);
|
||||
arr.total = element.total - duplicates;
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
{ sessions: [] }
|
||||
);
|
||||
arr.total =
|
||||
seriesId === 'all'
|
||||
? Math.max(...data.map((i: any) => i.total))
|
||||
: data.find((i: any) => i.seriesId === seriesId).total;
|
||||
return arr;
|
||||
const arr = data.reduce(
|
||||
(arr: any, element: any) => {
|
||||
if (seriesId === 'all') {
|
||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
|
||||
arr.sessions.push(...sessions);
|
||||
} else if (element.seriesId === seriesId) {
|
||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
|
||||
const duplicates = element.sessions.length - sessions.length;
|
||||
arr.sessions.push(...sessions);
|
||||
arr.total = element.total - duplicates;
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
{sessions: []}
|
||||
);
|
||||
arr.total =
|
||||
seriesId === 'all'
|
||||
? Math.max(...data.map((i: any) => i.total))
|
||||
: data.find((i: any) => i.seriesId === seriesId).total;
|
||||
return arr;
|
||||
};
|
||||
|
||||
export default observer(WidgetSessions);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withPermissions from 'HOCs/withPermissions';
|
||||
import FFlagsListHeader from 'Components/FFlags/FFlagsListHeader';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
|
@ -9,8 +8,10 @@ import FFlagItem from './FFlagItem';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import Select from 'Shared/Select';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
|
||||
function FFlagsList({ siteId }: { siteId: string }) {
|
||||
usePageTitle('Feature Flags - OpenReplay');
|
||||
const { featureFlagsStore, userStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -31,7 +32,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
|
|||
options={[
|
||||
{ label: 'All', value: '0' as const },
|
||||
{ label: 'Enabled', value: '1' as const },
|
||||
{ label: 'Disabled', value: '2' as const },
|
||||
{ label: 'Disabled', value: '2' as const }
|
||||
]}
|
||||
defaultValue={featureFlagsStore.activity}
|
||||
plain
|
||||
|
|
@ -45,7 +46,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
|
|||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'DESC' },
|
||||
{ label: 'Oldest', value: 'ASC' },
|
||||
{ label: 'Oldest', value: 'ASC' }
|
||||
]}
|
||||
defaultValue={featureFlagsStore.sort.order}
|
||||
plain
|
||||
|
|
@ -65,7 +66,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
|
|||
<AnimatedSVG name={ICONS.NO_FFLAGS} size={60} />
|
||||
<div className="text-center mt-4 text-lg font-medium">
|
||||
{featureFlagsStore.sort.query === ''
|
||||
? "You haven't created any feature flags yet"
|
||||
? 'You haven\'t created any feature flags yet'
|
||||
: 'No matching results'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -121,6 +122,4 @@ function FFlagsList({ siteId }: { siteId: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('Feature Flags')(
|
||||
withPermissions(['FEATURE_FLAGS'])(observer(FFlagsList))
|
||||
);
|
||||
export default withPermissions(['FEATURE_FLAGS'])(observer(FFlagsList));
|
||||
|
|
|
|||
|
|
@ -32,13 +32,7 @@ function CreatePassword(props: Props) {
|
|||
if (!validatePassword(password)) {
|
||||
return;
|
||||
}
|
||||
resetPassword({ invitation, pass, password }).then((response: any) => {
|
||||
if (response && response.errors && response.errors.length > 0) {
|
||||
setError(response.errors[0]);
|
||||
} else {
|
||||
setUpdated(true);
|
||||
}
|
||||
});
|
||||
void resetPassword({ invitation, pass, password });
|
||||
};
|
||||
|
||||
const onSubmit = (e: any) => {
|
||||
|
|
@ -102,7 +96,7 @@ function CreatePassword(props: Props) {
|
|||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>{'Cofirm password'}</label>
|
||||
<label>{'Confirm password'}</label>
|
||||
<Input
|
||||
autoComplete="new-password"
|
||||
type="password"
|
||||
|
|
|
|||
|
|
@ -1,163 +1,187 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import Funnelbar, { UxTFunnelBar } from "./FunnelBar";
|
||||
import Funnelbar, { UxTFunnelBar } from './FunnelBar';
|
||||
import cn from 'classnames';
|
||||
import stl from './FunnelWidget.module.css';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from '@/mstore';
|
||||
import Filter from '@/mstore/types/filter';
|
||||
|
||||
interface Props {
|
||||
metric?: Widget;
|
||||
isWidget?: boolean;
|
||||
data: any;
|
||||
metric?: Widget;
|
||||
isWidget?: boolean;
|
||||
data: any;
|
||||
}
|
||||
|
||||
function FunnelWidget(props: Props) {
|
||||
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
|
||||
const { isWidget = false, data, metric } = props;
|
||||
const funnel = data.funnel || { stages: [] };
|
||||
const totalSteps = funnel.stages.length;
|
||||
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
|
||||
const hasMoreSteps = funnel.stages.length > 2;
|
||||
const lastStage = funnel.stages[funnel.stages.length - 1];
|
||||
const remainingSteps = totalSteps - 2;
|
||||
const { hideModal } = useModal();
|
||||
const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
|
||||
const { dashboardStore, searchStore } = useStore();
|
||||
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
|
||||
const { isWidget = false, data, metric } = props;
|
||||
const funnel = data.funnel || { stages: [] };
|
||||
const totalSteps = funnel.stages.length;
|
||||
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
|
||||
const hasMoreSteps = funnel.stages.length > 2;
|
||||
const lastStage = funnel.stages[funnel.stages.length - 1];
|
||||
const remainingSteps = totalSteps - 2;
|
||||
const { hideModal } = useModal();
|
||||
const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const metricFilters = metric?.series[0]?.filter.filters || [];
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isWidget) return;
|
||||
hideModal();
|
||||
const applyDrillDown = (index: number) => {
|
||||
const filter = new Filter().fromData({ filters: metricFilters.slice(0, index + 1) });
|
||||
const periodTimestamps = drillDownPeriod.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
filters: filter.toJson().filters,
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isWidget) return;
|
||||
hideModal();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const focusStage = (index: number) => {
|
||||
funnel.stages.forEach((s, i) => {
|
||||
// turning on all filters if one was focused already
|
||||
if (focusedFilter === index) {
|
||||
s.updateKey('isActive', true);
|
||||
setFocusedFilter(null);
|
||||
} else {
|
||||
setFocusedFilter(index);
|
||||
if (i === index) {
|
||||
s.updateKey('isActive', true);
|
||||
} else {
|
||||
s.updateKey('isActive', false);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
});
|
||||
|
||||
const focusStage = (index: number) => {
|
||||
funnel.stages.forEach((s, i) => {
|
||||
// turning on all filters if one was focused already
|
||||
if (focusedFilter === index) {
|
||||
s.updateKey('isActive', true)
|
||||
setFocusedFilter(null)
|
||||
} else {
|
||||
setFocusedFilter(index)
|
||||
if (i === index) {
|
||||
s.updateKey('isActive', true)
|
||||
} else {
|
||||
s.updateKey('isActive', false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
applyDrillDown(focusedFilter === index ? -1 : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
title={
|
||||
<div className="flex items-center text-lg">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
}
|
||||
show={!stages || stages.length === 0}
|
||||
>
|
||||
<div className="w-full">
|
||||
{ !isWidget && (
|
||||
stages.map((filter: any, index: any) => (
|
||||
<Stage
|
||||
key={index}
|
||||
index={index + 1}
|
||||
isWidget={isWidget}
|
||||
stage={filter}
|
||||
focusStage={focusStage}
|
||||
focusedFilter={focusedFilter}
|
||||
metricLabel={metricLabel}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
title={
|
||||
<div className="flex items-center text-lg">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
}
|
||||
show={!stages || stages.length === 0}
|
||||
>
|
||||
<div className="w-full">
|
||||
{!isWidget && (
|
||||
stages.map((filter: any, index: any) => (
|
||||
<Stage
|
||||
key={index}
|
||||
index={index + 1}
|
||||
isWidget={isWidget}
|
||||
stage={filter}
|
||||
focusStage={focusStage}
|
||||
focusedFilter={focusedFilter}
|
||||
metricLabel={metricLabel}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{ isWidget && (
|
||||
<>
|
||||
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
|
||||
{isWidget && (
|
||||
<>
|
||||
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
|
||||
|
||||
{ hasMoreSteps && (
|
||||
<>
|
||||
<EmptyStage total={remainingSteps} />
|
||||
</>
|
||||
)}
|
||||
{hasMoreSteps && (
|
||||
<>
|
||||
<EmptyStage total={remainingSteps} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{funnel.stages.length > 1 && (
|
||||
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center pb-4">
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Lost conversion</span>
|
||||
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="red" className='text-lg font-medium rounded-lg'>
|
||||
{funnel.lostConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mx-3" />
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Total conversion</span>
|
||||
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="green" className='text-lg font-medium rounded-lg'>
|
||||
{funnel.totalConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}
|
||||
</NoContent>
|
||||
);
|
||||
{funnel.stages.length > 1 && (
|
||||
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center pb-4">
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Lost conversion</span>
|
||||
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="red" className="text-lg font-medium rounded-lg">
|
||||
{funnel.lostConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mx-3" />
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Total conversion</span>
|
||||
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="green" className="text-lg font-medium rounded-lg">
|
||||
{funnel.totalConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span
|
||||
className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export const EmptyStage = observer(({ total }: any) => {
|
||||
return (
|
||||
<div className={cn("flex items-center mb-4 pb-3", stl.step)}>
|
||||
<IndexNumber index={0} />
|
||||
<div className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal" style={{ width: '100px'}}>
|
||||
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
|
||||
</div>
|
||||
<div className="border-b w-full border-dashed"></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={cn('flex items-center mb-4 pb-3', stl.step)}>
|
||||
<IndexNumber index={0} />
|
||||
<div
|
||||
className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal"
|
||||
style={{ width: '100px' }}>
|
||||
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
|
||||
</div>
|
||||
<div className="border-b w-full border-dashed"></div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Stage = observer(({ metricLabel, stage, index, isWidget, uxt, focusStage, focusedFilter }: any) => {
|
||||
return stage ? (
|
||||
<div
|
||||
className={cn('flex items-start', stl.step, { [stl['step-disabled']]: !stage.isActive })}
|
||||
>
|
||||
<IndexNumber index={index} />
|
||||
{!uxt ? <Funnelbar metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage} focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
|
||||
{/*{!isWidget && !uxt && <BarActions bar={stage} />}*/}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
})
|
||||
return stage ? (
|
||||
<div
|
||||
className={cn('flex items-start', stl.step, { [stl['step-disabled']]: !stage.isActive })}
|
||||
>
|
||||
<IndexNumber index={index} />
|
||||
{!uxt ? <Funnelbar metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage}
|
||||
focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
|
||||
{/*{!isWidget && !uxt && <BarActions bar={stage} />}*/}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
});
|
||||
|
||||
export const IndexNumber = observer(({ index }: any) => {
|
||||
return (
|
||||
<div className="z-10 w-6 h-6 border shrink-0 mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
|
||||
{index === 0 ? <Icon size="14" color="gray-dark" name="list" /> : index}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
return (
|
||||
<div
|
||||
className="z-10 w-6 h-6 border shrink-0 mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
|
||||
{index === 0 ? <Icon size="14" color="gray-dark" name="list" /> : index}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const BarActions = observer(({ bar }: any) => {
|
||||
return (
|
||||
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
|
||||
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
|
||||
<Icon name="eye-slash-fill" color={bar.isActive ? "gray-light" : "gray-darkest"} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px' }}>
|
||||
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
|
||||
<Icon name="eye-slash-fill" color={bar.isActive ? 'gray-light' : 'gray-darkest'} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default observer(FunnelWidget);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import NoSessionsMessage from 'Shared/NoSessionsMessage';
|
||||
import MainSearchBar from 'Shared/MainSearchBar';
|
||||
import SessionSearch from 'Shared/SessionSearch';
|
||||
import SessionsTabOverview from 'Shared/SessionsTabOverview/SessionsTabOverview';
|
||||
import FFlagsList from 'Components/FFlags';
|
||||
import NewFFlag from 'Components/FFlags/NewFFlag';
|
||||
|
|
@ -12,6 +9,8 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
|||
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from '@/mstore';
|
||||
import NotesList from 'Shared/SessionsTabOverview/components/Notes/NoteList';
|
||||
import Bookmarks from 'Shared/SessionsTabOverview/components/Bookmarks/Bookmarks';
|
||||
|
||||
// @ts-ignore
|
||||
interface IProps extends RouteComponentProps {
|
||||
|
|
@ -36,15 +35,22 @@ function Overview({ match: { params } }: IProps) {
|
|||
return (
|
||||
<Switch>
|
||||
<Route exact strict
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
|
||||
path={withSiteId(sessions(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NoSessionsMessage siteId={siteId} />
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
<div className="my-4" />
|
||||
<SessionsTabOverview />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict
|
||||
path={withSiteId(bookmarks(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<Bookmarks />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(notes(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NotesList />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(fflags(), siteId)}>
|
||||
<FFlagsList siteId={siteId} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import { useStore } from 'App/mstore';
|
||||
import { session as sessionRoute, withSiteId } from 'App/routes';
|
||||
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
|
||||
import { MobEventsList, WebEventsList } from "../../../Session_/Player/Controls/EventsList";
|
||||
import useShortcuts from '../ReplayPlayer/useShortcuts';
|
||||
|
||||
export const SKIP_INTERVALS = {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ function PlayerBlockHeader(props: any) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
|
||||
<div className="px-2 relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
|
||||
<Tabs
|
||||
tabs={TABS}
|
||||
active={activeTab}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Segmented } from 'antd';
|
||||
import React from 'react';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
import { PlayerContext } from "App/components/Session/playerContext";
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { processLog, UnifiedLog } from './utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -12,13 +11,12 @@ import {
|
|||
} from 'App/components/Client/Integrations/apiMethods';
|
||||
import BottomBlock from 'App/components/shared/DevTools/BottomBlock';
|
||||
import { capitalize } from 'App/utils';
|
||||
import { Icon, Input } from 'UI';
|
||||
import { Icon } from 'UI';
|
||||
import { Segmented, Input, Tooltip } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { client } from 'App/mstore';
|
||||
import { FailedFetch, LoadingFetch } from "./StatusMessages";
|
||||
import {
|
||||
TableHeader,
|
||||
LogRow
|
||||
} from './Table'
|
||||
import { FailedFetch, LoadingFetch } from './StatusMessages';
|
||||
import { TableHeader, LogRow } from './Table';
|
||||
|
||||
async function fetchLogs(
|
||||
tab: string,
|
||||
|
|
@ -30,23 +28,24 @@ async function fetchLogs(
|
|||
);
|
||||
const json = await data.json();
|
||||
try {
|
||||
const logsResp = await fetch(json.url)
|
||||
const logsResp = await fetch(json.url);
|
||||
if (logsResp.ok) {
|
||||
const logJson = await logsResp.json()
|
||||
if (logJson.length === 0) return []
|
||||
return processLog(logJson)
|
||||
const logJson = await logsResp.json();
|
||||
if (logJson.length === 0) return [];
|
||||
return processLog(logJson);
|
||||
} else {
|
||||
throw new Error('Failed to fetch logs')
|
||||
throw new Error('Failed to fetch logs');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw e
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function BackendLogsPanel() {
|
||||
const { projectsStore, sessionStore, integrationsStore } = useStore();
|
||||
const integratedServices = integrationsStore.integrations.backendLogIntegrations;
|
||||
const integratedServices =
|
||||
integrationsStore.integrations.backendLogIntegrations;
|
||||
const defaultTab = integratedServices[0]!.name;
|
||||
const sessionId = sessionStore.currentId;
|
||||
const projectId = projectsStore.siteId!;
|
||||
|
|
@ -82,40 +81,59 @@ function BackendLogsPanel() {
|
|||
return (
|
||||
<BottomBlock style={{ height: '100%' }}>
|
||||
<BottomBlock.Header>
|
||||
<div className={'flex gap-2 items-center w-full'}>
|
||||
<div className={'font-semibold'}>Traces</div>
|
||||
{tabs.length && tab ? (
|
||||
<div>
|
||||
<Segmented options={tabs} value={tab} onChange={setTab} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<div className={'font-semibold'}>Traces</div>
|
||||
{tabs.length && tab ? (
|
||||
<div>
|
||||
<Segmented
|
||||
options={tabs}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={'ml-auto'} />
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'All Tabs', value: 'all' },
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="Backend logs are fetched for all tabs combined.">
|
||||
<span>Current Tab</span>
|
||||
</Tooltip>
|
||||
),
|
||||
value: 'current',
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
defaultValue="all"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size="small"
|
||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
{isPending ? (
|
||||
<LoadingFetch provider={capitalize(tab)} />
|
||||
) : null}
|
||||
{isPending ? <LoadingFetch provider={capitalize(tab)} /> : null}
|
||||
{isError ? (
|
||||
<FailedFetch
|
||||
provider={capitalize(tab)}
|
||||
onRetry={refetch}
|
||||
/>
|
||||
) : null}
|
||||
{isSuccess ? (
|
||||
<LogsTable data={data} />
|
||||
<FailedFetch provider={capitalize(tab)} onRetry={refetch} />
|
||||
) : null}
|
||||
{isSuccess ? <LogsTable data={data} /> : null}
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
|
|
@ -128,8 +146,10 @@ const LogsTable = observer(({ data }: { data: UnifiedLog[] }) => {
|
|||
const _list = React.useRef<VListHandle>(null);
|
||||
const activeIndex = React.useMemo(() => {
|
||||
const currTs = time + sessionStart;
|
||||
const index = data.findIndex(
|
||||
(log) => log.timestamp !== 'N/A' ? new Date(log.timestamp).getTime() >= currTs : false
|
||||
const index = data.findIndex((log) =>
|
||||
log.timestamp !== 'N/A'
|
||||
? new Date(log.timestamp).getTime() >= currTs
|
||||
: false
|
||||
);
|
||||
return index === -1 ? data.length - 1 : index;
|
||||
}, [time, data.length]);
|
||||
|
|
@ -141,17 +161,22 @@ const LogsTable = observer(({ data }: { data: UnifiedLog[] }) => {
|
|||
|
||||
const onJump = (ts: number) => {
|
||||
player.jump(ts - sessionStart);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableHeader size={data.length} />
|
||||
<VList ref={_list} count={data.length}>
|
||||
{data.map((log, index) => (
|
||||
<LogRow key={index} isActive={index === activeIndex} log={log} onJump={onJump} />
|
||||
<LogRow
|
||||
key={index}
|
||||
isActive={index === activeIndex}
|
||||
log={log}
|
||||
onJump={onJump}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
</>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export default observer(BackendLogsPanel);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
|
|||
'w-full h-full flex items-center justify-center flex-col gap-2'
|
||||
}
|
||||
>
|
||||
<LoadingOutlined style={{ fontSize: 32 }} />
|
||||
<LoadingOutlined size={32} />
|
||||
<div>Fetching logs from {provider}...</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -33,16 +33,23 @@ export function FailedFetch({
|
|||
'w-full h-full flex flex-col items-center justify-center gap-2'
|
||||
}
|
||||
>
|
||||
<Icon name={'exclamation-circle'} size={32} />
|
||||
<div className={'flex items-center gap-1'}>
|
||||
|
||||
<div className={'flex items-center gap-1 font-medium'}>
|
||||
<Icon name={'exclamation-circle'} size={14} />
|
||||
<span>Failed to fetch logs from {provider}. </span>
|
||||
<div className={'link'} onClick={onRetry}>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
|
||||
<Button type='text' size='small' onClick={onRetry}>
|
||||
Retry
|
||||
</div>
|
||||
</div>
|
||||
<div className={'link'} onClick={() => history.push(intPath)}>
|
||||
</Button>
|
||||
|
||||
<Button type='text' size='small' onClick={() => history.push(intPath)}>
|
||||
Check Configuration
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function processLog(log: any): UnifiedLog[] {
|
|||
} else if (isDynatraceLog(log)) {
|
||||
return log.map(processDynatraceLog);
|
||||
} else {
|
||||
throw new Error("Unknown log format");
|
||||
console.error("Unknown log format");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ function isElasticLog(log: any): boolean {
|
|||
}
|
||||
|
||||
function isSentryLog(log: any): boolean {
|
||||
return log && log[0].id && log[0].message && log[0].title;
|
||||
return log && 'id' in log[0] && 'message' in log[0] && 'title' in log[0];
|
||||
}
|
||||
|
||||
function processDynatraceLog(log: any): UnifiedLog {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,51 @@ import { useStore } from 'App/mstore';
|
|||
import SaveModal from 'Components/Session/Player/TagWatch/SaveModal';
|
||||
import React from 'react';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import { Button, Input } from 'antd';
|
||||
import { CopyButton } from 'UI';
|
||||
import { SearchOutlined, ZoomInOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Tooltip } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ZoomInOutlined } from '@ant-design/icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { toast } from 'react-toastify';
|
||||
import { FilterKey } from "App/types/filter/filterType";
|
||||
import { addOptionsToFilter } from "App/types/filter/newFilter";
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { addOptionsToFilter } from 'App/types/filter/newFilter';
|
||||
|
||||
interface CopyableTextAreaProps {
|
||||
selector: string;
|
||||
setSelector: (value: string) => void;
|
||||
}
|
||||
|
||||
const CopyableTextArea: React.FC<CopyableTextAreaProps> = ({ selector, setSelector }) => {
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(selector);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<Input.TextArea
|
||||
value={selector}
|
||||
onChange={(e) => setSelector(e.target.value)}
|
||||
className="rounded-lg font-mono text-sm placeholder:font-sans placeholder:text-base placeholder:text-gray-400"
|
||||
rows={4}
|
||||
style={{ paddingRight: '40px' }}
|
||||
placeholder='Enter selector to tag elements. E.g. .btn-primary'
|
||||
/>
|
||||
<Tooltip title="Copy">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function TagWatch() {
|
||||
const { tagWatchStore, searchStore } = useStore();
|
||||
|
|
@ -50,7 +87,7 @@ function TagWatch() {
|
|||
ignoreClickRage: ignoreClRage,
|
||||
ignoreDeadClick: ignoreDeadCl,
|
||||
});
|
||||
const tags = await tagWatchStore.getTags()
|
||||
const tags = await tagWatchStore.getTags();
|
||||
if (tags) {
|
||||
addOptionsToFilter(
|
||||
FilterKey.TAGGED_ELEMENT,
|
||||
|
|
@ -58,40 +95,39 @@ function TagWatch() {
|
|||
);
|
||||
searchStore.refreshFilterOptions();
|
||||
}
|
||||
// @ts-ignore
|
||||
toast.success('Tag created');
|
||||
setSelector('');
|
||||
return tag
|
||||
return tag;
|
||||
} catch {
|
||||
// @ts-ignore
|
||||
toast.error('Failed to create tag');
|
||||
}
|
||||
};
|
||||
|
||||
const openSaveModal = () => {
|
||||
if (selector === '') {
|
||||
return;
|
||||
}
|
||||
showModal(<SaveModal onSave={onSave} hideModal={hideModal} />, { right: true, width: 400 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'w-full h-full p-2 flex flex-col gap-2'}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div className={'font-semibold text-xl'}>Element Selector</div>
|
||||
<CopyButton content={selector} />
|
||||
<div className="w-full h-full p-4 flex flex-col gap-2">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<p>Select elements in the session play area to tag by class selector and filter sessions to verify their rendering.</p>
|
||||
|
||||
</div>
|
||||
<Input.TextArea value={selector} onChange={(e) => setSelector(e.target.value)} />
|
||||
|
||||
<CopyableTextArea selector={selector} setSelector={setSelector} />
|
||||
|
||||
<Button
|
||||
onClick={openSaveModal}
|
||||
type={'primary'}
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ZoomInOutlined />}
|
||||
disabled={selector === ''}
|
||||
>
|
||||
Tag Element
|
||||
</Button>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
Create and filter sessions by ‘watch elements’ to determine if they rendered or not.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function RightBlock({
|
|||
switch (activeTab) {
|
||||
case 'EVENTS':
|
||||
return (
|
||||
<div className={cn('flex flex-col bg-white border-l', stl.panel)}>
|
||||
<div className={cn('flex flex-col border-l', stl.panel)}>
|
||||
<EventsBlock setActiveTab={setActiveTab} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
|
|||
return (
|
||||
<div className={cn(stl.tabs, className, { [stl.bordered]: border })} role="tablist">
|
||||
<Segmented
|
||||
size="small"
|
||||
value={active}
|
||||
options={tabs.map(({ key, text, hidden = false, disabled = false, iconComp = null }) => ({
|
||||
label: (
|
||||
|
|
@ -29,9 +30,9 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
|
|||
onClick={() => {
|
||||
onClick(key);
|
||||
}}
|
||||
className={'font-semibold flex gap-1 items-center'}
|
||||
className={'font-medium flex gap-1 items-center hover:text-teal rounded-lg'}
|
||||
>
|
||||
{iconComp ? iconComp : <Icon size={16} color={'black'} name={iconMap[key as keyof typeof iconMap]} />}
|
||||
{iconComp ? iconComp : <Icon size={14} color="currentColor" style={{ fill: 'currentColor', strokeWidth:'0' }} name={iconMap[key as keyof typeof iconMap]} />}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ const Event: React.FC<Props> = ({
|
|||
>
|
||||
<div className={cn(cls.main, 'flex flex-col w-full')}>
|
||||
<div
|
||||
className={cn('flex items-center w-full', { 'px-4': isLocation })}
|
||||
className={cn('flex items-start w-full', { 'px-4': isLocation })}
|
||||
>
|
||||
<div style={{ minWidth: '16px' }}>
|
||||
{event.type && iconName ? (
|
||||
|
|
@ -169,20 +169,18 @@ const Event: React.FC<Props> = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex w-full items-start">
|
||||
<div
|
||||
className="flex items-center w-full"
|
||||
className="flex flex-col justify-center items-start w-full"
|
||||
style={{ minWidth: '0' }}
|
||||
>
|
||||
<span
|
||||
className={cn(cls.title, { 'font-medium': isLocation })}
|
||||
>
|
||||
<span className={cn(cls.title, 'font-medium')}>
|
||||
{title}
|
||||
</span>
|
||||
{body && !isLocation && (
|
||||
<TextEllipsis
|
||||
maxWidth="60%"
|
||||
className="w-full ml-2 text-sm color-gray-medium"
|
||||
maxWidth="80%"
|
||||
className="w-full text-sm color-gray-medium"
|
||||
text={body}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -202,8 +200,7 @@ const Event: React.FC<Props> = ({
|
|||
{isLocation && (
|
||||
<div className="pt-1 px-4">
|
||||
<TextEllipsis
|
||||
maxWidth="80%"
|
||||
className="text-sm font-normal color-gray-medium"
|
||||
className="text-sm ms-8 font-normal color-gray-medium"
|
||||
text={body}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Icon, TextEllipsis } from 'UI';
|
|||
import Event from './Event';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import cn from 'classnames'
|
||||
|
||||
function EventGroupWrapper(props) {
|
||||
const { userStore } = useStore();
|
||||
|
|
@ -132,7 +133,7 @@ function EventGroupWrapper(props) {
|
|||
{isFirst && isLocation && event.referrer && (
|
||||
<TextEllipsis>
|
||||
<div className={stl.referrer}>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
Referrer: <span className={cn(stl.url, '!font-normal')}>{safeRef}</span>
|
||||
</div>
|
||||
</TextEllipsis>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Input, Button } from 'UI';
|
||||
import {Input, Button, Tooltip} from 'antd';
|
||||
import {CloseOutlined, SearchOutlined} from '@ant-design/icons';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
||||
function EventSearch(props) {
|
||||
const { player } = React.useContext(PlayerContext);
|
||||
|
||||
const { onChange, value, header, setActiveTab } = props;
|
||||
const { onChange, value, header, setActiveTab, eventsText } = props;
|
||||
|
||||
const toggleEvents = () => player.toggleEvents();
|
||||
|
||||
|
|
@ -16,25 +17,25 @@ function EventSearch(props) {
|
|||
<Input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Filter"
|
||||
className="inset-0 w-full"
|
||||
placeholder={`Filter ${eventsText}`}
|
||||
className="w-full rounded-lg"
|
||||
name="query"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
wrapperClassName="w-full"
|
||||
style={{ height: '32px' }}
|
||||
autoComplete="off chromebugfix"
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="ml-2"
|
||||
icon="close"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
toggleEvents();
|
||||
}}
|
||||
/>
|
||||
<Tooltip title="Close Panel" placement='bottom' >
|
||||
<Button
|
||||
className="ml-2"
|
||||
type='text'
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
toggleEvents();
|
||||
}}
|
||||
icon={<CloseOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ function EventsBlock(props: IProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(styles.header, 'p-4')}>
|
||||
<div className={cn(styles.header, 'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-50 h-[57px]' )}>
|
||||
{uxtestingStore.isUxt() ? (
|
||||
<div style={{ width: 240, height: 130 }} className={'relative'}>
|
||||
<video
|
||||
|
|
@ -219,14 +219,14 @@ function EventsBlock(props: IProps) {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={cn(styles.hAndProgress, 'mt-3')}>
|
||||
<div className={cn(styles.hAndProgress, 'mt-0')}>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
eventsText={usedEvents.length ? `${usedEvents.length} Events` : '0 Events'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 color-gray-medium">{eventsText}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn('flex-1 pb-4', styles.eventsList)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Segmented } from 'antd';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons'
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
|
|
@ -12,6 +13,7 @@ import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
|
|||
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
|
||||
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
|
||||
import { Icon, NoContent } from 'UI';
|
||||
import TabSelector from "../../shared/DevTools/TabSelector";
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import EventRow from './components/EventRow';
|
||||
|
|
@ -136,14 +138,60 @@ function WebOverviewPanelCont() {
|
|||
|
||||
const { endTime, currentTab, tabStates } = store.get();
|
||||
|
||||
const stackEventList = tabStates[currentTab]?.stackList || [];
|
||||
const frustrationsList = tabStates[currentTab]?.frustrationsList || [];
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
const resourceListUnmap = tabStates[currentTab]?.resourceList || [];
|
||||
const fetchList = tabStates[currentTab]?.fetchList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const performanceChartData =
|
||||
tabStates[currentTab]?.performanceChartData || [];
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
|
||||
const {
|
||||
stackEventList = [],
|
||||
frustrationsList = [],
|
||||
exceptionsList = [],
|
||||
resourceListUnmap = [],
|
||||
fetchList = [],
|
||||
graphqlList = [],
|
||||
performanceChartData = [],
|
||||
} = React.useMemo(() => {
|
||||
if (showSingleTab) {
|
||||
const stackEventList = tabStates[currentTab].stackList;
|
||||
const frustrationsList = tabStates[currentTab].frustrationsList;
|
||||
const exceptionsList = tabStates[currentTab].exceptionsList;
|
||||
const resourceListUnmap = tabStates[currentTab].resourceList;
|
||||
const fetchList = tabStates[currentTab].fetchList;
|
||||
const graphqlList = tabStates[currentTab].graphqlList;
|
||||
const performanceChartData =
|
||||
tabStates[currentTab].performanceChartData;
|
||||
|
||||
return {
|
||||
stackEventList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceListUnmap,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
performanceChartData,
|
||||
}
|
||||
} else {
|
||||
const stackEventList = tabValues.flatMap((tab) => tab.stackList);
|
||||
// these two are global
|
||||
const frustrationsList = tabValues[0].frustrationsList;
|
||||
const exceptionsList = tabValues[0].exceptionsList;
|
||||
// we can't compute global chart data because some tabs coexist
|
||||
const performanceChartData: any = [];
|
||||
const resourceListUnmap = tabValues.flatMap((tab) => tab.resourceList);
|
||||
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
|
||||
const graphqlList = tabValues.flatMap((tab) => tab.graphqlList);
|
||||
|
||||
return {
|
||||
stackEventList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceListUnmap,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
performanceChartData,
|
||||
}
|
||||
}
|
||||
}, [tabStates, currentTab, dataSource, tabValues]);
|
||||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
const resourceList = resourceListUnmap
|
||||
|
|
@ -168,7 +216,18 @@ function WebOverviewPanelCont() {
|
|||
PERFORMANCE: checkInZoomRange(performanceChartData),
|
||||
FRUSTRATIONS: checkInZoomRange(frustrationsList),
|
||||
};
|
||||
}, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
|
||||
}, [
|
||||
tabStates,
|
||||
currentTab,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
resourceList.length,
|
||||
exceptionsList.length,
|
||||
stackEventList.length,
|
||||
performanceChartData.length,
|
||||
frustrationsList.length,
|
||||
]);
|
||||
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
|
|
@ -187,6 +246,7 @@ function WebOverviewPanelCont() {
|
|||
sessionId={sessionId}
|
||||
setZoomTab={setZoomTab}
|
||||
zoomTab={zoomTab}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -238,6 +298,7 @@ function PanelComponent({
|
|||
spotTime,
|
||||
spotEndTime,
|
||||
onClose,
|
||||
showSingleTab,
|
||||
}: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -280,12 +341,13 @@ function PanelComponent({
|
|||
) : null}
|
||||
</div>
|
||||
{isSpot ? null : (
|
||||
<div className="flex items-center h-20 mr-4 gap-2">
|
||||
<TimelineZoomButton />
|
||||
<div className="flex items-center h-20 mr-4 gap-3">
|
||||
<FeatureSelection
|
||||
list={selectedFeatures}
|
||||
updateList={setSelectedFeatures}
|
||||
/>
|
||||
{!isMobile ? <TabSelector /> : null}
|
||||
<TimelineZoomButton />
|
||||
</div>
|
||||
)}
|
||||
</BottomBlock.Header>
|
||||
|
|
@ -302,12 +364,19 @@ function PanelComponent({
|
|||
style={{ height: '60px', minHeight: 'unset', padding: 0 }}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<InfoCircleOutlined size={18} />
|
||||
Select a debug option to visualize on timeline.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isSpot ? <VerticalPointerLineComp time={spotTime} endTime={spotEndTime} /> : <VerticalPointerLine />}
|
||||
{isSpot ? (
|
||||
<VerticalPointerLineComp
|
||||
time={spotTime}
|
||||
endTime={spotEndTime}
|
||||
/>
|
||||
) : (
|
||||
<VerticalPointerLine />
|
||||
)}
|
||||
{selectedFeatures.map((feature: any, index: number) => (
|
||||
<div
|
||||
key={feature}
|
||||
|
|
@ -318,6 +387,7 @@ function PanelComponent({
|
|||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
title={feature}
|
||||
disabled={!isMobile && !showSingleTab}
|
||||
list={resources[feature]}
|
||||
renderElement={(pointer: any[], isGrouped: boolean) => (
|
||||
<TimelinePointer
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { getTimelinePosition } from 'App/utils';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { Icon } from 'UI';
|
||||
import { InfoCircleOutlined} from '@ant-design/icons'
|
||||
import {Tooltip} from 'antd';
|
||||
import PerformanceGraph from '../PerformanceGraph';
|
||||
interface Props {
|
||||
list?: any[];
|
||||
|
|
@ -13,9 +15,10 @@ interface Props {
|
|||
isGraph?: boolean;
|
||||
zIndex?: number;
|
||||
noMargin?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const EventRow = React.memo((props: Props) => {
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '', disabled } = props;
|
||||
const scale = 100 / endTime;
|
||||
const _list =
|
||||
isGraph ? [] :
|
||||
|
|
@ -82,7 +85,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
}
|
||||
|
||||
return groupedItems;
|
||||
}, [list]);
|
||||
}, [list.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -91,21 +94,24 @@ const EventRow = React.memo((props: Props) => {
|
|||
>
|
||||
<div
|
||||
className={cn(
|
||||
'uppercase text-sm flex items-center py-1',
|
||||
'uppercase text-sm flex items-center py-1 gap-1',
|
||||
props.noMargin ? '' : 'ml-2'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{ zIndex: props.zIndex ? props.zIndex : undefined }}
|
||||
className="mr-2 leading-none"
|
||||
className="leading-none mt-0.5"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{message ? <RowInfo message={message} /> : null}
|
||||
|
||||
<Tooltip title={message} placement='left'>
|
||||
<InfoCircleOutlined className='text-neutral-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
|
||||
{isGraph ? (
|
||||
<PerformanceGraph list={list} />
|
||||
<PerformanceGraph disabled={disabled} list={list} />
|
||||
) : _list.length > 0 ? (
|
||||
_list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => {
|
||||
const left = item.left
|
||||
|
|
@ -123,7 +129,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<div className={cn('color-gray-medium text-sm', props.noMargin ? '' : 'ml-4')}>
|
||||
<div className={cn('color-gray-medium text-xs', props.noMargin ? '' : 'ml-2')}>
|
||||
None captured.
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -133,11 +139,3 @@ const EventRow = React.memo((props: Props) => {
|
|||
});
|
||||
|
||||
export default EventRow;
|
||||
|
||||
function RowInfo({ message }: any) {
|
||||
return (
|
||||
<Tooltip title={message} delay={0}>
|
||||
<Icon name="info-circle" color="gray-medium" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Popover, Checkbox } from 'antd';
|
||||
import { Popover, Checkbox, Button } from 'antd';
|
||||
import {EyeInvisibleOutlined} from '@ant-design/icons';
|
||||
import { Icon } from 'UI'
|
||||
import Funnel from '@/types/funnel';
|
||||
|
||||
const NETWORK = 'NETWORK';
|
||||
const ERRORS = 'ERRORS';
|
||||
|
|
@ -59,7 +61,7 @@ function FeatureSelection(props: Props) {
|
|||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
onClick={() => toggleAllFeatures()}
|
||||
|
|
@ -81,10 +83,9 @@ function FeatureSelection(props: Props) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'font-semibold flex items-center gap-2 text-main cursor-pointer'}>
|
||||
<Icon size={16} name={'funnel'} color={'main'} />
|
||||
<div>X-Ray Events</div>
|
||||
</div>
|
||||
<Button color='primary' size='small' type='text' className={'font-medium'} icon={<EyeInvisibleOutlined size={12} />} >
|
||||
Hide / Show
|
||||
</Button>
|
||||
</Popover>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,82 +1,107 @@
|
|||
import React from 'react';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons'
|
||||
|
||||
interface Props {
|
||||
list: any;
|
||||
list: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const PerformanceGraph = React.memo((props: Props) => {
|
||||
const { list } = props;
|
||||
const { list, disabled } = props;
|
||||
|
||||
const finalValues = React.useMemo(() => {
|
||||
const cpuMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.cpu);
|
||||
}, 0);
|
||||
const cpuMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.cpu);
|
||||
}, Infinity);
|
||||
const finalValues = React.useMemo(() => {
|
||||
const cpuMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.cpu);
|
||||
}, 0);
|
||||
const cpuMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.cpu);
|
||||
}, Infinity);
|
||||
|
||||
const memoryMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.usedHeap);
|
||||
}, Infinity);
|
||||
const memoryMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.usedHeap);
|
||||
}, 0);
|
||||
const memoryMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.usedHeap);
|
||||
}, Infinity);
|
||||
const memoryMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.usedHeap);
|
||||
}, 0);
|
||||
|
||||
const convertToPercentage = (val: number, max: number, min: number) => {
|
||||
return ((val - min) / (max - min)) * 100;
|
||||
};
|
||||
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
|
||||
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
|
||||
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
|
||||
const maxLength = Math.max(arr1.length, arr2.length);
|
||||
const result = [];
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
|
||||
result.push(num > 60 ? num : 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
|
||||
return finalValues;
|
||||
}, []);
|
||||
|
||||
const data = list.map((item: any, index: number) => {
|
||||
return {
|
||||
time: item.time,
|
||||
cpu: finalValues[index],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={35}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* <Tooltip filterNull={false} /> */}
|
||||
<Area
|
||||
dataKey="cpu"
|
||||
baseValue={5}
|
||||
type="monotone"
|
||||
stroke="none"
|
||||
activeDot={false}
|
||||
fill="url(#cpuGradientTimeline)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
const convertToPercentage = (val: number, max: number, min: number) => {
|
||||
return ((val - min) / (max - min)) * 100;
|
||||
};
|
||||
const cpuValues = list.map((item: any) =>
|
||||
convertToPercentage(item.cpu, cpuMax, cpuMin)
|
||||
);
|
||||
const memoryValues = list.map((item: any) =>
|
||||
convertToPercentage(item.usedHeap, memoryMax, memoryMin)
|
||||
);
|
||||
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
|
||||
const maxLength = Math.max(arr1.length, arr2.length);
|
||||
const result = [];
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
|
||||
result.push(num > 60 ? num : 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
|
||||
return finalValues;
|
||||
}, [list.length]);
|
||||
|
||||
const data = list.map((item: any, index: number) => {
|
||||
return {
|
||||
time: item.time,
|
||||
cpu: finalValues[index],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={'relative'}>
|
||||
{disabled ? (
|
||||
<div
|
||||
className={
|
||||
'flex justify-center'
|
||||
}
|
||||
>
|
||||
<div className={'text-xs text-neutral-400 ps-2'}>
|
||||
Multi-tab performance overview is not available.
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ResponsiveContainer height={35}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="cpuGradientTimeline"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* <Tooltip filterNull={false} /> */}
|
||||
<Area
|
||||
dataKey="cpu"
|
||||
baseValue={5}
|
||||
type="monotone"
|
||||
stroke="none"
|
||||
activeDot={false}
|
||||
fill="url(#cpuGradientTimeline)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PerformanceGraph;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function FrustrationElement({ item, createEventClickHandler }: CommonProp
|
|||
const elData = getFrustration(item);
|
||||
return (
|
||||
<Tooltip
|
||||
placement={'right'}
|
||||
placement={'top'}
|
||||
title={
|
||||
<div className="">
|
||||
<b>{elData.name}</b>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ function GroupedIssue({
|
|||
<div
|
||||
onClick={onClick}
|
||||
className={
|
||||
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-sm'
|
||||
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-xs'
|
||||
}
|
||||
>
|
||||
{items.length}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Loader, Icon } from 'UI';
|
||||
import { Loader } from 'UI';
|
||||
import {Button, Tooltip} from 'antd';
|
||||
import {CloseOutlined} from '@ant-design/icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SelectorsList from './components/SelectorsList/SelectorsList';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { compareJsonObjects } from 'App/utils';
|
||||
|
||||
import Select from 'Shared/Select';
|
||||
import {Select, Form} from 'antd';
|
||||
|
||||
const JUMP_OFFSET = 1000;
|
||||
interface Props {
|
||||
|
|
@ -58,34 +60,29 @@ function PageInsightsPanel({ setActiveTab }: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white">
|
||||
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-xl">Clicks</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
}}
|
||||
className="ml-auto flex items-center justify-center bg-white cursor-pointer"
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="mr-2 flex-shrink-0">In Page</div>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
right
|
||||
placeholder="change"
|
||||
options={urlOptions}
|
||||
name="url"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onPageSelect}
|
||||
id="change-dropdown"
|
||||
className="w-full"
|
||||
style={{ width: '100%' }}
|
||||
<div className="p-2 py-4 bg-white">
|
||||
<div className="flex items-center gap-2 mb-3 overflow-hidden">
|
||||
<div className="flex-shrink-0 font-medium">Page</div>
|
||||
<Form.Item name="url" className='mb-0 w-[176px]'>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="change"
|
||||
options={urlOptions}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onPageSelect}
|
||||
id="change-dropdown"
|
||||
className="w-full rounded-lg max-w-[270px]"
|
||||
dropdownStyle={{ }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Close Panel" placement='bottomRight'>
|
||||
<Button
|
||||
className="ml-2"
|
||||
type='text'
|
||||
onClick={() => { setActiveTab(''); }}
|
||||
icon={<CloseOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<SelectorsList />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
.wrapper {
|
||||
padding: 10px;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
background-color: $gray-lightest;
|
||||
margin-bottom: 15px;
|
||||
|
||||
|
|
@ -18,8 +16,6 @@
|
|||
border-radius: 10px;
|
||||
background-color: $tealx;
|
||||
flex-shrink: 0;
|
||||
border: solid thin white;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@ export default function SelectorCard({ index = 1, target, showContent }: Props)
|
|||
|
||||
return (
|
||||
// @ts-ignore TODO for Alex
|
||||
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={cn(stl.wrapper, 'rounded-xl', { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={stl.top}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip position="top" title="Rank of the most clicked element">
|
||||
<div className={stl.index}>{index + 1}</div>
|
||||
</Tooltip>
|
||||
<div className="truncate">{target.selector}</div>
|
||||
<div className="truncate font-mono">{target.selector}</div>
|
||||
</div>
|
||||
{showContent && (
|
||||
<div className={stl.counts}>
|
||||
<div>
|
||||
{target.count} Clicks - {target.percent}%
|
||||
{target.count} Click{target.count > 1 ? 's' : ''} - {target.percent}%
|
||||
</div>
|
||||
<div className="color-gray-medium">TOTAL CLICKS</div>
|
||||
<div className="text-neutral-400">TOTAL CLICKS</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ import {
|
|||
} from 'recharts';
|
||||
import { durationFromMsFormatted } from 'App/date';
|
||||
import { formatBytes } from 'App/utils';
|
||||
import {Tooltip as TooltipANT} from 'antd';
|
||||
|
||||
import stl from './performance.module.css';
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { Segmented } from 'antd'
|
||||
|
||||
const CPU_VISUAL_OFFSET = 10;
|
||||
|
||||
|
|
@ -457,15 +459,33 @@ function Performance() {
|
|||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Heap Size"
|
||||
value={formatBytes(userDeviceHeapSize)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Heap Size"
|
||||
value={formatBytes(userDeviceHeapSize)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center gap-3'}>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: (
|
||||
<TooltipANT title="Performance overview isn't supported across tabs.">
|
||||
<span>All Tabs</span>
|
||||
</TooltipANT>
|
||||
), value: 'all', disabled: true, },
|
||||
{ label: 'Current Tab', value: 'current' },
|
||||
]}
|
||||
defaultValue="current"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ function AssistSessionsModal(props: ConnectProps) {
|
|||
|
||||
const sortOptions = metaList
|
||||
.map((i: any) => ({
|
||||
label: capitalize(i),
|
||||
value: i
|
||||
label: capitalize(i.key),
|
||||
value: i.key
|
||||
}));
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { Icon } from 'UI';
|
|||
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
|
||||
|
||||
import ControlButton from './ControlButton';
|
||||
import { WebEventsList } from "./EventsList";
|
||||
import Timeline from './Timeline';
|
||||
import PlayerControls from './components/PlayerControls';
|
||||
import styles from './controls.module.css';
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerCon
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { getTimelinePosition } from './getTimelinePosition'
|
||||
|
||||
function EventsList({ scale }: { scale: number }) {
|
||||
function EventsList() {
|
||||
const { store } = useContext(PlayerContext);
|
||||
|
||||
const { tabStates, eventCount } = store.get();
|
||||
const { eventCount, endTime } = store.get();
|
||||
const tabStates = store.get().tabStates;
|
||||
const scale = 100 / endTime;
|
||||
const events = React.useMemo(() => {
|
||||
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
|
||||
}, [eventCount]);
|
||||
|
|
@ -34,11 +36,12 @@ function EventsList({ scale }: { scale: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MobileEventsList({ scale }: { scale: number }) {
|
||||
function MobileEventsList() {
|
||||
const { store } = useContext(MobilePlayerContext);
|
||||
const { eventList } = store.get();
|
||||
const { eventList, endTime } = store.get();
|
||||
const events = eventList.filter(e => e.type !== 'SWIPE')
|
||||
|
||||
const scale = 100/endTime;
|
||||
return (
|
||||
<>
|
||||
{events.map((e) => (
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ import NotesList from './NotesList';
|
|||
import SkipIntervalsList from './SkipIntervalsList';
|
||||
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
|
||||
|
||||
interface IProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
function Timeline({ isMobile }: { isMobile: boolean }) {
|
||||
const { player, store } = useContext(PlayerContext);
|
||||
const [wasPlaying, setWasPlaying] = useState(false);
|
||||
const [maxWidth, setMaxWidth] = useState(0);
|
||||
|
|
@ -158,7 +154,7 @@ function Timeline(props: IProps) {
|
|||
{devtoolsLoading || domLoading || !ready ? <div className={stl.stripes} /> : null}
|
||||
</div>
|
||||
|
||||
{props.isMobile ? <MobEventsList scale={scale} /> : <WebEventsList scale={scale} />}
|
||||
{isMobile ? <MobEventsList /> : <WebEventsList />}
|
||||
<NotesList scale={scale} />
|
||||
<SkipIntervalsList scale={scale} />
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function TimelineZoomButton() {
|
|||
}, [])
|
||||
return (
|
||||
<Tooltip title="Select a portion of the timeline to view the x-ray and activity for that specific selection." placement='top'>
|
||||
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-semibold'}>
|
||||
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-medium'}>
|
||||
Focus Mode: {enabled ? 'On' : 'Off'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { JSONTree, NoContent, Tooltip } from 'UI';
|
||||
import { formatMs } from 'App/date';
|
||||
import diff from 'microdiff'
|
||||
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
|
||||
import diff from 'microdiff';
|
||||
import {
|
||||
STORAGE_TYPES,
|
||||
selectStorageList,
|
||||
selectStorageListNow,
|
||||
selectStorageType,
|
||||
} from 'Player';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock/index';
|
||||
import DiffRow from './DiffRow';
|
||||
import cn from 'classnames';
|
||||
import stl from './storage.module.css';
|
||||
import logger from "App/logger";
|
||||
import ReduxViewer from './ReduxViewer'
|
||||
import logger from 'App/logger';
|
||||
import ReduxViewer from './ReduxViewer';
|
||||
import { Segmented } from 'antd'
|
||||
|
||||
function getActionsName(type: string) {
|
||||
switch (type) {
|
||||
|
|
@ -31,7 +37,7 @@ const storageDecodeKeys = {
|
|||
[STORAGE_TYPES.ZUSTAND]: ['state', 'mutation'],
|
||||
[STORAGE_TYPES.MOBX]: ['payload'],
|
||||
[STORAGE_TYPES.NONE]: ['state, action', 'payload', 'mutation'],
|
||||
}
|
||||
};
|
||||
|
||||
function Storage() {
|
||||
const { uiPlayerStore } = useStore();
|
||||
|
|
@ -42,49 +48,48 @@ function Storage() {
|
|||
const [stateObject, setState] = React.useState({});
|
||||
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { tabStates, currentTab } = store.get()
|
||||
const state = tabStates[currentTab] || {}
|
||||
const { tabStates, currentTab } = store.get();
|
||||
const state = tabStates[currentTab] || {};
|
||||
|
||||
const listNow = selectStorageListNow(state) || [];
|
||||
const list = selectStorageList(state) || [];
|
||||
const type = selectStorageType(state) || STORAGE_TYPES.NONE
|
||||
const type = selectStorageType(state) || STORAGE_TYPES.NONE;
|
||||
|
||||
React.useEffect(() => {
|
||||
let currentState;
|
||||
if (listNow.length === 0) {
|
||||
currentState = decodeMessage(list[0])
|
||||
currentState = decodeMessage(list[0]);
|
||||
} else {
|
||||
currentState = decodeMessage(listNow[listNow.length - 1])
|
||||
currentState = decodeMessage(listNow[listNow.length - 1]);
|
||||
}
|
||||
const stateObj = currentState?.state || currentState?.payload?.state || {}
|
||||
const stateObj = currentState?.state || currentState?.payload?.state || {};
|
||||
const newState = Object.assign(stateObject, stateObj);
|
||||
setState(newState);
|
||||
|
||||
}, [listNow.length]);
|
||||
|
||||
const decodeMessage = (msg: any) => {
|
||||
const decoded = {};
|
||||
const pureMSG = { ...msg }
|
||||
const pureMSG = { ...msg };
|
||||
const keys = storageDecodeKeys[type];
|
||||
try {
|
||||
keys.forEach(key => {
|
||||
keys.forEach((key) => {
|
||||
if (pureMSG[key]) {
|
||||
// @ts-ignore TODO: types for decoder
|
||||
decoded[key] = player.decodeMessage(pureMSG[key]);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error on message decoding: ", e, pureMSG);
|
||||
logger.error('Error on message decoding: ', e, pureMSG);
|
||||
return null;
|
||||
}
|
||||
return { ...pureMSG, ...decoded };
|
||||
}
|
||||
};
|
||||
|
||||
const decodedList = React.useMemo(() => {
|
||||
return listNow.map(msg => {
|
||||
return decodeMessage(msg)
|
||||
})
|
||||
}, [listNow.length])
|
||||
return listNow.map((msg) => {
|
||||
return decodeMessage(msg);
|
||||
});
|
||||
}, [listNow.length]);
|
||||
|
||||
const focusNextButton = () => {
|
||||
if (lastBtnRef.current) {
|
||||
|
|
@ -99,7 +104,10 @@ function Storage() {
|
|||
focusNextButton();
|
||||
}, [listNow]);
|
||||
|
||||
const renderDiff = (item: Record<string, any>, prevItem?: Record<string, any>) => {
|
||||
const renderDiff = (
|
||||
item: Record<string, any>,
|
||||
prevItem?: Record<string, any>
|
||||
) => {
|
||||
if (!showDiffs) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -113,7 +121,10 @@ function Storage() {
|
|||
|
||||
if (!stateDiff) {
|
||||
return (
|
||||
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
|
||||
<div
|
||||
style={{ flex: 3 }}
|
||||
className="flex flex-col p-2 pr-0 font-mono text-disabled-text"
|
||||
>
|
||||
No diff
|
||||
</div>
|
||||
);
|
||||
|
|
@ -121,13 +132,15 @@ function Storage() {
|
|||
|
||||
return (
|
||||
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
|
||||
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
|
||||
{stateDiff.map((d: Record<string, any>, i: number) =>
|
||||
renderDiffs(d, i)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDiffs = (diff: Record<string, any>, i: number) => {
|
||||
const path = diff.path.join('.')
|
||||
const path = diff.path.join('.');
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<DiffRow path={path} diff={diff} />
|
||||
|
|
@ -145,12 +158,16 @@ function Storage() {
|
|||
player.jump(list[listNow.length].time);
|
||||
};
|
||||
|
||||
const renderItem = (item: Record<string, any>, i: number, prevItem?: Record<string, any>) => {
|
||||
const renderItem = (
|
||||
item: Record<string, any>,
|
||||
i: number,
|
||||
prevItem?: Record<string, any>
|
||||
) => {
|
||||
let src;
|
||||
let name;
|
||||
|
||||
const itemD = item
|
||||
const prevItemD = prevItem ? prevItem : undefined
|
||||
const itemD = item;
|
||||
const prevItemD = prevItem ? prevItem : undefined;
|
||||
|
||||
switch (type) {
|
||||
case STORAGE_TYPES.REDUX:
|
||||
|
|
@ -177,7 +194,10 @@ function Storage() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
|
||||
className={cn(
|
||||
'flex justify-between items-start',
|
||||
src !== null ? 'border-b' : ''
|
||||
)}
|
||||
key={`store-${i}`}
|
||||
>
|
||||
{src === null ? (
|
||||
|
|
@ -187,7 +207,10 @@ function Storage() {
|
|||
) : (
|
||||
<>
|
||||
{renderDiff(itemD, prevItemD)}
|
||||
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
|
||||
<div
|
||||
style={{ flex: 2 }}
|
||||
className={cn('flex pt-2', showDiffs && 'pl-10')}
|
||||
>
|
||||
<JSONTree
|
||||
name={ensureString(name)}
|
||||
src={src}
|
||||
|
|
@ -202,11 +225,16 @@ function Storage() {
|
|||
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
|
||||
>
|
||||
{typeof item?.duration === 'number' && (
|
||||
<div className="font-size-12 color-gray-medium">{formatMs(itemD.duration)}</div>
|
||||
<div className="font-size-12 color-gray-medium">
|
||||
{formatMs(itemD.duration)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-12">
|
||||
{i + 1 < listNow.length && (
|
||||
<button className={stl.button} onClick={() => player.jump(item.time)}>
|
||||
<button
|
||||
className={stl.button}
|
||||
onClick={() => player.jump(item.time)}
|
||||
>
|
||||
{'JUMP'}
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -222,31 +250,36 @@ function Storage() {
|
|||
};
|
||||
|
||||
if (type === STORAGE_TYPES.REDUX) {
|
||||
return <ReduxViewer />
|
||||
return <ReduxViewer />;
|
||||
}
|
||||
return (
|
||||
<BottomBlock>
|
||||
{/*@ts-ignore*/}
|
||||
<>
|
||||
<BottomBlock.Header>
|
||||
{list.length > 0 && (
|
||||
<div className="flex w-full">
|
||||
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
|
||||
{'STATE'}
|
||||
</h3>
|
||||
{showDiffs ? (
|
||||
<h3 style={{ width: '39%' }} className="font-semibold">
|
||||
DIFFS
|
||||
</h3>
|
||||
) : null}
|
||||
<h3 style={{ width: '30%' }} className="font-semibold">
|
||||
{getActionsName(type)}
|
||||
</h3>
|
||||
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
|
||||
<Tooltip title="Time to execute">TTE</Tooltip>
|
||||
</h3>
|
||||
<div className="flex w-full items-center">
|
||||
<div
|
||||
style={{ width: '25%', marginRight: 20 }}
|
||||
className="font-semibold flex items-center gap-2"
|
||||
>
|
||||
<h3>{'STATE'}</h3>
|
||||
</div>
|
||||
)}
|
||||
{showDiffs ? (
|
||||
<h3 style={{ width: '39%' }} className="font-semibold">
|
||||
DIFFS
|
||||
</h3>
|
||||
) : null}
|
||||
<h3 style={{ width: '30%' }} className="font-semibold">
|
||||
{getActionsName(type)}
|
||||
</h3>
|
||||
<h3
|
||||
style={{ paddingRight: 30, marginLeft: 'auto' }}
|
||||
className="font-semibold"
|
||||
>
|
||||
<Tooltip title="Time to execute">TTE</Tooltip>
|
||||
</h3>
|
||||
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="flex">
|
||||
<NoContent
|
||||
|
|
@ -307,7 +340,10 @@ function Storage() {
|
|||
.
|
||||
<br />
|
||||
<br />
|
||||
<button className="color-teal" onClick={() => hideHint('storage')}>
|
||||
<button
|
||||
className="color-teal"
|
||||
onClick={() => hideHint('storage')}
|
||||
>
|
||||
Got It!
|
||||
</button>
|
||||
</>
|
||||
|
|
@ -322,8 +358,7 @@ function Storage() {
|
|||
{'Empty state.'}
|
||||
</div>
|
||||
) : (
|
||||
<JSONTree collapsed={2} src={stateObject}
|
||||
/>
|
||||
<JSONTree collapsed={2} src={stateObject} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex" style={{ width: '75%' }}>
|
||||
|
|
@ -342,7 +377,6 @@ function Storage() {
|
|||
|
||||
export default observer(Storage);
|
||||
|
||||
|
||||
/**
|
||||
* TODO: compute diff and only decode the required parts
|
||||
* WIP example
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ function SpotConsole({ onClose }: { onClose: () => void }) {
|
|||
jump={jump}
|
||||
iconProps={getIconProps(log.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
showSingleTab
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ function SpotPlayerHeader({
|
|||
{browserVersion && (
|
||||
<>
|
||||
<div>·</div>
|
||||
<div className="capitalize">Chrome v{browserVersion}</div>
|
||||
<div>Chromium v{browserVersion}</div>
|
||||
</>
|
||||
)}
|
||||
{resolution && (
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import cn from 'classnames';
|
|||
import cls from './infoLine.module.css';
|
||||
|
||||
const InfoLine = ({ children }) => (
|
||||
<div className={ cls.info }>
|
||||
<div className={ cn(cls.info, 'text-sm')}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
|
||||
const Point = ({ label = '', value = '', display=true, color, dotColor }) => display
|
||||
? <div className={ cls.infoPoint } style={{ color }}>
|
||||
? <div className={ cn(cls.infoPoint, 'text-sm') } style={{ color }}>
|
||||
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
|
||||
<span className={cls.label}>{ `${label}` }</span> { value }
|
||||
<span className={cn(cls.label, 'text-sm')}>{ `${label}` }</span> { value }
|
||||
</div>
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { LogLevel, ILog } from 'Player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { Tabs, Input, Icon, NoContent } from 'UI';
|
||||
import { Tabs, Icon, NoContent } from 'UI';
|
||||
import {Input} from 'antd';
|
||||
import {SearchOutlined, InfoCircleOutlined} from '@ant-design/icons';
|
||||
import cn from 'classnames';
|
||||
import ConsoleRow from '../ConsoleRow';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
|
@ -9,6 +11,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { useStore } from 'App/mstore';
|
||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import TabSelector from "../TabSelector";
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { VList, VListHandle } from "virtua";
|
||||
|
|
@ -93,6 +96,7 @@ function ConsolePanel({
|
|||
sessionStore: { devTools },
|
||||
uiPlayerStore,
|
||||
} = useStore();
|
||||
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
|
|
@ -109,29 +113,34 @@ function ConsolePanel({
|
|||
const jump = (t: number) => player.jump(t);
|
||||
|
||||
const { currentTab, tabStates } = store.get();
|
||||
const {
|
||||
logList = [],
|
||||
exceptionsList = [],
|
||||
logListNow = [],
|
||||
exceptionsListNow = [],
|
||||
} = tabStates[currentTab] ?? {};
|
||||
const tabsArr = Object.keys(tabStates);
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = React.useMemo(() => {
|
||||
if (showSingleTab) {
|
||||
return tabStates[currentTab] ?? {};
|
||||
} else {
|
||||
const logList = tabValues.flatMap(tab => tab.logList);
|
||||
const exceptionsList = tabValues.flatMap(tab => tab.exceptionsList);
|
||||
const logListNow = isLive ? tabValues.flatMap(tab => tab.logListNow) : [];
|
||||
const exceptionsListNow = isLive ? tabValues.flatMap(tab => tab.exceptionsListNow) : [];
|
||||
return { logList, exceptionsList, logListNow, exceptionsListNow }
|
||||
}
|
||||
}, [currentTab, tabStates, dataSource, tabValues, isLive])
|
||||
const getTabNum = (tab: string) => (tabsArr.findIndex((t) => t === tab) + 1);
|
||||
|
||||
const list = isLive
|
||||
? (useMemo(
|
||||
() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time),
|
||||
[logListNow.length, exceptionsListNow.length]
|
||||
) as ILog[])
|
||||
: (useMemo(
|
||||
() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
|
||||
[logList.length, exceptionsList.length]
|
||||
).filter((l) =>
|
||||
zoomEnabled ? l.time >= zoomStartTs && l.time <= zoomEndTs : true
|
||||
) as ILog[]);
|
||||
const list = useMemo(() => {
|
||||
if (isLive) {
|
||||
return logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time)
|
||||
} else {
|
||||
const logs = logList.concat(exceptionsList).sort((a, b) => a.time - b.time)
|
||||
return zoomEnabled ? logs.filter(l => l.time >= zoomStartTs && l.time <= zoomEndTs) : logs
|
||||
}
|
||||
}, [isLive, logList.length, exceptionsList.length, logListNow.length, exceptionsListNow.length, zoomEnabled, zoomStartTs, zoomEndTs])
|
||||
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
|
||||
|
||||
React.useEffect(() => {
|
||||
}, [activeTab, filter]);
|
||||
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: any) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
|
|
@ -180,23 +189,26 @@ function ConsolePanel({
|
|||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<TabSelector />
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size='small'
|
||||
prefix={<SearchOutlined className='text-neutral-400' />}
|
||||
/>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
</BottomBlock.Header>
|
||||
{/* @ts-ignore */}
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
@ -211,6 +223,8 @@ function ConsolePanel({
|
|||
iconProps={getIconProps(log.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(log)}
|
||||
showSingleTab={showSingleTab}
|
||||
getTabNum={getTabNum}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { LogLevel, ILog } from 'Player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { Tabs, Input, Icon, NoContent } from 'UI';
|
||||
import { Tabs, Input, NoContent } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import ConsoleRow from '../ConsoleRow';
|
||||
import { IOSPlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import {
|
||||
IOSPlayerContext,
|
||||
MobilePlayerContext,
|
||||
} from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -12,6 +15,7 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { InfoCircleOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -26,7 +30,10 @@ const LEVEL_TAB = {
|
|||
[LogLevel.EXCEPTION]: ERRORS,
|
||||
} as const;
|
||||
|
||||
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
|
||||
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({
|
||||
text: tab,
|
||||
key: tab,
|
||||
}));
|
||||
|
||||
function renderWithNL(s: string | null = '') {
|
||||
if (typeof s !== 'string') return '';
|
||||
|
|
@ -73,20 +80,23 @@ function MobileConsolePanel() {
|
|||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const { player, store } = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||
const { player, store } =
|
||||
React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
|
||||
const {
|
||||
logList,
|
||||
logListNow,
|
||||
exceptionsListNow,
|
||||
} = store.get();
|
||||
const { logList, logListNow, exceptionsListNow } = store.get();
|
||||
|
||||
const list = logList as ILog[];
|
||||
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
|
||||
filteredList = useTabListFilterMemo(
|
||||
filteredList,
|
||||
(l) => LEVEL_TAB[l.level],
|
||||
ALL,
|
||||
activeTab
|
||||
);
|
||||
|
||||
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
|
||||
const onTabClick = (activeTab: any) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: any) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
|
||||
|
|
@ -136,34 +146,35 @@ function MobileConsolePanel() {
|
|||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
|
||||
<Tabs
|
||||
tabs={TABS}
|
||||
active={activeTab}
|
||||
onClick={onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size="small"
|
||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
<VList
|
||||
ref={_list}
|
||||
itemSize={25}
|
||||
count={filteredList.length || 1}
|
||||
>
|
||||
<VList ref={_list} itemSize={25} count={filteredList.length || 1}>
|
||||
{filteredList.map((log, index) => (
|
||||
<ConsoleRow
|
||||
key={log.time + index}
|
||||
|
|
@ -172,6 +183,7 @@ function MobileConsolePanel() {
|
|||
iconProps={getIconProps(log.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(log)}
|
||||
showSingleTab
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import JumpButton from 'Shared/DevTools/JumpButton';
|
||||
import { Tag } from 'antd';
|
||||
import TabTag from "../TabTag";
|
||||
|
||||
interface Props {
|
||||
log: any;
|
||||
|
|
@ -10,6 +12,8 @@ interface Props {
|
|||
renderWithNL?: any;
|
||||
style?: any;
|
||||
onClick?: () => void;
|
||||
getTabNum?: (tab: string) => number;
|
||||
showSingleTab: boolean;
|
||||
}
|
||||
function ConsoleRow(props: Props) {
|
||||
const { log, iconProps, jump, renderWithNL, style } = props;
|
||||
|
|
@ -41,11 +45,13 @@ function ConsoleRow(props: Props) {
|
|||
|
||||
const titleLine = lines[0];
|
||||
const restLines = lines.slice(1);
|
||||
const logSource = props.showSingleTab ? -1 : props.getTabNum?.(log.tabId);
|
||||
const logTabId = log.tabId
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={cn(
|
||||
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
'border-b border-neutral-950/5 flex items-start gap-2 py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
{
|
||||
info: !log.isYellow && !log.isRed,
|
||||
warn: log.isYellow,
|
||||
|
|
@ -55,11 +61,10 @@ function ConsoleRow(props: Props) {
|
|||
)}
|
||||
onClick={clickable ? () => (!!log.errorId ? props.onClick?.() : toggleExpand()) : undefined}
|
||||
>
|
||||
<div className="mr-2">
|
||||
<Icon size="14" {...iconProps} />
|
||||
</div>
|
||||
{logSource !== -1 && <TabTag logSource={logSource} logTabId={logTabId} />}
|
||||
<Icon size="14" {...iconProps} className='mt-0.5' />
|
||||
<div key={log.key} data-scroll-item={log.isRed}>
|
||||
<div className="flex items-start text-sm ">
|
||||
<div className="flex items-start text-sm">
|
||||
<div className={cn('flex items-start', { 'cursor-pointer underline decoration-dotted decoration-gray-400': !!log.errorId })}>
|
||||
{canExpand && (
|
||||
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { shortDurationFromMs } from "App/date";
|
||||
import { Tooltip } from 'UI';
|
||||
import { CaretRightOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { shortDurationFromMs } from 'App/date';
|
||||
|
||||
interface Props {
|
||||
onClick: any;
|
||||
|
|
@ -12,19 +14,24 @@ function JumpButton(props: Props) {
|
|||
return (
|
||||
<div className="absolute right-2 top-0 bottom-0 my-auto flex items-center">
|
||||
<Tooltip title={tooltip} disabled={!tooltip}>
|
||||
<div
|
||||
className="border cursor-pointer hidden group-hover:flex rounded bg-white text-xs items-center px-2 py-1 color-teal hover:shadow h-6"
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="hidden group-hover:flex rounded-lg text-xs p-1 py-0 gap-0 h-6"
|
||||
iconPosition="end"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
icon={<CaretRightOutlined />}
|
||||
>
|
||||
<Icon name="caret-right-fill" size="12" color="teal" />
|
||||
<span>JUMP</span>
|
||||
</div>
|
||||
{props.time ? <div className={'block group-hover:hidden mr-2'}>
|
||||
{shortDurationFromMs(props.time)}
|
||||
</div> : null}
|
||||
JUMP
|
||||
</Button>
|
||||
{props.time ? (
|
||||
<div className={'block group-hover:hidden mr-2 text-sm'}>
|
||||
{shortDurationFromMs(props.time)}
|
||||
</div>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { ResourceType, Timed } from 'Player';
|
||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||
import WebPlayer from 'Player/web/WebPlayer';
|
||||
import { Duration } from 'luxon';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
|
|
@ -13,17 +12,19 @@ import {
|
|||
import { formatMs } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { formatBytes } from 'App/utils';
|
||||
import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI';
|
||||
import { Icon, NoContent, Tabs } from 'UI';
|
||||
import { Tooltip, Input, Switch, Form } from 'antd';
|
||||
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||
import { WsChannel } from "App/player/web/messages";
|
||||
import { WsChannel } from 'App/player/web/messages';
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import TabSelector from '../TabSelector';
|
||||
import TimeTable from '../TimeTable';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import WSModal from './WSModal';
|
||||
import WSPanel from './WSPanel';
|
||||
|
||||
const INDEX_KEY = 'network';
|
||||
|
|
@ -57,12 +58,6 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
|
|||
const DOM_LOADED_TIME_COLOR = 'teal';
|
||||
const LOAD_TIME_COLOR = 'red';
|
||||
|
||||
function compare(a: any, b: any, key: string) {
|
||||
if (a[key] > b[key]) return 1;
|
||||
if (a[key] < b[key]) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function renderType(r: any) {
|
||||
return (
|
||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||
|
|
@ -79,14 +74,6 @@ export function renderName(r: any) {
|
|||
);
|
||||
}
|
||||
|
||||
export function renderStart(r: any) {
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSize(r: any) {
|
||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
||||
let triggerText;
|
||||
|
|
@ -125,13 +112,10 @@ export function renderDuration(r: any) {
|
|||
if (!r.isRed && !r.isYellow) return text;
|
||||
|
||||
let tooltipText;
|
||||
let className = 'w-full h-full flex items-center ';
|
||||
if (r.isYellow) {
|
||||
tooltipText = 'Slower than average';
|
||||
className += 'warn color-orange';
|
||||
} else {
|
||||
tooltipText = 'Much slower than average';
|
||||
className += 'error color-red';
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -151,7 +135,7 @@ function renderStatus({
|
|||
error?: string;
|
||||
}) {
|
||||
const displayedStatus = error ? (
|
||||
<Tooltip delay={0} title={error}>
|
||||
<Tooltip title={error}>
|
||||
<div
|
||||
style={{ width: 90 }}
|
||||
className={'overflow-hidden overflow-ellipsis'}
|
||||
|
|
@ -165,7 +149,7 @@ function renderStatus({
|
|||
return (
|
||||
<>
|
||||
{cached ? (
|
||||
<Tooltip title={'Served from cache'}>
|
||||
<Tooltip title={'Served from cache'} placement="top">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">{displayedStatus}</span>
|
||||
<Icon name="wifi" size={16} />
|
||||
|
|
@ -178,13 +162,10 @@ function renderStatus({
|
|||
);
|
||||
}
|
||||
|
||||
function NetworkPanelCont({
|
||||
panelHeight,
|
||||
}: {
|
||||
panelHeight: number;
|
||||
}) {
|
||||
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { sessionStore } = useStore();
|
||||
const { sessionStore, uiPlayerStore } = useStore();
|
||||
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
const {
|
||||
domContentLoadedTime,
|
||||
|
|
@ -192,7 +173,12 @@ function NetworkPanelCont({
|
|||
domBuildingTime,
|
||||
tabStates,
|
||||
currentTab,
|
||||
tabNames,
|
||||
} = store.get();
|
||||
const tabsArr = Object.keys(tabStates);
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
|
|
@ -200,8 +186,34 @@ function NetworkPanelCont({
|
|||
resourceListNow = [],
|
||||
websocketList = [],
|
||||
websocketListNow = [],
|
||||
} = tabStates[currentTab];
|
||||
|
||||
} = React.useMemo(() => {
|
||||
if (showSingleTab) {
|
||||
return tabStates[currentTab] ?? {};
|
||||
} else {
|
||||
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
|
||||
const resourceList = tabValues.flatMap((tab) => tab.resourceList);
|
||||
const fetchListNow = tabValues
|
||||
.flatMap((tab) => tab.fetchListNow)
|
||||
.filter(Boolean);
|
||||
const resourceListNow = tabValues
|
||||
.flatMap((tab) => tab.resourceListNow)
|
||||
.filter(Boolean);
|
||||
const websocketList = tabValues.flatMap((tab) => tab.websocketList);
|
||||
const websocketListNow = tabValues
|
||||
.flatMap((tab) => tab.websocketListNow)
|
||||
.filter(Boolean);
|
||||
return {
|
||||
fetchList,
|
||||
resourceList,
|
||||
fetchListNow,
|
||||
resourceListNow,
|
||||
websocketList,
|
||||
websocketListNow,
|
||||
};
|
||||
}
|
||||
}, [currentTab, tabStates, dataSource, tabValues]);
|
||||
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
||||
const getTabName = (tabId: string) => tabNames[tabId]
|
||||
return (
|
||||
<NetworkPanelComp
|
||||
loadTime={loadTime}
|
||||
|
|
@ -216,15 +228,14 @@ function NetworkPanelCont({
|
|||
startedAt={startedAt}
|
||||
websocketList={websocketList as WSMessage[]}
|
||||
websocketListNow={websocketListNow as WSMessage[]}
|
||||
getTabNum={getTabNum}
|
||||
getTabName={getTabName}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNetworkPanelCont({
|
||||
panelHeight,
|
||||
}: {
|
||||
panelHeight: number;
|
||||
}) {
|
||||
function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const { uiPlayerStore, sessionStore } = useStore();
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
|
|
@ -301,6 +312,9 @@ interface Props {
|
|||
onClose?: () => void;
|
||||
activeOutsideIndex?: number;
|
||||
isSpot?: boolean;
|
||||
getTabNum?: (tab: string) => number;
|
||||
getTabName?: (tabId: string) => string;
|
||||
showSingleTab?: boolean;
|
||||
}
|
||||
|
||||
export const NetworkPanelComp = observer(
|
||||
|
|
@ -323,8 +337,13 @@ export const NetworkPanelComp = observer(
|
|||
onClose,
|
||||
activeOutsideIndex,
|
||||
isSpot,
|
||||
getTabNum,
|
||||
showSingleTab,
|
||||
getTabName,
|
||||
}: Props) => {
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<
|
||||
WsChannel[] | null
|
||||
>(null);
|
||||
const { showModal } = useModal();
|
||||
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
||||
|
||||
|
|
@ -480,10 +499,10 @@ export const NetworkPanelComp = observer(
|
|||
const showDetailsModal = (item: any) => {
|
||||
if (item.type === 'websocket') {
|
||||
const socketMsgList = websocketList.filter(
|
||||
(ws) => ws.channelName === item.channelName
|
||||
);
|
||||
(ws) => ws.channelName === item.channelName
|
||||
);
|
||||
|
||||
return setSelectedWsChannel(socketMsgList)
|
||||
return setSelectedWsChannel(socketMsgList);
|
||||
}
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
|
|
@ -507,6 +526,61 @@ export const NetworkPanelComp = observer(
|
|||
stopAutoscroll();
|
||||
};
|
||||
|
||||
const tableCols = React.useMemo(() => {
|
||||
const cols: any[] = [
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 90,
|
||||
render: renderStatus,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
dataKey: 'type',
|
||||
width: 90,
|
||||
render: renderType,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
width: 80,
|
||||
dataKey: 'method',
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
dataKey: 'name',
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 80,
|
||||
dataKey: 'decodedBodySize',
|
||||
render: renderSize,
|
||||
hidden: activeTab === XHR,
|
||||
},
|
||||
{
|
||||
label: 'Duration',
|
||||
width: 80,
|
||||
dataKey: 'duration',
|
||||
render: renderDuration,
|
||||
},
|
||||
];
|
||||
if (!showSingleTab && !isSpot) {
|
||||
cols.unshift({
|
||||
label: 'Source',
|
||||
width: 64,
|
||||
render: (r: Record<string, any>) => (
|
||||
<Tooltip title={`${getTabName?.(r.tabId) ?? `Tab ${getTabNum?.(r.tabId) ?? 0}`}`} placement="left">
|
||||
<div className="bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default">
|
||||
{getTabNum?.(r.tabId) ?? 0}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
});
|
||||
}
|
||||
return cols;
|
||||
}, [showSingleTab]);
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
|
|
@ -529,26 +603,39 @@ export const NetworkPanelComp = observer(
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by name, type, method or value"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
height={28}
|
||||
width={280}
|
||||
value={filter}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{!isMobile && !isSpot ? <TabSelector /> : null}
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by name, type, method or value"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
width={280}
|
||||
value={filter}
|
||||
size="small"
|
||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
||||
<div>
|
||||
<Toggler
|
||||
checked={showOnlyErrors}
|
||||
name="show-errors-only"
|
||||
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
|
||||
label="4xx-5xx Only"
|
||||
/>
|
||||
<Form.Item name="show-errors-only" className="mb-0">
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={showOnlyErrors}
|
||||
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
|
||||
size="small"
|
||||
/>
|
||||
<span className="text-sm ms-2">4xx-5xx Only</span>
|
||||
</label>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
|
|
@ -588,8 +675,8 @@ export const NetworkPanelComp = observer(
|
|||
</div>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
@ -613,52 +700,13 @@ export const NetworkPanelComp = observer(
|
|||
}}
|
||||
activeIndex={activeIndex}
|
||||
>
|
||||
{[
|
||||
// {
|
||||
// label: 'Start',
|
||||
// width: 120,
|
||||
// render: renderStart,
|
||||
// },
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 90,
|
||||
render: renderStatus,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
dataKey: 'type',
|
||||
width: 90,
|
||||
render: renderType,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
width: 80,
|
||||
dataKey: 'method',
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
dataKey: 'name',
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 80,
|
||||
dataKey: 'decodedBodySize',
|
||||
render: renderSize,
|
||||
hidden: activeTab === XHR,
|
||||
},
|
||||
{
|
||||
label: 'Duration',
|
||||
width: 80,
|
||||
dataKey: 'duration',
|
||||
render: renderDuration,
|
||||
},
|
||||
]}
|
||||
{tableCols}
|
||||
</TimeTable>
|
||||
{selectedWsChannel ? (
|
||||
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />
|
||||
<WSPanel
|
||||
socketMsgList={selectedWsChannel}
|
||||
onClose={() => setSelectedWsChannel(null)}
|
||||
/>
|
||||
) : null}
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ const lineLength = 40;
|
|||
function WSPanel({ socketMsgList, onClose }: Props) {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [list, setList] = React.useState(socketMsgList);
|
||||
const [selectedRow, setSelectedRow] = React.useState<SocketMsg | null>(null);
|
||||
const [selectedRow, setSelectedRow] = React.useState<{ msg: SocketMsg, id: number } | null>(null);
|
||||
|
||||
const onQueryChange = (e) => {
|
||||
const onQueryChange = (e: any) => {
|
||||
setQuery(e.target.value);
|
||||
const newList = filterList(socketMsgList, e.target.value, [
|
||||
'data',
|
||||
|
|
@ -69,15 +69,16 @@ function WSPanel({ socketMsgList, onClose }: Props) {
|
|||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{list.map((msg) => (
|
||||
{list.map((msg, i) => (
|
||||
<Row
|
||||
msg={msg}
|
||||
key={msg.timestamp}
|
||||
onSelect={() => setSelectedRow(msg)}
|
||||
onSelect={() => setSelectedRow({ msg, id: i })}
|
||||
isSelected={selectedRow ? selectedRow.id === i : false}
|
||||
/>
|
||||
))}
|
||||
{selectedRow ? (
|
||||
<SelectedRow msg={selectedRow} onClose={() => setSelectedRow(null)} />
|
||||
<SelectedRow msg={selectedRow.msg} onClose={() => setSelectedRow(null)} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -127,7 +128,7 @@ function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
|
||||
function Row({ msg, onSelect, isSelected }: { msg: SocketMsg; isSelected: boolean; onSelect: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -149,7 +150,7 @@ function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
|
|||
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
|
||||
}
|
||||
>
|
||||
<span>{isOpen ? '-' : '+'}</span>
|
||||
<span>{isSelected ? '-' : '+'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { Timed } from 'Player';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Tabs, Input, NoContent, Icon } from 'UI';
|
||||
import { PlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { Tabs, NoContent, Icon } from 'UI';
|
||||
import { Input } from 'antd';
|
||||
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlayerContext,
|
||||
MobilePlayerContext,
|
||||
} from 'App/components/Session/playerContext';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -10,6 +15,7 @@ import { typeList } from 'Types/session/stackEvent';
|
|||
import StackEventRow from 'Shared/DevTools/StackEventRow';
|
||||
|
||||
import StackEventModal from '../StackEventModal';
|
||||
import { Segmented, Tooltip } from 'antd';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
|
@ -24,198 +30,247 @@ const ALL = 'ALL';
|
|||
const TAB_KEYS = [ALL, ...typeList] as const;
|
||||
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
type EventsList = Array<Timed & { name: string; source: string; key: string; payload?: string[] }>;
|
||||
type EventsList = Array<
|
||||
Timed & { name: string; source: string; key: string; payload?: string[] }
|
||||
>;
|
||||
|
||||
const WebStackEventPanelComp = observer(
|
||||
() => {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { currentTab, tabStates } = store.get();
|
||||
const WebStackEventPanelComp = observer(() => {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { currentTab, tabStates } = store.get();
|
||||
|
||||
const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab];
|
||||
const { stackList: list = [], stackListNow: listNow = [] } =
|
||||
tabStates[currentTab];
|
||||
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const WebStackEventPanel = WebStackEventPanelComp;
|
||||
|
||||
const MobileStackEventPanelComp = observer(
|
||||
() => {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
|
||||
const MobileStackEventPanelComp = observer(() => {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
|
||||
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
isMobile
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const MobileStackEventPanel = MobileStackEventPanelComp;
|
||||
|
||||
const EventsPanel = observer(({
|
||||
list,
|
||||
listNow,
|
||||
jump,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
list: EventsList;
|
||||
listNow: EventsList;
|
||||
jump: (t: number) => void;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) => {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore();
|
||||
const { showModal } = useModal();
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal
|
||||
const filter = devTools[INDEX_KEY].filter;
|
||||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
const EventsPanel = observer(
|
||||
({
|
||||
list,
|
||||
listNow,
|
||||
jump,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
isMobile,
|
||||
}: {
|
||||
list: EventsList;
|
||||
listNow: EventsList;
|
||||
jump: (t: number) => void;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
isMobile?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore();
|
||||
const { showModal } = useModal();
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal
|
||||
const filter = devTools[INDEX_KEY].filter;
|
||||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
|
||||
const inZoomRangeList = list.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
const inZoomRangeListNow = listNow.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
const inZoomRangeList = list.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
const inZoomRangeListNow = listNow.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
|
||||
let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => {
|
||||
const searchBy = [it.name]
|
||||
if (it.payload) {
|
||||
const payload = Array.isArray(it.payload) ? it.payload.join(',') : JSON.stringify(it.payload);
|
||||
searchBy.push(payload);
|
||||
}
|
||||
return searchBy
|
||||
}, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab);
|
||||
|
||||
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(
|
||||
() => TABS.filter(({ key }) => key === ALL || inZoomRangeList.some(({ source }) => key === source)),
|
||||
[inZoomRangeList.length]
|
||||
);
|
||||
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(inZoomRangeListNow),
|
||||
activeIndex,
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
if (isDetailsModalActive) {
|
||||
return;
|
||||
}
|
||||
timeoutStartAutoscroll();
|
||||
};
|
||||
|
||||
const showDetails = (item: any) => {
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(<StackEventModal event={item} />, {
|
||||
right: true,
|
||||
width: 500,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
let filteredList = useRegExListFilterMemo(
|
||||
inZoomRangeList,
|
||||
(it) => {
|
||||
const searchBy = [it.name];
|
||||
if (it.payload) {
|
||||
const payload = Array.isArray(it.payload)
|
||||
? it.payload.join(',')
|
||||
: JSON.stringify(it.payload);
|
||||
searchBy.push(payload);
|
||||
}
|
||||
return searchBy;
|
||||
},
|
||||
});
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
||||
stopAutoscroll();
|
||||
};
|
||||
filter
|
||||
);
|
||||
filteredList = useTabListFilterMemo(
|
||||
filteredList,
|
||||
(it) => it.source,
|
||||
ALL,
|
||||
activeTab
|
||||
);
|
||||
|
||||
const _list = React.useRef<VListHandle>(null);
|
||||
useEffect(() => {
|
||||
if (_list.current) {
|
||||
_list.current.scrollToIndex(activeIndex);
|
||||
}
|
||||
}, [activeIndex]);
|
||||
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({
|
||||
target: { value },
|
||||
}: React.ChangeEvent<HTMLInputElement>) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
TABS.filter(
|
||||
({ key }) =>
|
||||
key === ALL || inZoomRangeList.some(({ source }) => key === source)
|
||||
),
|
||||
[inZoomRangeList.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomBlock style={{ height: '100%' }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Stack Events</span>
|
||||
<Tabs
|
||||
renameTab={mapNames}
|
||||
tabs={tabs}
|
||||
active={activeTab}
|
||||
onClick={onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
<VList
|
||||
ref={_list}
|
||||
count={filteredList.length || 1}
|
||||
>
|
||||
{filteredList.map((item, index) => (
|
||||
<StackEventRow
|
||||
isActive={activeIndex === index}
|
||||
key={item.key}
|
||||
event={item}
|
||||
onJump={() => {
|
||||
stopAutoscroll();
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
||||
jump(item.time);
|
||||
}}
|
||||
onClick={() => showDetails(item)}
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(inZoomRangeListNow),
|
||||
activeIndex,
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
if (isDetailsModalActive) {
|
||||
return;
|
||||
}
|
||||
timeoutStartAutoscroll();
|
||||
};
|
||||
|
||||
const showDetails = (item: any) => {
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(<StackEventModal event={item} />, {
|
||||
right: true,
|
||||
width: 500,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
},
|
||||
});
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
||||
stopAutoscroll();
|
||||
};
|
||||
|
||||
const _list = React.useRef<VListHandle>(null);
|
||||
useEffect(() => {
|
||||
if (_list.current) {
|
||||
_list.current.scrollToIndex(activeIndex);
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">
|
||||
Stack Events
|
||||
</span>
|
||||
<Tabs
|
||||
renameTab={mapNames}
|
||||
tabs={tabs}
|
||||
active={activeTab}
|
||||
onClick={onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{isMobile ? null : (
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'All Tabs', value: 'all' },
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="Stack Events overview is available only for all tabs combined.">
|
||||
<span>Current Tab</span>
|
||||
</Tooltip>
|
||||
),
|
||||
value: 'current',
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
defaultValue="all"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
});
|
||||
)}
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size="small"
|
||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
<VList ref={_list} count={filteredList.length || 1}>
|
||||
{filteredList.map((item, index) => (
|
||||
<StackEventRow
|
||||
isActive={activeIndex === index}
|
||||
key={item.key}
|
||||
event={item}
|
||||
onJump={() => {
|
||||
stopAutoscroll();
|
||||
devTools.update(INDEX_KEY, {
|
||||
index: filteredList.indexOf(item),
|
||||
});
|
||||
jump(item.time);
|
||||
}}
|
||||
onClick={() => showDetails(item)}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { Segmented } from 'antd'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
function TabSelector() {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const currentValue = uiPlayerStore.dataSource;
|
||||
const options = [
|
||||
{ label: 'All Tabs', value: 'all' },
|
||||
{ label: 'Current Tab', value: 'current' }
|
||||
]
|
||||
|
||||
const onChange = (value: 'all' | 'current') => {
|
||||
uiPlayerStore.changeDataSource(value)
|
||||
}
|
||||
return (
|
||||
<Segmented options={options} value={currentValue} onChange={onChange} className='font-medium rounded-lg' size='small' />
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TabSelector)
|
||||
23
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
23
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
|
||||
function TabTag({ logSource, logTabId }: { logSource: number; logTabId: string }) {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const { tabNames } = store.get();
|
||||
|
||||
return (
|
||||
<Tooltip title={`${tabNames[logTabId] ?? `Tab ${logSource}`}`} placement="left">
|
||||
<div
|
||||
className={
|
||||
'bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default'
|
||||
}
|
||||
>
|
||||
{logSource}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(TabTag);
|
||||
|
|
@ -199,7 +199,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'dev-row border-b border-color-gray-light-shade group items-center',
|
||||
'dev-row border-b border-neutral-950/5 group items-center text-sm',
|
||||
stl.row,
|
||||
{
|
||||
[stl.hoverable]: hoverable,
|
||||
|
|
@ -215,7 +215,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
{columns
|
||||
.filter((i: any) => !i.hidden)
|
||||
.map(({ dataKey, render, width, label }) => (
|
||||
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden')} style={{ width: `${width}px` }}>
|
||||
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden !py-0.5')} style={{ width: `${width}px` }}>
|
||||
{render
|
||||
? render(row)
|
||||
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Select from 'react-select';
|
|||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { searchService} from 'App/services';
|
||||
import { searchService } from 'App/services';
|
||||
|
||||
const dropdownStyles = {
|
||||
option: (provided: any, state: any) => ({
|
||||
|
|
@ -31,22 +31,18 @@ const dropdownStyles = {
|
|||
backgroundColor: colors['active-blue']
|
||||
}
|
||||
}),
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin transparent !important',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
height: '26px',
|
||||
minHeight: '26px',
|
||||
borderRadius: '.5rem',
|
||||
boxShadow: 'none !important'
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
control: (provided: any) => ({
|
||||
...provided,
|
||||
border: 'solid thin transparent !important',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
height: '26px',
|
||||
minHeight: '26px',
|
||||
borderRadius: '.5rem',
|
||||
boxShadow: 'none !important'
|
||||
}),
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
// paddingRight: '0px',
|
||||
width: 'fit-content',
|
||||
alignItems: 'center',
|
||||
height: '26px',
|
||||
|
|
@ -77,7 +73,6 @@ const dropdownStyles = {
|
|||
noOptionsMessage: (provided: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap !important'
|
||||
// minWidth: 'fit-content',
|
||||
}),
|
||||
container: (provided: any) => ({
|
||||
...provided,
|
||||
|
|
@ -93,12 +88,10 @@ const dropdownStyles = {
|
|||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return {
|
||||
...provided,
|
||||
opacity,
|
||||
transition,
|
||||
transition: 'opacity 300ms',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '20px'
|
||||
|
|
@ -187,30 +180,44 @@ const FilterAutoComplete: React.FC<Props> = ({
|
|||
setQuery(value);
|
||||
}, [value]);
|
||||
|
||||
const loadOptions = async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
|
||||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
setOptions(mappedValues);
|
||||
callback(mappedValues);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const loadOptions = useCallback(
|
||||
async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
|
||||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
setOptions(mappedValues);
|
||||
callback(mappedValues);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// const response = await new APIClient()[method.toLowerCase()](endpoint, { ..._params, q: inputValue });
|
||||
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue })
|
||||
// const data = await response.json();
|
||||
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
callback(_options);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue });
|
||||
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
if (inputRef.current === document.activeElement) {
|
||||
setMenuIsOpen(true);
|
||||
}
|
||||
callback(_options);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[topValues, _params]
|
||||
);
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 1000), [params, topValues]);
|
||||
const debouncedLoadOptions = useRef(
|
||||
debounce((inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
|
||||
loadOptions(inputValue, callback);
|
||||
}, 1000)
|
||||
).current;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedLoadOptions.cancel && debouncedLoadOptions.cancel();
|
||||
};
|
||||
}, [debouncedLoadOptions]);
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
import React from 'react';
|
||||
// import Select from 'Shared/Select';
|
||||
import { Dropdown, MenuProps, Select, Space } from 'antd';
|
||||
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { MenuProps, Select } from 'antd';
|
||||
|
||||
interface Props {
|
||||
payload: any;
|
||||
}
|
||||
|
||||
function NodeDropdown(props: Props) {
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://www.antgroup.com'>
|
||||
1st menu item
|
||||
</a>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Select style={{ width: 120 }} placeholder='Slect Event' dropdownStyle={{
|
||||
border: 'none'
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue