Compare commits

...
Sign in to create a new pull request.

47 commits

Author SHA1 Message Date
dependabot[bot]
367ab417bf
chore(deps): bump the npm_and_yarn group across 9 directories with 8 updates
Bumps the npm_and_yarn group with 2 updates in the /assist directory: [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express).
Bumps the npm_and_yarn group with 2 updates in the /ee/assist directory: [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express).
Bumps the npm_and_yarn group with 3 updates in the /frontend directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript), [cross-spawn](https://github.com/moxystudio/node-cross-spawn) and [nanoid](https://github.com/ai/nanoid).
Bumps the npm_and_yarn group with 2 updates in the /networkProxy directory: [rollup](https://github.com/rollup/rollup) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 2 updates in the /peers directory: [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express).
Bumps the npm_and_yarn group with 2 updates in the /sourcemapreader directory: [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express).
Bumps the npm_and_yarn group with 4 updates in the /spot directory: [cross-spawn](https://github.com/moxystudio/node-cross-spawn), [nanoid](https://github.com/ai/nanoid), [rollup](https://github.com/rollup/rollup) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /tracker/tracker-axios directory: [axios](https://github.com/axios/axios).
Bumps the npm_and_yarn group with 1 update in the /tracker/tracker-testing-playground directory: [axios](https://github.com/axios/axios).


Updates `path-to-regexp` from 0.1.10 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12)

Updates `express` from 4.21.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2)

Updates `path-to-regexp` from 0.1.10 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12)

Updates `express` from 4.21.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2)

Updates `@sentry/browser` from 5.30.0 to 7.119.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.119.1/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/5.30.0...7.119.1)

Updates `cross-spawn` from 6.0.5 to 6.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/v6.0.6/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v6.0.6)

Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `rollup` from 4.22.5 to 4.28.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.5...v4.28.1)

Updates `vite` from 5.4.8 to 5.4.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

Updates `path-to-regexp` from 0.1.10 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12)

Updates `express` from 4.21.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2)

Updates `path-to-regexp` from 0.1.10 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12)

Updates `express` from 4.21.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2)

Updates `cross-spawn` from 7.0.3 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/v6.0.6/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v6.0.6)

Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `rollup` from 4.21.0 to 4.28.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.5...v4.28.1)

Updates `vite` from 5.4.2 to 5.4.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

Updates `axios` from 0.26.1 to 1.7.9
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.26.1...v1.7.9)

Updates `axios` from 0.27.2 to 1.7.9
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.26.1...v1.7.9)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@sentry/browser"
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: axios
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-11 12:34:25 +00:00
Delirium
7c23521cb8
ui: fix spot tab lookup, improve js build speed (#2852) 2024-12-11 13:29:38 +01:00
Mehdi Osman
8a8df0a8cb
Increment frontend chart version (#2845)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 16:38:03 +01:00
Delirium
e54f62a0e6
ui: more fixes... (#2844) 2024-12-10 16:27:59 +01:00
Mehdi Osman
0bdb416594
Increment frontend chart version (#2842)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 13:44:59 +01:00
Delirium
3484da2f60
Fixup UI crashes (#2840)
* ui: fix log panel crashing

* ui: improve log list filtering

* ui: fix tab name lookup
2024-12-10 13:36:59 +01:00
Mehdi Osman
2f164708e7
Increment frontend chart version (#2838)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 11:29:37 +01:00
Delirium
c66296a050
Fixup (#2837)
* ui: trim logs

* ui: fixup
2024-12-10 11:14:34 +01:00
Mehdi Osman
a1cf508cb3
Increment frontend chart version (#2836)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 10:37:39 +01:00
Delirium
38594319f0
Player improvs (#2835)
* ui: fix performance bottlenecks, split data sources in devtools panes

* ui: move xray warn

* Player ux improvements (#2834)

* Player UX improvements.

DevTools (Including multi-tab)
Actions panel (User events, Click maps, Tag Elements)

* ui: remove unused imports, remove str templ classnames

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
2024-12-10 10:31:09 +01:00
Shekar Siri
c963ec5e91
Fix card filters for path analysis (#2827)
* fix(ui): card filters restriction for path analysis

* fix(ui): path analysis showing multi series
2024-12-09 13:05:07 +01:00
Shekar Siri
c04090a778
fix(ui): card filters restriction for path analysis (#2826) 2024-12-09 12:40:35 +01:00
Mehdi Osman
fc48ba4149
Increment chalice chart version (#2815)
Co-authored-by: GitHub Action <action@github.com>
2024-12-04 13:10:03 +01:00
Kraiem Taha Yassine
04db322e54
fix(chalice): support webhook default ports (#2814)
* fix(chalice): support webhook default ports

* fix(chalice): support webhook default ports EE
2024-12-04 13:05:38 +01:00
Mehdi Osman
c9ea3651db
Increment frontend chart version (#2813)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 17:16:31 +01:00
Shekar Siri
1dc63bf88b
change(ui): webhooks url allow port (#2812) 2024-12-03 17:09:06 +01:00
Gabriele Angrisani
2fb7b3d542
fix redis volume reference folder (#2805) 2024-12-03 16:33:18 +01:00
Mehdi Osman
57a21eb31d
Increment chalice chart version (#2811)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 16:30:38 +01:00
Kraiem Taha Yassine
e9a1a8c4eb
fix(chalice): fixed edit user's role (#2810) 2024-12-03 16:27:42 +01:00
Mehdi Osman
14191c1de4
Increment chalice chart version (#2807)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:36:33 +01:00
Mehdi Osman
7e52c97d62
Increment frontend chart version (#2809)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:35:53 +01:00
Shekar Siri
1cdb9bd06d
fix(ui): do not allow protected roles (#2808) 2024-12-03 14:15:38 +01:00
Kraiem Taha Yassine
e7ad4c8bd0
fix(chalice): support session's search null duration (#2806) 2024-12-03 13:24:24 +01:00
Mehdi Osman
29d69e5b24
Increment chalice chart version (#2804)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 23:14:07 +01:00
Kraiem Taha Yassine
2e5517509b
fix(chalice): fixed accept invitation response (#2803) 2024-12-02 23:06:41 +01:00
rjshrjndrn
c95a4f6770 ci(action): show latest build log 2024-12-02 21:21:32 +01:00
Mehdi Osman
8af7d1a263
Increment frontend chart version (#2801)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 21:01:08 +01:00
Shekar Siri
332cbb3516
fix(ui): 403 clear the token (#2800) 2024-12-02 20:55:05 +01:00
Mehdi Osman
1b564f53d5
Increment frontend chart version (#2799)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 18:24:57 +01:00
Shekar Siri
1aa3b4b4e5
fix(ui): trigger session search on change (#2798) 2024-12-02 18:17:03 +01:00
Mehdi Osman
d531b5da7e
Increment frontend chart version (#2790)
Co-authored-by: GitHub Action <action@github.com>
2024-11-29 16:50:02 +01:00
Shekar Siri
e173591d88
fix(assist): pagination (#2789) 2024-11-29 15:44:45 +01:00
Mehdi Osman
359ecc85af
Increment frontend chart version (#2786)
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 13:11:22 +01:00
Shekar Siri
f0e8100283
fix(ui) - sessions refresh and navigation (#2785)
* change(ui): search query params improvements

* fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues
2024-11-26 13:03:58 +01:00
Mehdi Osman
251d727375
Increment chalice chart version (#2783)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 17:42:22 +01:00
Kraiem Taha Yassine
b00a90484e
fix(chalice): support user-city for assist (#2782) 2024-11-25 17:35:51 +01:00
Mehdi Osman
ce0686eec3
Increment frontend chart version (#2780)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 14:49:59 +01:00
Shekar Siri
34232ed23c
Fix sessions list (#2779)
* fix(ui): sessions list persist page, show latest sessions

* fix(ui): latest sessions check clear the list
2024-11-25 13:43:07 +01:00
Mehdi Osman
954bfbf8f7
Increment frontend chart version (#2778)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 12:22:33 +01:00
Shekar Siri
c0197cdfeb
fix(ui): sessions list persist page, show latest sessions (#2777) 2024-11-25 12:12:16 +01:00
Andrés Carrillo
12ab110e0e
Update common.env (#2776) 2024-11-23 10:24:31 -05:00
Mehdi Osman
f48808f42e
Increment frontend chart version (#2774)
Co-authored-by: GitHub Action <action@github.com>
2024-11-22 14:15:28 +01:00
Delirium
b080a98764
ui: fix ws panel crash (#2773) 2024-11-22 14:07:42 +01:00
Mehdi Osman
dd885c65ac
Increment frontend chart version (#2770)
Co-authored-by: GitHub Action <action@github.com>
2024-11-20 15:35:32 +01:00
Shekar Siri
0ad2836650
cherry-pick(ui): 54abbe58a (#2769) 2024-11-20 14:02:42 +01:00
Mehdi Osman
20b76a0ed9
Updated patch build from main 884f3499ef (#2768)
* Increment chalice chart version

* Increment alerts chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-11-20 12:45:49 +01:00
Kraiem Taha Yassine
884f3499ef
fix(chalice): support top graphql autocomplete (#2767)
refactor(chalice): enforce UTC TZ
refactor(crons): enforce UTC TZ
refactor(alerts): enforce UTC TZ
2024-11-20 12:34:25 +01:00
109 changed files with 2345 additions and 1342 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning}

View file

@ -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)
@ -1357,6 +1360,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

View file

@ -11,7 +11,7 @@
"dependencies": {
"@fastify/deepmerge": "^2.0.0",
"@maxmind/geoip2-node": "^4.2.0",
"express": "^4.21.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"prom-client": "^15.0.0",
"socket.io": "^4.8.0",
@ -433,9 +433,10 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -456,7 +457,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -471,6 +472,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/extsprintf": {
@ -930,9 +935,10 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/prom-client": {
"version": "15.1.3",

View file

@ -20,7 +20,7 @@
"dependencies": {
"@fastify/deepmerge": "^2.0.0",
"@maxmind/geoip2-node": "^4.2.0",
"express": "^4.21.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"prom-client": "^15.0.0",
"socket.io": "^4.8.0",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -11,7 +11,7 @@
"dependencies": {
"@maxmind/geoip2-node": "^4.2.0",
"@socket.io/redis-adapter": "^8.2.1",
"express": "^4.21.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"prom-client": "^15.0.0",
"redis": "^4.6.10",
@ -507,9 +507,10 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -530,7 +531,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -545,6 +546,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@ -1033,9 +1038,10 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/prom-client": {
"version": "15.1.3",

View file

@ -20,7 +20,7 @@
"dependencies": {
"@maxmind/geoip2-node": "^4.2.0",
"@socket.io/redis-adapter": "^8.2.1",
"express": "^4.21.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"prom-client": "^15.0.0",
"redis": "^4.6.10",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import { useStore } from 'App/mstore';
import React from 'react';
// import Select from 'Shared/Select';
import { Select } from 'antd';
const sortOptions = [

View file

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

View file

@ -12,6 +12,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 NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
// @ts-ignore
interface IProps extends RouteComponentProps {
@ -36,15 +38,16 @@ function Overview({ match: { params } }: IProps) {
return (
<Switch>
<Route exact strict
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
path={[withSiteId(sessions(), siteId), withSiteId(bookmarks(), 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(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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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,42 +95,41 @@ 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>
);
}
export default observer(TagWatch);
export default observer(TagWatch);

View file

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

View file

@ -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,14 +30,14 @@ 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>
),
value: key,
disabled: disabled,
disabled: disabled,
}))}
/>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
@ -384,4 +418,4 @@ export default observer(Storage);
* }, [list.length])
* }
*
* */
* */

View file

@ -70,6 +70,7 @@ function SpotConsole({ onClose }: { onClose: () => void }) {
jump={jump}
iconProps={getIconProps(log.level)}
renderWithNL={renderWithNL}
showSingleTab
/>
))}
</VList>

View file

@ -143,7 +143,7 @@ function SpotPlayerHeader({
{browserVersion && (
<>
<div>·</div>
<div className="capitalize">Chrome v{browserVersion}</div>
<div>Chromium v{browserVersion}</div>
</>
)}
{resolution && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ function LiveSessionList() {
const totalLiveSessions = sessionStore.totalLiveSessions;
const loading = sessionStore.loadingLiveSessions;
const { currentPage } = searchStoreLive;
const metaList = customFieldStore.list
const metaList = customFieldStore.list;
const metaListLoading = customFieldStore.isLoading;
let timeoutId: any;
@ -32,7 +32,7 @@ function LiveSessionList() {
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const sortOptions = [{ label: 'Start Time', value: 'timestamp' }].concat(
metaList
.map(({ key} : any) => ({
.map(({ key }: any) => ({
label: capitalize(key),
value: key
}))
@ -40,21 +40,33 @@ function LiveSessionList() {
useEffect(() => {
if (metaListLoading) return;
const _filter = { ...filter };
let shouldUpdate = false;
// Set default sort if not already set
if (sortOptions[1] && !filter.sort) {
_filter.sort = sortOptions[1].value;
shouldUpdate = true;
}
searchStoreLive.edit(_filter);
// Only update filters if there's a change
if (shouldUpdate) {
searchStoreLive.edit(_filter);
}
// Start auto-refresh timeout
timeout();
// Cleanup on component unmount or re-run
return () => {
clearTimeout(timeoutId);
};
}, [metaListLoading]);
}, [metaListLoading, filter.sort]); // Add necessary dependencies
const refetch = () => {
searchStoreLive.edit({ ...filter })
void searchStoreLive.fetchSessions();
}
};
const onUserClick = (userId: string, userAnonymousId: string) => {
if (userId) {
@ -66,7 +78,6 @@ function LiveSessionList() {
const onSortChange = ({ value }: any) => {
searchStoreLive.edit({ sort: value.value });
void searchStoreLive.fetchSessions();
};
const timeout = () => {
@ -102,8 +113,7 @@ function LiveSessionList() {
<div className="mx-2" />
<SortOrderButton
onChange={(state: any) => {
searchStoreLive.edit({ order: state })
void searchStoreLive.fetchSessions();
searchStoreLive.edit({ order: state });
}}
sortOrder={filter.order}
/>

View file

@ -22,7 +22,6 @@ function LiveSessionSearch() {
const onUpdateFilter = (filterIndex: number, filter: any) => {
searchStoreLive.updateFilter(filterIndex, filter);
void searchStoreLive.fetchSessions();
};
const onRemoveFilter = (filterIndex: number) => {
@ -33,16 +32,12 @@ function LiveSessionSearch() {
searchStoreLive.edit({
filters: newFilters
});
void searchStoreLive.fetchSessions();
};
const onChangeEventsOrder = (e: any, { name, value }: any) => {
searchStoreLive.edit({
eventsOrder: value
});
void searchStoreLive.fetchSessions();
};
return (hasEvents || hasFilters) ? (

View file

@ -11,50 +11,57 @@ import { useStore } from 'App/mstore';
import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
let debounceFetch: any = () => {
};
let debounceFetch: () => void;
function SessionSearch() {
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
const hasEvents = appliedFilter.filters.some((i: any) => i.isEvent);
const hasFilters = appliedFilter.filters.some((i: any) => !i.isEvent);
const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false;
useSessionSearchQueryHandler({
appliedFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
searchStore.refreshFilterOptions();
try {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
searchStore.refreshFilterOptions();
}
} catch (error) {
console.error('Error during onBeforeLoad:', error);
}
}
});
useEffect(() => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
// void searchStore.fetchSessions(true)
}, []);
useEffect(() => {
if (searchStore.urlParsed) return;
debounceFetch();
}, [appliedFilter.filters]);
const onAddFilter = (filter: any) => {
searchStore.addFilter(filter);
debounceFetch();
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
searchStore.updateFilter(filterIndex, filter);
debounceFetch();
};
const onFilterMove = (newFilters: any) => {
@ -85,49 +92,47 @@ function SessionSearch() {
};
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<>
{showPanel ? (
<div className="border bg-white rounded-lg mt-4">
<div className="p-5">
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 mb-2'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
{hasEvents || hasFilters ? (
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
onFilterMove={onFilterMove}
saveRequestPayloads={saveRequestPayloads}
/>
) : null}
</div>
{hasEvents || hasFilters ? (
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
) : null}
if (metaLoading) return null;
if (!showPanel) return null;
return (
<div className="border bg-white rounded-lg mt-4">
<div className="p-5">
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 mb-2'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
{hasEvents || hasFilters ? (
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
onFilterMove={onFilterMove}
saveRequestPayloads={saveRequestPayloads}
/>
) : null}
</div>
{hasEvents || hasFilters ? (
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
) : (
<></>
)}
</>
) : null;
) : null}
</div>
);
}
export default observer(SessionSearch);

View file

@ -3,16 +3,17 @@ import React from 'react';
import { useStore } from 'App/mstore';
import LatestSessionsMessage from './components/LatestSessionsMessage';
import NotesList from './components/Notes/NoteList';
import SessionHeader from './components/SessionHeader';
import SessionList from './components/SessionList';
import { observer } from 'mobx-react-lite';
import NoSessionsMessage from 'Shared/NoSessionsMessage/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch/SessionSearch';
function SessionsTabOverview() {
const [query, setQuery] = React.useState('');
const { aiFiltersStore, searchStore } = useStore();
const appliedFilter = searchStore.instance;
const isNotesRoute = searchStore.activeTab.type === 'notes';
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
@ -25,25 +26,27 @@ function SessionsTabOverview() {
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return (
<div className="widget-wrapper">
{testingKey ? (
<Input
value={query}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value)}
className={'mb-2'}
placeholder={'ask session ai'}
/>
) : null}
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
{!isNotesRoute ? (
<>
<NoSessionsMessage />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<div className="widget-wrapper">
{testingKey ? (
<Input
value={query}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value)}
className={'mb-2'}
placeholder={'ask session ai'}
/>
) : null}
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
<SessionList />
) : (
<NotesList />
)}
</div>
</div>
</>
);
}

View file

@ -7,11 +7,16 @@ import { observer } from 'mobx-react-lite';
function LatestSessionsMessage() {
const { searchStore } = useStore();
const count = searchStore.latestList.size;
const onShowNewSessions = () => {
void searchStore.updateCurrentPage(1, true);
};
return count > 0 ? (
<div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }}
onClick={() => searchStore.updateCurrentPage(1)}
onClick={onShowNewSessions}
>
Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'}
</div>

View file

@ -5,57 +5,70 @@ import NoteItem from './NoteItem';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
function NotesList() {
const { notesStore } = useStore();
React.useEffect(() => {
void notesStore.fetchNotes();
void notesStore.fetchNotes();
}, [notesStore.page]);
const list = notesStore.notes;
return (
<Loader loading={notesStore.loading}>
<NoContent
show={list.length === 0}
title={
<div className="flex flex-col items-center justify-center">
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
</div>
}
subtext={
<div className="text-center flex justify-center items-center flex-col">
Note observations during session replays and share them with your team.
</div>
}
>
<div className="border-b rounded bg-white">
{list.map((note) => (
<React.Fragment key={note.noteId}>
<NoteItem note={note} />
</React.Fragment>
))}
</div>
<>
<div className="widget-wrapper">
<div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">Notes</h2>
<div className="w-full flex items-center justify-between py-4 px-6">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
of <span className="font-semibold">{notesStore.total}</span> notes
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
<Pagination
page={notesStore.page}
total={notesStore.total}
onPageChange={(page) => notesStore.changePage(page)}
limit={notesStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
</Loader>
<div className="border-b" />
<Loader loading={notesStore.loading}>
<NoContent
show={list.length === 0}
title={
<div className="flex flex-col items-center justify-center">
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
</div>
}
subtext={
<div className="text-center flex justify-center items-center flex-col">
Note observations during session replays and share them with your team.
</div>
}
>
<div className="border-b rounded bg-white">
{list.map((note) => (
<React.Fragment key={note.noteId}>
<NoteItem note={note} />
</React.Fragment>
))}
</div>
<div className="w-full flex items-center justify-between py-4 px-6">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
of <span className="font-semibold">{notesStore.total}</span> notes
</div>
<Pagination
page={notesStore.page}
total={notesStore.total}
onPageChange={(page) => notesStore.changePage(page)}
limit={notesStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
</Loader>
</div>
</>
);
}

View file

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import Period from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange';
import SessionTags from '../SessionTags';
import NoteTags from '../Notes/NoteTags';
import SessionSort from '../SessionSort';
import { Space } from 'antd';
import { useStore } from 'App/mstore';
@ -17,9 +16,6 @@ function SessionHeader() {
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const title = useMemo(() => {
if (activeTab.type === 'notes') {
return 'Notes';
}
if (activeTab.type === 'bookmarks') {
return isEnterprise ? 'Vault' : 'Bookmarks';
}
@ -35,26 +31,15 @@ function SessionHeader() {
return (
<div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">{title}</h2>
{activeTab.type !== 'notes' ? (
<div className="flex items-center w-full justify-end">
{activeTab.type !== 'bookmarks' && (
<>
<SessionTags />
<div className="mr-auto" />
<Space>
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />
<SessionSort />
</Space>
</>
)}
</div>
) : null}
{activeTab.type === 'notes' && (
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
)}
<div className="flex items-center w-full justify-end">
{activeTab.type !== 'bookmarks' && <SessionTags />}
<div className="mr-auto" />
<Space>
{activeTab.type !== 'bookmarks' &&
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />}
<SessionSort />
</Space>
</div>
</div>
);
}

View file

@ -73,7 +73,6 @@ function SessionList() {
}, [isBookmark, isVault, activeTab, location.pathname]);
const [statusData, setStatusData] = React.useState<SessionStatus>({ status: 0, count: 0 });
const fetchStatus = async () => {
const response = await sessionService.getRecordingStatus();
setStatusData({

View file

@ -2,6 +2,8 @@
import { DateTime, Duration } from 'luxon'; // TODO
import { Timezone } from 'App/mstore/types/sessionSettings';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
import { CUSTOM_RANGE } from '@/dateRange';
export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SSS'): string {
return DateTime.fromISO(date).toFormat(format);
@ -191,3 +193,35 @@ export const countDaysFrom = (timestamp: number): number => {
const d = new Date();
return Math.round(Math.abs(d.getTime() - date.toJSDate().getTime()) / (1000 * 3600 * 24));
}
export const getDateRangeUTC = (rangeName: string, customStartDate?: number, customEndDate?: number): {
startDate: number;
endDate: number
} => {
let endDate = new Date().getTime();
let startDate: number;
switch (rangeName) {
case LAST_7_DAYS:
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
break;
case LAST_30_DAYS:
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
break;
case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) {
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.');
}
startDate = customStartDate;
endDate = customEndDate;
break;
case LAST_24_HOURS:
default:
startDate = endDate - 24 * 60 * 60 * 1000;
}
return {
startDate,
endDate
};
}

View file

@ -6,46 +6,59 @@ import Search from '@/mstore/types/search';
import { getFilterFromJson } from 'Types/filter/newFilter';
interface Props {
onBeforeLoad?: () => Promise<any>;
appliedFilter: any;
onBeforeLoad?: () => Promise<void>;
appliedFilter: Record<string, any>;
loading: boolean;
}
const useSessionSearchQueryHandler = (props: Props) => {
const useSessionSearchQueryHandler = ({ onBeforeLoad, appliedFilter, loading }: Props) => {
const { searchStore } = useStore();
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!props.onBeforeLoad);
const { appliedFilter, loading } = props;
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!onBeforeLoad);
const history = useHistory();
// Apply filter from the query string when the component mounts
useEffect(() => {
const applyFilterFromQuery = async () => {
if (!loading && !searchStore.urlParsed) {
if (props.onBeforeLoad) {
await props.onBeforeLoad();
setBeforeHookLoaded(true);
}
try {
if (onBeforeLoad) {
await onBeforeLoad();
setBeforeHookLoaded(true);
}
const converter = JsonUrlConverter.urlParamsToJson(history.location.search);
const json: any = getFilterFromJson(converter.toJSON());
const filter = new Search(json);
searchStore.applyFilter(filter, true);
searchStore.setUrlParsed()
const converter = JsonUrlConverter.urlParamsToJson(history.location.search);
const json = getFilterFromJson(converter.toJSON());
const filter = new Search(json);
searchStore.applyFilter(filter, true);
searchStore.setUrlParsed();
} catch (error) {
console.error('Error applying filter from query:', error);
}
}
};
void applyFilterFromQuery();
}, [loading]);
}, [loading, onBeforeLoad, searchStore, history.location.search]);
// Update the URL whenever the appliedFilter changes
useEffect(() => {
const generateUrlQuery = () => {
const updateUrlWithFilter = () => {
if (!loading && beforeHookLoaded) {
const converter = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: converter });
const query = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: query });
}
};
generateUrlQuery();
}, [appliedFilter, loading, beforeHookLoaded]);
updateUrlWithFilter();
}, [appliedFilter, loading, beforeHookLoaded, history]);
// Ensure the URL syncs on remount if already parsed
useEffect(() => {
if (searchStore.urlParsed) {
const query = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: query });
}
}, [appliedFilter, searchStore.urlParsed, history]);
return null;
};

View file

@ -20,7 +20,7 @@ export default class FilterStore {
}
setTopValues = (key: string, values: TopValue[]) => {
this.topValues[key] = values.filter((value) => value !== null && value.value !== '');
this.topValues[key] = values?.filter((value) => value !== null && value.value !== '');
};
fetchTopValues = async (key: string, source?: string) => {

View file

@ -162,15 +162,14 @@ class SearchStore {
});
}
updateCurrentPage(page: number) {
updateCurrentPage(page: number, force = false) {
this.currentPage = page;
void this.fetchSessions();
void this.fetchSessions(force);
}
setActiveTab(tab: string) {
runInAction(() => {
this.activeTab = TAB_MAP[tab];
this.currentPage = 1;
});
}
@ -229,12 +228,13 @@ class SearchStore {
if (this.latestRequestTime) {
const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() });
const newTimestamps: any = period.toJSON();
filter.startTimestamp = newTimestamps.startDate;
filter.endTimestamp = newTimestamps.endDate;
filter.startDate = newTimestamps.startDate;
filter.endDate = newTimestamps.endDate;
}
searchService.checkLatestSessions(filter).then((response: any) => {
this.latestList = response;
this.latestRequestTime = Date.now();
runInAction(() => {
this.latestList = List(response);
});
});
}
@ -264,8 +264,10 @@ class SearchStore {
});
}
this.currentPage = 1;
if (filter.value && filter.value[0] && filter.value[0] !== '') {
this.fetchSessions();
void this.fetchSessions();
}
}
@ -336,6 +338,9 @@ class SearchStore {
filter.filters = filter.filters.concat(tagFilter);
}
this.latestRequestTime = Date.now();
this.latestList = List();
await sessionStore.fetchSessions({
...filter,
page: this.currentPage,

View file

@ -2,10 +2,10 @@ import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import {
filtersMap,
generateFilterOptions,
liveFiltersMap,
liveFiltersMap
} from 'Types/filter/newFilter';
import { List } from 'immutable';
import { makeAutoObservable } from 'mobx';
import { makeAutoObservable, reaction } from 'mobx';
import Search from 'App/mstore/types/search';
import { checkFilterValue, IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem';
@ -63,11 +63,29 @@ class SearchStoreLive {
constructor() {
makeAutoObservable(this);
// Reset currentPage to 1 only on filter changes
reaction(
() => this.instance,
() => {
this.currentPage = 1;
void this.fetchSessions();
}
);
// Fetch sessions when currentPage changes
reaction(
() => this.currentPage,
() => {
void this.fetchSessions();
}
);
}
get filterList() {
return generateFilterOptions(filtersMap);
}
get filterListLive() {
return generateFilterOptions(liveFiltersMap);
}
@ -96,14 +114,12 @@ class SearchStoreLive {
edit(instance: Partial<Search>) {
this.instance = new Search(Object.assign({ ...this.instance }, instance));
this.currentPage = 1;
}
apply(filter: any, fromUrl: boolean) {
if (fromUrl) {
this.instance = new Search(filter);
this.currentPage = 1;
} else {
this.instance = { ...this.instance, ...filter };
}
@ -115,7 +131,6 @@ class SearchStoreLive {
updateCurrentPage(page: number) {
this.currentPage = page;
this.fetchSessions();
}
clearSearch() {
@ -140,22 +155,25 @@ class SearchStoreLive {
: null;
if (index > -1) {
const oldFilter = this.instance.filters[index];
const updatedFilter = {
...oldFilter,
value: oldFilter.value.concat(filter.value)
// Update existing filter
// @ts-ignore
this.instance.filters[index] = {
...this.instance.filters[index],
value: this.instance.filters[index].value.concat(filter.value)
};
oldFilter.merge(updatedFilter);
} else {
this.instance.filters.push(filter);
this.instance = new Search({
...this.instance.toData()
});
// Add new filter (create a new array reference to notify MobX)
this.instance.filters = [...this.instance.filters, filter];
}
if (filter.value && filter.value[0] && filter.value[0] !== '') {
this.fetchSessions();
}
// Update the instance to trigger reactions
this.instance = new Search({
...this.instance.toData()
});
// if (filter.value && filter.value[0] && filter.value[0] !== '') {
// void this.fetchSessions();
// }
}
addFilterByKeyAndValue(key: any, value: any, operator?: string, sourceOperator?: string, source?: string) {
@ -200,7 +218,7 @@ class SearchStoreLive {
};
async fetchSessions() {
await sessionStore.fetchLiveSessions(this.instance.toSearch());
await sessionStore.fetchLiveSessions({ ...this.instance.toSearch(), page: this.currentPage });
};
}

View file

@ -170,7 +170,7 @@ export default class SessionStore {
}
const nextEntryNum =
keys.length > 0
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key].entryNum || 0)) + 1
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key]?.entryNum || 0)) + 1
: 0;
this.prefetchedMobUrls[sessionId] = {
data: fileData,

View file

@ -1,7 +1,8 @@
import { DATE_RANGE_VALUES, CUSTOM_RANGE, getDateRangeFromValue } from 'App/dateRange';
import Filter, { checkFilterValue, IFilter } from 'App/mstore/types/filter';
import { CUSTOM_RANGE, DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange';
import Filter, { IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem';
import { action, makeAutoObservable, observable } from 'mobx';
import { makeAutoObservable, observable } from 'mobx';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
// @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
@ -69,7 +70,7 @@ export default class Search {
constructor(initialData?: Partial<ISearch>) {
makeAutoObservable(this, {
filters: observable,
filters: observable
});
Object.assign(this, {
name: '',
@ -142,11 +143,48 @@ export default class Search {
return new FilterItem(filter).toJson();
});
const { startDate, endDate } = this.getDateRange(js.rangeValue, js.startDate, js.endDate);
js.startDate = startDate;
js.endDate = endDate;
delete js.createdAt;
delete js.key;
return js;
}
private getDateRange(rangeName: string, customStartDate: number, customEndDate: number): {
startDate: number;
endDate: number
} {
let endDate = new Date().getTime();
let startDate: number;
switch (rangeName) {
case LAST_7_DAYS:
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
break;
case LAST_30_DAYS:
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
break;
case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) {
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.');
}
startDate = customStartDate;
endDate = customEndDate;
break;
case LAST_24_HOURS:
default:
startDate = endDate - 24 * 60 * 60 * 1000;
}
return {
startDate,
endDate
};
}
fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) {
let startDate, endDate;
const rValue = filterData.rangeValue || rangeValue;
@ -176,3 +214,4 @@ export default class Search {
});
}
}

View file

@ -66,11 +66,16 @@ export default class UiPlayerStore {
endTs: 0,
}
zoomTab: 'overview' | 'journey' | 'issues' | 'errors' = 'overview'
dataSource: 'all' | 'current' = 'all'
constructor() {
makeAutoObservable(this);
}
changeDataSource = (source: 'all' | 'current') => {
this.dataSource = source;
}
toggleFullscreen = (val?: boolean) => {
this.fullscreen = val ?? !this.fullscreen;
}

View file

@ -88,7 +88,6 @@ class UserStore {
get isEnterprise() {
return (
this.account?.edition === 'ee' ||
this.account?.edition === 'msaas' ||
this.authStore.authDetails?.edition === 'ee'
);
}

View file

@ -1,13 +1,18 @@
import { Store } from './types'
export default class SimpleSore<G extends Object, S extends Object = G> implements Store<G, S> {
export default class SimpleStore<G extends Record<string, any>, S extends Record<string, any> = G> implements Store<G, S> {
constructor(private state: G){}
get(): G {
return this.state
}
update(newState: Partial<S>) {
update = (newState: Partial<S>) => {
Object.assign(this.state, newState)
}
updateTabStates = (id: string, newState: Partial<S>) => {
try {
Object.assign(this.state.tabStates[id], newState)
} catch (e) {
console.log('Error updating tab state', e, id, newState, this.state, this)
}
}
}

View file

@ -27,6 +27,7 @@ export interface Interval {
export interface Store<G extends Object, S extends Object = G> {
get(): G
update(state: Partial<S>): void
updateTabStates(id: string, state: Partial<S>): void
}

View file

@ -236,6 +236,7 @@ export default class MessageLoader {
try {
await this.loadMobs();
} catch (sessionLoadError) {
console.info('!', sessionLoadError);
try {
await this.loadEFSMobs();
} catch (unprocessedLoadError) {

View file

@ -99,7 +99,7 @@ export default class MessageManager {
closedTabs: [],
sessionStart: 0,
tabNames: {},
};
};
private clickManager: ListWalker<MouseClick> = new ListWalker();
private mouseThrashingManager: ListWalker<MouseThrashing> = new ListWalker();
@ -179,6 +179,7 @@ export default class MessageManager {
this.activityManager.end();
this.state.update({ skipIntervals: this.activityManager.list });
}
Object.values(this.tabs).forEach((tab) => tab.onFileReadSuccess?.());
};
@ -317,6 +318,7 @@ export default class MessageManager {
if (msg.tp === 9999) return;
if (!this.tabs[msg.tabId]) {
this.tabsAmount++;
this.state.update({ tabStates: { ...this.state.get().tabStates, [msg.tabId]: TabSessionManager.INITIAL_STATE } });
this.tabs[msg.tabId] = new TabSessionManager(
this.session,
this.state,

View file

@ -163,15 +163,7 @@ export default class TabSessionManager {
* Because we use main state (from messageManager), we have to update it this way
* */
updateLocalState(state: Partial<TabState>) {
this.state.update({
tabStates: {
...this.state.get().tabStates,
[this.id]: {
...this.state.get().tabStates[this.id],
...state,
},
},
});
this.state.updateTabStates(this.id, state);
}
private setCSSLoading = (cssLoading: boolean) => {
@ -202,6 +194,7 @@ export default class TabSessionManager {
);
}
firstTitleSet = false
distributeMessage(msg: Message): void {
this.lastMessageTs = msg.time;
switch (msg.tp) {
@ -242,6 +235,10 @@ export default class TabSessionManager {
case MType.SetPageLocationDeprecated:
case MType.SetPageLocation:
this.locationManager.append(msg);
if ('documentTitle' in msg && !this.firstTitleSet) {
this.state.update({ tabNames: { ...this.state.get().tabNames, [this.id]: msg.documentTitle } });
this.firstTitleSet = true
}
if (msg.navigationStart > 0) {
this.loadedLocationManager.append(msg);
}
@ -414,8 +411,9 @@ export default class TabSessionManager {
}
Object.assign(stateToUpdate, this.lists.moveGetState(t));
Object.keys(stateToUpdate).length > 0 &&
if (Object.keys(stateToUpdate).length > 0) {
this.updateLocalState(stateToUpdate);
}
/* Sequence of the managers is important here */
// Preparing the size of "screen"
const lastResize = this.resizeManager.moveGetLast(t, index);

View file

@ -436,3 +436,7 @@ p {
display: flex;
align-items: center;
}
.ant-segmented-item{
border-radius: .5rem !important;
}

View file

@ -1,6 +1,8 @@
import Period, { CUSTOM_RANGE } from 'Types/app/period';
import { filtersMap } from 'Types/filter/newFilter';
import Period, { CUSTOM_RANGE, LAST_24_HOURS } from 'Types/app/period';
const DEFAULT_SORT = 'startTs';
const DEFAULT_ORDER = 'desc';
const DEFAULT_EVENTS_ORDER = 'then';
class Filter {
key: string;
@ -25,24 +27,28 @@ class Filter {
}
}
class InputJson {
export class InputJson {
filters: Filter[];
rangeValue: string;
startDate: number;
endDate: number;
startDate?: number;
endDate?: number;
sort: string;
order: string;
eventsOrder: string;
constructor(filters: Filter[], rangeValue: string, startDate: number, endDate: number, sort: string, order: string, eventsOrder: string) {
constructor(
filters: Filter[],
rangeValue: string,
sort: string,
order: string,
eventsOrder: string,
startDate?: string | number,
endDate?: string | number
) {
this.filters = filters;
// .map((f: any) => {
// const subFilters = f.filters ? f.filters.map((sf: any) => new Filter(sf.key, sf.operator, sf.value, sf.filters)) : undefined;
// return new Filter(f.key, f.operator, f.value, subFilters);
// });
this.rangeValue = rangeValue;
this.startDate = startDate;
this.endDate = endDate;
this.startDate = startDate ? +startDate : undefined;
this.endDate = endDate ? +endDate : undefined;
this.sort = sort;
this.order = order;
this.eventsOrder = eventsOrder;
@ -50,17 +56,28 @@ class InputJson {
toJSON() {
return {
filters: this.filters.map(f => f.toJSON()),
filters: this.filters.map((f) => f.toJSON()),
rangeValue: this.rangeValue,
startDate: this.startDate,
endDate: this.endDate,
startDate: this.startDate ?? null,
endDate: this.endDate ?? null,
sort: this.sort,
order: this.order,
eventsOrder: this.eventsOrder
};
}
}
fromJSON(json: Record<string, any>): InputJson {
return new InputJson(
json.filters.map((f: any) => new Filter(f.key, f.operator, f.value, f.filters)),
json.rangeValue,
json.sort,
json.order,
json.eventsOrder,
json.startDate,
json.endDate
);
}
}
export class JsonUrlConverter {
static keyMap = {
@ -76,35 +93,46 @@ export class JsonUrlConverter {
filters: 'f'
};
static getDateRangeValues(rangeValue: string, startDate: number | undefined, endDate: number | undefined): [number, number] {
if (rangeValue === 'CUSTOM_RANGE') {
return [startDate!, endDate!];
static getDateRangeValues(
rangeValue: string,
startDate: string | null,
endDate: string | null
): [string, string] {
if (rangeValue === CUSTOM_RANGE) {
return [startDate || '', endDate || ''];
}
const period = Period({ rangeName: rangeValue });
const period: any = Period({ rangeName: rangeValue });
return [period.start, period.end];
}
static jsonToUrlParams(json: InputJson): string {
static jsonToUrlParams(json: Record<string, any>): string {
const params = new URLSearchParams();
const addFilterParams = (filter: Filter, prefix: string) => {
params.append(`${prefix}${this.keyMap.key}`, filter.key);
params.append(`${prefix}${this.keyMap.operator}`, filter.operator);
if (filter.value) {
filter.value.forEach((v, i) => params.append(`${prefix}${this.keyMap.value}[${i}]`, v || ''));
}
if (filter.filters) {
filter.filters.forEach((f, i) => addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`));
}
filter.value?.forEach((v, i) =>
params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '')
);
filter.filters?.forEach((f, i) =>
addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`)
);
};
json.filters.forEach((filter, index) => addFilterParams(filter, `${this.keyMap.filters}[${index}].`));
const rangeValues = this.getDateRangeValues(json.rangeValue, json.startDate, json.endDate);
json.filters.forEach((filter: any, index: number) =>
addFilterParams(filter, `${this.keyMap.filters}[${index}].`)
);
params.append(this.keyMap.rangeValue, json.rangeValue);
params.append(this.keyMap.startDate, rangeValues[0].toString());
params.append(this.keyMap.endDate, rangeValues[1].toString());
if (json.rangeValue === CUSTOM_RANGE) {
const rangeValues = this.getDateRangeValues(
json.rangeValue,
json.startDate?.toString() || null,
json.endDate?.toString() || null
);
params.append(this.keyMap.startDate, rangeValues[0]);
params.append(this.keyMap.endDate, rangeValues[1]);
}
params.append(this.keyMap.sort, json.sort);
params.append(this.keyMap.order, json.order);
params.append(this.keyMap.eventsOrder, json.eventsOrder);
@ -130,7 +158,7 @@ export class JsonUrlConverter {
filters.push(getFilterParams(`${prefix}${this.keyMap.filters}[${index}].`));
index++;
}
return new Filter(key, operator, value.length ? value : '', filters.length ? filters : []);
return new Filter(key, operator, value.length ? value : [], filters.length ? filters : []);
};
const filters: Filter[] = [];
@ -140,23 +168,22 @@ export class JsonUrlConverter {
index++;
}
const rangeValue = params.get(this.keyMap.rangeValue) || 'LAST_24_HOURS';
const rangeValue = params.get(this.keyMap.rangeValue) || LAST_24_HOURS;
const rangeValues = this.getDateRangeValues(rangeValue, params.get(this.keyMap.startDate), params.get(this.keyMap.endDate));
const startDate = rangeValues[0];
const endDate = rangeValues[1];
return new InputJson(
filters,
rangeValue,
startDate,
endDate,
params.get(this.keyMap.sort) || 'startTs',
params.get(this.keyMap.order) || 'desc',
params.get(this.keyMap.eventsOrder) || 'then'
params.get(this.keyMap.sort) || DEFAULT_SORT,
params.get(this.keyMap.order) || DEFAULT_ORDER,
params.get(this.keyMap.eventsOrder) || DEFAULT_EVENTS_ORDER,
rangeValues[0],
rangeValues[1]
);
}
}
// Example usage
// const urlParams = '?f[0].k=click&f[0].op=on&f[0].v[0]=Refresh&f[1].k=fetch&f[1].op=is&f[1].v[0]=&f[1].f[0].k=fetchUrl&f[1].f[0].op=is&f[1].f[0].v[0]=/g/collect&f[1].f[1].k=fetchStatusCode&f[1].f[1].op=>=&f[1].f[1].v[0]=400&f[1].f[2].k=fetchMethod&f[1].f[2].op=is&f[1].f[2].v[0]=&f[1].f[3].k=fetchDuration&f[1].f[3].op==&f[1].f[3].v[0]=&f[1].f[4].k=fetchRequestBody&f[1].f[4].op=is&f[1].f[4].v[0]=&f[1].f[5].k=fetchResponseBody&f[1].f[5].op=is&f[1].f[5].v[0]=&rv=LAST_24_HOURS&sd=1731343412555&ed=1731429812555&s=startTs&o=desc&st=false&eo=then';
// const parsedJson = JsonUrlConverter.urlParamsToJson(urlParams);

View file

@ -5,7 +5,7 @@ export function validateIP(value) {
export function validateURL(value) {
if (typeof value !== 'string') return false;
const urlRegex = /^(http|https):\/\/(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(\/\S*)?$/i;
const urlRegex = /^(http|https):\/\/(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(:\d+)?(\/\S*)?$/i;
const ipRegex = /^(http|https):\/\/(?:localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?(\/\S*)?$/i;
return urlRegex.test(value) || ipRegex.test(value);
}
@ -89,4 +89,4 @@ export const validatePassword = (password) => {
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])[A-Za-z\d!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]{8,}$/;
return regex.test(password);
};
};

View file

@ -26,7 +26,7 @@
"@babel/plugin-transform-private-methods": "^7.23.3",
"@floating-ui/react-dom-interactions": "^0.10.3",
"@medv/finder": "^3.1.0",
"@sentry/browser": "^5.21.1",
"@sentry/browser": "^7.119.1",
"@svg-maps/world": "^1.0.1",
"@tanstack/react-query": "^5.56.2",
"@wojtekmaj/react-daterange-picker": "^6.0.0",
@ -91,7 +91,7 @@
"@babel/preset-typescript": "^7.23.2",
"@babel/runtime": "^7.23.2",
"@jest/globals": "^29.7.0",
"@openreplay/sourcemap-uploader": "^3.0.8",
"@openreplay/sourcemap-uploader": "^3.0.10",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/luxon": "^3.4.2",
"@types/node": "^22.7.8",
@ -116,6 +116,7 @@
"cypress": "^13.3.0",
"cypress-image-snapshot": "^4.0.1",
"dotenv": "^6.2.0",
"esbuild-loader": "^4.2.2",
"eslint": "^8.15.0",
"eslint-plugin-react": "^7.29.4",
"file-loader": "^6.2.0",

View file

@ -6,6 +6,8 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CompressionPlugin from "compression-webpack-plugin";
import { EsbuildPlugin } from 'esbuild-loader';
const dotenv = require('dotenv').config({ path: __dirname + '/.env' })
const isDevelopment = process.env.NODE_ENV !== 'production'
const stylesHandler = MiniCssExtractPlugin.loader;
@ -28,23 +30,32 @@ const config: Configuration = {
splitChunks: {
chunks: 'all',
},
minimizer: [
new EsbuildPlugin({
target: 'es2020',
css: true
})
]
},
module: {
exprContextCritical: false,
rules: [
{
test: /\.(ts|js)x?$/i,
test: /\.tsx?$/i,
exclude: isDevelopment ? /node_modules/ : undefined,
use: ['thread-loader', {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
}],
loader: "esbuild-loader",
options: {
target: 'es2020',
},
},
{
test: /\.jsx?$/i,
exclude: isDevelopment ? /node_modules/ : undefined,
loader: "esbuild-loader",
options: {
loader: 'jsx',
target: 'es2020',
},
},
{
test: /\.s[ac]ss$/i,
@ -111,7 +122,11 @@ const config: Configuration = {
},
},
plugins: [
new CompressionPlugin(),
(isDevelopment ? false : new CompressionPlugin({
test: /\.(js|css|html|svg)$/,
algorithm: 'brotliCompress',
threshold: 10240,
})),
new webpack.DefinePlugin({
// 'process.env': ENV_VARIABLES,
'window.env': ENV_VARIABLES,
@ -131,6 +146,7 @@ const config: Configuration = {
performance: {
hints: false,
},
watchOptions: { ignored: "**/node_modules/**" },
devServer: {
// static: path.join(__dirname, "public"),
historyApiFallback: true,
@ -138,7 +154,6 @@ const config: Configuration = {
open: true,
port: 3333,
hot: true,
compress: true,
allowedHosts: "all",
client: {
overlay: {

View file

@ -1898,6 +1898,167 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/aix-ppc64@npm:0.21.5"
conditions: os=aix & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/android-arm64@npm:0.21.5"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@esbuild/android-arm@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/android-arm@npm:0.21.5"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@esbuild/android-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/android-x64@npm:0.21.5"
conditions: os=android & cpu=x64
languageName: node
linkType: hard
"@esbuild/darwin-arm64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/darwin-arm64@npm:0.21.5"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@esbuild/darwin-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/darwin-x64@npm:0.21.5"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@esbuild/freebsd-arm64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/freebsd-arm64@npm:0.21.5"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/freebsd-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/freebsd-x64@npm:0.21.5"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/linux-arm64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-arm64@npm:0.21.5"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
"@esbuild/linux-arm@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-arm@npm:0.21.5"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@esbuild/linux-ia32@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-ia32@npm:0.21.5"
conditions: os=linux & cpu=ia32
languageName: node
linkType: hard
"@esbuild/linux-loong64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-loong64@npm:0.21.5"
conditions: os=linux & cpu=loong64
languageName: node
linkType: hard
"@esbuild/linux-mips64el@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-mips64el@npm:0.21.5"
conditions: os=linux & cpu=mips64el
languageName: node
linkType: hard
"@esbuild/linux-ppc64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-ppc64@npm:0.21.5"
conditions: os=linux & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/linux-riscv64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-riscv64@npm:0.21.5"
conditions: os=linux & cpu=riscv64
languageName: node
linkType: hard
"@esbuild/linux-s390x@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-s390x@npm:0.21.5"
conditions: os=linux & cpu=s390x
languageName: node
linkType: hard
"@esbuild/linux-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/linux-x64@npm:0.21.5"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@esbuild/netbsd-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/netbsd-x64@npm:0.21.5"
conditions: os=netbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/openbsd-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/openbsd-x64@npm:0.21.5"
conditions: os=openbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/sunos-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/sunos-x64@npm:0.21.5"
conditions: os=sunos & cpu=x64
languageName: node
linkType: hard
"@esbuild/win32-arm64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/win32-arm64@npm:0.21.5"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@esbuild/win32-ia32@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/win32-ia32@npm:0.21.5"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@esbuild/win32-x64@npm:0.21.5":
version: 0.21.5
resolution: "@esbuild/win32-x64@npm:0.21.5"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.2.0":
version: 4.4.1
resolution: "@eslint-community/eslint-utils@npm:4.4.1"
@ -2471,15 +2632,16 @@ __metadata:
languageName: node
linkType: hard
"@openreplay/sourcemap-uploader@npm:^3.0.8":
version: 3.0.10
resolution: "@openreplay/sourcemap-uploader@npm:3.0.10"
"@openreplay/sourcemap-uploader@npm:^3.0.10":
version: 3.0.13
resolution: "@openreplay/sourcemap-uploader@npm:3.0.13"
dependencies:
argparse: "npm:^2.0.1"
glob: "npm:^8.0.3"
glob-promise: "npm:^6.0.7"
bin:
sourcemap-uploader: cli.js
checksum: 10c1/aa22a161020f55e96c3835cb719f4c530728ccb55e90d1a0bbc96c5243bffb900e204dd6275f1dee2927db3c4439b5d33c38c60774e5340ccb36acab95f30cc5
checksum: 10c1/1bab68b1a2f348973d9c027e00b9e027a833cec998e9a8a6c2872585d46fcdfb6f1492b98fd42f04ed2e6c4f913ec19e44a24be95460e1f7d1e08f6edf15eabe
languageName: node
linkType: hard
@ -2790,67 +2952,103 @@ __metadata:
languageName: node
linkType: hard
"@sentry/browser@npm:^5.21.1":
version: 5.30.0
resolution: "@sentry/browser@npm:5.30.0"
"@sentry-internal/feedback@npm:7.120.1":
version: 7.120.1
resolution: "@sentry-internal/feedback@npm:7.120.1"
dependencies:
"@sentry/core": "npm:5.30.0"
"@sentry/types": "npm:5.30.0"
"@sentry/utils": "npm:5.30.0"
tslib: "npm:^1.9.3"
checksum: 10c1/4787cc3ea90600b36b548a8403afb30f13e1e562dd426871879d824536c16005d0734b7406498f1a6dd4fa7e0a49808e17a1c2c24750430ba7f86f909a9eb95a
"@sentry/core": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/df6f20c36f3f3415bf32e51285f48e2195f407e4f89dd67e212843d5b2b3eb8fdd5258b28d36d5ea7d6cb112246d0cdeb43db035600bf37f0980080d1457a7d0
languageName: node
linkType: hard
"@sentry/core@npm:5.30.0":
version: 5.30.0
resolution: "@sentry/core@npm:5.30.0"
"@sentry-internal/replay-canvas@npm:7.120.1":
version: 7.120.1
resolution: "@sentry-internal/replay-canvas@npm:7.120.1"
dependencies:
"@sentry/hub": "npm:5.30.0"
"@sentry/minimal": "npm:5.30.0"
"@sentry/types": "npm:5.30.0"
"@sentry/utils": "npm:5.30.0"
tslib: "npm:^1.9.3"
checksum: 10c1/5c6dcdccc48a9d6957af7745226eacd3d4926574593e852ccbad0fbaa71355879b9c4707c194e3d9b1ef389d98171a3d85d2c75636a5c6d1cc3c9950cd06334a
"@sentry/core": "npm:7.120.1"
"@sentry/replay": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/64cd18114814548b6fb5ed5bf81b82bcfc7f1e43acbbd75f3ad7b1133368913ea4ff60b35efab9c4f2a252767b389a31956d16fdd261ac6046bf3178ab91cc6d
languageName: node
linkType: hard
"@sentry/hub@npm:5.30.0":
version: 5.30.0
resolution: "@sentry/hub@npm:5.30.0"
"@sentry-internal/tracing@npm:7.120.1":
version: 7.120.1
resolution: "@sentry-internal/tracing@npm:7.120.1"
dependencies:
"@sentry/types": "npm:5.30.0"
"@sentry/utils": "npm:5.30.0"
tslib: "npm:^1.9.3"
checksum: 10c1/28b86742c72427b5831ee3077c377d1f305d2eb080f7dc977e81b8f29e8eb0dfa07f129c1f5cda29bda9238fe50e292ab719119c4c5a5b7ef580a24bcb6356a3
"@sentry/core": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/825b40ab7a97453914200029563ccbc309a0f9a9b1ef06907d682c50650ae5cc8edb669042295bd82cbcaf68df939f9ad6ac901894c9f6e66a4c0bcec5419020
languageName: node
linkType: hard
"@sentry/minimal@npm:5.30.0":
version: 5.30.0
resolution: "@sentry/minimal@npm:5.30.0"
"@sentry/browser@npm:^7.119.1":
version: 7.120.1
resolution: "@sentry/browser@npm:7.120.1"
dependencies:
"@sentry/hub": "npm:5.30.0"
"@sentry/types": "npm:5.30.0"
tslib: "npm:^1.9.3"
checksum: 10c1/d28ad14e43d3c5c06783288ace1fcf1474437070f04d1476b04d0288656351d9a6285cc66d346e8d84a3e73cf895944c06fa7c82bad93415831e4449e11f2d89
"@sentry-internal/feedback": "npm:7.120.1"
"@sentry-internal/replay-canvas": "npm:7.120.1"
"@sentry-internal/tracing": "npm:7.120.1"
"@sentry/core": "npm:7.120.1"
"@sentry/integrations": "npm:7.120.1"
"@sentry/replay": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/f4ec68aca6ba9d6dde906cc69a74fe97ca8e6f2c90e5ab4d4d3e04e2055e852a952a9b3679ab7f63352710c39952f79c0693323de35b8f8b01b5b59eafc7afd7
languageName: node
linkType: hard
"@sentry/types@npm:5.30.0":
version: 5.30.0
resolution: "@sentry/types@npm:5.30.0"
checksum: 10c1/07fe7f04f6aae13f037761fe56a20e06fa4a768bf024fb81970d3087ab9ab5b45bd85b9081945ef5019d93b7de742918374a0e7b70a992dbb29a5078982ddfd9
"@sentry/core@npm:7.120.1":
version: 7.120.1
resolution: "@sentry/core@npm:7.120.1"
dependencies:
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/21289e91c3110e4990e22134edbfba4796c07e10dc0c1931afdd19ef76acfb17a8d2c0216ca535258f5e914899ccb8901fcf2a64f4bc8328844648698148e12e
languageName: node
linkType: hard
"@sentry/utils@npm:5.30.0":
version: 5.30.0
resolution: "@sentry/utils@npm:5.30.0"
"@sentry/integrations@npm:7.120.1":
version: 7.120.1
resolution: "@sentry/integrations@npm:7.120.1"
dependencies:
"@sentry/types": "npm:5.30.0"
tslib: "npm:^1.9.3"
checksum: 10c1/311ad0be0e40af9f4ab7be2dfb8a782a779fa56700a0662f49ebcbff0dbbe4ea5dff690ad2c0ed4ecb6a6721a3066186b3c8f677fa302c5b606f86dfaa654de3
"@sentry/core": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
localforage: "npm:^1.8.1"
checksum: 10c1/84b6836538ba6319ae923c85ad114b7b8d81edbe735f87115347202e9ca1e9cff9c2adc4f3de6761a165ecc594954b7e070cd30695ed86d273af28ab707c6854
languageName: node
linkType: hard
"@sentry/replay@npm:7.120.1":
version: 7.120.1
resolution: "@sentry/replay@npm:7.120.1"
dependencies:
"@sentry-internal/tracing": "npm:7.120.1"
"@sentry/core": "npm:7.120.1"
"@sentry/types": "npm:7.120.1"
"@sentry/utils": "npm:7.120.1"
checksum: 10c1/d32a040cf52c105e0b325b748b501405283e62a50f85b73ee940f6484cf303256c211763432386d8550a1282bbe7727617b61df36cd85eb5ecc5fcc0f03a7333
languageName: node
linkType: hard
"@sentry/types@npm:7.120.1":
version: 7.120.1
resolution: "@sentry/types@npm:7.120.1"
checksum: 10c1/8a81d5f34acc39cf22ffe2b6904e2d5c9d47eb59ebe1ab7ccd32e4e4f4f1677d6abc9747742482216ab7f9a55ddc73eaabf23b52bc77d596ab9cc48d486a3219
languageName: node
linkType: hard
"@sentry/utils@npm:7.120.1":
version: 7.120.1
resolution: "@sentry/utils@npm:7.120.1"
dependencies:
"@sentry/types": "npm:7.120.1"
checksum: 10c1/8c2edc24ee15ad5099864dbe9822f5c20c0070bcf0d37f27a456f044c636d1da85c779b28b431397e03c6f63c0f46576693d6022e6282d0405a52fbe06db75ce
languageName: node
linkType: hard
@ -5729,15 +5927,15 @@ __metadata:
linkType: hard
"cross-spawn@npm:^6.0.0":
version: 6.0.5
resolution: "cross-spawn@npm:6.0.5"
version: 6.0.6
resolution: "cross-spawn@npm:6.0.6"
dependencies:
nice-try: "npm:^1.0.4"
path-key: "npm:^2.0.1"
semver: "npm:^5.5.0"
shebang-command: "npm:^1.2.0"
which: "npm:^1.2.9"
checksum: 10c1/ea71fd2b6b32cb716f2fb2768377b7cf531bffec212be25b04d6826af95d6973dcb9fb8ee056932e32549a2b5b07d632f0fe9ec93d1ebf2ec66b10903b15848d
checksum: 10c1/aa1908a9201525d1fb0a67f0d0d5c1abd26fc6e052a05da243c8320d0bf5f4711174682271ea030a547c10202c7ef72af3f7db0c47dfb94132213697fb370412
languageName: node
linkType: hard
@ -7001,6 +7199,100 @@ __metadata:
languageName: node
linkType: hard
"esbuild-loader@npm:^4.2.2":
version: 4.2.2
resolution: "esbuild-loader@npm:4.2.2"
dependencies:
esbuild: "npm:^0.21.0"
get-tsconfig: "npm:^4.7.0"
loader-utils: "npm:^2.0.4"
webpack-sources: "npm:^1.4.3"
peerDependencies:
webpack: ^4.40.0 || ^5.0.0
checksum: 10c1/2e29724312e75ffdb06d6421536078f36c135e9d8563bd413d78b061bf24459e06661f6baadd11a7f8e630e22abdf0d9d18921d51460bd2eeee47e000e29fd17
languageName: node
linkType: hard
"esbuild@npm:^0.21.0":
version: 0.21.5
resolution: "esbuild@npm:0.21.5"
dependencies:
"@esbuild/aix-ppc64": "npm:0.21.5"
"@esbuild/android-arm": "npm:0.21.5"
"@esbuild/android-arm64": "npm:0.21.5"
"@esbuild/android-x64": "npm:0.21.5"
"@esbuild/darwin-arm64": "npm:0.21.5"
"@esbuild/darwin-x64": "npm:0.21.5"
"@esbuild/freebsd-arm64": "npm:0.21.5"
"@esbuild/freebsd-x64": "npm:0.21.5"
"@esbuild/linux-arm": "npm:0.21.5"
"@esbuild/linux-arm64": "npm:0.21.5"
"@esbuild/linux-ia32": "npm:0.21.5"
"@esbuild/linux-loong64": "npm:0.21.5"
"@esbuild/linux-mips64el": "npm:0.21.5"
"@esbuild/linux-ppc64": "npm:0.21.5"
"@esbuild/linux-riscv64": "npm:0.21.5"
"@esbuild/linux-s390x": "npm:0.21.5"
"@esbuild/linux-x64": "npm:0.21.5"
"@esbuild/netbsd-x64": "npm:0.21.5"
"@esbuild/openbsd-x64": "npm:0.21.5"
"@esbuild/sunos-x64": "npm:0.21.5"
"@esbuild/win32-arm64": "npm:0.21.5"
"@esbuild/win32-ia32": "npm:0.21.5"
"@esbuild/win32-x64": "npm:0.21.5"
dependenciesMeta:
"@esbuild/aix-ppc64":
optional: true
"@esbuild/android-arm":
optional: true
"@esbuild/android-arm64":
optional: true
"@esbuild/android-x64":
optional: true
"@esbuild/darwin-arm64":
optional: true
"@esbuild/darwin-x64":
optional: true
"@esbuild/freebsd-arm64":
optional: true
"@esbuild/freebsd-x64":
optional: true
"@esbuild/linux-arm":
optional: true
"@esbuild/linux-arm64":
optional: true
"@esbuild/linux-ia32":
optional: true
"@esbuild/linux-loong64":
optional: true
"@esbuild/linux-mips64el":
optional: true
"@esbuild/linux-ppc64":
optional: true
"@esbuild/linux-riscv64":
optional: true
"@esbuild/linux-s390x":
optional: true
"@esbuild/linux-x64":
optional: true
"@esbuild/netbsd-x64":
optional: true
"@esbuild/openbsd-x64":
optional: true
"@esbuild/sunos-x64":
optional: true
"@esbuild/win32-arm64":
optional: true
"@esbuild/win32-ia32":
optional: true
"@esbuild/win32-x64":
optional: true
bin:
esbuild: bin/esbuild
checksum: 10c1/1bed0f5871043244bc2033f323a7e86e06d808df955b47bc5579bb3855d107b67f0adace7d4c747deea856cfc7bb798c3320a4b96ebb832112c915f377b5c9ed
languageName: node
linkType: hard
"escalade@npm:^3.1.1, escalade@npm:^3.2.0":
version: 3.2.0
resolution: "escalade@npm:3.2.0"
@ -7967,6 +8259,15 @@ __metadata:
languageName: node
linkType: hard
"get-tsconfig@npm:^4.7.0":
version: 4.8.1
resolution: "get-tsconfig@npm:4.8.1"
dependencies:
resolve-pkg-maps: "npm:^1.0.0"
checksum: 10c1/680263b7ee8ceb66e88d6625d5b62fe432e280cf313e962e52bfaaae65d286ffe8f22ee35032a40ba0f9988469bcac92b1146ac837da33b8fad97a5b5b763806
languageName: node
linkType: hard
"get-user-locale@npm:^2.2.1":
version: 2.3.2
resolution: "get-user-locale@npm:2.3.2"
@ -8058,7 +8359,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^8.0.1":
"glob@npm:^8.0.1, glob@npm:^8.0.3":
version: 8.1.0
resolution: "glob@npm:8.1.0"
dependencies:
@ -8610,6 +8911,13 @@ __metadata:
languageName: node
linkType: hard
"immediate@npm:~3.0.5":
version: 3.0.6
resolution: "immediate@npm:3.0.6"
checksum: 10c1/80367e577e9000eab06902678ee126385ddc4a28ece1a4bb7bdfd60005cfbff083e0313d2ab69f1b70f3eda065c6dbf1e0797c3cbd7e701327369addaa230b5d
languageName: node
linkType: hard
"immutable@npm:^3.7.2":
version: 3.8.2
resolution: "immutable@npm:3.8.2"
@ -10138,6 +10446,15 @@ __metadata:
languageName: node
linkType: hard
"lie@npm:3.1.1":
version: 3.1.1
resolution: "lie@npm:3.1.1"
dependencies:
immediate: "npm:~3.0.5"
checksum: 10c1/06818c9f87f0606c2ad3e18071d20e7dfd2e38af95b84cf96ee5abd17c15f1a6ec46b13df3709110954e9f295c59e16e23061e853416bdc7e8cd5fc49ccdcb9b
languageName: node
linkType: hard
"lilconfig@npm:^2.1.0":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
@ -10198,7 +10515,7 @@ __metadata:
languageName: node
linkType: hard
"loader-utils@npm:^2.0.0":
"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.4":
version: 2.0.4
resolution: "loader-utils@npm:2.0.4"
dependencies:
@ -10209,6 +10526,15 @@ __metadata:
languageName: node
linkType: hard
"localforage@npm:^1.8.1":
version: 1.10.0
resolution: "localforage@npm:1.10.0"
dependencies:
lie: "npm:3.1.1"
checksum: 10c1/7569b18f5ed93a2d450a5e0b1b4f36ea49f026e352354e55cee683baace98186aa95478aac9438c1035d48924c63ea5a9cdd03b8cc02a7cd740591ad77a13d37
languageName: node
linkType: hard
"locate-path@npm:^3.0.0":
version: 3.0.0
resolution: "locate-path@npm:3.0.0"
@ -10921,11 +11247,11 @@ __metadata:
linkType: hard
"nanoid@npm:^3.3.7":
version: 3.3.7
resolution: "nanoid@npm:3.3.7"
version: 3.3.8
resolution: "nanoid@npm:3.3.8"
bin:
nanoid: bin/nanoid.cjs
checksum: 10c1/f0f6b94f1084ebfd13003698273665aef653c4a7adbb822e82383ce723c4856149597de033b339b523d124b55763a60a6b2c74349b98fd25f71387968cce49d0
checksum: 10c1/b7f68bc6a105c13b0429d990ceecacffef79dd7ec2759a26220d91d73f693a73ab9888b2ba7e4c24160e4273de977d87d40b4be461a5f2e5fd2f55095d922fe2
languageName: node
linkType: hard
@ -11308,8 +11634,8 @@ __metadata:
"@floating-ui/react-dom-interactions": "npm:^0.10.3"
"@jest/globals": "npm:^29.7.0"
"@medv/finder": "npm:^3.1.0"
"@openreplay/sourcemap-uploader": "npm:^3.0.8"
"@sentry/browser": "npm:^5.21.1"
"@openreplay/sourcemap-uploader": "npm:^3.0.10"
"@sentry/browser": "npm:^7.119.1"
"@svg-maps/world": "npm:^1.0.1"
"@tanstack/react-query": "npm:^5.56.2"
"@trivago/prettier-plugin-sort-imports": "npm:^4.3.0"
@ -11342,6 +11668,7 @@ __metadata:
cypress: "npm:^13.3.0"
cypress-image-snapshot: "npm:^4.0.1"
dotenv: "npm:^6.2.0"
esbuild-loader: "npm:^4.2.2"
eslint: "npm:^8.15.0"
eslint-plugin-react: "npm:^7.29.4"
fflate: "npm:^0.8.2"
@ -13847,6 +14174,13 @@ __metadata:
languageName: node
linkType: hard
"resolve-pkg-maps@npm:^1.0.0":
version: 1.0.0
resolution: "resolve-pkg-maps@npm:1.0.0"
checksum: 10c1/e6f8dbe20ebbfdea61503670e5f325782e6d983e59e33c81b314a48910f2edcb9534975c5a14d789d2830c3ab3ae49f022dd6e2fdb56330f242ee3fbd60b46c5
languageName: node
linkType: hard
"resolve.exports@npm:^2.0.0":
version: 2.0.2
resolution: "resolve.exports@npm:2.0.2"
@ -14514,6 +14848,13 @@ __metadata:
languageName: node
linkType: hard
"source-list-map@npm:^2.0.0":
version: 2.0.1
resolution: "source-list-map@npm:2.0.1"
checksum: 10c1/bb3b0d59f90518f20b996eb57a29a62fbd0735f84649f2949591f01ef43a31a27b095a4c9b2f27edce4ea4c8d5b757b86476c532c813e1de84c340d7f2a41264
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
@ -15482,7 +15823,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^1.8.1, tslib@npm:^1.9.3":
"tslib@npm:^1.8.1":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
checksum: 10c1/24ee51ea8fb127ca8ad30a25fdac22c5bb11a2b043781757ddde0daf2e03126e1e13e88ab1748d9c50f786a648b5b038e70782063fd15c3ad07ebec039df8f6f
@ -16154,6 +16495,16 @@ __metadata:
languageName: node
linkType: hard
"webpack-sources@npm:^1.4.3":
version: 1.4.3
resolution: "webpack-sources@npm:1.4.3"
dependencies:
source-list-map: "npm:^2.0.0"
source-map: "npm:~0.6.1"
checksum: 10c1/fc3c601c48df84178b6e8a297b3d844ea5580011b8cd7d382ffe0241b9fae1f44124337a2981d55f314cd4517f25d9fda20549cd96b279b47a00ac0727cea80f
languageName: node
linkType: hard
"webpack-sources@npm:^3.2.3":
version: 3.2.3
resolution: "webpack-sources@npm:3.2.3"

View file

@ -331,85 +331,100 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@rollup/rollup-android-arm-eabi@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz#e0f5350845090ca09690fe4a472717f3b8aae225"
integrity sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==
"@rollup/rollup-android-arm-eabi@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e"
integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==
"@rollup/rollup-android-arm64@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz#08270faef6747e2716d3e978a8bbf479f75fb19a"
integrity sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==
"@rollup/rollup-android-arm64@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d"
integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==
"@rollup/rollup-darwin-arm64@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz#691671133b350661328d42c8dbdedd56dfb97dfd"
integrity sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==
"@rollup/rollup-darwin-arm64@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999"
integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==
"@rollup/rollup-darwin-x64@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz#b2ec52a1615f24b1cd40bc8906ae31af81e8a342"
integrity sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==
"@rollup/rollup-darwin-x64@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2"
integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==
"@rollup/rollup-linux-arm-gnueabihf@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz#217f01f304808920680bd269002df38e25d9205f"
integrity sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==
"@rollup/rollup-freebsd-arm64@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946"
integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==
"@rollup/rollup-linux-arm-musleabihf@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz#93ac1c5a1e389f4482a2edaeec41fcffee54a930"
integrity sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==
"@rollup/rollup-freebsd-x64@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282"
integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==
"@rollup/rollup-linux-arm64-gnu@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz#a7f146787d6041fecc4ecdf1aa72234661ca94a4"
integrity sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==
"@rollup/rollup-linux-arm-gnueabihf@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df"
integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==
"@rollup/rollup-linux-arm64-musl@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz#6a37236189648e678bd564d6e8ca798f42cf42c5"
integrity sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==
"@rollup/rollup-linux-arm-musleabihf@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb"
integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==
"@rollup/rollup-linux-powerpc64le-gnu@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz#5661420dc463bec31ecb2d17d113de858cfcfe2d"
integrity sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==
"@rollup/rollup-linux-arm64-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9"
integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==
"@rollup/rollup-linux-riscv64-gnu@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz#cb00342b7432bdef723aa606281de2f522d6dcf7"
integrity sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==
"@rollup/rollup-linux-arm64-musl@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788"
integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==
"@rollup/rollup-linux-s390x-gnu@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz#0708889674dccecccd28e2befccf791e0767fcb7"
integrity sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==
"@rollup/rollup-linux-loongarch64-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87"
integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==
"@rollup/rollup-linux-x64-gnu@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz#a135b040b21582e91cfed2267ccfc7d589e1dbc6"
integrity sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==
"@rollup/rollup-linux-powerpc64le-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c"
integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==
"@rollup/rollup-linux-x64-musl@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz#88395a81a3ab7ee3dc8dc31a73ff62ed3185f34d"
integrity sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==
"@rollup/rollup-linux-riscv64-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd"
integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==
"@rollup/rollup-win32-arm64-msvc@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz#12ee49233b1125f2c1da38392f63b1dbb0c31bba"
integrity sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==
"@rollup/rollup-linux-s390x-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd"
integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==
"@rollup/rollup-win32-ia32-msvc@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz#0f987b134c6b3123c22842b33ba0c2b6fb78cc3b"
integrity sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==
"@rollup/rollup-linux-x64-gnu@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e"
integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==
"@rollup/rollup-win32-x64-msvc@4.22.5":
version "4.22.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz#f2feb149235a5dc1deb5439758f8871255e5a161"
integrity sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==
"@rollup/rollup-linux-x64-musl@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b"
integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==
"@rollup/rollup-win32-arm64-msvc@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11"
integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==
"@rollup/rollup-win32-ia32-msvc@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f"
integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==
"@rollup/rollup-win32-x64-msvc@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0"
integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==
"@types/estree@1.0.6", "@types/estree@^1.0.0":
version "1.0.6"
@ -1090,28 +1105,31 @@ punycode@^2.3.1:
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
rollup@^4.20.0:
version "4.22.5"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.5.tgz#d5108cc470249417e50492456253884d19f5d40f"
integrity sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==
version "4.28.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de"
integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.22.5"
"@rollup/rollup-android-arm64" "4.22.5"
"@rollup/rollup-darwin-arm64" "4.22.5"
"@rollup/rollup-darwin-x64" "4.22.5"
"@rollup/rollup-linux-arm-gnueabihf" "4.22.5"
"@rollup/rollup-linux-arm-musleabihf" "4.22.5"
"@rollup/rollup-linux-arm64-gnu" "4.22.5"
"@rollup/rollup-linux-arm64-musl" "4.22.5"
"@rollup/rollup-linux-powerpc64le-gnu" "4.22.5"
"@rollup/rollup-linux-riscv64-gnu" "4.22.5"
"@rollup/rollup-linux-s390x-gnu" "4.22.5"
"@rollup/rollup-linux-x64-gnu" "4.22.5"
"@rollup/rollup-linux-x64-musl" "4.22.5"
"@rollup/rollup-win32-arm64-msvc" "4.22.5"
"@rollup/rollup-win32-ia32-msvc" "4.22.5"
"@rollup/rollup-win32-x64-msvc" "4.22.5"
"@rollup/rollup-android-arm-eabi" "4.28.1"
"@rollup/rollup-android-arm64" "4.28.1"
"@rollup/rollup-darwin-arm64" "4.28.1"
"@rollup/rollup-darwin-x64" "4.28.1"
"@rollup/rollup-freebsd-arm64" "4.28.1"
"@rollup/rollup-freebsd-x64" "4.28.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.28.1"
"@rollup/rollup-linux-arm-musleabihf" "4.28.1"
"@rollup/rollup-linux-arm64-gnu" "4.28.1"
"@rollup/rollup-linux-arm64-musl" "4.28.1"
"@rollup/rollup-linux-loongarch64-gnu" "4.28.1"
"@rollup/rollup-linux-powerpc64le-gnu" "4.28.1"
"@rollup/rollup-linux-riscv64-gnu" "4.28.1"
"@rollup/rollup-linux-s390x-gnu" "4.28.1"
"@rollup/rollup-linux-x64-gnu" "4.28.1"
"@rollup/rollup-linux-x64-musl" "4.28.1"
"@rollup/rollup-win32-arm64-msvc" "4.28.1"
"@rollup/rollup-win32-ia32-msvc" "4.28.1"
"@rollup/rollup-win32-x64-msvc" "4.28.1"
fsevents "~2.3.2"
rrweb-cssom@^0.7.1:
@ -1178,7 +1196,16 @@ std-env@^3.7.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -1196,7 +1223,14 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -1318,9 +1352,9 @@ vite-node@2.1.1:
vite "^5.0.0"
vite@^5.0.0:
version "5.4.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
version "5.4.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"

View file

@ -9,7 +9,7 @@
"version": "v1.12.0",
"license": "Elastic License 2.0 (ELv2)",
"dependencies": {
"express": "^4.21.1",
"express": "^4.21.2",
"peer": "^v1.0.1",
"winston": "^3.13.0"
}
@ -450,9 +450,10 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -473,7 +474,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -488,6 +489,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/fecha": {
@ -893,9 +898,10 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/peer": {
"version": "1.0.2",

View file

@ -18,7 +18,7 @@
},
"homepage": "https://github.com/openreplay/openreplay#readme",
"dependencies": {
"express": "^4.21.1",
"express": "^4.21.2",
"peer": "^v1.0.1",
"winston": "^3.13.0"
}

View file

@ -9,7 +9,7 @@ COMMON_PG_PASSWORD="change_me_pg_password"
COMMON_VERSION="v1.16.0"
## DB versions
######################################
POSTGRES_VERSION="14.5.0"
POSTGRES_VERSION="15.10.0"
REDIS_VERSION="6.0.12-debian-10-r33"
MINIO_VERSION="2023.2.10-debian-11-r1"
######################################

View file

@ -15,7 +15,7 @@ services:
image: bitnami/redis:${REDIS_VERSION}
container_name: redis
volumes:
- redisdata:/var/lib/postgresql/data
- redisdata:/bitnami/redis/data
networks:
- openreplay-net
environment:

Some files were not shown because too many files have changed in this diff Show more