Merge branch 'dev' into live-se-red
This commit is contained in:
commit
e1bb2e8bcb
39 changed files with 547 additions and 375 deletions
|
|
@ -5,7 +5,7 @@ name = "pypi"
|
|||
|
||||
[packages]
|
||||
sqlparse = "==0.5.2"
|
||||
urllib3 = "==1.26.16"
|
||||
urllib3 = "==2.2.3"
|
||||
requests = "==2.32.3"
|
||||
boto3 = "==1.35.76"
|
||||
pyjwt = "==2.10.1"
|
||||
|
|
@ -21,7 +21,7 @@ uvicorn = {extras = ["standard"], version = "==0.32.1"}
|
|||
python-decouple = "==3.8"
|
||||
pydantic = {extras = ["email"], version = "==2.10.3"}
|
||||
apscheduler = "==3.11.0"
|
||||
redis = "==5.2.0"
|
||||
redis = "==5.2.1"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ class JWTAuth(HTTPBearer):
|
|||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid authentication scheme.")
|
||||
jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials)
|
||||
logger.info("------ jwt_payload ------")
|
||||
logger.info(jwt_payload)
|
||||
auth_exists = jwt_payload is not None and users.auth_exists(user_id=jwt_payload.get("userId", -1),
|
||||
jwt_iat=jwt_payload.get("iat", 100))
|
||||
if jwt_payload is None \
|
||||
|
|
@ -120,8 +118,7 @@ class JWTAuth(HTTPBearer):
|
|||
jwt_payload = None
|
||||
else:
|
||||
jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=request.cookies["spotRefreshToken"])
|
||||
logger.info("__process_spot_refresh_call")
|
||||
logger.info(jwt_payload)
|
||||
|
||||
if jwt_payload is None or jwt_payload.get("jti") is None:
|
||||
logger.warning("Null spotRefreshToken's payload, or null JTI.")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
|
|
|||
|
|
@ -12,20 +12,6 @@ from chalicelib.utils.TimeUTC import TimeUTC
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: refactor this to split
|
||||
# timeseries /
|
||||
# table of errors / table of issues / table of browsers / table of devices / table of countries / table of URLs
|
||||
# remove "table of" calls from this function
|
||||
def __try_live(project_id, data: schemas.CardSchema):
|
||||
results = []
|
||||
for i, s in enumerate(data.series):
|
||||
results.append(sessions.search2_series(data=s.filter, project_id=project_id, density=data.density,
|
||||
view_type=data.view_type, metric_type=data.metric_type,
|
||||
metric_of=data.metric_of, metric_value=data.metric_value))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def __get_table_of_series(project_id, data: schemas.CardSchema):
|
||||
results = []
|
||||
for i, s in enumerate(data.series):
|
||||
|
|
@ -43,9 +29,6 @@ def __get_funnel_chart(project: schemas.ProjectContext, data: schemas.CardFunnel
|
|||
"totalDropDueToIssues": 0
|
||||
}
|
||||
|
||||
# return funnels.get_top_insights_on_the_fly_widget(project_id=project_id,
|
||||
# data=data.series[0].filter,
|
||||
# metric_format=data.metric_format)
|
||||
return funnels.get_simple_funnel(project=project,
|
||||
data=data.series[0].filter,
|
||||
metric_format=data.metric_format)
|
||||
|
|
@ -93,7 +76,12 @@ def __get_path_analysis_chart(project: schemas.ProjectContext, user_id: int, dat
|
|||
|
||||
|
||||
def __get_timeseries_chart(project: schemas.ProjectContext, data: schemas.CardTimeSeries, user_id: int = None):
|
||||
series_charts = __try_live(project_id=project.project_id, data=data)
|
||||
series_charts = []
|
||||
for i, s in enumerate(data.series):
|
||||
series_charts.append(sessions.search2_series(data=s.filter, project_id=project.project_id, density=data.density,
|
||||
view_type=data.view_type, metric_type=data.metric_type,
|
||||
metric_of=data.metric_of, metric_value=data.metric_value))
|
||||
|
||||
results = [{}] * len(series_charts[0])
|
||||
for i in range(len(results)):
|
||||
for j, series_chart in enumerate(series_charts):
|
||||
|
|
@ -179,12 +167,6 @@ def get_chart(project: schemas.ProjectContext, data: schemas.CardSchema, user_id
|
|||
|
||||
|
||||
def get_sessions_by_card_id(project_id, user_id, metric_id, data: schemas.CardSessionsSchema):
|
||||
# No need for this because UI is sending the full payload
|
||||
# card: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
# if card is None:
|
||||
# return None
|
||||
# metric: schemas.CardSchema = schemas.CardSchema(**card)
|
||||
# metric: schemas.CardSchema = __merge_metric_with_data(metric=metric, data=data)
|
||||
if not card_exists(metric_id=metric_id, project_id=project_id, user_id=user_id):
|
||||
return None
|
||||
results = []
|
||||
|
|
@ -553,17 +535,7 @@ def change_state(project_id, metric_id, user_id, status):
|
|||
|
||||
|
||||
def get_funnel_sessions_by_issue(user_id, project_id, metric_id, issue_id,
|
||||
data: schemas.CardSessionsSchema
|
||||
# , range_value=None, start_date=None, end_date=None
|
||||
):
|
||||
# No need for this because UI is sending the full payload
|
||||
# card: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
# if card is None:
|
||||
# return None
|
||||
# metric: schemas.CardSchema = schemas.CardSchema(**card)
|
||||
# metric: schemas.CardSchema = __merge_metric_with_data(metric=metric, data=data)
|
||||
# if metric is None:
|
||||
# return None
|
||||
data: schemas.CardSessionsSchema):
|
||||
if not card_exists(metric_id=metric_id, project_id=project_id, user_id=user_id):
|
||||
return None
|
||||
for s in data.series:
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ import logging
|
|||
import threading
|
||||
import time
|
||||
from functools import wraps
|
||||
from queue import Queue
|
||||
from queue import Queue, Empty
|
||||
|
||||
import clickhouse_connect
|
||||
from clickhouse_connect.driver.query import QueryContext
|
||||
from clickhouse_connect.driver.exceptions import DatabaseError
|
||||
from decouple import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CH_CONFIG = {"host": config("ch_host"),
|
||||
"user": config("ch_user", default="default"),
|
||||
"password": config("ch_password", default=""),
|
||||
"port": config("ch_port_http", cast=int),
|
||||
"client_name": config("APP_NAME", default="PY")}
|
||||
CH_CONFIG = dict(_CH_CONFIG)
|
||||
|
||||
settings = {}
|
||||
if config('ch_timeout', cast=int, default=-1) > 0:
|
||||
logging.info(f"CH-max_execution_time set to {config('ch_timeout')}s")
|
||||
|
|
@ -26,6 +35,7 @@ if config("CH_COMPRESSION", cast=bool, default=True):
|
|||
def transform_result(original_function):
|
||||
@wraps(original_function)
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.info("Executing query on CH")
|
||||
result = original_function(*args, **kwargs)
|
||||
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
|
||||
column_names = result.column_names
|
||||
|
|
@ -38,21 +48,17 @@ def transform_result(original_function):
|
|||
|
||||
|
||||
class ClickHouseConnectionPool:
|
||||
def __init__(self, min_size, max_size, settings):
|
||||
def __init__(self, min_size, max_size):
|
||||
self.min_size = min_size
|
||||
self.max_size = max_size
|
||||
self.pool = Queue()
|
||||
self.lock = threading.Lock()
|
||||
self.total_connections = 0
|
||||
self.settings = settings
|
||||
|
||||
# Initialize the pool with min_size connections
|
||||
for _ in range(self.min_size):
|
||||
client = clickhouse_connect.get_client(host=config("ch_host"),
|
||||
client = clickhouse_connect.get_client(**CH_CONFIG,
|
||||
database=config("ch_database", default="default"),
|
||||
user=config("ch_user", default="default"),
|
||||
password=config("ch_password", default=""),
|
||||
port=config("ch_port_http", cast=int),
|
||||
settings=settings,
|
||||
**extra_args)
|
||||
self.pool.put(client)
|
||||
|
|
@ -66,15 +72,10 @@ class ClickHouseConnectionPool:
|
|||
except Empty:
|
||||
with self.lock:
|
||||
if self.total_connections < self.max_size:
|
||||
client = clickhouse_connect.get_client(
|
||||
host=config("ch_host"),
|
||||
database=config("ch_database", default="default"),
|
||||
user=config("ch_user", default="default"),
|
||||
password=config("ch_password", default=""),
|
||||
port=config("ch_port_http", cast=int),
|
||||
settings=settings,
|
||||
**extra_args
|
||||
)
|
||||
client = clickhouse_connect.get_client(**CH_CONFIG,
|
||||
database=config("ch_database", default="default"),
|
||||
settings=settings,
|
||||
**extra_args)
|
||||
self.total_connections += 1
|
||||
return client
|
||||
# If max_size reached, wait until a connection is available
|
||||
|
|
@ -110,12 +111,11 @@ def make_pool():
|
|||
except Exception as error:
|
||||
logger.error("Error while closing all connexions to CH", error)
|
||||
try:
|
||||
CH_pool = ClickHouseConnectionPool(min_size=config("PG_MINCONN", cast=int, default=4),
|
||||
max_size=config("PG_MAXCONN", cast=int, default=8),
|
||||
settings=settings)
|
||||
CH_pool = ClickHouseConnectionPool(min_size=config("CH_MINCONN", cast=int, default=4),
|
||||
max_size=config("CH_MAXCONN", cast=int, default=8))
|
||||
if CH_pool is not None:
|
||||
logger.info("Connection pool created successfully for CH")
|
||||
except Exception as error:
|
||||
except ConnectionError as error:
|
||||
logger.error("Error while connecting to CH", error)
|
||||
if RETRY < RETRY_MAX:
|
||||
RETRY += 1
|
||||
|
|
@ -131,15 +131,12 @@ class ClickHouseClient:
|
|||
|
||||
def __init__(self, database=None):
|
||||
if self.__client is None:
|
||||
if config('CH_POOL', cast=bool, default=True):
|
||||
if database is None and config('CH_POOL', cast=bool, default=True):
|
||||
self.__client = CH_pool.get_connection()
|
||||
else:
|
||||
self.__client = clickhouse_connect.get_client(host=config("ch_host"),
|
||||
self.__client = clickhouse_connect.get_client(**CH_CONFIG,
|
||||
database=database if database else config("ch_database",
|
||||
default="default"),
|
||||
user=config("ch_user", default="default"),
|
||||
password=config("ch_password", default=""),
|
||||
port=config("ch_port_http", cast=int),
|
||||
settings=settings,
|
||||
**extra_args)
|
||||
self.__client.execute = transform_result(self.__client.query)
|
||||
|
|
@ -164,7 +161,7 @@ class ClickHouseClient:
|
|||
|
||||
|
||||
async def init():
|
||||
logger.info(f">CH_POOL:{config('CH_POOL', default=None)}")
|
||||
logger.info(f">use CH_POOL:{config('CH_POOL', default=True)}")
|
||||
if config('CH_POOL', cast=bool, default=True):
|
||||
make_pool()
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class PostgresClient:
|
|||
|
||||
|
||||
async def init():
|
||||
logger.info(f">PG_POOL:{config('PG_POOL', default=None)}")
|
||||
logger.info(f">use PG_POOL:{config('PG_POOL', default=True)}")
|
||||
if config('PG_POOL', cast=bool, default=True):
|
||||
make_pool()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ captcha_key=
|
|||
captcha_server=
|
||||
CH_COMPRESSION=true
|
||||
ch_host=
|
||||
ch_port=
|
||||
ch_port_http=
|
||||
ch_port=9000
|
||||
ch_port_http=8123
|
||||
ch_receive_timeout=10
|
||||
ch_timeout=30
|
||||
change_password_link=/reset-password?invitation=%s&&pass=%s
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# Keep this version to not have conflicts between requests and boto3
|
||||
urllib3==1.26.16
|
||||
urllib3==2.2.3
|
||||
requests==2.32.3
|
||||
boto3==1.35.76
|
||||
pyjwt==2.10.1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# Keep this version to not have conflicts between requests and boto3
|
||||
urllib3==1.26.16
|
||||
urllib3==2.2.3
|
||||
requests==2.32.3
|
||||
boto3==1.35.76
|
||||
pyjwt==2.10.1
|
||||
|
|
@ -19,4 +18,4 @@ python-decouple==3.8
|
|||
pydantic[email]==2.10.3
|
||||
apscheduler==3.11.0
|
||||
|
||||
redis==5.2.0
|
||||
redis==5.2.1
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
|
|
@ -55,7 +54,7 @@ func (e *elasticsearchClient) FetchSessionData(credentials interface{}, sessionI
|
|||
// Create Elasticsearch client
|
||||
es, err := elasticsearch.NewClient(clientCfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating the client: %s", err)
|
||||
return nil, fmt.Errorf("error creating the client: %s", err)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
|
|
@ -79,17 +78,17 @@ func (e *elasticsearchClient) FetchSessionData(credentials interface{}, sessionI
|
|||
es.Search.WithTrackTotalHits(true),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting response: %s", err)
|
||||
return nil, fmt.Errorf("error getting response: %s", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
log.Fatalf("Error: %s", res.String())
|
||||
return nil, fmt.Errorf("error: %s", res.String())
|
||||
}
|
||||
|
||||
var r map[string]interface{}
|
||||
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
|
||||
log.Fatalf("Error parsing the response body: %s", err)
|
||||
return nil, fmt.Errorf("error parsing the response body: %s", err)
|
||||
}
|
||||
if r["hits"] == nil {
|
||||
return nil, fmt.Errorf("no logs found")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
|
@ -62,7 +61,7 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
|
|||
// Create a new request
|
||||
req, err := http.NewRequest("GET", requestUrl, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create request: %v", err)
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Add Authorization header
|
||||
|
|
@ -72,26 +71,26 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
|
|||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to send request: %v", err)
|
||||
return nil, fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if the response status is OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatalf("Failed to fetch logs, status code: %v", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch logs, status code: %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read response body: %v", err)
|
||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
var events []SentryEvent
|
||||
err = json.Unmarshal(body, &events)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse JSON: %v", err)
|
||||
return nil, fmt.Errorf("failed to parse JSON: %v", err)
|
||||
}
|
||||
if events == nil || len(events) == 0 {
|
||||
return nil, fmt.Errorf("no logs found")
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ verify_ssl = true
|
|||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
urllib3 = "==1.26.16"
|
||||
urllib3 = "==2.2.3"
|
||||
requests = "==2.32.3"
|
||||
boto3 = "==1.35.76"
|
||||
pyjwt = "==2.10.1"
|
||||
psycopg2-binary = "==2.9.10"
|
||||
psycopg = {extras = ["binary", "pool"], version = "==3.2.3"}
|
||||
psycopg = {extras = ["pool", "binary"], version = "==3.2.3"}
|
||||
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
|
||||
clickhouse-connect = "==0.8.9"
|
||||
elasticsearch = "==8.16.0"
|
||||
|
|
@ -21,10 +21,10 @@ gunicorn = "==23.0.0"
|
|||
python-decouple = "==3.8"
|
||||
pydantic = {extras = ["email"], version = "==2.10.3"}
|
||||
apscheduler = "==3.11.0"
|
||||
redis = "==5.2.1"
|
||||
python3-saml = "==1.16.0"
|
||||
python-multipart = "==0.0.17"
|
||||
redis = "==5.2.0"
|
||||
azure-storage-blob = "==12.23.1"
|
||||
azure-storage-blob = "==12.24.0"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ captcha_key=
|
|||
captcha_server=
|
||||
CH_COMPRESSION=true
|
||||
ch_host=
|
||||
ch_port=
|
||||
ch_port_http=
|
||||
ch_port=9000
|
||||
ch_port_http=8123
|
||||
ch_receive_timeout=10
|
||||
ch_timeout=30
|
||||
change_password_link=/reset-password?invitation=%s&&pass=%s
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# Keep this version to not have conflicts between requests and boto3
|
||||
urllib3==1.26.16
|
||||
urllib3==2.2.3
|
||||
requests==2.32.3
|
||||
boto3==1.35.76
|
||||
pyjwt==2.10.1
|
||||
|
|
@ -19,4 +18,4 @@ python-decouple==3.8
|
|||
pydantic[email]==2.10.3
|
||||
apscheduler==3.11.0
|
||||
|
||||
azure-storage-blob==12.23.1
|
||||
azure-storage-blob==12.24.0
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
# Keep this version to not have conflicts between requests and boto3
|
||||
urllib3==1.26.16
|
||||
urllib3==2.2.3
|
||||
requests==2.32.3
|
||||
boto3==1.35.76
|
||||
pyjwt==2.10.1
|
||||
|
|
@ -19,4 +18,4 @@ pydantic[email]==2.10.3
|
|||
apscheduler==3.11.0
|
||||
|
||||
redis==5.2.0
|
||||
azure-storage-blob==12.23.1
|
||||
azure-storage-blob==12.24.0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# Keep this version to not have conflicts between requests and boto3
|
||||
urllib3==1.26.16
|
||||
urllib3==2.2.3
|
||||
requests==2.32.3
|
||||
boto3==1.35.76
|
||||
pyjwt==2.10.1
|
||||
|
|
@ -20,12 +19,13 @@ python-decouple==3.8
|
|||
pydantic[email]==2.10.3
|
||||
apscheduler==3.11.0
|
||||
|
||||
redis==5.2.1
|
||||
|
||||
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
|
||||
#--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml
|
||||
python3-saml==1.16.0
|
||||
--no-binary=lxml
|
||||
python-multipart==0.0.17
|
||||
python-multipart==0.0.18
|
||||
|
||||
redis==5.2.0
|
||||
#confluent-kafka==2.1.0
|
||||
azure-storage-blob==12.23.1
|
||||
azure-storage-blob==12.24.0
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ function BackendLogsPanel() {
|
|||
) : null}
|
||||
|
||||
<div className={'ml-auto'} />
|
||||
<Segmented options={[{ label: 'All Tabs', value: 'all' }]} />
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,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';
|
||||
|
|
@ -133,17 +134,66 @@ function WebOverviewPanelCont() {
|
|||
'ERRORS',
|
||||
'NETWORK',
|
||||
]);
|
||||
const globalTabs = ['FRUSTRATIONS', 'ERRORS']
|
||||
|
||||
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]);
|
||||
|
||||
console.log(showSingleTab, frustrationsList, performanceChartData);
|
||||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
const resourceList = resourceListUnmap
|
||||
|
|
@ -168,7 +218,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 +248,7 @@ function WebOverviewPanelCont() {
|
|||
sessionId={sessionId}
|
||||
setZoomTab={setZoomTab}
|
||||
zoomTab={zoomTab}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -238,6 +300,7 @@ function PanelComponent({
|
|||
spotTime,
|
||||
spotEndTime,
|
||||
onClose,
|
||||
showSingleTab,
|
||||
}: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -281,6 +344,7 @@ function PanelComponent({
|
|||
</div>
|
||||
{isSpot ? null : (
|
||||
<div className="flex items-center h-20 mr-4 gap-2">
|
||||
<TabSelector />
|
||||
<TimelineZoomButton />
|
||||
<FeatureSelection
|
||||
list={selectedFeatures}
|
||||
|
|
@ -318,6 +382,7 @@ function PanelComponent({
|
|||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
title={feature}
|
||||
disabled={!showSingleTab}
|
||||
list={resources[feature]}
|
||||
renderElement={(pointer: any[], isGrouped: boolean) => (
|
||||
<TimelinePointer
|
||||
|
|
|
|||
|
|
@ -13,9 +13,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 +83,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
}
|
||||
|
||||
return groupedItems;
|
||||
}, [list]);
|
||||
}, [list.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -105,7 +106,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
</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
|
||||
|
|
|
|||
|
|
@ -2,81 +2,103 @@ import React from 'react';
|
|||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
|
||||
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={
|
||||
'absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center'
|
||||
}
|
||||
>
|
||||
<div className={'text-disabled-text decoration-dotted'}>Disabled for all tabs</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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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;
|
||||
|
||||
|
|
@ -459,13 +460,16 @@ function Performance() {
|
|||
<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 gap-2'}>
|
||||
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Heap Size"
|
||||
value={formatBytes(userDeviceHeapSize)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { Icon } from 'UI';
|
|||
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
|
||||
|
||||
import ControlButton from './ControlButton';
|
||||
import { WebEventsList } from "./EventsList";
|
||||
import Timeline from './Timeline';
|
||||
import PlayerControls from './components/PlayerControls';
|
||||
import styles from './controls.module.css';
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerCon
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { getTimelinePosition } from './getTimelinePosition'
|
||||
|
||||
function EventsList({ scale }: { scale: number }) {
|
||||
function EventsList() {
|
||||
const { store } = useContext(PlayerContext);
|
||||
|
||||
const { tabStates, eventCount } = store.get();
|
||||
const { eventCount, endTime } = store.get();
|
||||
const tabStates = store.get().tabStates;
|
||||
const scale = 100 / endTime;
|
||||
const events = React.useMemo(() => {
|
||||
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
|
||||
}, [eventCount]);
|
||||
|
|
@ -34,11 +36,12 @@ function EventsList({ scale }: { scale: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MobileEventsList({ scale }: { scale: number }) {
|
||||
function MobileEventsList() {
|
||||
const { store } = useContext(MobilePlayerContext);
|
||||
const { eventList } = store.get();
|
||||
const { eventList, endTime } = store.get();
|
||||
const events = eventList.filter(e => e.type !== 'SWIPE')
|
||||
|
||||
const scale = 100/endTime;
|
||||
return (
|
||||
<>
|
||||
{events.map((e) => (
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ import NotesList from './NotesList';
|
|||
import SkipIntervalsList from './SkipIntervalsList';
|
||||
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
|
||||
|
||||
interface IProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
function Timeline({ isMobile }: { isMobile: boolean }) {
|
||||
const { player, store } = useContext(PlayerContext);
|
||||
const [wasPlaying, setWasPlaying] = useState(false);
|
||||
const [maxWidth, setMaxWidth] = useState(0);
|
||||
|
|
@ -126,6 +122,7 @@ function Timeline(props: IProps) {
|
|||
return Math.max(Math.round(p * targetTime), 0);
|
||||
};
|
||||
|
||||
console.log(devtoolsLoading , domLoading, !ready)
|
||||
return (
|
||||
<div
|
||||
className="flex items-center absolute w-full"
|
||||
|
|
@ -158,7 +155,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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
* }
|
||||
*
|
||||
* */
|
||||
* */
|
||||
|
|
|
|||
|
|
@ -9,6 +9,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 +94,7 @@ function ConsolePanel({
|
|||
sessionStore: { devTools },
|
||||
uiPlayerStore,
|
||||
} = useStore();
|
||||
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
|
|
@ -109,12 +111,22 @@ 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(
|
||||
|
|
@ -180,15 +192,18 @@ 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="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
</BottomBlock.Header>
|
||||
{/* @ts-ignore */}
|
||||
|
|
@ -211,6 +226,8 @@ function ConsolePanel({
|
|||
iconProps={getIconProps(log.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(log)}
|
||||
showSingleTab={showSingleTab}
|
||||
getTabNum={getTabNum}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import JumpButton from 'Shared/DevTools/JumpButton';
|
||||
import { Tag } from 'antd';
|
||||
import TabTag from "../TabTag";
|
||||
|
||||
interface Props {
|
||||
log: any;
|
||||
|
|
@ -10,6 +12,8 @@ interface Props {
|
|||
renderWithNL?: any;
|
||||
style?: any;
|
||||
onClick?: () => void;
|
||||
getTabNum: (tab: string) => number;
|
||||
showSingleTab: boolean;
|
||||
}
|
||||
function ConsoleRow(props: Props) {
|
||||
const { log, iconProps, jump, renderWithNL, style } = props;
|
||||
|
|
@ -41,11 +45,12 @@ function ConsoleRow(props: Props) {
|
|||
|
||||
const titleLine = lines[0];
|
||||
const restLines = lines.slice(1);
|
||||
const logSource = props.showSingleTab ? -1 : props.getTabNum(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 flex items-start gap-1 py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
{
|
||||
info: !log.isYellow && !log.isRed,
|
||||
warn: log.isYellow,
|
||||
|
|
@ -55,11 +60,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 tabNum={logSource} />}
|
||||
<Icon size="14" {...iconProps} />
|
||||
<div key={log.key} data-scroll-item={log.isRed}>
|
||||
<div className="flex items-start text-sm ">
|
||||
<div className="flex items-start text-sm">
|
||||
<div className={cn('flex items-start', { 'cursor-pointer underline decoration-dotted decoration-gray-400': !!log.errorId })}>
|
||||
{canExpand && (
|
||||
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ResourceType, Timed } from 'Player';
|
||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||
import WebPlayer from 'Player/web/WebPlayer';
|
||||
import { Duration } from 'luxon';
|
||||
import TabTag from "../TabTag";
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
|
|
@ -20,10 +20,10 @@ 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 +57,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 +73,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 +111,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 (
|
||||
|
|
@ -184,7 +167,8 @@ function NetworkPanelCont({
|
|||
panelHeight: number;
|
||||
}) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { sessionStore } = useStore();
|
||||
const { sessionStore, uiPlayerStore } = useStore();
|
||||
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
const {
|
||||
domContentLoadedTime,
|
||||
|
|
@ -193,6 +177,10 @@ function NetworkPanelCont({
|
|||
tabStates,
|
||||
currentTab,
|
||||
} = store.get();
|
||||
const tabsArr = Object.keys(tabStates);
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
|
|
@ -200,7 +188,20 @@ 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);
|
||||
|
||||
return (
|
||||
<NetworkPanelComp
|
||||
|
|
@ -216,6 +217,8 @@ function NetworkPanelCont({
|
|||
startedAt={startedAt}
|
||||
websocketList={websocketList as WSMessage[]}
|
||||
websocketListNow={websocketListNow as WSMessage[]}
|
||||
getTabNum={getTabNum}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -301,6 +304,8 @@ interface Props {
|
|||
onClose?: () => void;
|
||||
activeOutsideIndex?: number;
|
||||
isSpot?: boolean;
|
||||
getTabNum?: (tab: string) => number;
|
||||
showSingleTab?: boolean;
|
||||
}
|
||||
|
||||
export const NetworkPanelComp = observer(
|
||||
|
|
@ -323,6 +328,8 @@ export const NetworkPanelComp = observer(
|
|||
onClose,
|
||||
activeOutsideIndex,
|
||||
isSpot,
|
||||
getTabNum,
|
||||
showSingleTab,
|
||||
}: Props) => {
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
|
||||
const { showModal } = useModal();
|
||||
|
|
@ -507,6 +514,55 @@ 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) {
|
||||
cols.unshift({
|
||||
label: 'Source',
|
||||
width: 64,
|
||||
render: (r: Record<string, any>) => <div>Tab {getTabNum?.(r.tabId) ?? 0}</div>,
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}, [showSingleTab])
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
|
|
@ -529,16 +585,19 @@ 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'}>
|
||||
<TabSelector />
|
||||
<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>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
||||
|
|
@ -613,49 +672,7 @@ 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)} />
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { typeList } from 'Types/session/stackEvent';
|
|||
import StackEventRow from 'Shared/DevTools/StackEventRow';
|
||||
|
||||
import StackEventModal from '../StackEventModal';
|
||||
import { Segmented } from 'antd'
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
|
@ -175,15 +176,18 @@ const EventsPanel = observer(({
|
|||
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'}>
|
||||
<Segmented options={[{ label: 'All Tabs', value: 'all' }]} />
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
|
|
|
|||
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { Segmented } from 'antd'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
function TabSelector() {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const currentValue = uiPlayerStore.dataSource;
|
||||
const options = [
|
||||
{ label: 'All Tabs', value: 'all' },
|
||||
{ label: 'Current Tab', value: 'current' }
|
||||
]
|
||||
|
||||
const onChange = (value: 'all' | 'current') => {
|
||||
uiPlayerStore.changeDataSource(value)
|
||||
}
|
||||
return (
|
||||
<Segmented options={options} value={currentValue} onChange={onChange} />
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TabSelector)
|
||||
11
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
11
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
|
||||
function TabTag({ tabNum }: { tabNum?: React.ReactNode }) {
|
||||
return (
|
||||
<div className={'w-fit px-2 border border-gray-light rounded text-sm whitespace-nowrap'}>
|
||||
{tabNum}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabTag
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ export default class MessageLoader {
|
|||
try {
|
||||
await this.loadMobs();
|
||||
} catch (sessionLoadError) {
|
||||
console.info('!', sessionLoadError);
|
||||
try {
|
||||
await this.loadEFSMobs();
|
||||
} catch (unprocessedLoadError) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
@ -414,8 +406,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);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ clickhouse: &clickhouse
|
|||
password: ""
|
||||
service:
|
||||
webPort: 9000
|
||||
dataPort: 8123
|
||||
|
||||
# For enterpriseEdition
|
||||
quickwit: &quickwit
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue