pulled dev
This commit is contained in:
commit
c6d64fc986
113 changed files with 3535 additions and 1692 deletions
57
api/chalicelib/core/product_analytics/autocomplete.py
Normal file
57
api/chalicelib/core/product_analytics/autocomplete.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from typing import Optional
|
||||
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.ch_client import ClickHouseClient
|
||||
|
||||
|
||||
def search_events(project_id: int, q: Optional[str] = None):
|
||||
with ClickHouseClient() as ch_client:
|
||||
full_args = {"project_id": project_id, "limit": 20}
|
||||
|
||||
constraints = ["project_id = %(project_id)s",
|
||||
"_timestamp >= now()-INTERVAL 1 MONTH"]
|
||||
if q:
|
||||
constraints += ["value ILIKE %(q)s"]
|
||||
full_args["q"] = helper.string_to_sql_like(q)
|
||||
query = ch_client.format(
|
||||
f"""SELECT value,data_count
|
||||
FROM product_analytics.autocomplete_events_grouped
|
||||
WHERE {" AND ".join(constraints)}
|
||||
ORDER BY data_count DESC
|
||||
LIMIT %(limit)s;""",
|
||||
parameters=full_args)
|
||||
rows = ch_client.execute(query)
|
||||
|
||||
return {"values": helper.list_to_camel_case(rows), "_src": 2}
|
||||
|
||||
|
||||
def search_properties(project_id: int, property_name: Optional[str] = None, event_name: Optional[str] = None,
|
||||
q: Optional[str] = None):
|
||||
with ClickHouseClient() as ch_client:
|
||||
select = "value"
|
||||
full_args = {"project_id": project_id, "limit": 20,
|
||||
"event_name": event_name, "property_name": property_name}
|
||||
|
||||
constraints = ["project_id = %(project_id)s",
|
||||
"_timestamp >= now()-INTERVAL 1 MONTH"]
|
||||
if event_name:
|
||||
constraints += ["event_name = %(event_name)s"]
|
||||
if property_name and q:
|
||||
constraints += ["property_name = %(property_name)s"]
|
||||
elif property_name:
|
||||
select = "DISTINCT ON(property_name) property_name AS value"
|
||||
constraints += ["property_name ILIKE %(property_name)s"]
|
||||
full_args["property_name"] = helper.string_to_sql_like(property_name)
|
||||
if q:
|
||||
constraints += ["value ILIKE %(q)s"]
|
||||
full_args["q"] = helper.string_to_sql_like(q)
|
||||
query = ch_client.format(
|
||||
f"""SELECT {select},data_count
|
||||
FROM product_analytics.autocomplete_event_properties_grouped
|
||||
WHERE {" AND ".join(constraints)}
|
||||
ORDER BY data_count DESC
|
||||
LIMIT %(limit)s;""",
|
||||
parameters=full_args)
|
||||
rows = ch_client.execute(query)
|
||||
|
||||
return {"values": helper.list_to_camel_case(rows), "_src": 2}
|
||||
|
|
@ -7,30 +7,69 @@ from chalicelib.utils.ch_client import ClickHouseClient
|
|||
from chalicelib.utils.exp_ch_helper import get_sub_condition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
PREDEFINED_EVENTS = {
|
||||
"CLICK": "String",
|
||||
"INPUT": "String",
|
||||
"LOCATION": "String",
|
||||
"ERROR": "String",
|
||||
"PERFORMANCE": "String",
|
||||
"REQUEST": "String"
|
||||
}
|
||||
|
||||
|
||||
def get_events(project_id: int, page: schemas.PaginatedSchema):
|
||||
with ClickHouseClient() as ch_client:
|
||||
r = ch_client.format(
|
||||
"""SELECT DISTINCT ON(event_name,auto_captured)
|
||||
COUNT(1) OVER () AS total,
|
||||
event_name AS name, display_name, description,
|
||||
auto_captured
|
||||
FROM product_analytics.all_events
|
||||
WHERE project_id=%(project_id)s
|
||||
ORDER BY auto_captured,display_name
|
||||
LIMIT %(limit)s OFFSET %(offset)s;""",
|
||||
"""SELECT DISTINCT
|
||||
ON(event_name,auto_captured)
|
||||
COUNT (1) OVER () AS total,
|
||||
event_name AS name, display_name, description,
|
||||
auto_captured
|
||||
FROM product_analytics.all_events
|
||||
WHERE project_id=%(project_id)s
|
||||
ORDER BY auto_captured, display_name
|
||||
LIMIT %(limit)s
|
||||
OFFSET %(offset)s;""",
|
||||
parameters={"project_id": project_id, "limit": page.limit, "offset": (page.page - 1) * page.limit})
|
||||
rows = ch_client.execute(r)
|
||||
if len(rows) == 0:
|
||||
return {"total": 0, "list": []}
|
||||
return {"total": len(PREDEFINED_EVENTS), "list": [{
|
||||
"name": e,
|
||||
"displayName": "",
|
||||
"description": "",
|
||||
"autoCaptured": True,
|
||||
"id": "event_0",
|
||||
"dataType": "string",
|
||||
"possibleTypes": [
|
||||
"string"
|
||||
],
|
||||
"_foundInPredefinedList": False
|
||||
} for e in PREDEFINED_EVENTS]}
|
||||
total = rows[0]["total"]
|
||||
rows = helper.list_to_camel_case(rows)
|
||||
for i, row in enumerate(rows):
|
||||
row["id"] = f"event_{i}"
|
||||
row["icon"] = None
|
||||
row["dataType"] = "string"
|
||||
row["possibleTypes"] = ["string"]
|
||||
row["_foundInPredefinedList"] = True
|
||||
row.pop("total")
|
||||
return {"total": total, "list": helper.list_to_camel_case(rows)}
|
||||
keys = [r["name"] for r in rows]
|
||||
for e in PREDEFINED_EVENTS:
|
||||
if e not in keys:
|
||||
total += 1
|
||||
rows.append({
|
||||
"name": e,
|
||||
"displayName": "",
|
||||
"description": "",
|
||||
"autoCaptured": True,
|
||||
"id": "event_0",
|
||||
"dataType": "string",
|
||||
"possibleTypes": [
|
||||
"string"
|
||||
],
|
||||
"_foundInPredefinedList": False
|
||||
})
|
||||
return {"total": total, "list": rows}
|
||||
|
||||
|
||||
def search_events(project_id: int, data: schemas.EventsSearchPayloadSchema):
|
||||
|
|
@ -109,31 +148,33 @@ def search_events(project_id: int, data: schemas.EventsSearchPayloadSchema):
|
|||
parameters=full_args)
|
||||
rows = ch_client.execute(query)
|
||||
if len(rows) == 0:
|
||||
return {"total": 0, "rows": [], "src": 2}
|
||||
return {"total": 0, "rows": [], "_src": 2}
|
||||
total = rows[0]["total"]
|
||||
for r in rows:
|
||||
r.pop("total")
|
||||
return {"total": total, "rows": rows, "src": 2}
|
||||
return {"total": total, "rows": rows, "_src": 2}
|
||||
|
||||
|
||||
def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
|
||||
with ClickHouseClient() as ch_client:
|
||||
r = ch_client.format(
|
||||
"""SELECT COUNT(1) OVER () AS total,
|
||||
all_events.event_name AS name,
|
||||
*
|
||||
FROM product_analytics.all_events
|
||||
WHERE project_id=%(project_id)s
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s OFFSET %(offset)s;""",
|
||||
"""SELECT COUNT(1) OVER () AS total, all_events.event_name AS name,
|
||||
*
|
||||
FROM product_analytics.all_events
|
||||
WHERE project_id = %(project_id)s
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s
|
||||
OFFSET %(offset)s;""",
|
||||
parameters={"project_id": project_id, "limit": page.limit, "offset": (page.page - 1) * page.limit})
|
||||
rows = ch_client.execute(r)
|
||||
if len(rows) == 0:
|
||||
return {"total": 0, "list": []}
|
||||
total = rows[0]["total"]
|
||||
rows = helper.list_to_camel_case(rows)
|
||||
for i, row in enumerate(rows):
|
||||
row["id"] = f"event_{i}"
|
||||
row["icon"] = None
|
||||
row["dataType"] = "string"
|
||||
row["possibleTypes"] = ["string"]
|
||||
row["_foundInPredefinedList"] = True
|
||||
row.pop("total")
|
||||
return {"total": total, "list": helper.list_to_camel_case(rows)}
|
||||
return {"total": total, "list": rows}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,77 @@
|
|||
import re
|
||||
from functools import cache
|
||||
|
||||
import schemas
|
||||
from chalicelib.utils import helper, exp_ch_helper
|
||||
from chalicelib.utils.ch_client import ClickHouseClient
|
||||
|
||||
|
||||
@cache
|
||||
def get_predefined_property_types():
|
||||
with ClickHouseClient() as ch_client:
|
||||
properties_type = ch_client.execute("""\
|
||||
SELECT type
|
||||
FROM system.columns
|
||||
WHERE database = 'product_analytics'
|
||||
AND table = 'events'
|
||||
AND name = '$properties';""")
|
||||
if len(properties_type) == 0:
|
||||
return {}
|
||||
properties_type = properties_type[0]["type"]
|
||||
|
||||
pattern = r'(\w+)\s+(Enum8\([^\)]+\)|[A-Za-z0-9_]+(?:\([^\)]+\))?)'
|
||||
|
||||
# Find all matches
|
||||
matches = re.findall(pattern, properties_type)
|
||||
|
||||
# Create a dictionary of attribute names and types
|
||||
attributes = {match[0]: match[1] for match in matches}
|
||||
return attributes
|
||||
PREDEFINED_PROPERTIES = {
|
||||
"label": "String",
|
||||
"hesitation_time": "UInt32",
|
||||
"name": "String",
|
||||
"payload": "String",
|
||||
"level": "Enum8",
|
||||
"source": "Enum8",
|
||||
"message": "String",
|
||||
"error_id": "String",
|
||||
"duration": "UInt16",
|
||||
"context": "Enum8",
|
||||
"url_host": "String",
|
||||
"url_path": "String",
|
||||
"url_hostpath": "String",
|
||||
"request_start": "UInt16",
|
||||
"response_start": "UInt16",
|
||||
"response_end": "UInt16",
|
||||
"dom_content_loaded_event_start": "UInt16",
|
||||
"dom_content_loaded_event_end": "UInt16",
|
||||
"load_event_start": "UInt16",
|
||||
"load_event_end": "UInt16",
|
||||
"first_paint": "UInt16",
|
||||
"first_contentful_paint_time": "UInt16",
|
||||
"speed_index": "UInt16",
|
||||
"visually_complete": "UInt16",
|
||||
"time_to_interactive": "UInt16",
|
||||
"ttfb": "UInt16",
|
||||
"ttlb": "UInt16",
|
||||
"response_time": "UInt16",
|
||||
"dom_building_time": "UInt16",
|
||||
"dom_content_loaded_event_time": "UInt16",
|
||||
"load_event_time": "UInt16",
|
||||
"min_fps": "UInt8",
|
||||
"avg_fps": "UInt8",
|
||||
"max_fps": "UInt8",
|
||||
"min_cpu": "UInt8",
|
||||
"avg_cpu": "UInt8",
|
||||
"max_cpu": "UInt8",
|
||||
"min_total_js_heap_size": "UInt64",
|
||||
"avg_total_js_heap_size": "UInt64",
|
||||
"max_total_js_heap_size": "UInt64",
|
||||
"min_used_js_heap_size": "UInt64",
|
||||
"avg_used_js_heap_size": "UInt64",
|
||||
"max_used_js_heap_size": "UInt64",
|
||||
"method": "Enum8",
|
||||
"status": "UInt16",
|
||||
"success": "UInt8",
|
||||
"request_body": "String",
|
||||
"response_body": "String",
|
||||
"transfer_size": "UInt32",
|
||||
"selector": "String",
|
||||
"normalized_x": "Float32",
|
||||
"normalized_y": "Float32",
|
||||
"message_id": "UInt64"
|
||||
}
|
||||
|
||||
|
||||
def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
|
||||
with ClickHouseClient() as ch_client:
|
||||
r = ch_client.format(
|
||||
"""SELECT COUNT(1) OVER () AS total,
|
||||
property_name AS name, display_name,
|
||||
array_agg(DISTINCT event_properties.value_type) AS possible_types
|
||||
FROM product_analytics.all_properties
|
||||
"""SELECT COUNT(1) OVER () AS total, property_name AS name,
|
||||
display_name,
|
||||
array_agg(DISTINCT event_properties.value_type) AS possible_types
|
||||
FROM product_analytics.all_properties
|
||||
LEFT JOIN product_analytics.event_properties USING (project_id, property_name)
|
||||
WHERE all_properties.project_id=%(project_id)s
|
||||
GROUP BY property_name,display_name
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s OFFSET %(offset)s;""",
|
||||
WHERE all_properties.project_id = %(project_id)s
|
||||
GROUP BY property_name, display_name
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s
|
||||
OFFSET %(offset)s;""",
|
||||
parameters={"project_id": project_id,
|
||||
"limit": page.limit,
|
||||
"offset": (page.page - 1) * page.limit})
|
||||
|
|
@ -49,56 +80,80 @@ def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
|
|||
return {"total": 0, "list": []}
|
||||
total = properties[0]["total"]
|
||||
properties = helper.list_to_camel_case(properties)
|
||||
predefined_properties = get_predefined_property_types()
|
||||
for i, p in enumerate(properties):
|
||||
p["id"] = f"prop_{i}"
|
||||
p["icon"] = None
|
||||
if p["name"] in predefined_properties:
|
||||
p["possibleTypes"].insert(0, predefined_properties[p["name"]])
|
||||
p["possibleTypes"] = list(set(p["possibleTypes"]))
|
||||
p["possibleTypes"] = exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])
|
||||
p["_foundInPredefinedList"] = False
|
||||
if p["name"] in PREDEFINED_PROPERTIES:
|
||||
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
|
||||
p["_foundInPredefinedList"] = True
|
||||
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
|
||||
p.pop("total")
|
||||
keys = [p["name"] for p in properties]
|
||||
for p in PREDEFINED_PROPERTIES:
|
||||
if p not in keys:
|
||||
total += 1
|
||||
properties.append({
|
||||
"name": p,
|
||||
"displayName": "",
|
||||
"possibleTypes": [
|
||||
],
|
||||
"id": f"prop_{len(properties) + 1}",
|
||||
"_foundInPredefinedList": False,
|
||||
"dataType": PREDEFINED_PROPERTIES[p]
|
||||
})
|
||||
return {"total": total, "list": properties}
|
||||
|
||||
|
||||
def get_event_properties(project_id: int, event_name):
|
||||
with ClickHouseClient() as ch_client:
|
||||
r = ch_client.format(
|
||||
"""SELECT all_properties.property_name,
|
||||
all_properties.display_name
|
||||
FROM product_analytics.event_properties
|
||||
INNER JOIN product_analytics.all_properties USING (property_name)
|
||||
WHERE event_properties.project_id=%(project_id)s
|
||||
AND all_properties.project_id=%(project_id)s
|
||||
AND event_properties.event_name=%(event_name)s
|
||||
ORDER BY created_at;""",
|
||||
"""SELECT all_properties.property_name AS name,
|
||||
all_properties.display_name,
|
||||
array_agg(DISTINCT event_properties.value_type) AS possible_types
|
||||
FROM product_analytics.event_properties
|
||||
INNER JOIN product_analytics.all_properties USING (property_name)
|
||||
WHERE event_properties.project_id = %(project_id)s
|
||||
AND all_properties.project_id = %(project_id)s
|
||||
AND event_properties.event_name = %(event_name)s
|
||||
GROUP BY ALL
|
||||
ORDER BY 1;""",
|
||||
parameters={"project_id": project_id, "event_name": event_name})
|
||||
properties = ch_client.execute(r)
|
||||
properties = helper.list_to_camel_case(properties)
|
||||
for i, p in enumerate(properties):
|
||||
p["id"] = f"prop_{i}"
|
||||
p["_foundInPredefinedList"] = False
|
||||
if p["name"] in PREDEFINED_PROPERTIES:
|
||||
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
|
||||
p["_foundInPredefinedList"] = True
|
||||
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
|
||||
|
||||
return helper.list_to_camel_case(properties)
|
||||
return properties
|
||||
|
||||
|
||||
def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
|
||||
with ClickHouseClient() as ch_client:
|
||||
r = ch_client.format(
|
||||
"""SELECT COUNT(1) OVER () AS total,
|
||||
all_properties.property_name AS name,
|
||||
all_properties.*,
|
||||
possible_types.values AS possible_types,
|
||||
possible_values.values AS sample_values
|
||||
FROM product_analytics.all_properties
|
||||
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value_type) AS values
|
||||
FROM product_analytics.event_properties
|
||||
WHERE project_id=%(project_id)s
|
||||
GROUP BY 1, 2) AS possible_types
|
||||
USING (project_id, property_name)
|
||||
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value) AS values
|
||||
FROM product_analytics.property_values_samples
|
||||
WHERE project_id=%(project_id)s
|
||||
GROUP BY 1, 2) AS possible_values USING (project_id, property_name)
|
||||
WHERE project_id=%(project_id)s
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s OFFSET %(offset)s;""",
|
||||
"""SELECT COUNT(1) OVER () AS total, all_properties.property_name AS name,
|
||||
all_properties.*,
|
||||
possible_types.values AS possible_types,
|
||||
possible_values.values AS sample_values
|
||||
FROM product_analytics.all_properties
|
||||
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value_type) AS
|
||||
values
|
||||
FROM product_analytics.event_properties
|
||||
WHERE project_id=%(project_id)s
|
||||
GROUP BY 1, 2) AS possible_types
|
||||
USING (project_id, property_name)
|
||||
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value) AS
|
||||
values
|
||||
FROM product_analytics.property_values_samples
|
||||
WHERE project_id=%(project_id)s
|
||||
GROUP BY 1, 2) AS possible_values USING (project_id, property_name)
|
||||
WHERE project_id = %(project_id)s
|
||||
ORDER BY display_name
|
||||
LIMIT %(limit)s
|
||||
OFFSET %(offset)s;""",
|
||||
parameters={"project_id": project_id,
|
||||
"limit": page.limit,
|
||||
"offset": (page.page - 1) * page.limit})
|
||||
|
|
@ -108,6 +163,5 @@ def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
|
|||
total = properties[0]["total"]
|
||||
for i, p in enumerate(properties):
|
||||
p["id"] = f"prop_{i}"
|
||||
p["icon"] = None
|
||||
p.pop("total")
|
||||
return {"total": total, "list": helper.list_to_camel_case(properties)}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
|||
return {
|
||||
'total': 0,
|
||||
'sessions': [],
|
||||
'src': 2
|
||||
'_src': 2
|
||||
}
|
||||
if project.platform == "web":
|
||||
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
|
||||
|
|
@ -216,7 +216,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
|||
return {
|
||||
'total': total,
|
||||
'sessions': sessions_list,
|
||||
'src': 2
|
||||
'_src': 2
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
|||
return {
|
||||
'total': 0,
|
||||
'sessions': [],
|
||||
'src': 1
|
||||
'_src': 1
|
||||
}
|
||||
full_args, query_part = sessions_legacy.search_query_parts(data=data, error_status=error_status,
|
||||
errors_only=errors_only,
|
||||
|
|
@ -177,7 +177,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
|||
return {
|
||||
'total': total,
|
||||
'sessions': helper.list_to_camel_case(sessions),
|
||||
'src': 1
|
||||
'_src': 1
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -240,7 +240,7 @@ def search_by_metadata(tenant_id, user_id, m_key, m_value, project_id=None):
|
|||
cur.execute("\nUNION\n".join(sub_queries))
|
||||
rows = cur.fetchall()
|
||||
for i in rows:
|
||||
i["src"] = 1
|
||||
i["_src"] = 1
|
||||
results[str(i["project_id"])]["sessions"].append(helper.dict_to_camel_case(i))
|
||||
return results
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ def search_by_metadata(tenant_id, user_id, m_key, m_value, project_id=None):
|
|||
def search_sessions_by_ids(project_id: int, session_ids: list, sort_by: str = 'session_id',
|
||||
ascending: bool = False) -> dict:
|
||||
if session_ids is None or len(session_ids) == 0:
|
||||
return {"total": 0, "sessions": [], "src": 1}
|
||||
return {"total": 0, "sessions": [], "_src": 1}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
meta_keys = metadata.get(project_id=project_id)
|
||||
params = {"project_id": project_id, "session_ids": tuple(session_ids)}
|
||||
|
|
@ -267,4 +267,4 @@ def search_sessions_by_ids(project_id: int, session_ids: list, sort_by: str = 's
|
|||
s["metadata"] = {}
|
||||
for m in meta_keys:
|
||||
s["metadata"][m["key"]] = s.pop(f'metadata_{m["index"]}')
|
||||
return {"total": len(rows), "sessions": helper.list_to_camel_case(rows), "src": 1}
|
||||
return {"total": len(rows), "sessions": helper.list_to_camel_case(rows), "_src": 1}
|
||||
|
|
|
|||
|
|
@ -99,12 +99,13 @@ def simplify_clickhouse_type(ch_type: str) -> str:
|
|||
return "int"
|
||||
|
||||
# Floats: Float32, Float64
|
||||
if re.match(r'^float(32|64)$', normalized_type):
|
||||
if re.match(r'^float(32|64)|double$', normalized_type):
|
||||
return "float"
|
||||
|
||||
# Decimal: Decimal(P, S)
|
||||
if normalized_type.startswith("decimal"):
|
||||
return "decimal"
|
||||
# return "decimal"
|
||||
return "float"
|
||||
|
||||
# Date/DateTime
|
||||
if normalized_type.startswith("date"):
|
||||
|
|
@ -120,11 +121,13 @@ def simplify_clickhouse_type(ch_type: str) -> str:
|
|||
|
||||
# UUID
|
||||
if normalized_type.startswith("uuid"):
|
||||
return "uuid"
|
||||
# return "uuid"
|
||||
return "string"
|
||||
|
||||
# Enums: Enum8(...) or Enum16(...)
|
||||
if normalized_type.startswith("enum8") or normalized_type.startswith("enum16"):
|
||||
return "enum"
|
||||
# return "enum"
|
||||
return "string"
|
||||
|
||||
# Arrays: Array(T)
|
||||
if normalized_type.startswith("array"):
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
urllib3==2.3.0
|
||||
urllib3==2.4.0
|
||||
requests==2.32.3
|
||||
boto3==1.37.21
|
||||
boto3==1.38.10
|
||||
pyjwt==2.10.1
|
||||
psycopg2-binary==2.9.10
|
||||
psycopg[pool,binary]==3.2.6
|
||||
clickhouse-connect==0.8.15
|
||||
elasticsearch==8.17.2
|
||||
psycopg[pool,binary]==3.2.7
|
||||
clickhouse-connect==0.8.17
|
||||
elasticsearch==9.0.1
|
||||
jira==3.8.0
|
||||
cachetools==5.5.2
|
||||
|
||||
fastapi==0.115.12
|
||||
uvicorn[standard]==0.34.0
|
||||
uvicorn[standard]==0.34.2
|
||||
python-decouple==3.8
|
||||
pydantic[email]==2.10.6
|
||||
pydantic[email]==2.11.4
|
||||
apscheduler==3.11.0
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
urllib3==2.3.0
|
||||
urllib3==2.4.0
|
||||
requests==2.32.3
|
||||
boto3==1.37.21
|
||||
boto3==1.38.10
|
||||
pyjwt==2.10.1
|
||||
psycopg2-binary==2.9.10
|
||||
psycopg[pool,binary]==3.2.6
|
||||
clickhouse-connect==0.8.15
|
||||
elasticsearch==8.17.2
|
||||
psycopg[pool,binary]==3.2.7
|
||||
clickhouse-connect==0.8.17
|
||||
elasticsearch==9.0.1
|
||||
jira==3.8.0
|
||||
cachetools==5.5.2
|
||||
|
||||
fastapi==0.115.12
|
||||
uvicorn[standard]==0.34.0
|
||||
uvicorn[standard]==0.34.2
|
||||
python-decouple==3.8
|
||||
pydantic[email]==2.10.6
|
||||
pydantic[email]==2.11.4
|
||||
apscheduler==3.11.0
|
||||
|
||||
redis==5.2.1
|
||||
redis==6.0.0
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ from fastapi import Body, Depends, Query
|
|||
|
||||
import schemas
|
||||
from chalicelib.core import metadata
|
||||
from chalicelib.core.product_analytics import events, properties
|
||||
from chalicelib.core.product_analytics import events, properties, autocomplete
|
||||
from or_dependencies import OR_context
|
||||
from routers.base import get_routers
|
||||
from typing import Optional
|
||||
|
||||
public_app, app, app_apikey = get_routers()
|
||||
|
||||
|
|
@ -53,3 +54,20 @@ def get_all_lexicon_events(projectId: int, filter_query: Annotated[schemas.Pagin
|
|||
def get_all_lexicon_properties(projectId: int, filter_query: Annotated[schemas.PaginatedSchema, Query()],
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": properties.get_lexicon(project_id=projectId, page=filter_query)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/events/autocomplete', tags=["autocomplete"])
|
||||
def autocomplete_events(projectId: int, q: Optional[str] = None,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": autocomplete.search_events(project_id=projectId, q=None if not q or len(q) == 0 else q)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/properties/autocomplete', tags=["autocomplete"])
|
||||
def autocomplete_properties(projectId: int, propertyName: str, eventName: Optional[str] = None,
|
||||
q: Optional[str] = None, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": autocomplete.search_properties(project_id=projectId,
|
||||
event_name=None if not eventName \
|
||||
or len(eventName) == 0 else eventName,
|
||||
property_name=None if not propertyName \
|
||||
or len(propertyName) == 0 else propertyName,
|
||||
q=None if not q or len(q) == 0 else q)}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func main() {
|
|||
messages.MsgMetadata, messages.MsgIssueEvent, messages.MsgSessionStart, messages.MsgSessionEnd,
|
||||
messages.MsgUserID, messages.MsgUserAnonymousID, messages.MsgIntegrationEvent, messages.MsgPerformanceTrackAggr,
|
||||
messages.MsgJSException, messages.MsgResourceTiming, messages.MsgCustomEvent, messages.MsgCustomIssue,
|
||||
messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick,
|
||||
messages.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick,
|
||||
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
||||
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
||||
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ func main() {
|
|||
// Process assets
|
||||
if msg.TypeID() == messages.MsgSetNodeAttributeURLBased ||
|
||||
msg.TypeID() == messages.MsgSetCSSDataURLBased ||
|
||||
msg.TypeID() == messages.MsgCSSInsertRuleURLBased ||
|
||||
msg.TypeID() == messages.MsgAdoptedSSReplaceURLBased ||
|
||||
msg.TypeID() == messages.MsgAdoptedSSInsertRuleURLBased {
|
||||
m := msg.Decode()
|
||||
|
|
|
|||
|
|
@ -133,6 +133,17 @@ func (e *AssetsCache) ParseAssets(msg messages.Message) messages.Message {
|
|||
}
|
||||
newMsg.SetMeta(msg.Meta())
|
||||
return newMsg
|
||||
case *messages.CSSInsertRuleURLBased:
|
||||
if e.shouldSkipAsset(m.BaseURL) {
|
||||
return msg
|
||||
}
|
||||
newMsg := &messages.CSSInsertRule{
|
||||
ID: m.ID,
|
||||
Index: m.Index,
|
||||
Rule: e.handleCSS(m.SessionID(), m.BaseURL, m.Rule),
|
||||
}
|
||||
newMsg.SetMeta(msg.Meta())
|
||||
return newMsg
|
||||
case *messages.AdoptedSSReplaceURLBased:
|
||||
if e.shouldSkipAsset(m.BaseURL) {
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ func (c *connectorImpl) InsertWebInputDuration(session *sessions.Session, msg *m
|
|||
"hesitation_time": nullableUint32(uint32(msg.HesitationTime)),
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal input event: %s", err)
|
||||
|
|
@ -298,6 +299,7 @@ func (c *connectorImpl) InsertMouseThrashing(session *sessions.Session, msg *mes
|
|||
"url_hostpath": hostpath,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal issue event: %s", err)
|
||||
|
|
@ -361,6 +363,7 @@ func (c *connectorImpl) InsertIssue(session *sessions.Session, msg *messages.Iss
|
|||
"url_hostpath": hostpath,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal issue event: %s", err)
|
||||
|
|
@ -457,6 +460,7 @@ func (c *connectorImpl) InsertWebPageEvent(session *sessions.Session, msg *messa
|
|||
"load_event_time": loadEventTime,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal page event: %s", err)
|
||||
|
|
@ -523,6 +527,7 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
|
|||
"url_hostpath": hostpath,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal click event: %s", err)
|
||||
|
|
@ -568,6 +573,7 @@ func (c *connectorImpl) InsertWebErrorEvent(session *sessions.Session, msg *type
|
|||
"message": msg.Message,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal error event: %s", err)
|
||||
|
|
@ -625,6 +631,7 @@ func (c *connectorImpl) InsertWebPerformanceTrackAggr(session *sessions.Session,
|
|||
"max_used_js_heap_size": msg.MaxUsedJSHeapSize,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal performance event: %s", err)
|
||||
|
|
@ -683,6 +690,7 @@ func (c *connectorImpl) InsertRequest(session *sessions.Session, msg *messages.N
|
|||
"url_hostpath": hostpath,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal request event: %s", err)
|
||||
|
|
@ -721,6 +729,7 @@ func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.Cu
|
|||
"payload": msg.Payload,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal custom event: %s", err)
|
||||
|
|
@ -759,6 +768,7 @@ func (c *connectorImpl) InsertGraphQL(session *sessions.Session, msg *messages.G
|
|||
"response_body": nullableString(msg.Response),
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal graphql event: %s", err)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type ErrorEvent struct {
|
|||
Tags map[string]*string
|
||||
OriginType int
|
||||
Url string
|
||||
PageTitle string
|
||||
}
|
||||
|
||||
func WrapJSException(m *JSException) (*ErrorEvent, error) {
|
||||
|
|
@ -37,6 +38,7 @@ func WrapJSException(m *JSException) (*ErrorEvent, error) {
|
|||
Payload: m.Payload,
|
||||
OriginType: m.TypeID(),
|
||||
Url: m.Url,
|
||||
PageTitle: m.PageTitle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +52,7 @@ func WrapIntegrationEvent(m *IntegrationEvent) *ErrorEvent {
|
|||
Payload: m.Payload,
|
||||
OriginType: m.TypeID(),
|
||||
Url: m.Url,
|
||||
PageTitle: m.PageTitle,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ func (d *DeadClickDetector) Handle(message Message, timestamp uint64) Message {
|
|||
*MoveNode,
|
||||
*RemoveNode,
|
||||
*SetCSSData,
|
||||
*CSSInsertRule,
|
||||
*CSSDeleteRule,
|
||||
*SetInputValue,
|
||||
*SetInputChecked:
|
||||
return d.Build()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package messages
|
||||
|
||||
func IsReplayerType(id int) bool {
|
||||
return 1 != id && 17 != id && 23 != id && 24 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 63 != id && 64 != id && 66 != id && 78 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id
|
||||
}
|
||||
|
||||
func IsMobileType(id int) bool {
|
||||
|
|
@ -10,5 +10,5 @@ func IsMobileType(id int) bool {
|
|||
}
|
||||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 60 == id || 61 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||
}
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ func NewMessageIterator(log logger.Logger, messageHandler MessageHandler, messag
|
|||
iter.filter = filter
|
||||
}
|
||||
iter.preFilter = map[int]struct{}{
|
||||
MsgBatchMetadata: {}, MsgTimestamp: {}, MsgSessionStart: {},
|
||||
MsgSessionEnd: {}, MsgSetPageLocation: {}, MsgMobileBatchMeta: {},
|
||||
MsgBatchMetadata: {}, MsgBatchMeta: {}, MsgTimestamp: {},
|
||||
MsgSessionStart: {}, MsgSessionEnd: {}, MsgSetPageLocation: {},
|
||||
MsgMobileBatchMeta: {},
|
||||
}
|
||||
return iter
|
||||
}
|
||||
|
|
@ -151,6 +152,20 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
|
|||
i.version = m.Version
|
||||
i.batchInfo.version = m.Version
|
||||
|
||||
case *BatchMeta: // Is not required to be present in batch since Mobile doesn't have it (though we might change it)
|
||||
if i.messageInfo.Index > 1 { // Might be several 0-0 BatchMeta in a row without an error though
|
||||
return fmt.Errorf("batchMeta found at the end of the batch, info: %s", i.batchInfo.Info())
|
||||
}
|
||||
i.messageInfo.Index = m.PageNo<<32 + m.FirstIndex // 2^32 is the maximum count of messages per page (ha-ha)
|
||||
i.messageInfo.Timestamp = uint64(m.Timestamp)
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("BatchMeta")
|
||||
}
|
||||
// Try to get saved session's page url
|
||||
if savedURL := i.urls.Get(i.messageInfo.batch.sessionID); savedURL != "" {
|
||||
i.messageInfo.Url = savedURL
|
||||
}
|
||||
|
||||
case *Timestamp:
|
||||
i.messageInfo.Timestamp = m.Timestamp
|
||||
if m.Timestamp == 0 {
|
||||
|
|
@ -176,6 +191,7 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
|
|||
|
||||
case *SetPageLocation:
|
||||
i.messageInfo.Url = m.URL
|
||||
i.messageInfo.PageTitle = m.DocumentTitle
|
||||
// Save session page url in cache for using in next batches
|
||||
i.urls.Set(i.messageInfo.batch.sessionID, m.URL)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@ package messages
|
|||
|
||||
func transformDeprecated(msg Message) Message {
|
||||
switch m := msg.(type) {
|
||||
case *JSExceptionDeprecated:
|
||||
return &JSException{
|
||||
Name: m.Name,
|
||||
Message: m.Message,
|
||||
Payload: m.Payload,
|
||||
Metadata: "{}",
|
||||
}
|
||||
case *Fetch:
|
||||
return &NetworkRequest{
|
||||
Type: "fetch",
|
||||
Method: m.Method,
|
||||
URL: m.URL,
|
||||
Request: m.Request,
|
||||
Response: m.Response,
|
||||
Status: m.Status,
|
||||
Timestamp: m.Timestamp,
|
||||
Duration: m.Duration,
|
||||
}
|
||||
case *IssueEventDeprecated:
|
||||
return &IssueEvent{
|
||||
MessageID: m.MessageID,
|
||||
Timestamp: m.Timestamp,
|
||||
Type: m.Type,
|
||||
ContextString: m.ContextString,
|
||||
Context: m.Context,
|
||||
Payload: m.Payload,
|
||||
URL: "",
|
||||
}
|
||||
case *ResourceTimingDeprecated:
|
||||
return &ResourceTiming{
|
||||
Timestamp: m.Timestamp,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ type message struct {
|
|||
Timestamp uint64
|
||||
Index uint64
|
||||
Url string
|
||||
PageTitle string
|
||||
batch *BatchInfo
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ func (m *message) SetMeta(origin *message) {
|
|||
m.Timestamp = origin.Timestamp
|
||||
m.Index = origin.Index
|
||||
m.Url = origin.Url
|
||||
m.PageTitle = origin.PageTitle
|
||||
}
|
||||
|
||||
func (m *message) SessionID() uint64 {
|
||||
|
|
|
|||
|
|
@ -186,6 +186,27 @@ func (msg *SessionStart) TypeID() int {
|
|||
return 1
|
||||
}
|
||||
|
||||
type SessionEndDeprecated struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
}
|
||||
|
||||
func (msg *SessionEndDeprecated) Encode() []byte {
|
||||
buf := make([]byte, 11)
|
||||
buf[0] = 3
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *SessionEndDeprecated) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *SessionEndDeprecated) TypeID() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
type SetPageLocationDeprecated struct {
|
||||
message
|
||||
URL string
|
||||
|
|
@ -711,6 +732,31 @@ func (msg *PageRenderTiming) TypeID() int {
|
|||
return 24
|
||||
}
|
||||
|
||||
type JSExceptionDeprecated struct {
|
||||
message
|
||||
Name string
|
||||
Message string
|
||||
Payload string
|
||||
}
|
||||
|
||||
func (msg *JSExceptionDeprecated) Encode() []byte {
|
||||
buf := make([]byte, 31+len(msg.Name)+len(msg.Message)+len(msg.Payload))
|
||||
buf[0] = 25
|
||||
p := 1
|
||||
p = WriteString(msg.Name, buf, p)
|
||||
p = WriteString(msg.Message, buf, p)
|
||||
p = WriteString(msg.Payload, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *JSExceptionDeprecated) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *JSExceptionDeprecated) TypeID() int {
|
||||
return 25
|
||||
}
|
||||
|
||||
type IntegrationEvent struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
|
|
@ -1013,6 +1059,87 @@ func (msg *SetNodeAttributeDictGlobal) TypeID() int {
|
|||
return 35
|
||||
}
|
||||
|
||||
type CSSInsertRule struct {
|
||||
message
|
||||
ID uint64
|
||||
Rule string
|
||||
Index uint64
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRule) Encode() []byte {
|
||||
buf := make([]byte, 31+len(msg.Rule))
|
||||
buf[0] = 37
|
||||
p := 1
|
||||
p = WriteUint(msg.ID, buf, p)
|
||||
p = WriteString(msg.Rule, buf, p)
|
||||
p = WriteUint(msg.Index, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRule) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRule) TypeID() int {
|
||||
return 37
|
||||
}
|
||||
|
||||
type CSSDeleteRule struct {
|
||||
message
|
||||
ID uint64
|
||||
Index uint64
|
||||
}
|
||||
|
||||
func (msg *CSSDeleteRule) Encode() []byte {
|
||||
buf := make([]byte, 21)
|
||||
buf[0] = 38
|
||||
p := 1
|
||||
p = WriteUint(msg.ID, buf, p)
|
||||
p = WriteUint(msg.Index, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *CSSDeleteRule) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *CSSDeleteRule) TypeID() int {
|
||||
return 38
|
||||
}
|
||||
|
||||
type Fetch struct {
|
||||
message
|
||||
Method string
|
||||
URL string
|
||||
Request string
|
||||
Response string
|
||||
Status uint64
|
||||
Timestamp uint64
|
||||
Duration uint64
|
||||
}
|
||||
|
||||
func (msg *Fetch) Encode() []byte {
|
||||
buf := make([]byte, 71+len(msg.Method)+len(msg.URL)+len(msg.Request)+len(msg.Response))
|
||||
buf[0] = 39
|
||||
p := 1
|
||||
p = WriteString(msg.Method, buf, p)
|
||||
p = WriteString(msg.URL, buf, p)
|
||||
p = WriteString(msg.Request, buf, p)
|
||||
p = WriteString(msg.Response, buf, p)
|
||||
p = WriteUint(msg.Status, buf, p)
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
p = WriteUint(msg.Duration, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *Fetch) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *Fetch) TypeID() int {
|
||||
return 39
|
||||
}
|
||||
|
||||
type Profiler struct {
|
||||
message
|
||||
Name string
|
||||
|
|
@ -1506,6 +1633,39 @@ func (msg *SetNodeFocus) TypeID() int {
|
|||
return 58
|
||||
}
|
||||
|
||||
type LongTask struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
Duration uint64
|
||||
Context uint64
|
||||
ContainerType uint64
|
||||
ContainerSrc string
|
||||
ContainerId string
|
||||
ContainerName string
|
||||
}
|
||||
|
||||
func (msg *LongTask) Encode() []byte {
|
||||
buf := make([]byte, 71+len(msg.ContainerSrc)+len(msg.ContainerId)+len(msg.ContainerName))
|
||||
buf[0] = 59
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
p = WriteUint(msg.Duration, buf, p)
|
||||
p = WriteUint(msg.Context, buf, p)
|
||||
p = WriteUint(msg.ContainerType, buf, p)
|
||||
p = WriteString(msg.ContainerSrc, buf, p)
|
||||
p = WriteString(msg.ContainerId, buf, p)
|
||||
p = WriteString(msg.ContainerName, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *LongTask) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *LongTask) TypeID() int {
|
||||
return 59
|
||||
}
|
||||
|
||||
type SetNodeAttributeURLBased struct {
|
||||
message
|
||||
ID uint64
|
||||
|
|
@ -1558,6 +1718,37 @@ func (msg *SetCSSDataURLBased) TypeID() int {
|
|||
return 61
|
||||
}
|
||||
|
||||
type IssueEventDeprecated struct {
|
||||
message
|
||||
MessageID uint64
|
||||
Timestamp uint64
|
||||
Type string
|
||||
ContextString string
|
||||
Context string
|
||||
Payload string
|
||||
}
|
||||
|
||||
func (msg *IssueEventDeprecated) Encode() []byte {
|
||||
buf := make([]byte, 61+len(msg.Type)+len(msg.ContextString)+len(msg.Context)+len(msg.Payload))
|
||||
buf[0] = 62
|
||||
p := 1
|
||||
p = WriteUint(msg.MessageID, buf, p)
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
p = WriteString(msg.Type, buf, p)
|
||||
p = WriteString(msg.ContextString, buf, p)
|
||||
p = WriteString(msg.Context, buf, p)
|
||||
p = WriteString(msg.Payload, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *IssueEventDeprecated) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *IssueEventDeprecated) TypeID() int {
|
||||
return 62
|
||||
}
|
||||
|
||||
type TechnicalInfo struct {
|
||||
message
|
||||
Type string
|
||||
|
|
@ -1625,6 +1816,33 @@ func (msg *AssetCache) TypeID() int {
|
|||
return 66
|
||||
}
|
||||
|
||||
type CSSInsertRuleURLBased struct {
|
||||
message
|
||||
ID uint64
|
||||
Rule string
|
||||
Index uint64
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRuleURLBased) Encode() []byte {
|
||||
buf := make([]byte, 41+len(msg.Rule)+len(msg.BaseURL))
|
||||
buf[0] = 67
|
||||
p := 1
|
||||
p = WriteUint(msg.ID, buf, p)
|
||||
p = WriteString(msg.Rule, buf, p)
|
||||
p = WriteUint(msg.Index, buf, p)
|
||||
p = WriteString(msg.BaseURL, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRuleURLBased) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *CSSInsertRuleURLBased) TypeID() int {
|
||||
return 67
|
||||
}
|
||||
|
||||
type MouseClick struct {
|
||||
message
|
||||
ID uint64
|
||||
|
|
@ -1925,6 +2143,31 @@ func (msg *Zustand) TypeID() int {
|
|||
return 79
|
||||
}
|
||||
|
||||
type BatchMeta struct {
|
||||
message
|
||||
PageNo uint64
|
||||
FirstIndex uint64
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
func (msg *BatchMeta) Encode() []byte {
|
||||
buf := make([]byte, 31)
|
||||
buf[0] = 80
|
||||
p := 1
|
||||
p = WriteUint(msg.PageNo, buf, p)
|
||||
p = WriteUint(msg.FirstIndex, buf, p)
|
||||
p = WriteInt(msg.Timestamp, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *BatchMeta) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *BatchMeta) TypeID() int {
|
||||
return 80
|
||||
}
|
||||
|
||||
type BatchMetadata struct {
|
||||
message
|
||||
Version uint64
|
||||
|
|
|
|||
|
|
@ -68,6 +68,15 @@ func DecodeSessionStart(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeSessionEndDeprecated(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &SessionEndDeprecated{}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeSetPageLocationDeprecated(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &SetPageLocationDeprecated{}
|
||||
|
|
@ -381,6 +390,21 @@ func DecodePageRenderTiming(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeJSExceptionDeprecated(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &JSExceptionDeprecated{}
|
||||
if msg.Name, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Message, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Payload, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIntegrationEvent(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IntegrationEvent{}
|
||||
|
|
@ -609,6 +633,60 @@ func DecodeSetNodeAttributeDictGlobal(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeCSSInsertRule(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &CSSInsertRule{}
|
||||
if msg.ID, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Rule, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Index, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeCSSDeleteRule(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &CSSDeleteRule{}
|
||||
if msg.ID, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Index, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeFetch(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &Fetch{}
|
||||
if msg.Method, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.URL, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Request, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Response, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Status, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Duration, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeProfiler(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &Profiler{}
|
||||
|
|
@ -921,6 +999,33 @@ func DecodeSetNodeFocus(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeLongTask(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &LongTask{}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Duration, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Context, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ContainerType, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ContainerSrc, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ContainerId, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ContainerName, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeSetNodeAttributeURLBased(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &SetNodeAttributeURLBased{}
|
||||
|
|
@ -954,6 +1059,30 @@ func DecodeSetCSSDataURLBased(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIssueEventDeprecated(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IssueEventDeprecated{}
|
||||
if msg.MessageID, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Type, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ContextString, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Context, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Payload, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeTechnicalInfo(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &TechnicalInfo{}
|
||||
|
|
@ -987,6 +1116,24 @@ func DecodeAssetCache(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeCSSInsertRuleURLBased(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &CSSInsertRuleURLBased{}
|
||||
if msg.ID, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Rule, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Index, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.BaseURL, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeMouseClick(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &MouseClick{}
|
||||
|
|
@ -1167,6 +1314,21 @@ func DecodeZustand(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeBatchMeta(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &BatchMeta{}
|
||||
if msg.PageNo, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.FirstIndex, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Timestamp, err = reader.ReadInt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeBatchMetadata(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &BatchMetadata{}
|
||||
|
|
@ -1941,6 +2103,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeTimestamp(reader)
|
||||
case 1:
|
||||
return DecodeSessionStart(reader)
|
||||
case 3:
|
||||
return DecodeSessionEndDeprecated(reader)
|
||||
case 4:
|
||||
return DecodeSetPageLocationDeprecated(reader)
|
||||
case 5:
|
||||
|
|
@ -1983,6 +2147,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodePageLoadTiming(reader)
|
||||
case 24:
|
||||
return DecodePageRenderTiming(reader)
|
||||
case 25:
|
||||
return DecodeJSExceptionDeprecated(reader)
|
||||
case 26:
|
||||
return DecodeIntegrationEvent(reader)
|
||||
case 27:
|
||||
|
|
@ -2003,6 +2169,12 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeStringDictGlobal(reader)
|
||||
case 35:
|
||||
return DecodeSetNodeAttributeDictGlobal(reader)
|
||||
case 37:
|
||||
return DecodeCSSInsertRule(reader)
|
||||
case 38:
|
||||
return DecodeCSSDeleteRule(reader)
|
||||
case 39:
|
||||
return DecodeFetch(reader)
|
||||
case 40:
|
||||
return DecodeProfiler(reader)
|
||||
case 41:
|
||||
|
|
@ -2041,16 +2213,22 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeLoadFontFace(reader)
|
||||
case 58:
|
||||
return DecodeSetNodeFocus(reader)
|
||||
case 59:
|
||||
return DecodeLongTask(reader)
|
||||
case 60:
|
||||
return DecodeSetNodeAttributeURLBased(reader)
|
||||
case 61:
|
||||
return DecodeSetCSSDataURLBased(reader)
|
||||
case 62:
|
||||
return DecodeIssueEventDeprecated(reader)
|
||||
case 63:
|
||||
return DecodeTechnicalInfo(reader)
|
||||
case 64:
|
||||
return DecodeCustomIssue(reader)
|
||||
case 66:
|
||||
return DecodeAssetCache(reader)
|
||||
case 67:
|
||||
return DecodeCSSInsertRuleURLBased(reader)
|
||||
case 68:
|
||||
return DecodeMouseClick(reader)
|
||||
case 69:
|
||||
|
|
@ -2075,6 +2253,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeJSException(reader)
|
||||
case 79:
|
||||
return DecodeZustand(reader)
|
||||
case 80:
|
||||
return DecodeBatchMeta(reader)
|
||||
case 81:
|
||||
return DecodeBatchMetadata(reader)
|
||||
case 82:
|
||||
|
|
|
|||
|
|
@ -89,13 +89,15 @@ func (m *messageReaderImpl) Parse() (err error) {
|
|||
if err != nil {
|
||||
return fmt.Errorf("read message err: %s", err)
|
||||
}
|
||||
if m.msgType == MsgBatchMetadata {
|
||||
if m.msgType == MsgBatchMeta || m.msgType == MsgBatchMetadata {
|
||||
if len(m.list) > 0 {
|
||||
return fmt.Errorf("batch meta not at the start of batch")
|
||||
}
|
||||
switch message := msg.(type) {
|
||||
case *BatchMetadata:
|
||||
m.version = int(message.Version)
|
||||
case *BatchMeta:
|
||||
m.version = 0
|
||||
}
|
||||
if m.version != 1 {
|
||||
// Unsupported tracker version, reset reader
|
||||
|
|
|
|||
|
|
@ -79,34 +79,31 @@ func (e *handlersImpl) GetAll() []*api.Description {
|
|||
}
|
||||
}
|
||||
|
||||
func getSessionTimestamp(req *StartSessionRequest, startTimeMili int64) uint64 {
|
||||
func getSessionTimestamp(req *StartSessionRequest, startTimeMili int64) (ts uint64) {
|
||||
ts = uint64(req.Timestamp)
|
||||
if req.IsOffline {
|
||||
return uint64(req.Timestamp)
|
||||
return
|
||||
}
|
||||
ts := uint64(startTimeMili)
|
||||
if req.BufferDiff > 0 && req.BufferDiff < 5*60*1000 {
|
||||
ts -= req.BufferDiff
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
func validateTrackerVersion(ver string) error {
|
||||
c, err := semver.NewConstraint(">=6.0.0")
|
||||
c, err := semver.NewConstraint(">=4.1.6")
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
ver := req.TrackerVersion
|
||||
parts := strings.Split(ver, "-")
|
||||
if len(parts) > 1 {
|
||||
ver = parts[0]
|
||||
}
|
||||
v, err := semver.NewVersion(ver)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
if !c.Check(v) {
|
||||
return errors.New("unsupported tracker version")
|
||||
if c.Check(v) {
|
||||
ts = uint64(startTimeMili)
|
||||
if req.BufferDiff > 0 && req.BufferDiff < 5*60*1000 {
|
||||
ts -= req.BufferDiff
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (e *handlersImpl) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ def get_details(project_id, error_id, user_id, **data):
|
|||
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(0)
|
||||
|
||||
ch_basic_query = errors_helper.__get_basic_constraints_ch(time_constraint=False)
|
||||
ch_basic_query.append("toString(`$properties`.error_id) = %(error_id)s")
|
||||
ch_basic_query.append("error_id = %(error_id)s")
|
||||
|
||||
with ch_client.ClickHouseClient() as ch:
|
||||
data["startDate24"] = TimeUTC.now(-1)
|
||||
|
|
@ -95,7 +95,7 @@ def get_details(project_id, error_id, user_id, **data):
|
|||
"error_id": error_id}
|
||||
|
||||
main_ch_query = f"""\
|
||||
WITH pre_processed AS (SELECT toString(`$properties`.error_id) AS error_id,
|
||||
WITH pre_processed AS (SELECT error_id,
|
||||
toString(`$properties`.name) AS name,
|
||||
toString(`$properties`.message) AS message,
|
||||
session_id,
|
||||
|
|
@ -183,7 +183,7 @@ def get_details(project_id, error_id, user_id, **data):
|
|||
AND `$event_name` = 'ERROR'
|
||||
AND events.created_at >= toDateTime(timestamp / 1000)
|
||||
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
|
||||
AND toString(`$properties`.error_id) = %(error_id)s
|
||||
AND error_id = %(error_id)s
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp) AS chart_details
|
||||
) AS chart_details24 ON TRUE
|
||||
|
|
@ -196,7 +196,7 @@ def get_details(project_id, error_id, user_id, **data):
|
|||
AND `$event_name` = 'ERROR'
|
||||
AND events.created_at >= toDateTime(timestamp / 1000)
|
||||
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
|
||||
AND toString(`$properties`.error_id) = %(error_id)s
|
||||
AND error_id = %(error_id)s
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp) AS chart_details
|
||||
) AS chart_details30 ON TRUE;"""
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
urllib3==2.3.0
|
||||
urllib3==2.4.0
|
||||
requests==2.32.3
|
||||
boto3==1.37.21
|
||||
boto3==1.38.10
|
||||
pyjwt==2.10.1
|
||||
psycopg2-binary==2.9.10
|
||||
psycopg[pool,binary]==3.2.6
|
||||
clickhouse-connect==0.8.15
|
||||
elasticsearch==8.17.2
|
||||
psycopg[pool,binary]==3.2.7
|
||||
clickhouse-connect==0.8.17
|
||||
elasticsearch==9.0.1
|
||||
jira==3.8.0
|
||||
cachetools==5.5.2
|
||||
|
||||
fastapi==0.115.12
|
||||
uvicorn[standard]==0.34.0
|
||||
uvicorn[standard]==0.34.2
|
||||
python-decouple==3.8
|
||||
pydantic[email]==2.10.6
|
||||
pydantic[email]==2.11.4
|
||||
apscheduler==3.11.0
|
||||
|
||||
azure-storage-blob==12.25.0
|
||||
azure-storage-blob==12.25.1
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ requests==2.32.3
|
|||
boto3==1.37.21
|
||||
pyjwt==2.10.1
|
||||
psycopg2-binary==2.9.10
|
||||
psycopg[pool,binary]==3.2.6
|
||||
clickhouse-connect==0.8.15
|
||||
elasticsearch==8.17.2
|
||||
psycopg[pool,binary]==3.2.7
|
||||
clickhouse-connect==0.8.17
|
||||
elasticsearch==9.0.1
|
||||
jira==3.8.0
|
||||
cachetools==5.5.2
|
||||
|
||||
fastapi==0.115.12
|
||||
python-decouple==3.8
|
||||
pydantic[email]==2.10.6
|
||||
pydantic[email]==2.11.4
|
||||
apscheduler==3.11.0
|
||||
|
||||
redis==5.2.1
|
||||
azure-storage-blob==12.25.0
|
||||
redis==6.0.0
|
||||
azure-storage-blob==12.25.1
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
urllib3==2.3.0
|
||||
urllib3==2.4.0
|
||||
requests==2.32.3
|
||||
boto3==1.37.21
|
||||
boto3==1.38.10
|
||||
pyjwt==2.10.1
|
||||
psycopg2-binary==2.9.10
|
||||
psycopg[pool,binary]==3.2.6
|
||||
clickhouse-connect==0.8.15
|
||||
elasticsearch==8.17.2
|
||||
psycopg[pool,binary]==3.2.7
|
||||
clickhouse-connect==0.8.17
|
||||
elasticsearch==9.0.1
|
||||
jira==3.8.0
|
||||
cachetools==5.5.2
|
||||
|
||||
fastapi==0.115.12
|
||||
uvicorn[standard]==0.34.0
|
||||
uvicorn[standard]==0.34.2
|
||||
gunicorn==23.0.0
|
||||
python-decouple==3.8
|
||||
pydantic[email]==2.10.6
|
||||
pydantic[email]==2.11.4
|
||||
apscheduler==3.11.0
|
||||
|
||||
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
|
||||
|
|
@ -21,6 +21,6 @@ apscheduler==3.11.0
|
|||
python3-saml==1.16.0 --no-binary=lxml
|
||||
python-multipart==0.0.20
|
||||
|
||||
redis==5.2.1
|
||||
redis==6.0.0
|
||||
#confluent-kafka==2.1.0
|
||||
azure-storage-blob==12.25.0
|
||||
azure-storage-blob==12.25.1
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ class SessionStart(Message):
|
|||
self.user_id = user_id
|
||||
|
||||
|
||||
class SessionEndDeprecated(Message):
|
||||
__id__ = 3
|
||||
|
||||
def __init__(self, timestamp):
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
class SetPageLocationDeprecated(Message):
|
||||
__id__ = 4
|
||||
|
||||
|
|
@ -224,6 +231,15 @@ class PageRenderTiming(Message):
|
|||
self.time_to_interactive = time_to_interactive
|
||||
|
||||
|
||||
class JSExceptionDeprecated(Message):
|
||||
__id__ = 25
|
||||
|
||||
def __init__(self, name, message, payload):
|
||||
self.name = name
|
||||
self.message = message
|
||||
self.payload = payload
|
||||
|
||||
|
||||
class IntegrationEvent(Message):
|
||||
__id__ = 26
|
||||
|
||||
|
|
@ -323,21 +339,34 @@ class PageEvent(Message):
|
|||
self.web_vitals = web_vitals
|
||||
|
||||
|
||||
class StringDictGlobal(Message):
|
||||
__id__ = 34
|
||||
class CSSInsertRule(Message):
|
||||
__id__ = 37
|
||||
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
class SetNodeAttributeDictGlobal(Message):
|
||||
__id__ = 35
|
||||
|
||||
def __init__(self, id, name, value):
|
||||
def __init__(self, id, rule, index):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.rule = rule
|
||||
self.index = index
|
||||
|
||||
|
||||
class CSSDeleteRule(Message):
|
||||
__id__ = 38
|
||||
|
||||
def __init__(self, id, index):
|
||||
self.id = id
|
||||
self.index = index
|
||||
|
||||
|
||||
class Fetch(Message):
|
||||
__id__ = 39
|
||||
|
||||
def __init__(self, method, url, request, response, status, timestamp, duration):
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.status = status
|
||||
self.timestamp = timestamp
|
||||
self.duration = duration
|
||||
|
||||
|
||||
class Profiler(Message):
|
||||
|
|
@ -520,6 +549,19 @@ class SetNodeFocus(Message):
|
|||
self.id = id
|
||||
|
||||
|
||||
class LongTask(Message):
|
||||
__id__ = 59
|
||||
|
||||
def __init__(self, timestamp, duration, context, container_type, container_src, container_id, container_name):
|
||||
self.timestamp = timestamp
|
||||
self.duration = duration
|
||||
self.context = context
|
||||
self.container_type = container_type
|
||||
self.container_src = container_src
|
||||
self.container_id = container_id
|
||||
self.container_name = container_name
|
||||
|
||||
|
||||
class SetNodeAttributeURLBased(Message):
|
||||
__id__ = 60
|
||||
|
||||
|
|
@ -539,6 +581,18 @@ class SetCSSDataURLBased(Message):
|
|||
self.base_url = base_url
|
||||
|
||||
|
||||
class IssueEventDeprecated(Message):
|
||||
__id__ = 62
|
||||
|
||||
def __init__(self, message_id, timestamp, type, context_string, context, payload):
|
||||
self.message_id = message_id
|
||||
self.timestamp = timestamp
|
||||
self.type = type
|
||||
self.context_string = context_string
|
||||
self.context = context
|
||||
self.payload = payload
|
||||
|
||||
|
||||
class TechnicalInfo(Message):
|
||||
__id__ = 63
|
||||
|
||||
|
|
@ -562,6 +616,16 @@ class AssetCache(Message):
|
|||
self.url = url
|
||||
|
||||
|
||||
class CSSInsertRuleURLBased(Message):
|
||||
__id__ = 67
|
||||
|
||||
def __init__(self, id, rule, index, base_url):
|
||||
self.id = id
|
||||
self.rule = rule
|
||||
self.index = index
|
||||
self.base_url = base_url
|
||||
|
||||
|
||||
class MouseClick(Message):
|
||||
__id__ = 68
|
||||
|
||||
|
|
@ -670,6 +734,15 @@ class Zustand(Message):
|
|||
self.state = state
|
||||
|
||||
|
||||
class BatchMeta(Message):
|
||||
__id__ = 80
|
||||
|
||||
def __init__(self, page_no, first_index, timestamp):
|
||||
self.page_no = page_no
|
||||
self.first_index = first_index
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
class BatchMetadata(Message):
|
||||
__id__ = 81
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ cdef class SessionStart(PyMessage):
|
|||
self.user_id = user_id
|
||||
|
||||
|
||||
cdef class SessionEndDeprecated(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
|
||||
def __init__(self, unsigned long timestamp):
|
||||
self.__id__ = 3
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
cdef class SetPageLocationDeprecated(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str url
|
||||
|
|
@ -331,6 +340,19 @@ cdef class PageRenderTiming(PyMessage):
|
|||
self.time_to_interactive = time_to_interactive
|
||||
|
||||
|
||||
cdef class JSExceptionDeprecated(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str name
|
||||
cdef public str message
|
||||
cdef public str payload
|
||||
|
||||
def __init__(self, str name, str message, str payload):
|
||||
self.__id__ = 25
|
||||
self.name = name
|
||||
self.message = message
|
||||
self.payload = payload
|
||||
|
||||
|
||||
cdef class IntegrationEvent(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
|
|
@ -489,28 +511,49 @@ cdef class PageEvent(PyMessage):
|
|||
self.web_vitals = web_vitals
|
||||
|
||||
|
||||
cdef class StringDictGlobal(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long key
|
||||
cdef public str value
|
||||
|
||||
def __init__(self, unsigned long key, str value):
|
||||
self.__id__ = 34
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
cdef class SetNodeAttributeDictGlobal(PyMessage):
|
||||
cdef class CSSInsertRule(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long id
|
||||
cdef public unsigned long name
|
||||
cdef public unsigned long value
|
||||
cdef public str rule
|
||||
cdef public unsigned long index
|
||||
|
||||
def __init__(self, unsigned long id, unsigned long name, unsigned long value):
|
||||
self.__id__ = 35
|
||||
def __init__(self, unsigned long id, str rule, unsigned long index):
|
||||
self.__id__ = 37
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.rule = rule
|
||||
self.index = index
|
||||
|
||||
|
||||
cdef class CSSDeleteRule(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long id
|
||||
cdef public unsigned long index
|
||||
|
||||
def __init__(self, unsigned long id, unsigned long index):
|
||||
self.__id__ = 38
|
||||
self.id = id
|
||||
self.index = index
|
||||
|
||||
|
||||
cdef class Fetch(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str method
|
||||
cdef public str url
|
||||
cdef public str request
|
||||
cdef public str response
|
||||
cdef public unsigned long status
|
||||
cdef public unsigned long timestamp
|
||||
cdef public unsigned long duration
|
||||
|
||||
def __init__(self, str method, str url, str request, str response, unsigned long status, unsigned long timestamp, unsigned long duration):
|
||||
self.__id__ = 39
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.status = status
|
||||
self.timestamp = timestamp
|
||||
self.duration = duration
|
||||
|
||||
|
||||
cdef class Profiler(PyMessage):
|
||||
|
|
@ -778,6 +821,27 @@ cdef class SetNodeFocus(PyMessage):
|
|||
self.id = id
|
||||
|
||||
|
||||
cdef class LongTask(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long timestamp
|
||||
cdef public unsigned long duration
|
||||
cdef public unsigned long context
|
||||
cdef public unsigned long container_type
|
||||
cdef public str container_src
|
||||
cdef public str container_id
|
||||
cdef public str container_name
|
||||
|
||||
def __init__(self, unsigned long timestamp, unsigned long duration, unsigned long context, unsigned long container_type, str container_src, str container_id, str container_name):
|
||||
self.__id__ = 59
|
||||
self.timestamp = timestamp
|
||||
self.duration = duration
|
||||
self.context = context
|
||||
self.container_type = container_type
|
||||
self.container_src = container_src
|
||||
self.container_id = container_id
|
||||
self.container_name = container_name
|
||||
|
||||
|
||||
cdef class SetNodeAttributeURLBased(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long id
|
||||
|
|
@ -806,6 +870,25 @@ cdef class SetCSSDataURLBased(PyMessage):
|
|||
self.base_url = base_url
|
||||
|
||||
|
||||
cdef class IssueEventDeprecated(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long message_id
|
||||
cdef public unsigned long timestamp
|
||||
cdef public str type
|
||||
cdef public str context_string
|
||||
cdef public str context
|
||||
cdef public str payload
|
||||
|
||||
def __init__(self, unsigned long message_id, unsigned long timestamp, str type, str context_string, str context, str payload):
|
||||
self.__id__ = 62
|
||||
self.message_id = message_id
|
||||
self.timestamp = timestamp
|
||||
self.type = type
|
||||
self.context_string = context_string
|
||||
self.context = context
|
||||
self.payload = payload
|
||||
|
||||
|
||||
cdef class TechnicalInfo(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str type
|
||||
|
|
@ -837,6 +920,21 @@ cdef class AssetCache(PyMessage):
|
|||
self.url = url
|
||||
|
||||
|
||||
cdef class CSSInsertRuleURLBased(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long id
|
||||
cdef public str rule
|
||||
cdef public unsigned long index
|
||||
cdef public str base_url
|
||||
|
||||
def __init__(self, unsigned long id, str rule, unsigned long index, str base_url):
|
||||
self.__id__ = 67
|
||||
self.id = id
|
||||
self.rule = rule
|
||||
self.index = index
|
||||
self.base_url = base_url
|
||||
|
||||
|
||||
cdef class MouseClick(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long id
|
||||
|
|
@ -993,6 +1091,19 @@ cdef class Zustand(PyMessage):
|
|||
self.state = state
|
||||
|
||||
|
||||
cdef class BatchMeta(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long page_no
|
||||
cdef public unsigned long first_index
|
||||
cdef public long timestamp
|
||||
|
||||
def __init__(self, unsigned long page_no, unsigned long first_index, long timestamp):
|
||||
self.__id__ = 80
|
||||
self.page_no = page_no
|
||||
self.first_index = first_index
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
cdef class BatchMetadata(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public unsigned long version
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ class MessageCodec(Codec):
|
|||
user_id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 3:
|
||||
return SessionEndDeprecated(
|
||||
timestamp=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 4:
|
||||
return SetPageLocationDeprecated(
|
||||
url=self.read_string(reader),
|
||||
|
|
@ -265,6 +270,13 @@ class MessageCodec(Codec):
|
|||
time_to_interactive=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 25:
|
||||
return JSExceptionDeprecated(
|
||||
name=self.read_string(reader),
|
||||
message=self.read_string(reader),
|
||||
payload=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 26:
|
||||
return IntegrationEvent(
|
||||
timestamp=self.read_uint(reader),
|
||||
|
|
@ -348,17 +360,28 @@ class MessageCodec(Codec):
|
|||
web_vitals=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 34:
|
||||
return StringDictGlobal(
|
||||
key=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
if message_id == 37:
|
||||
return CSSInsertRule(
|
||||
id=self.read_uint(reader),
|
||||
rule=self.read_string(reader),
|
||||
index=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 35:
|
||||
return SetNodeAttributeDictGlobal(
|
||||
if message_id == 38:
|
||||
return CSSDeleteRule(
|
||||
id=self.read_uint(reader),
|
||||
name=self.read_uint(reader),
|
||||
value=self.read_uint(reader)
|
||||
index=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 39:
|
||||
return Fetch(
|
||||
method=self.read_string(reader),
|
||||
url=self.read_string(reader),
|
||||
request=self.read_string(reader),
|
||||
response=self.read_string(reader),
|
||||
status=self.read_uint(reader),
|
||||
timestamp=self.read_uint(reader),
|
||||
duration=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 40:
|
||||
|
|
@ -503,6 +526,17 @@ class MessageCodec(Codec):
|
|||
id=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 59:
|
||||
return LongTask(
|
||||
timestamp=self.read_uint(reader),
|
||||
duration=self.read_uint(reader),
|
||||
context=self.read_uint(reader),
|
||||
container_type=self.read_uint(reader),
|
||||
container_src=self.read_string(reader),
|
||||
container_id=self.read_string(reader),
|
||||
container_name=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 60:
|
||||
return SetNodeAttributeURLBased(
|
||||
id=self.read_uint(reader),
|
||||
|
|
@ -518,6 +552,16 @@ class MessageCodec(Codec):
|
|||
base_url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 62:
|
||||
return IssueEventDeprecated(
|
||||
message_id=self.read_uint(reader),
|
||||
timestamp=self.read_uint(reader),
|
||||
type=self.read_string(reader),
|
||||
context_string=self.read_string(reader),
|
||||
context=self.read_string(reader),
|
||||
payload=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 63:
|
||||
return TechnicalInfo(
|
||||
type=self.read_string(reader),
|
||||
|
|
@ -535,6 +579,14 @@ class MessageCodec(Codec):
|
|||
url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 67:
|
||||
return CSSInsertRuleURLBased(
|
||||
id=self.read_uint(reader),
|
||||
rule=self.read_string(reader),
|
||||
index=self.read_uint(reader),
|
||||
base_url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 68:
|
||||
return MouseClick(
|
||||
id=self.read_uint(reader),
|
||||
|
|
@ -619,6 +671,13 @@ class MessageCodec(Codec):
|
|||
state=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 80:
|
||||
return BatchMeta(
|
||||
page_no=self.read_uint(reader),
|
||||
first_index=self.read_uint(reader),
|
||||
timestamp=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 81:
|
||||
return BatchMetadata(
|
||||
version=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -216,6 +216,11 @@ cdef class MessageCodec:
|
|||
user_id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 3:
|
||||
return SessionEndDeprecated(
|
||||
timestamp=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 4:
|
||||
return SetPageLocationDeprecated(
|
||||
url=self.read_string(reader),
|
||||
|
|
@ -363,6 +368,13 @@ cdef class MessageCodec:
|
|||
time_to_interactive=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 25:
|
||||
return JSExceptionDeprecated(
|
||||
name=self.read_string(reader),
|
||||
message=self.read_string(reader),
|
||||
payload=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 26:
|
||||
return IntegrationEvent(
|
||||
timestamp=self.read_uint(reader),
|
||||
|
|
@ -446,17 +458,28 @@ cdef class MessageCodec:
|
|||
web_vitals=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 34:
|
||||
return StringDictGlobal(
|
||||
key=self.read_uint(reader),
|
||||
value=self.read_string(reader)
|
||||
if message_id == 37:
|
||||
return CSSInsertRule(
|
||||
id=self.read_uint(reader),
|
||||
rule=self.read_string(reader),
|
||||
index=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 35:
|
||||
return SetNodeAttributeDictGlobal(
|
||||
if message_id == 38:
|
||||
return CSSDeleteRule(
|
||||
id=self.read_uint(reader),
|
||||
name=self.read_uint(reader),
|
||||
value=self.read_uint(reader)
|
||||
index=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 39:
|
||||
return Fetch(
|
||||
method=self.read_string(reader),
|
||||
url=self.read_string(reader),
|
||||
request=self.read_string(reader),
|
||||
response=self.read_string(reader),
|
||||
status=self.read_uint(reader),
|
||||
timestamp=self.read_uint(reader),
|
||||
duration=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 40:
|
||||
|
|
@ -601,6 +624,17 @@ cdef class MessageCodec:
|
|||
id=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 59:
|
||||
return LongTask(
|
||||
timestamp=self.read_uint(reader),
|
||||
duration=self.read_uint(reader),
|
||||
context=self.read_uint(reader),
|
||||
container_type=self.read_uint(reader),
|
||||
container_src=self.read_string(reader),
|
||||
container_id=self.read_string(reader),
|
||||
container_name=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 60:
|
||||
return SetNodeAttributeURLBased(
|
||||
id=self.read_uint(reader),
|
||||
|
|
@ -616,6 +650,16 @@ cdef class MessageCodec:
|
|||
base_url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 62:
|
||||
return IssueEventDeprecated(
|
||||
message_id=self.read_uint(reader),
|
||||
timestamp=self.read_uint(reader),
|
||||
type=self.read_string(reader),
|
||||
context_string=self.read_string(reader),
|
||||
context=self.read_string(reader),
|
||||
payload=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 63:
|
||||
return TechnicalInfo(
|
||||
type=self.read_string(reader),
|
||||
|
|
@ -633,6 +677,14 @@ cdef class MessageCodec:
|
|||
url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 67:
|
||||
return CSSInsertRuleURLBased(
|
||||
id=self.read_uint(reader),
|
||||
rule=self.read_string(reader),
|
||||
index=self.read_uint(reader),
|
||||
base_url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 68:
|
||||
return MouseClick(
|
||||
id=self.read_uint(reader),
|
||||
|
|
@ -717,6 +769,13 @@ cdef class MessageCodec:
|
|||
state=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 80:
|
||||
return BatchMeta(
|
||||
page_no=self.read_uint(reader),
|
||||
first_index=self.read_uint(reader),
|
||||
timestamp=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 81:
|
||||
return BatchMetadata(
|
||||
version=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -1,65 +1,5 @@
|
|||
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.23.0-ee';
|
||||
|
||||
SET allow_experimental_json_type = 1;
|
||||
SET enable_json_type = 1;
|
||||
ALTER TABLE product_analytics.events
|
||||
MODIFY COLUMN `$properties` JSON(
|
||||
max_dynamic_paths=0,
|
||||
label String ,
|
||||
hesitation_time UInt32 ,
|
||||
name String ,
|
||||
payload String ,
|
||||
level Enum8 ('info'=0, 'error'=1),
|
||||
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
|
||||
message String ,
|
||||
error_id String ,
|
||||
duration UInt16,
|
||||
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
|
||||
url_host String ,
|
||||
url_path String ,
|
||||
url_hostpath String ,
|
||||
request_start UInt16 ,
|
||||
response_start UInt16 ,
|
||||
response_end UInt16 ,
|
||||
dom_content_loaded_event_start UInt16 ,
|
||||
dom_content_loaded_event_end UInt16 ,
|
||||
load_event_start UInt16 ,
|
||||
load_event_end UInt16 ,
|
||||
first_paint UInt16 ,
|
||||
first_contentful_paint_time UInt16 ,
|
||||
speed_index UInt16 ,
|
||||
visually_complete UInt16 ,
|
||||
time_to_interactive UInt16,
|
||||
ttfb UInt16,
|
||||
ttlb UInt16,
|
||||
response_time UInt16,
|
||||
dom_building_time UInt16,
|
||||
dom_content_loaded_event_time UInt16,
|
||||
load_event_time UInt16,
|
||||
min_fps UInt8,
|
||||
avg_fps UInt8,
|
||||
max_fps UInt8,
|
||||
min_cpu UInt8,
|
||||
avg_cpu UInt8,
|
||||
max_cpu UInt8,
|
||||
min_total_js_heap_size UInt64,
|
||||
avg_total_js_heap_size UInt64,
|
||||
max_total_js_heap_size UInt64,
|
||||
min_used_js_heap_size UInt64,
|
||||
avg_used_js_heap_size UInt64,
|
||||
max_used_js_heap_size UInt64,
|
||||
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
|
||||
status UInt16,
|
||||
success UInt8,
|
||||
request_body String,
|
||||
response_body String,
|
||||
transfer_size UInt32,
|
||||
selector String,
|
||||
normalized_x Float32,
|
||||
normalized_y Float32,
|
||||
message_id UInt64
|
||||
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events';
|
||||
|
||||
DROP TABLE IF EXISTS product_analytics.all_events;
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.all_events
|
||||
(
|
||||
|
|
@ -225,3 +165,92 @@ FROM product_analytics.events
|
|||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||
AND value != ''
|
||||
LIMIT 2 BY project_id,property_name;
|
||||
|
||||
-- Autocomplete
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
_timestamp DateTime
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
|
||||
TO product_analytics.autocomplete_events AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
WHERE _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
|
||||
SELECT project_id,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_events
|
||||
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, value;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, event_name, property_name, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
|
||||
TO product_analytics.autocomplete_event_properties AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS event_name,
|
||||
property_name,
|
||||
JSONExtractString(toString(`$properties`), property_name) AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, event_name, property_name, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
|
||||
SELECT project_id,
|
||||
event_name,
|
||||
property_name,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_event_properties
|
||||
WHERE length(value) > 0
|
||||
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, event_name, property_name, value;
|
||||
|
||||
|
|
|
|||
|
|
@ -431,62 +431,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
|
|||
"$source" LowCardinality(String) DEFAULT '' COMMENT 'the name of the integration that sent the event',
|
||||
"$duration_s" UInt16 DEFAULT 0 COMMENT 'the duration from session-start in seconds',
|
||||
properties JSON DEFAULT '{}',
|
||||
"$properties" JSON(
|
||||
max_dynamic_paths=0,
|
||||
label String ,
|
||||
hesitation_time UInt32 ,
|
||||
name String ,
|
||||
payload String ,
|
||||
level Enum8 ('info'=0, 'error'=1),
|
||||
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
|
||||
message String ,
|
||||
error_id String ,
|
||||
duration UInt16,
|
||||
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
|
||||
url_host String ,
|
||||
url_path String ,
|
||||
url_hostpath String ,
|
||||
request_start UInt16 ,
|
||||
response_start UInt16 ,
|
||||
response_end UInt16 ,
|
||||
dom_content_loaded_event_start UInt16 ,
|
||||
dom_content_loaded_event_end UInt16 ,
|
||||
load_event_start UInt16 ,
|
||||
load_event_end UInt16 ,
|
||||
first_paint UInt16 ,
|
||||
first_contentful_paint_time UInt16 ,
|
||||
speed_index UInt16 ,
|
||||
visually_complete UInt16 ,
|
||||
time_to_interactive UInt16,
|
||||
ttfb UInt16,
|
||||
ttlb UInt16,
|
||||
response_time UInt16,
|
||||
dom_building_time UInt16,
|
||||
dom_content_loaded_event_time UInt16,
|
||||
load_event_time UInt16,
|
||||
min_fps UInt8,
|
||||
avg_fps UInt8,
|
||||
max_fps UInt8,
|
||||
min_cpu UInt8,
|
||||
avg_cpu UInt8,
|
||||
max_cpu UInt8,
|
||||
min_total_js_heap_size UInt64,
|
||||
avg_total_js_heap_size UInt64,
|
||||
max_total_js_heap_size UInt64,
|
||||
min_used_js_heap_size UInt64,
|
||||
avg_used_js_heap_size UInt64,
|
||||
max_used_js_heap_size UInt64,
|
||||
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
|
||||
status UInt16,
|
||||
success UInt8,
|
||||
request_body String,
|
||||
response_body String,
|
||||
transfer_size UInt32,
|
||||
selector String,
|
||||
normalized_x Float32,
|
||||
normalized_y Float32,
|
||||
message_id UInt64
|
||||
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
|
||||
"$properties" JSON DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
|
||||
description String DEFAULT '',
|
||||
group_id1 Array(String) DEFAULT [],
|
||||
group_id2 Array(String) DEFAULT [],
|
||||
|
|
@ -868,3 +813,92 @@ FROM product_analytics.events
|
|||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||
AND value != ''
|
||||
LIMIT 2 BY project_id,property_name;
|
||||
|
||||
-- Autocomplete
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
_timestamp DateTime
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
|
||||
TO product_analytics.autocomplete_events AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
WHERE _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
|
||||
SELECT project_id,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_events
|
||||
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, value;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, event_name, property_name, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
|
||||
TO product_analytics.autocomplete_event_properties AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS event_name,
|
||||
property_name,
|
||||
JSONExtractString(toString(`$properties`), property_name) AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, event_name, property_name, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
|
||||
SELECT project_id,
|
||||
event_name,
|
||||
property_name,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_event_properties
|
||||
WHERE length(value) > 0
|
||||
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, event_name, property_name, value;
|
||||
|
||||
|
|
|
|||
64
frontend/app/ThemeContext.tsx
Normal file
64
frontend/app/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
type ThemeType = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: ThemeType;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const getInitialTheme = (): ThemeType => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
|
||||
return savedTheme;
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
const [theme, setTheme] = useState<ThemeType>(getInitialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
// Only apply system preference if user hasn't manually set a preference
|
||||
if (!localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === 'dark' ? 'light' : 'dark'));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -111,9 +111,13 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
|
||||
if (echartNodes.length === 0) return;
|
||||
|
||||
const mainNodeLink = startPoint === 'end' ? echartNodes.findIndex(n => n.id === 0) : 0;
|
||||
const startDepth = startPoint === 'end' ? Math.max(...echartNodes.map(n => n.depth ?? 0)) : 0;
|
||||
const mainNodeLinks = echartNodes.filter(n => n.depth === startDepth).map(n => echartNodes.findIndex(node => node.id === n.id))
|
||||
const startNodeValue = echartLinks
|
||||
.filter((link) => startPoint === 'start' ? link.source === mainNodeLink : link.target === mainNodeLink)
|
||||
.filter((link) => startPoint === 'start'
|
||||
? mainNodeLinks.includes(link.source)
|
||||
: mainNodeLinks.includes(link.target)
|
||||
)
|
||||
.reduce((sum, link) => sum + link.value, 0);
|
||||
|
||||
Object.keys(nodeValues).forEach((nodeId) => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ const ProjectCodeSnippet: React.FC<Props> = (props) => {
|
|||
<div
|
||||
className={cn(
|
||||
stl.info,
|
||||
'rounded-lg bg-gray mb-4 ml-8 bg-amber-50 w-fit text-sm mt-2',
|
||||
'rounded-lg bg-gray mb-4 ml-8 bg-amber w-fit text-sm mt-2',
|
||||
{ hidden: !changed },
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ function UserForm() {
|
|||
/>
|
||||
</div>
|
||||
{!isSmtp && (
|
||||
<div className={cn('mb-4 p-2 bg-yellow rounded')}>
|
||||
<div className={cn('mb-4 p-2 bg-amber rounded')}>
|
||||
{t('SMTP is not configured (see')}
|
||||
<a
|
||||
className="link"
|
||||
|
|
|
|||
|
|
@ -44,16 +44,6 @@ function AddCardSelectionModal(props: Props) {
|
|||
className="addCard"
|
||||
width={isSaas ? 900 : undefined}
|
||||
>
|
||||
{isSaas ? (
|
||||
<>
|
||||
<Row gutter={16} justify="center" className="py-2">
|
||||
<AiQuery />
|
||||
</Row>
|
||||
<div className="flex items-center justify-center w-full text-disabled-text">
|
||||
{t('or')}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<Row gutter={16} justify="center" className="py-5">
|
||||
<Col span={12}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function ClickMapRagePicker() {
|
|||
<Checkbox onChange={onToggle} label={t('Include rage clicks')} />
|
||||
|
||||
<Button size="small" onClick={refreshHeatmapSession}>
|
||||
{t('Get new session')}
|
||||
{t('Get new image')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -181,9 +181,10 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
prevMetricRef.current = _metric;
|
||||
const timestmaps = drillDownPeriod.toTimestamps();
|
||||
const density = props.isPreview ? metric.density : dashboardStore.selectedDensity
|
||||
const payload = isSaved
|
||||
? { ...metricParams }
|
||||
: { ...params, ...timestmaps, ..._metric.toJson() };
|
||||
? { ...metricParams, density }
|
||||
: { ...params, ...timestmaps, ..._metric.toJson(), density };
|
||||
debounceRequest(
|
||||
_metric,
|
||||
payload,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function RangeGranularity({
|
|||
}
|
||||
|
||||
const PAST_24_HR_MS = 24 * 60 * 60 * 1000;
|
||||
function calculateGranularities(periodDurationMs: number) {
|
||||
export function calculateGranularities(periodDurationMs: number) {
|
||||
const granularities = [
|
||||
{ label: 'Hourly', durationMs: 60 * 60 * 1000 },
|
||||
{ label: 'Daily', durationMs: 24 * 60 * 60 * 1000 },
|
||||
|
|
|
|||
|
|
@ -116,13 +116,11 @@ function PlayerBlockHeader(props: any) {
|
|||
)}
|
||||
|
||||
{_metaList.length > 0 && (
|
||||
<div className="h-full flex items-center px-2 gap-1">
|
||||
<SessionMetaList
|
||||
className=""
|
||||
metaList={_metaList}
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
<SessionMetaList
|
||||
horizontal
|
||||
metaList={_metaList}
|
||||
maxLength={2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{uiPlayerStore.showSearchEventsSwitchButton ? (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
height: 50px;
|
||||
border-bottom: solid thin $gray-lighter;
|
||||
padding-right: 0;
|
||||
background-color: white;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ function EventsBlock(props: IProps) {
|
|||
<div
|
||||
className={cn(
|
||||
styles.header,
|
||||
'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-50 h-[57px]',
|
||||
'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-gray-lightest h-[57px]',
|
||||
)}
|
||||
>
|
||||
{uxtestingStore.isUxt() ? (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
.event {
|
||||
position: relative;
|
||||
background: #f6f6f6;
|
||||
background: $gray-lightest;
|
||||
/* border-radius: 3px; */
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
|
|
@ -147,5 +147,5 @@
|
|||
}
|
||||
|
||||
.lastInGroup {
|
||||
background: white;
|
||||
background: $white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function TimelineScale(props: Props) {
|
|||
el.style.opacity = '0.8';
|
||||
el.innerHTML = `${txt}`;
|
||||
el.style.fontSize = '12px';
|
||||
el.style.color = 'white';
|
||||
el.classList.add('text-white')
|
||||
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Key({ label }: { label: string }) {
|
|||
return (
|
||||
<div
|
||||
style={{ minWidth: 52 }}
|
||||
className="whitespace-nowrap font-normal bg-indigo-50 rounded-lg px-2 py-1 text-figmaColors-text-primary text-center font-mono"
|
||||
className="whitespace-nowrap font-normal bg-active-blue rounded-lg px-2 py-1 text-black text-center font-mono"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function ReadNote(props: Props) {
|
|||
className="flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded-lg bg-amber-50"
|
||||
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded-lg bg-amber"
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<div className="flex items-center w-full">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
|
||||
.checkers {
|
||||
background: repeating-linear-gradient(135deg, #f3f3f3, #f3f3f3 1px, #f6f6f6 1px, #FFF 4px);
|
||||
background: repeating-linear-gradient(135deg, var(--color-gray-lighter), var(--color-gray-lighter) 1px, var(--color-gray-lightest) 1px, var(--color-white) 4px);
|
||||
}
|
||||
.solidBg {
|
||||
background: $gray-lightest;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
border-bottom: solid thin $gray-light;
|
||||
padding-left: 15px;
|
||||
padding-right: 0;
|
||||
background-color: white;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function SpotPlayerControls() {
|
|||
<div className="w-full p-4 flex items-center gap-4 bg-white">
|
||||
<PlayButton togglePlay={togglePlay} state={playState} iconSize={36} />
|
||||
|
||||
<div className="px-2 py-1 bg-white rounded font-semibold text-black flex items-center gap-2">
|
||||
<div className="px-2 py-1 bg-white rounded font-semibold flex items-center gap-2">
|
||||
<PlayTime isCustom time={spotPlayerStore.time * 1000} format="mm:ss" />
|
||||
<span>/</span>
|
||||
<div>{spotPlayerStore.durationString}</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { Avatar, Icon } from 'UI';
|
|||
import { TABS, Tab } from '../consts';
|
||||
import AccessModal from './AccessModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const spotLink = spotsList();
|
||||
|
||||
|
|
@ -89,8 +90,12 @@ function SpotPlayerHeader({
|
|||
|
||||
const onMenuClick = async ({ key }: { key: string }) => {
|
||||
if (key === '1') {
|
||||
const loader = toast.loading('Retrieving Spot video...')
|
||||
const { url } = await spotStore.getVideo(spotStore.currentSpot!.spotId);
|
||||
await downloadFile(url, `${spotStore.currentSpot!.title}.mp4`);
|
||||
setTimeout(() => {
|
||||
toast.dismiss(loader)
|
||||
}, 0)
|
||||
} else if (key === '2') {
|
||||
spotStore.deleteSpot([spotStore.currentSpot!.spotId]).then(() => {
|
||||
history.push(spotsList());
|
||||
|
|
@ -245,12 +250,11 @@ function SpotPlayerHeader({
|
|||
}
|
||||
|
||||
async function downloadFile(url: string, fileName: string) {
|
||||
const { t } = useTranslation();
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('Network response was not ok'));
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
|
@ -263,6 +267,7 @@ async function downloadFile(url: string, fileName: string) {
|
|||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} catch (error) {
|
||||
toast.error('Error downloading file.')
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,8 +80,12 @@ function SpotListItem({
|
|||
case 'rename':
|
||||
return setIsEdit(true);
|
||||
case 'download':
|
||||
const loader = toast.loading('Retrieving Spot video...')
|
||||
const { url } = await onVideo(spot.spotId);
|
||||
await downloadFile(url, `${spot.title}.mp4`);
|
||||
setTimeout(() => {
|
||||
toast.dismiss(loader)
|
||||
}, 0)
|
||||
return;
|
||||
case 'copy':
|
||||
copy(
|
||||
|
|
|
|||
31
frontend/app/components/ThemeToggle/index.tsx
Normal file
31
frontend/app/components/ThemeToggle/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { BulbOutlined, BulbFilled } from '@ant-design/icons';
|
||||
import { useTheme } from 'App/ThemeContext';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: 'large' | 'middle' | 'small';
|
||||
}
|
||||
|
||||
const ThemeToggle: React.FC<ThemeToggleProps> = ({
|
||||
className = '',
|
||||
style = {},
|
||||
size = 'middle'
|
||||
}) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
icon={theme === 'dark' ? <BulbFilled /> : <BulbOutlined />}
|
||||
onClick={toggleTheme}
|
||||
className={className}
|
||||
style={style}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
|
|
@ -125,7 +125,7 @@ export function AutocompleteModal({
|
|||
if (index === blocksAmount - 1 && blocksAmount > 1) {
|
||||
str += ' and ';
|
||||
}
|
||||
str += `"${block.trim()}"`;
|
||||
str += block.trim();
|
||||
if (index < blocksAmount - 2) {
|
||||
str += ', ';
|
||||
}
|
||||
|
|
@ -188,10 +188,10 @@ export function AutocompleteModal({
|
|||
{query.length ? (
|
||||
<div className="border-y border-y-gray-light py-2">
|
||||
<div
|
||||
className="whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
|
||||
className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
|
||||
onClick={applyQuery}
|
||||
>
|
||||
{t('Apply')} {queryStr}
|
||||
{t('Apply')} <span className='font-semibold'>{queryStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -128,8 +128,10 @@ const FilterAutoComplete = observer(
|
|||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
if (!initialFocus) {
|
||||
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
|
||||
}
|
||||
setInitialFocus(true);
|
||||
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,68 +1,6 @@
|
|||
import React from 'react';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const dropdownStyles = {
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin #ddd',
|
||||
boxShadow: 'none !important',
|
||||
cursor: 'pointer',
|
||||
height: '26px',
|
||||
minHeight: '26px',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '.5rem',
|
||||
'&:hover': {
|
||||
borderColor: 'rgb(115 115 115 / 0.9)',
|
||||
},
|
||||
};
|
||||
|
||||
return obj;
|
||||
},
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
width: 'fit-content',
|
||||
height: 26,
|
||||
'& input': {
|
||||
marginTop: '-3px',
|
||||
},
|
||||
}),
|
||||
placeholder: (provided: any) => ({
|
||||
...provided,
|
||||
}),
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
display: 'none',
|
||||
}),
|
||||
// option: (provided: any, state: any) => ({
|
||||
// ...provided,
|
||||
// whiteSpace: 'nowrap',
|
||||
// }),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
marginTop: '0.5rem',
|
||||
left: 0,
|
||||
minWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
zIndex: 100,
|
||||
border: 'none',
|
||||
boxShadow: '0px 4px 10px rgba(0,0,0, 0.15)',
|
||||
}),
|
||||
container: (provided: any) => ({
|
||||
...provided,
|
||||
minWidth: 'max-content',
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return {
|
||||
...provided,
|
||||
opacity,
|
||||
transition,
|
||||
marginTop: '-3px',
|
||||
};
|
||||
},
|
||||
};
|
||||
interface Props {
|
||||
onChange: (e: any, { name, value }: any) => void;
|
||||
className?: string;
|
||||
|
|
@ -84,7 +22,8 @@ function FilterOperator(props: Props) {
|
|||
<Select
|
||||
name="operator"
|
||||
options={options || []}
|
||||
styles={dropdownStyles}
|
||||
styles={{ height: 26 }}
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder="Select"
|
||||
isDisabled={isDisabled}
|
||||
value={value ? options?.find((i: any) => i.value === value) : null}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function SearchActions() {
|
|||
// @ts-ignore
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
const showAiField = isSaas && activeTab.type === 'sessions';
|
||||
const showAiField = isSaas && activeTab?.type === 'sessions';
|
||||
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
|
||||
return !metaLoading ? (
|
||||
<div className="mb-2">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import React from 'react';
|
||||
import Select, { components, DropdownIndicatorProps } from 'react-select';
|
||||
import { Icon } from 'UI';
|
||||
import colors from 'App/theme/colors';
|
||||
|
||||
const { ValueContainer } = components;
|
||||
import { Select } from 'antd';
|
||||
|
||||
type ValueObject = {
|
||||
value: string | number;
|
||||
|
|
@ -12,190 +8,44 @@ type ValueObject = {
|
|||
|
||||
interface Props<Value extends ValueObject> {
|
||||
options: Value[];
|
||||
isSearchable?: boolean;
|
||||
defaultValue?: string | number;
|
||||
plain?: boolean;
|
||||
components?: any;
|
||||
styles?: Record<string, any>;
|
||||
controlStyle?: Record<string, any>;
|
||||
onChange: (newValue: { name: string; value: Value }) => void;
|
||||
showSearch?: boolean;
|
||||
defaultValue?: string | number | (string | number)[];
|
||||
onChange: (value: any, option: any) => void;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
mode?: 'multiple' | 'tags';
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export default function <Value extends ValueObject>({
|
||||
export default function CustomSelect<Value extends ValueObject>({
|
||||
placeholder = 'Select',
|
||||
name = '',
|
||||
onChange,
|
||||
right = false,
|
||||
plain = false,
|
||||
options,
|
||||
isSearchable = false,
|
||||
components = {},
|
||||
styles = {},
|
||||
showSearch = false,
|
||||
defaultValue = '',
|
||||
controlStyle = {},
|
||||
className = '',
|
||||
mode,
|
||||
styles,
|
||||
...rest
|
||||
}: Props<Value>) {
|
||||
const defaultSelected = Array.isArray(defaultValue)
|
||||
? defaultValue.map((value) =>
|
||||
options.find((option) => option.value === value),
|
||||
)
|
||||
: options.find((option) => option.value === defaultValue) || null;
|
||||
const customStyles = {
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.3s',
|
||||
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
|
||||
color: state.isFocused ? colors.teal : 'black',
|
||||
fontSize: '14px',
|
||||
'&:hover': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue'],
|
||||
},
|
||||
'&:focus': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue'],
|
||||
},
|
||||
}),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
top: 31,
|
||||
borderRadius: '.5rem',
|
||||
right: right ? 0 : undefined,
|
||||
border: `1px solid ${colors['gray-light']}`,
|
||||
// borderRadius: '3px',
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
|
||||
position: 'absolute',
|
||||
minWidth: 'fit-content',
|
||||
// zIndex: 99,
|
||||
overflow: 'hidden',
|
||||
zIndex: 100,
|
||||
...(right && { right: 0 }),
|
||||
}),
|
||||
menuList: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
padding: 0,
|
||||
}),
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin #ddd',
|
||||
cursor: 'pointer',
|
||||
minHeight: '36px',
|
||||
transition: 'all 0.5s',
|
||||
'&:hover': {
|
||||
backgroundColor: colors['gray-lightest'],
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
},
|
||||
...controlStyle,
|
||||
};
|
||||
if (plain) {
|
||||
obj.backgroundColor = 'transparent';
|
||||
obj.border = '1px solid transparent';
|
||||
obj.backgroundColor = 'transparent';
|
||||
obj['&:hover'] = {
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: colors['gray-light'],
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
};
|
||||
obj['&:focus'] = {
|
||||
borderColor: 'transparent',
|
||||
};
|
||||
obj['&:active'] = {
|
||||
borderColor: 'transparent',
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
...provided,
|
||||
maxHeight: '34px',
|
||||
padding: 0,
|
||||
}),
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
paddingRight: '0px',
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return {
|
||||
...provided,
|
||||
opacity,
|
||||
transition,
|
||||
fontWeight: '500',
|
||||
};
|
||||
},
|
||||
input: (provided: any) => ({
|
||||
...provided,
|
||||
'& input:focus': {
|
||||
border: 'none !important',
|
||||
},
|
||||
}),
|
||||
noOptionsMessage: (provided: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap !important',
|
||||
// minWidth: 'fit-content',
|
||||
}),
|
||||
// Handle onChange to maintain compatibility with the original component
|
||||
const handleChange = (value: any, option: any) => {
|
||||
onChange({ name, value: option });
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={`${className} btn-event-condition`}
|
||||
className={className}
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
defaultValue={defaultSelected}
|
||||
components={{
|
||||
IndicatorSeparator: () => null,
|
||||
DropdownIndicator,
|
||||
ValueContainer: CustomValueContainer,
|
||||
...components,
|
||||
}}
|
||||
onChange={(value) => onChange({ name, value })}
|
||||
styles={{ ...customStyles, ...styles }}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: '#394EFF',
|
||||
},
|
||||
})}
|
||||
blurInputOnSelect
|
||||
showSearch={showSearch}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
mode={mode}
|
||||
style={styles}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownIndicator(props: DropdownIndicatorProps<true>) {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<Icon name="chevron-down" size="16" />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomValueContainer({ children, ...rest }: any) {
|
||||
const selectedCount = rest.getValue().length;
|
||||
const conditional = selectedCount < 3;
|
||||
|
||||
let firstChild: any = [];
|
||||
|
||||
if (!conditional) {
|
||||
firstChild = [children[0].shift(), children[1]];
|
||||
}
|
||||
|
||||
return (
|
||||
<ValueContainer {...rest}>
|
||||
{conditional ? children : firstChild}
|
||||
{!conditional && ` and ${selectedCount - 1} others`}
|
||||
</ValueContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ export default function MetaItem(props: Props) {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-sm flex flex-row items-center px-2 py-0 gap-1 rounded-lg bg-white border border-neutral-100 overflow-hidden',
|
||||
'text-sm flex flex-row items-center px-2 py-0 gap-1 rounded-lg bg-white border border-gray-light overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<TextEllipsis
|
||||
text={label}
|
||||
className="p-0"
|
||||
maxWidth={'300px'}
|
||||
popupProps={{ size: 'small', disabled: true }}
|
||||
/>
|
||||
<span className="bg-neutral-200 inline-block w-[1px] min-h-[17px]"></span>
|
||||
<span className="bg-gray-light inline-block w-[1px] min-h-[17px]"></span>
|
||||
<TextEllipsis
|
||||
text={value}
|
||||
maxWidth={'350px'}
|
||||
className="p-0 text-neutral-500"
|
||||
popupProps={{ size: 'small', disabled: true }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ interface Props {
|
|||
metaList: any[];
|
||||
maxLength?: number;
|
||||
onMetaClick?: (meta: { name: string, value: string }) => void;
|
||||
horizontal?: boolean;
|
||||
}
|
||||
|
||||
export default function SessionMetaList(props: Props) {
|
||||
const { className = '', metaList, maxLength = 14 } = props;
|
||||
const { className = '', metaList, maxLength = 14, horizontal = false } = props;
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center flex-wrap gap-1', className)}>
|
||||
<div className={cn('flex items-center gap-1', horizontal ? '' : 'flex-wrap', className)}>
|
||||
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
|
||||
<div key={index} className='cursor-pointer' onClick={() => props.onMetaClick?.({ name: `_${label}`, value })}>
|
||||
<MetaItem label={label} value={`${value}`} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
.sessionItem {
|
||||
background-color: #fff;
|
||||
background-color: $white;
|
||||
user-select: none;
|
||||
/* border-radius: 3px; */
|
||||
/* border: solid thin #EEEEEE; */
|
||||
transition: all 0.4s;
|
||||
|
||||
& .favorite {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ function ListingVisibility() {
|
|||
onChange={({ value }) => {
|
||||
changeSettings({ operator: value.value });
|
||||
}}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
|
|
@ -55,6 +56,7 @@ function ListingVisibility() {
|
|||
type="number"
|
||||
name="count"
|
||||
min={0}
|
||||
height={32}
|
||||
placeholder="E.g 10"
|
||||
onChange={({ target: { value } }: any) => {
|
||||
changeSettings({ count: value > 0 ? value : '' });
|
||||
|
|
@ -63,6 +65,7 @@ function ListingVisibility() {
|
|||
</div>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
className='w-full'
|
||||
defaultValue={durationSettings.countType || periodOptions[0].value}
|
||||
options={periodOptions}
|
||||
onChange={({ value }) => {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ function LatestSessionsMessage() {
|
|||
|
||||
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)' }}
|
||||
className="bg-amber p-1 flex w-full border-b text-center justify-center link"
|
||||
onClick={onShowNewSessions}
|
||||
>
|
||||
{t('Show')} {numberWithCommas(count)} {t('New')}{' '}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function NoteItem(props: Props) {
|
|||
? `${props.note.message.slice(0, 150)}...`
|
||||
: props.note.message;
|
||||
return (
|
||||
<div className="flex items-center px-2 border-b hover:bg-amber-50 justify-between py-2">
|
||||
<div className="flex items-center px-2 border-b hover:bg-amber justify-between py-2">
|
||||
<Link
|
||||
style={{ width: '90%' }}
|
||||
to={
|
||||
|
|
|
|||
|
|
@ -15,10 +15,8 @@ export default class SlideModal extends React.PureComponent {
|
|||
if (prevProps.isDisplayed !== this.props.isDisplayed) {
|
||||
if (this.props.isDisplayed) {
|
||||
document.addEventListener('keydown', this.keyPressHandler);
|
||||
// document.body.classList.add('no-scroll');
|
||||
} else {
|
||||
document.removeEventListener('keydown', this.keyPressHandler);
|
||||
// document.body.classList.remove('no-scroll');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import './styles/index.scss';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './init';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import colors from 'App/theme/colors';
|
||||
import { StoreProvider, RootStore } from './mstore';
|
||||
import Router from './Router';
|
||||
import store from './store';
|
||||
|
||||
// @ts-ignore
|
||||
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
|
||||
|
||||
const customTheme = {
|
||||
// algorithm: theme.compactAlgorithm,
|
||||
components: {
|
||||
Layout: {
|
||||
colorBgBody: colors['gray-lightest'],
|
||||
colorBgHeader: colors['gray-lightest'],
|
||||
},
|
||||
Menu: {
|
||||
// algorithm: true,
|
||||
// itemColor: colors['red'],
|
||||
// "itemActiveBg": "rgb(242, 21, 158)",
|
||||
// itemBgHover: colors['red'],
|
||||
|
||||
// colorText: colors['red'],
|
||||
// colorIcon: colors['red'],
|
||||
// colorBg: colors['gray-lightest'],
|
||||
// colorItemText: '#394EFF',
|
||||
// colorItemTextSelected: colors['teal'],
|
||||
// colorItemBg: colors['gray-lightest']
|
||||
},
|
||||
Button: {
|
||||
colorPrimary: colors.teal,
|
||||
algorithm: true, // Enable algorithm
|
||||
},
|
||||
},
|
||||
token: {
|
||||
colorPrimary: colors.teal,
|
||||
colorPrimaryActive: '#394EFF',
|
||||
colorSecondary: '#3EAAAF',
|
||||
colorBgLayout: colors['gray-lightest'],
|
||||
colorBgContainer: colors.white,
|
||||
colorLink: colors.teal,
|
||||
colorLinkHover: colors['teal-dark'],
|
||||
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
fontFamily: '\'Roboto\', \'ArialMT\', \'Arial\'',
|
||||
siderBackgroundColor: colors['gray-lightest'],
|
||||
siderCollapsedWidth: 800,
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('app');
|
||||
const root = createRoot(container);
|
||||
|
||||
// const theme = window.localStorage.getItem('theme');
|
||||
root.render(
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<Provider store={store}>
|
||||
<StoreProvider store={new RootStore()}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Router />
|
||||
</DndProvider>
|
||||
</StoreProvider>
|
||||
</Provider>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
});
|
||||
|
|
@ -5,66 +5,163 @@ import { createRoot } from 'react-dom/client';
|
|||
import './init';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { ConfigProvider, App, theme, ThemeConfig } from 'antd';
|
||||
import colors from 'App/theme/colors';
|
||||
import { ConfigProvider, App, theme as antdTheme, ThemeConfig } from 'antd';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Notification, MountPoint } from 'UI';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { StoreProvider, RootStore } from './mstore';
|
||||
import Router from './Router';
|
||||
import './i18n';
|
||||
import { ThemeProvider, useTheme } from './ThemeContext';
|
||||
|
||||
// @ts-ignore
|
||||
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const customTheme: ThemeConfig = {
|
||||
// algorithm: theme.compactAlgorithm,
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: colors['gray-lightest'],
|
||||
siderBg: colors['gray-lightest'],
|
||||
},
|
||||
Segmented: {
|
||||
itemSelectedBg: '#FFFFFF',
|
||||
itemSelectedColor: colors.main,
|
||||
},
|
||||
Menu: {
|
||||
colorPrimary: colors.teal,
|
||||
colorBgContainer: colors['gray-lightest'],
|
||||
colorFillTertiary: colors['gray-lightest'],
|
||||
colorBgLayout: colors['gray-lightest'],
|
||||
subMenuItemBg: colors['gray-lightest'],
|
||||
|
||||
itemHoverBg: colors['active-blue'],
|
||||
itemHoverColor: colors.teal,
|
||||
const cssVar = (name: string) => `var(--color-${name})`;
|
||||
|
||||
itemActiveBg: colors['active-blue'],
|
||||
itemSelectedBg: colors['active-blue'],
|
||||
itemSelectedColor: colors.teal,
|
||||
const ThemedApp: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
itemMarginBlock: 0,
|
||||
// itemPaddingInline: 50,
|
||||
// iconMarginInlineEnd: 14,
|
||||
collapsedWidth: 180,
|
||||
// Create theme based on current theme setting
|
||||
const customTheme: ThemeConfig = {
|
||||
algorithm: theme === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: cssVar('gray-lightest'),
|
||||
siderBg: cssVar('gray-lightest'),
|
||||
},
|
||||
Segmented: {
|
||||
itemSelectedBg: theme === 'dark' ? cssVar('gray-darkest') : '#FFFFFF',
|
||||
itemSelectedColor: cssVar('main'),
|
||||
},
|
||||
Menu: {
|
||||
colorPrimary: cssVar('teal'),
|
||||
colorBgContainer: cssVar('gray-lightest'),
|
||||
colorFillTertiary: cssVar('gray-lightest'),
|
||||
colorBgLayout: cssVar('gray-lightest'),
|
||||
subMenuItemBg: cssVar('gray-lightest'),
|
||||
itemHoverBg: cssVar('active-blue'),
|
||||
itemHoverColor: cssVar('teal'),
|
||||
itemActiveBg: cssVar('tealx-light'),
|
||||
itemSelectedBg: cssVar('tealx-light'),
|
||||
itemSelectedColor: cssVar('teal'),
|
||||
itemColor: cssVar('gray-darkest'),
|
||||
itemMarginBlock: 0,
|
||||
collapsedWidth: 180,
|
||||
},
|
||||
Button: {
|
||||
colorPrimary: cssVar('main'),
|
||||
},
|
||||
Select: {
|
||||
colorBgContainer: cssVar('white'),
|
||||
colorBgElevated: cssVar('white'),
|
||||
colorBorder: cssVar('gray-light'),
|
||||
colorPrimaryHover: cssVar('main'),
|
||||
colorPrimary: cssVar('main'),
|
||||
colorText: cssVar('gray-darkest'),
|
||||
colorTextPlaceholder: cssVar('gray-medium'),
|
||||
colorTextQuaternary: cssVar('gray-medium'),
|
||||
controlItemBgActive: cssVar('active-blue'),
|
||||
controlItemBgHover: cssVar('active-blue'),
|
||||
},
|
||||
Radio: {
|
||||
colorPrimary: cssVar('main'),
|
||||
colorBorder: cssVar('gray-medium'),
|
||||
colorBgContainer: cssVar('white'),
|
||||
},
|
||||
Switch: {
|
||||
colorPrimary: cssVar('main'),
|
||||
colorPrimaryHover: cssVar('teal-dark'),
|
||||
colorTextQuaternary: cssVar('gray-light'),
|
||||
colorTextTertiary: cssVar('gray-medium'),
|
||||
colorBgContainer: cssVar('white'),
|
||||
},
|
||||
Input: {
|
||||
colorBgContainer: cssVar('white'),
|
||||
colorBorder: cssVar('gray-light'),
|
||||
colorText: cssVar('gray-darkest'),
|
||||
colorTextPlaceholder: cssVar('gray-medium'),
|
||||
activeBorderColor: cssVar('main'),
|
||||
hoverBorderColor: cssVar('main'),
|
||||
},
|
||||
Checkbox: {
|
||||
colorPrimary: cssVar('main'),
|
||||
colorBgContainer: cssVar('white'),
|
||||
colorBorder: cssVar('gray-medium'),
|
||||
},
|
||||
Table: {
|
||||
colorBgContainer: cssVar('white'),
|
||||
colorText: cssVar('gray-darkest'),
|
||||
colorTextHeading: cssVar('gray-darkest'),
|
||||
colorBorderSecondary: cssVar('gray-light'),
|
||||
headerBg: cssVar('gray-lightest'),
|
||||
rowHoverBg: cssVar('gray-lightest'),
|
||||
headerSortHoverBg: cssVar('gray-lighter'),
|
||||
headerSortActiveBg: cssVar('gray-lighter')
|
||||
},
|
||||
Modal: {
|
||||
colorBgElevated: cssVar('white'),
|
||||
colorText: cssVar('gray-darkest'),
|
||||
},
|
||||
Card: {
|
||||
colorBgContainer: cssVar('white'),
|
||||
colorBorderSecondary: cssVar('gray-light'),
|
||||
},
|
||||
Tooltip: {
|
||||
colorBgSpotlight: cssVar('white'),
|
||||
colorTextLightSolid: cssVar('gray-darkest'),
|
||||
},
|
||||
Tabs: {
|
||||
itemActiveColor: cssVar('main'),
|
||||
inkBarColor: cssVar('main'),
|
||||
itemSelectedColor: cssVar('main')
|
||||
},
|
||||
Tag: {
|
||||
defaultBg: cssVar('gray-lightest'),
|
||||
defaultColor: cssVar('gray-darkest')
|
||||
}
|
||||
},
|
||||
Button: {
|
||||
colorPrimary: colors.teal,
|
||||
token: {
|
||||
colorPrimary: cssVar('main'),
|
||||
colorPrimaryActive: cssVar('teal-dark'),
|
||||
colorPrimaryHover: cssVar('main'),
|
||||
colorPrimaryBorder: cssVar('main'),
|
||||
colorBorder: cssVar('gray-light'),
|
||||
colorBgLayout: cssVar('gray-lightest'),
|
||||
colorBgContainer: cssVar('white'),
|
||||
controlItemBgActive: cssVar('active-blue'),
|
||||
controlItemBgActiveHover: cssVar('active-blue'),
|
||||
controlItemBgHover: cssVar('active-blue'),
|
||||
colorLink: cssVar('teal'),
|
||||
colorLinkHover: cssVar('teal-dark'),
|
||||
colorText: cssVar('gray-darkest'),
|
||||
colorTextSecondary: cssVar('gray-dark'),
|
||||
colorTextDisabled: cssVar('disabled-text'),
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
fontFamily: "'Roboto', 'ArialMT', 'Arial'",
|
||||
fontWeightStrong: 400,
|
||||
colorSplit: cssVar('gray-light')
|
||||
},
|
||||
},
|
||||
token: {
|
||||
colorPrimary: colors.teal,
|
||||
colorPrimaryActive: '#394EFF',
|
||||
colorBgLayout: colors['gray-lightest'],
|
||||
colorBgContainer: colors.white,
|
||||
colorLink: colors.teal,
|
||||
colorLinkHover: colors['teal-dark'],
|
||||
};
|
||||
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
fontFamily: "'Roboto', 'ArialMT', 'Arial'",
|
||||
fontWeightStrong: 400,
|
||||
},
|
||||
return (
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<App>
|
||||
<StoreProvider store={new RootStore()}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<BrowserRouter>
|
||||
<Notification />
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
</DndProvider>
|
||||
<MountPoint />
|
||||
</StoreProvider>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
|
@ -72,22 +169,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
// @ts-ignore
|
||||
const root = createRoot(container);
|
||||
|
||||
// const theme = window.localStorage.getItem('theme');
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<App>
|
||||
<StoreProvider store={new RootStore()}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<BrowserRouter>
|
||||
<Notification />
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
</DndProvider>
|
||||
<MountPoint />
|
||||
</StoreProvider>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</QueryClientProvider>,
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<ThemedApp />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -210,10 +210,10 @@ function SideMenu(props: Props) {
|
|||
<Icon
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? 'teal' : ''}
|
||||
color={isActive ? 'teal' : 'black'}
|
||||
/>
|
||||
}
|
||||
className={cn('!rounded-lg hover-fill-teal')}
|
||||
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
|
||||
>
|
||||
{item.label}
|
||||
</Menu.Item>
|
||||
|
|
@ -228,17 +228,17 @@ function SideMenu(props: Props) {
|
|||
<Icon
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? 'teal' : ''}
|
||||
color={isActive ? 'teal' : 'black'}
|
||||
/>
|
||||
}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
className={cn('!rounded-lg !pe-0')}
|
||||
className={cn('!rounded-lg !pe-0', isActive ? 'color-main' : 'color-black')}
|
||||
itemIcon={
|
||||
item.leading ? (
|
||||
<Icon
|
||||
name={item.leading}
|
||||
size={16}
|
||||
color={isActive ? 'teal' : ''}
|
||||
color={isActive ? 'teal' : 'black'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -293,18 +293,18 @@ function SideMenu(props: Props) {
|
|||
<Icon
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? 'teal' : ''}
|
||||
color={isActive ? 'teal' : 'black'}
|
||||
className="hover-fill-teal"
|
||||
/>
|
||||
}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
className={cn('!rounded-lg hover-fill-teal')}
|
||||
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
|
||||
itemIcon={
|
||||
item.leading ? (
|
||||
<Icon
|
||||
name={item.leading}
|
||||
size={16}
|
||||
color={isActive ? 'teal' : ''}
|
||||
color={isActive ? 'teal' : 'black'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress
|
|||
import ProjectDropdown from 'Shared/ProjectDropdown';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import ThemeToggle from 'Components/ThemeToggle';
|
||||
|
||||
function TopRight() {
|
||||
const { userStore } = useStore();
|
||||
|
|
@ -27,6 +28,7 @@ function TopRight() {
|
|||
{account.name ? <HealthStatus /> : null}
|
||||
</>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
|
||||
<Popover content={<UserMenu />} placement="topRight">
|
||||
<div className="flex items-center cursor-pointer">
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@
|
|||
"Returning users between": "Returning users between",
|
||||
"Sessions": "Sessions",
|
||||
"No recordings found.": "No recordings found.",
|
||||
"Get new session": "Get new session",
|
||||
"Get new image": "Get new image",
|
||||
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.",
|
||||
"Add Card": "Add Card",
|
||||
"Create Dashboard": "Create Dashboard",
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@
|
|||
"Returning users between": "Usuarios recurrentes entre",
|
||||
"Sessions": "Sesiones",
|
||||
"No recordings found.": "No se encontraron grabaciones.",
|
||||
"Get new session": "Obtener nueva sesión",
|
||||
"Get new image": "Obtener nueva sesión",
|
||||
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.",
|
||||
"Add Card": "Agregar tarjeta",
|
||||
"Create Dashboard": "Crear panel",
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@
|
|||
"Returning users between": "Utilisateurs récurrents entre",
|
||||
"Sessions": "Sessions",
|
||||
"No recordings found.": "Aucun enregistrement trouvé.",
|
||||
"Get new session": "Obtenir une nouvelle session",
|
||||
"Get new image": "Obtenir une nouvelle session",
|
||||
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.",
|
||||
"Add Card": "Ajouter une carte",
|
||||
"Create Dashboard": "Créer un tableau de bord",
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@
|
|||
"Returning users between": "Возвращающиеся пользователи за период",
|
||||
"Sessions": "Сессии",
|
||||
"No recordings found.": "Записей не найдено.",
|
||||
"Get new session": "Получить новую сессию",
|
||||
"Get new image": "Получить новую сессию",
|
||||
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
|
||||
"Add Card": "Добавить карточку",
|
||||
"Create Dashboard": "Создать дашборд",
|
||||
|
|
@ -1502,4 +1502,4 @@
|
|||
"Interface Language": "Язык интерфейса",
|
||||
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
|
||||
"Language": "Язык"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@
|
|||
"Returning users between": "回访用户区间",
|
||||
"Sessions": "会话",
|
||||
"No recordings found.": "未找到录制。",
|
||||
"Get new session": "获取新会话",
|
||||
"Get new image": "获取新会话",
|
||||
"The number of cards in one dashboard is limited to 30.": "一个仪表板最多可包含30个卡片。",
|
||||
"Add Card": "添加卡片",
|
||||
"Create Dashboard": "创建仪表板",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { makeAutoObservable, runInAction, reaction } from 'mobx';
|
||||
import { dashboardService, metricService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
|
@ -6,6 +6,7 @@ import { getRE } from 'App/utils';
|
|||
import Filter from './types/filter';
|
||||
import Widget from './types/widget';
|
||||
import Dashboard from './types/dashboard';
|
||||
import { calculateGranularities } from '@/components/Dashboard/components/WidgetDateRange/RangeGranularity';
|
||||
|
||||
interface DashboardFilter {
|
||||
query?: string;
|
||||
|
|
@ -36,7 +37,7 @@ export default class DashboardStore {
|
|||
|
||||
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
|
||||
|
||||
selectedDensity: number = 7; // depends on default drilldown, 7 points here!!!;
|
||||
selectedDensity: number = 7;
|
||||
|
||||
comparisonPeriods: Record<string, any> = {};
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ export default class DashboardStore {
|
|||
metricsSearch: string = '';
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean = true;
|
||||
isLoading: boolean = false;
|
||||
|
||||
isSaving: boolean = false;
|
||||
|
||||
|
|
@ -83,10 +84,25 @@ export default class DashboardStore {
|
|||
makeAutoObservable(this);
|
||||
|
||||
this.resetDrillDownFilter();
|
||||
|
||||
this.createDensity(this.period.getDuration());
|
||||
reaction(
|
||||
() => this.period,
|
||||
(period) => {
|
||||
this.createDensity(period.getDuration());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setDensity = (density: any) => {
|
||||
this.selectedDensity = parseInt(density, 10);
|
||||
createDensity = (duration: number) => {
|
||||
const densityOpts = calculateGranularities(duration);
|
||||
const defaultOption = densityOpts[densityOpts.length - 2];
|
||||
|
||||
this.setDensity(defaultOption.key)
|
||||
}
|
||||
|
||||
setDensity = (density: number) => {
|
||||
this.selectedDensity = density;
|
||||
};
|
||||
|
||||
get sortedDashboards() {
|
||||
|
|
@ -529,7 +545,7 @@ export default class DashboardStore {
|
|||
const data = await metricService.getMetricChartData(
|
||||
metric,
|
||||
params,
|
||||
isSaved,
|
||||
isSaved
|
||||
);
|
||||
resolve(metric.setData(data, period, isComparison, density));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export default class Widget {
|
|||
fromJson(json: any, period?: any) {
|
||||
json.config = json.config || {};
|
||||
runInAction(() => {
|
||||
this.dashboardId = json.dashboardId;
|
||||
this.metricId = json.metricId;
|
||||
this.widgetId = json.widgetId;
|
||||
this.metricValue = this.metricValueFromArray(
|
||||
|
|
|
|||
|
|
@ -140,11 +140,16 @@ class SimpleHeatmap {
|
|||
ctx.drawImage(this.circle, p[0] - this.r, p[1] - this.r);
|
||||
});
|
||||
|
||||
const colored = ctx.getImageData(0, 0, this.width, this.height);
|
||||
this.colorize(colored.data, this.grad);
|
||||
ctx.putImageData(colored, 0, 0);
|
||||
|
||||
return this;
|
||||
try {
|
||||
const colored = ctx.getImageData(0, 0, this.width, this.height);
|
||||
this.colorize(colored.data, this.grad);
|
||||
ctx.putImageData(colored, 0, 0);
|
||||
} catch (e) {
|
||||
// usually happens if session is corrupted ?
|
||||
console.error('Error while colorizing heatmap:', e);
|
||||
} finally {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private colorize(
|
||||
|
|
|
|||
|
|
@ -255,6 +255,48 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 37: {
|
||||
const id = this.readUint(); if (id === null) { return resetPointer() }
|
||||
const rule = this.readString(); if (rule === null) { return resetPointer() }
|
||||
const index = this.readUint(); if (index === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.CssInsertRule,
|
||||
id,
|
||||
rule,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
case 38: {
|
||||
const id = this.readUint(); if (id === null) { return resetPointer() }
|
||||
const index = this.readUint(); if (index === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.CssDeleteRule,
|
||||
id,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
case 39: {
|
||||
const method = this.readString(); if (method === null) { return resetPointer() }
|
||||
const url = this.readString(); if (url === null) { return resetPointer() }
|
||||
const request = this.readString(); if (request === null) { return resetPointer() }
|
||||
const response = this.readString(); if (response === null) { return resetPointer() }
|
||||
const status = this.readUint(); if (status === null) { return resetPointer() }
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.Fetch,
|
||||
method,
|
||||
url,
|
||||
request,
|
||||
response,
|
||||
status,
|
||||
timestamp,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
case 40: {
|
||||
const name = this.readString(); if (name === null) { return resetPointer() }
|
||||
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
||||
|
|
@ -459,6 +501,26 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 59: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
||||
const context = this.readUint(); if (context === null) { return resetPointer() }
|
||||
const containerType = this.readUint(); if (containerType === null) { return resetPointer() }
|
||||
const containerSrc = this.readString(); if (containerSrc === null) { return resetPointer() }
|
||||
const containerId = this.readString(); if (containerId === null) { return resetPointer() }
|
||||
const containerName = this.readString(); if (containerName === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.LongTask,
|
||||
timestamp,
|
||||
duration,
|
||||
context,
|
||||
containerType,
|
||||
containerSrc,
|
||||
containerId,
|
||||
containerName,
|
||||
};
|
||||
}
|
||||
|
||||
case 60: {
|
||||
const id = this.readUint(); if (id === null) { return resetPointer() }
|
||||
const name = this.readString(); if (name === null) { return resetPointer() }
|
||||
|
|
@ -485,6 +547,20 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 67: {
|
||||
const id = this.readUint(); if (id === null) { return resetPointer() }
|
||||
const rule = this.readString(); if (rule === null) { return resetPointer() }
|
||||
const index = this.readUint(); if (index === null) { return resetPointer() }
|
||||
const baseURL = this.readString(); if (baseURL === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.CssInsertRuleURLBased,
|
||||
id,
|
||||
rule,
|
||||
index,
|
||||
baseURL,
|
||||
};
|
||||
}
|
||||
|
||||
case 68: {
|
||||
const id = this.readUint(); if (id === null) { return resetPointer() }
|
||||
const hesitationTime = this.readUint(); if (hesitationTime === null) { return resetPointer() }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { MType } from './raw.gen'
|
||||
|
||||
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,34,35,49,50,51,43,52,54,55,57,58,60,61,68,69,70,71,72,73,74,75,76,77,113,114,117,118,119,122]
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,34,35,37,38,49,50,51,43,52,54,55,57,58,59,60,61,67,68,69,70,71,72,73,74,75,76,77,113,114,117,118,119,122]
|
||||
export function isDOMType(t: MType) {
|
||||
return DOM_TYPES.includes(t)
|
||||
}
|
||||
|
|
@ -25,6 +25,9 @@ import type {
|
|||
RawConsoleLog,
|
||||
RawStringDictGlobal,
|
||||
RawSetNodeAttributeDictGlobal,
|
||||
RawCssInsertRule,
|
||||
RawCssDeleteRule,
|
||||
RawFetch,
|
||||
RawProfiler,
|
||||
RawOTable,
|
||||
RawReduxDeprecated,
|
||||
|
|
@ -42,8 +45,10 @@ import type {
|
|||
RawSetPageVisibility,
|
||||
RawLoadFontFace,
|
||||
RawSetNodeFocus,
|
||||
RawLongTask,
|
||||
RawSetNodeAttributeURLBased,
|
||||
RawSetCssDataURLBased,
|
||||
RawCssInsertRuleURLBased,
|
||||
RawMouseClick,
|
||||
RawMouseClickDeprecated,
|
||||
RawCreateIFrameDocument,
|
||||
|
|
@ -125,6 +130,12 @@ export type StringDictGlobal = RawStringDictGlobal & Timed
|
|||
|
||||
export type SetNodeAttributeDictGlobal = RawSetNodeAttributeDictGlobal & Timed
|
||||
|
||||
export type CssInsertRule = RawCssInsertRule & Timed
|
||||
|
||||
export type CssDeleteRule = RawCssDeleteRule & Timed
|
||||
|
||||
export type Fetch = RawFetch & Timed
|
||||
|
||||
export type Profiler = RawProfiler & Timed
|
||||
|
||||
export type OTable = RawOTable & Timed
|
||||
|
|
@ -159,10 +170,14 @@ export type LoadFontFace = RawLoadFontFace & Timed
|
|||
|
||||
export type SetNodeFocus = RawSetNodeFocus & Timed
|
||||
|
||||
export type LongTask = RawLongTask & Timed
|
||||
|
||||
export type SetNodeAttributeURLBased = RawSetNodeAttributeURLBased & Timed
|
||||
|
||||
export type SetCssDataURLBased = RawSetCssDataURLBased & Timed
|
||||
|
||||
export type CssInsertRuleURLBased = RawCssInsertRuleURLBased & Timed
|
||||
|
||||
export type MouseClick = RawMouseClick & Timed
|
||||
|
||||
export type MouseClickDeprecated = RawMouseClickDeprecated & Timed
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ export const enum MType {
|
|||
ConsoleLog = 22,
|
||||
StringDictGlobal = 34,
|
||||
SetNodeAttributeDictGlobal = 35,
|
||||
CssInsertRule = 37,
|
||||
CssDeleteRule = 38,
|
||||
Fetch = 39,
|
||||
Profiler = 40,
|
||||
OTable = 41,
|
||||
ReduxDeprecated = 44,
|
||||
|
|
@ -40,8 +43,10 @@ export const enum MType {
|
|||
SetPageVisibility = 55,
|
||||
LoadFontFace = 57,
|
||||
SetNodeFocus = 58,
|
||||
LongTask = 59,
|
||||
SetNodeAttributeURLBased = 60,
|
||||
SetCssDataURLBased = 61,
|
||||
CssInsertRuleURLBased = 67,
|
||||
MouseClick = 68,
|
||||
MouseClickDeprecated = 69,
|
||||
CreateIFrameDocument = 70,
|
||||
|
|
@ -218,6 +223,30 @@ export interface RawSetNodeAttributeDictGlobal {
|
|||
value: number,
|
||||
}
|
||||
|
||||
export interface RawCssInsertRule {
|
||||
tp: MType.CssInsertRule,
|
||||
id: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
}
|
||||
|
||||
export interface RawCssDeleteRule {
|
||||
tp: MType.CssDeleteRule,
|
||||
id: number,
|
||||
index: number,
|
||||
}
|
||||
|
||||
export interface RawFetch {
|
||||
tp: MType.Fetch,
|
||||
method: string,
|
||||
url: string,
|
||||
request: string,
|
||||
response: string,
|
||||
status: number,
|
||||
timestamp: number,
|
||||
duration: number,
|
||||
}
|
||||
|
||||
export interface RawProfiler {
|
||||
tp: MType.Profiler,
|
||||
name: string,
|
||||
|
|
@ -337,6 +366,17 @@ export interface RawSetNodeFocus {
|
|||
id: number,
|
||||
}
|
||||
|
||||
export interface RawLongTask {
|
||||
tp: MType.LongTask,
|
||||
timestamp: number,
|
||||
duration: number,
|
||||
context: number,
|
||||
containerType: number,
|
||||
containerSrc: string,
|
||||
containerId: string,
|
||||
containerName: string,
|
||||
}
|
||||
|
||||
export interface RawSetNodeAttributeURLBased {
|
||||
tp: MType.SetNodeAttributeURLBased,
|
||||
id: number,
|
||||
|
|
@ -352,6 +392,14 @@ export interface RawSetCssDataURLBased {
|
|||
baseURL: string,
|
||||
}
|
||||
|
||||
export interface RawCssInsertRuleURLBased {
|
||||
tp: MType.CssInsertRuleURLBased,
|
||||
id: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
baseURL: string,
|
||||
}
|
||||
|
||||
export interface RawMouseClick {
|
||||
tp: MType.MouseClick,
|
||||
id: number,
|
||||
|
|
@ -622,4 +670,4 @@ export interface RawMobileIssueEvent {
|
|||
}
|
||||
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawIncident | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawIncident | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ export const TP_MAP = {
|
|||
22: MType.ConsoleLog,
|
||||
34: MType.StringDictGlobal,
|
||||
35: MType.SetNodeAttributeDictGlobal,
|
||||
37: MType.CssInsertRule,
|
||||
38: MType.CssDeleteRule,
|
||||
39: MType.Fetch,
|
||||
40: MType.Profiler,
|
||||
41: MType.OTable,
|
||||
44: MType.ReduxDeprecated,
|
||||
|
|
@ -41,8 +44,10 @@ export const TP_MAP = {
|
|||
55: MType.SetPageVisibility,
|
||||
57: MType.LoadFontFace,
|
||||
58: MType.SetNodeFocus,
|
||||
59: MType.LongTask,
|
||||
60: MType.SetNodeAttributeURLBased,
|
||||
61: MType.SetCssDataURLBased,
|
||||
67: MType.CssInsertRuleURLBased,
|
||||
68: MType.MouseClick,
|
||||
69: MType.MouseClickDeprecated,
|
||||
70: MType.CreateIFrameDocument,
|
||||
|
|
|
|||
|
|
@ -186,6 +186,30 @@ type TrSetNodeAttributeDictGlobal = [
|
|||
value: number,
|
||||
]
|
||||
|
||||
type TrCSSInsertRule = [
|
||||
type: 37,
|
||||
id: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
]
|
||||
|
||||
type TrCSSDeleteRule = [
|
||||
type: 38,
|
||||
id: number,
|
||||
index: number,
|
||||
]
|
||||
|
||||
type TrFetch = [
|
||||
type: 39,
|
||||
method: string,
|
||||
url: string,
|
||||
request: string,
|
||||
response: string,
|
||||
status: number,
|
||||
timestamp: number,
|
||||
duration: number,
|
||||
]
|
||||
|
||||
type TrProfiler = [
|
||||
type: 40,
|
||||
name: string,
|
||||
|
|
@ -310,6 +334,17 @@ type TrSetNodeFocus = [
|
|||
id: number,
|
||||
]
|
||||
|
||||
type TrLongTask = [
|
||||
type: 59,
|
||||
timestamp: number,
|
||||
duration: number,
|
||||
context: number,
|
||||
containerType: number,
|
||||
containerSrc: string,
|
||||
containerId: string,
|
||||
containerName: string,
|
||||
]
|
||||
|
||||
type TrSetNodeAttributeURLBased = [
|
||||
type: 60,
|
||||
id: number,
|
||||
|
|
@ -337,6 +372,14 @@ type TrCustomIssue = [
|
|||
payload: string,
|
||||
]
|
||||
|
||||
type TrCSSInsertRuleURLBased = [
|
||||
type: 67,
|
||||
id: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
baseURL: string,
|
||||
]
|
||||
|
||||
type TrMouseClick = [
|
||||
type: 68,
|
||||
id: number,
|
||||
|
|
@ -547,7 +590,7 @@ type TrWebVitals = [
|
|||
]
|
||||
|
||||
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrIncident | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrIncident | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
|
||||
|
||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||
switch(tMsg[0]) {
|
||||
|
|
@ -725,6 +768,36 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 37: {
|
||||
return {
|
||||
tp: MType.CssInsertRule,
|
||||
id: tMsg[1],
|
||||
rule: tMsg[2],
|
||||
index: tMsg[3],
|
||||
}
|
||||
}
|
||||
|
||||
case 38: {
|
||||
return {
|
||||
tp: MType.CssDeleteRule,
|
||||
id: tMsg[1],
|
||||
index: tMsg[2],
|
||||
}
|
||||
}
|
||||
|
||||
case 39: {
|
||||
return {
|
||||
tp: MType.Fetch,
|
||||
method: tMsg[1],
|
||||
url: tMsg[2],
|
||||
request: tMsg[3],
|
||||
response: tMsg[4],
|
||||
status: tMsg[5],
|
||||
timestamp: tMsg[6],
|
||||
duration: tMsg[7],
|
||||
}
|
||||
}
|
||||
|
||||
case 40: {
|
||||
return {
|
||||
tp: MType.Profiler,
|
||||
|
|
@ -878,6 +951,19 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 59: {
|
||||
return {
|
||||
tp: MType.LongTask,
|
||||
timestamp: tMsg[1],
|
||||
duration: tMsg[2],
|
||||
context: tMsg[3],
|
||||
containerType: tMsg[4],
|
||||
containerSrc: tMsg[5],
|
||||
containerId: tMsg[6],
|
||||
containerName: tMsg[7],
|
||||
}
|
||||
}
|
||||
|
||||
case 60: {
|
||||
return {
|
||||
tp: MType.SetNodeAttributeURLBased,
|
||||
|
|
@ -897,6 +983,16 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 67: {
|
||||
return {
|
||||
tp: MType.CssInsertRuleURLBased,
|
||||
id: tMsg[1],
|
||||
rule: tMsg[2],
|
||||
index: tMsg[3],
|
||||
baseURL: tMsg[4],
|
||||
}
|
||||
}
|
||||
|
||||
case 68: {
|
||||
return {
|
||||
tp: MType.MouseClick,
|
||||
|
|
|
|||
|
|
@ -1,268 +1,342 @@
|
|||
/* Auto-generated, DO NOT EDIT */
|
||||
/* Uses CSS variables (--color-*) generated by Tailwind config */
|
||||
|
||||
/* fill */
|
||||
.fill-main { fill: #394EFF }
|
||||
.fill-gray-light-shade { fill: #EEEEEE }
|
||||
.fill-gray-lightest { fill: #f6f6f6 }
|
||||
.fill-gray-lighter { fill: #f1f1f1 }
|
||||
.fill-gray-light { fill: #ddd }
|
||||
.fill-gray-bg { fill: #CCC }
|
||||
.fill-gray-medium { fill: #888 }
|
||||
.fill-gray-dark { fill: #666 }
|
||||
.fill-gray-darkest { fill: #333 }
|
||||
.fill-gray-light-blue { fill: #F8F8FA }
|
||||
.fill-teal { fill: #394EFF }
|
||||
.fill-teal-dark { fill: #2331A8 }
|
||||
.fill-teal-light { fill: rgba(57, 78, 255, 0.1) }
|
||||
.fill-tealx { fill: #3EAAAF }
|
||||
.fill-tealx-light { fill: #E2F0EE }
|
||||
.fill-tealx-light-border { fill: #C6DCDA }
|
||||
.fill-tealx-lightest { fill: rgba(62, 170, 175, 0.1) }
|
||||
.fill-orange { fill: #E28940 }
|
||||
.fill-yellow { fill: #FFFBE5 }
|
||||
.fill-yellow2 { fill: #F5A623 }
|
||||
.fill-orange-dark { fill: #C26822 }
|
||||
.fill-green { fill: #42AE5E }
|
||||
.fill-green2 { fill: #00dc69 }
|
||||
.fill-green-dark { fill: #2C9848 }
|
||||
.fill-red { fill: #cc0000 }
|
||||
.fill-red2 { fill: #F5A623 }
|
||||
.fill-red-lightest { fill: rgba(204, 0, 0, 0.1) }
|
||||
.fill-blue { fill: #366CD9 }
|
||||
.fill-blue2 { fill: #0076FF }
|
||||
.fill-active-blue { fill: #F6F7FF }
|
||||
.fill-active-dark-blue { fill: #E2E4F6 }
|
||||
.fill-bg-blue { fill: #e3e6ff }
|
||||
.fill-active-blue-border { fill: #D0D4F2 }
|
||||
.fill-pink { fill: #ffb9b9 }
|
||||
.fill-light-blue-bg { fill: #E5F7F7 }
|
||||
.fill-white { fill: #fff }
|
||||
.fill-borderColor-default { fill: #DDDDDD }
|
||||
.fill-borderColor-gray-light-shade { fill: #EEEEEE }
|
||||
.fill-borderColor-primary { fill: #3490dc }
|
||||
.fill-borderColor-transparent { fill: transparent }
|
||||
.fill-transparent { fill: transparent }
|
||||
.fill-cyan { fill: #EBF4F5 }
|
||||
.fill-figmaColors-accent-secondary { fill: rgba(62, 170, 175, 1) }
|
||||
.fill-figmaColors-main { fill: rgba(57, 78, 255, 1) }
|
||||
.fill-figmaColors-primary-outlined-hover-background { fill: rgba(62, 170, 175, 0.08) }
|
||||
.fill-figmaColors-primary-outlined-resting-border { fill: rgba(62, 170, 175, 0.5) }
|
||||
.fill-figmaColors-secondary-outlined-hover-background { fill: rgba(63, 81, 181, 0.08) }
|
||||
.fill-figmaColors-secondary-outlined-resting-border { fill: rgba(63, 81, 181, 0.5) }
|
||||
.fill-figmaColors-text-disabled { fill: rgba(0,0,0, 0.38) }
|
||||
.fill-figmaColors-text-primary { fill: rgba(0,0,0, 0.87) }
|
||||
.fill-figmaColors-outlined-border { fill: rgba(0,0,0, 0.23) }
|
||||
.fill-figmaColors-divider { fill: rgba(0, 0, 0, 0.12) }
|
||||
.hover-fill-main:hover svg { fill: #394EFF }
|
||||
.hover-fill-gray-light-shade:hover svg { fill: #EEEEEE }
|
||||
.hover-fill-gray-lightest:hover svg { fill: #f6f6f6 }
|
||||
.hover-fill-gray-lighter:hover svg { fill: #f1f1f1 }
|
||||
.hover-fill-gray-light:hover svg { fill: #ddd }
|
||||
.hover-fill-gray-bg:hover svg { fill: #CCC }
|
||||
.hover-fill-gray-medium:hover svg { fill: #888 }
|
||||
.hover-fill-gray-dark:hover svg { fill: #666 }
|
||||
.hover-fill-gray-darkest:hover svg { fill: #333 }
|
||||
.hover-fill-gray-light-blue:hover svg { fill: #F8F8FA }
|
||||
.hover-fill-teal:hover svg { fill: #394EFF }
|
||||
.hover-fill-teal-dark:hover svg { fill: #2331A8 }
|
||||
.hover-fill-teal-light:hover svg { fill: rgba(57, 78, 255, 0.1) }
|
||||
.hover-fill-tealx:hover svg { fill: #3EAAAF }
|
||||
.hover-fill-tealx-light:hover svg { fill: #E2F0EE }
|
||||
.hover-fill-tealx-light-border:hover svg { fill: #C6DCDA }
|
||||
.hover-fill-tealx-lightest:hover svg { fill: rgba(62, 170, 175, 0.1) }
|
||||
.hover-fill-orange:hover svg { fill: #E28940 }
|
||||
.hover-fill-yellow:hover svg { fill: #FFFBE5 }
|
||||
.hover-fill-yellow2:hover svg { fill: #F5A623 }
|
||||
.hover-fill-orange-dark:hover svg { fill: #C26822 }
|
||||
.hover-fill-green:hover svg { fill: #42AE5E }
|
||||
.hover-fill-green2:hover svg { fill: #00dc69 }
|
||||
.hover-fill-green-dark:hover svg { fill: #2C9848 }
|
||||
.hover-fill-red:hover svg { fill: #cc0000 }
|
||||
.hover-fill-red2:hover svg { fill: #F5A623 }
|
||||
.hover-fill-red-lightest:hover svg { fill: rgba(204, 0, 0, 0.1) }
|
||||
.hover-fill-blue:hover svg { fill: #366CD9 }
|
||||
.hover-fill-blue2:hover svg { fill: #0076FF }
|
||||
.hover-fill-active-blue:hover svg { fill: #F6F7FF }
|
||||
.hover-fill-active-dark-blue:hover svg { fill: #E2E4F6 }
|
||||
.hover-fill-bg-blue:hover svg { fill: #e3e6ff }
|
||||
.hover-fill-active-blue-border:hover svg { fill: #D0D4F2 }
|
||||
.hover-fill-pink:hover svg { fill: #ffb9b9 }
|
||||
.hover-fill-light-blue-bg:hover svg { fill: #E5F7F7 }
|
||||
.hover-fill-white:hover svg { fill: #fff }
|
||||
.hover-fill-borderColor-default:hover svg { fill: #DDDDDD }
|
||||
.hover-fill-borderColor-gray-light-shade:hover svg { fill: #EEEEEE }
|
||||
.hover-fill-borderColor-primary:hover svg { fill: #3490dc }
|
||||
.hover-fill-borderColor-transparent:hover svg { fill: transparent }
|
||||
.hover-fill-transparent:hover svg { fill: transparent }
|
||||
.hover-fill-cyan:hover svg { fill: #EBF4F5 }
|
||||
.hover-fill-figmaColors-accent-secondary:hover svg { fill: rgba(62, 170, 175, 1) }
|
||||
.hover-fill-figmaColors-main:hover svg { fill: rgba(57, 78, 255, 1) }
|
||||
.hover-fill-figmaColors-primary-outlined-hover-background:hover svg { fill: rgba(62, 170, 175, 0.08) }
|
||||
.hover-fill-figmaColors-primary-outlined-resting-border:hover svg { fill: rgba(62, 170, 175, 0.5) }
|
||||
.hover-fill-figmaColors-secondary-outlined-hover-background:hover svg { fill: rgba(63, 81, 181, 0.08) }
|
||||
.hover-fill-figmaColors-secondary-outlined-resting-border:hover svg { fill: rgba(63, 81, 181, 0.5) }
|
||||
.hover-fill-figmaColors-text-disabled:hover svg { fill: rgba(0,0,0, 0.38) }
|
||||
.hover-fill-figmaColors-text-primary:hover svg { fill: rgba(0,0,0, 0.87) }
|
||||
.hover-fill-figmaColors-outlined-border:hover svg { fill: rgba(0,0,0, 0.23) }
|
||||
.hover-fill-figmaColors-divider:hover svg { fill: rgba(0, 0, 0, 0.12) }
|
||||
.fill-main { fill: var(--color-main) }
|
||||
.fill-gray-light-shade { fill: var(--color-gray-light-shade) }
|
||||
.fill-gray-lightest { fill: var(--color-gray-lightest) }
|
||||
.fill-gray-lighter { fill: var(--color-gray-lighter) }
|
||||
.fill-gray-light { fill: var(--color-gray-light) }
|
||||
.fill-gray-bg { fill: var(--color-gray-bg) }
|
||||
.fill-gray-medium { fill: var(--color-gray-medium) }
|
||||
.fill-gray-dark { fill: var(--color-gray-dark) }
|
||||
.fill-gray-darkest { fill: var(--color-gray-darkest) }
|
||||
.fill-gray-light-blue { fill: var(--color-gray-light-blue) }
|
||||
.fill-teal { fill: var(--color-teal) }
|
||||
.fill-teal-dark { fill: var(--color-teal-dark) }
|
||||
.fill-teal-light { fill: var(--color-teal-light) }
|
||||
.fill-tealx { fill: var(--color-tealx) }
|
||||
.fill-tealx-light { fill: var(--color-tealx-light) }
|
||||
.fill-tealx-light-border { fill: var(--color-tealx-light-border) }
|
||||
.fill-tealx-lightest { fill: var(--color-tealx-lightest) }
|
||||
.fill-orange { fill: var(--color-orange) }
|
||||
.fill-yellow { fill: var(--color-yellow) }
|
||||
.fill-yellow2 { fill: var(--color-yellow2) }
|
||||
.fill-orange-dark { fill: var(--color-orange-dark) }
|
||||
.fill-green { fill: var(--color-green) }
|
||||
.fill-green2 { fill: var(--color-green2) }
|
||||
.fill-green-dark { fill: var(--color-green-dark) }
|
||||
.fill-red { fill: var(--color-red) }
|
||||
.fill-red2 { fill: var(--color-red2) }
|
||||
.fill-red-lightest { fill: var(--color-red-lightest) }
|
||||
.fill-blue { fill: var(--color-blue) }
|
||||
.fill-blue2 { fill: var(--color-blue2) }
|
||||
.fill-active-blue { fill: var(--color-active-blue) }
|
||||
.fill-active-dark-blue { fill: var(--color-active-dark-blue) }
|
||||
.fill-bg-blue { fill: var(--color-bg-blue) }
|
||||
.fill-active-blue-border { fill: var(--color-active-blue-border) }
|
||||
.fill-pink { fill: var(--color-pink) }
|
||||
.fill-light-blue-bg { fill: var(--color-light-blue-bg) }
|
||||
.fill-white { fill: var(--color-white) }
|
||||
.fill-black { fill: var(--color-black) }
|
||||
.fill-gray-border { fill: var(--color-gray-border) }
|
||||
.fill-borderColor-default { fill: var(--color-borderColor-default) }
|
||||
.fill-borderColor-gray-light-shade { fill: var(--color-borderColor-gray-light-shade) }
|
||||
.fill-borderColor-primary { fill: var(--color-borderColor-primary) }
|
||||
.fill-borderColor-transparent { fill: var(--color-borderColor-transparent) }
|
||||
.fill-transparent { fill: var(--color-transparent) }
|
||||
.fill-cyan { fill: var(--color-cyan) }
|
||||
.fill-amber { fill: var(--color-amber) }
|
||||
.fill-figmaColors-accent-secondary { fill: var(--color-figmaColors-accent-secondary) }
|
||||
.fill-figmaColors-main { fill: var(--color-figmaColors-main) }
|
||||
.fill-figmaColors-primary-outlined-hover-background { fill: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.fill-figmaColors-primary-outlined-resting-border { fill: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.fill-figmaColors-secondary-outlined-hover-background { fill: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.fill-figmaColors-secondary-outlined-resting-border { fill: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.fill-figmaColors-text-disabled { fill: var(--color-figmaColors-text-disabled) }
|
||||
.fill-figmaColors-text-primary { fill: var(--color-figmaColors-text-primary) }
|
||||
.fill-figmaColors-outlined-border { fill: var(--color-figmaColors-outlined-border) }
|
||||
.fill-figmaColors-divider { fill: var(--color-figmaColors-divider) }
|
||||
.hover-fill-main:hover svg { fill: var(--color-main) }
|
||||
.hover-fill-gray-light-shade:hover svg { fill: var(--color-gray-light-shade) }
|
||||
.hover-fill-gray-lightest:hover svg { fill: var(--color-gray-lightest) }
|
||||
.hover-fill-gray-lighter:hover svg { fill: var(--color-gray-lighter) }
|
||||
.hover-fill-gray-light:hover svg { fill: var(--color-gray-light) }
|
||||
.hover-fill-gray-bg:hover svg { fill: var(--color-gray-bg) }
|
||||
.hover-fill-gray-medium:hover svg { fill: var(--color-gray-medium) }
|
||||
.hover-fill-gray-dark:hover svg { fill: var(--color-gray-dark) }
|
||||
.hover-fill-gray-darkest:hover svg { fill: var(--color-gray-darkest) }
|
||||
.hover-fill-gray-light-blue:hover svg { fill: var(--color-gray-light-blue) }
|
||||
.hover-fill-teal:hover svg { fill: var(--color-teal) }
|
||||
.hover-fill-teal-dark:hover svg { fill: var(--color-teal-dark) }
|
||||
.hover-fill-teal-light:hover svg { fill: var(--color-teal-light) }
|
||||
.hover-fill-tealx:hover svg { fill: var(--color-tealx) }
|
||||
.hover-fill-tealx-light:hover svg { fill: var(--color-tealx-light) }
|
||||
.hover-fill-tealx-light-border:hover svg { fill: var(--color-tealx-light-border) }
|
||||
.hover-fill-tealx-lightest:hover svg { fill: var(--color-tealx-lightest) }
|
||||
.hover-fill-orange:hover svg { fill: var(--color-orange) }
|
||||
.hover-fill-yellow:hover svg { fill: var(--color-yellow) }
|
||||
.hover-fill-yellow2:hover svg { fill: var(--color-yellow2) }
|
||||
.hover-fill-orange-dark:hover svg { fill: var(--color-orange-dark) }
|
||||
.hover-fill-green:hover svg { fill: var(--color-green) }
|
||||
.hover-fill-green2:hover svg { fill: var(--color-green2) }
|
||||
.hover-fill-green-dark:hover svg { fill: var(--color-green-dark) }
|
||||
.hover-fill-red:hover svg { fill: var(--color-red) }
|
||||
.hover-fill-red2:hover svg { fill: var(--color-red2) }
|
||||
.hover-fill-red-lightest:hover svg { fill: var(--color-red-lightest) }
|
||||
.hover-fill-blue:hover svg { fill: var(--color-blue) }
|
||||
.hover-fill-blue2:hover svg { fill: var(--color-blue2) }
|
||||
.hover-fill-active-blue:hover svg { fill: var(--color-active-blue) }
|
||||
.hover-fill-active-dark-blue:hover svg { fill: var(--color-active-dark-blue) }
|
||||
.hover-fill-bg-blue:hover svg { fill: var(--color-bg-blue) }
|
||||
.hover-fill-active-blue-border:hover svg { fill: var(--color-active-blue-border) }
|
||||
.hover-fill-pink:hover svg { fill: var(--color-pink) }
|
||||
.hover-fill-light-blue-bg:hover svg { fill: var(--color-light-blue-bg) }
|
||||
.hover-fill-white:hover svg { fill: var(--color-white) }
|
||||
.hover-fill-black:hover svg { fill: var(--color-black) }
|
||||
.hover-fill-gray-border:hover svg { fill: var(--color-gray-border) }
|
||||
.hover-fill-borderColor-default:hover svg { fill: var(--color-borderColor-default) }
|
||||
.hover-fill-borderColor-gray-light-shade:hover svg { fill: var(--color-borderColor-gray-light-shade) }
|
||||
.hover-fill-borderColor-primary:hover svg { fill: var(--color-borderColor-primary) }
|
||||
.hover-fill-borderColor-transparent:hover svg { fill: var(--color-borderColor-transparent) }
|
||||
.hover-fill-transparent:hover svg { fill: var(--color-transparent) }
|
||||
.hover-fill-cyan:hover svg { fill: var(--color-cyan) }
|
||||
.hover-fill-amber:hover svg { fill: var(--color-amber) }
|
||||
.hover-fill-figmaColors-accent-secondary:hover svg { fill: var(--color-figmaColors-accent-secondary) }
|
||||
.hover-fill-figmaColors-main:hover svg { fill: var(--color-figmaColors-main) }
|
||||
.hover-fill-figmaColors-primary-outlined-hover-background:hover svg { fill: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.hover-fill-figmaColors-primary-outlined-resting-border:hover svg { fill: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.hover-fill-figmaColors-secondary-outlined-hover-background:hover svg { fill: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.hover-fill-figmaColors-secondary-outlined-resting-border:hover svg { fill: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.hover-fill-figmaColors-text-disabled:hover svg { fill: var(--color-figmaColors-text-disabled) }
|
||||
.hover-fill-figmaColors-text-primary:hover svg { fill: var(--color-figmaColors-text-primary) }
|
||||
.hover-fill-figmaColors-outlined-border:hover svg { fill: var(--color-figmaColors-outlined-border) }
|
||||
.hover-fill-figmaColors-divider:hover svg { fill: var(--color-figmaColors-divider) }
|
||||
|
||||
/* color */
|
||||
.color-main { color: #394EFF }
|
||||
.color-gray-light-shade { color: #EEEEEE }
|
||||
.color-gray-lightest { color: #f6f6f6 }
|
||||
.color-gray-lighter { color: #f1f1f1 }
|
||||
.color-gray-light { color: #ddd }
|
||||
.color-gray-bg { color: #CCC }
|
||||
.color-gray-medium { color: #888 }
|
||||
.color-gray-dark { color: #666 }
|
||||
.color-gray-darkest { color: #333 }
|
||||
.color-gray-light-blue { color: #F8F8FA }
|
||||
.color-teal { color: #394EFF }
|
||||
.color-teal-dark { color: #2331A8 }
|
||||
.color-teal-light { color: rgba(57, 78, 255, 0.1) }
|
||||
.color-tealx { color: #3EAAAF }
|
||||
.color-tealx-light { color: #E2F0EE }
|
||||
.color-tealx-light-border { color: #C6DCDA }
|
||||
.color-tealx-lightest { color: rgba(62, 170, 175, 0.1) }
|
||||
.color-orange { color: #E28940 }
|
||||
.color-yellow { color: #FFFBE5 }
|
||||
.color-yellow2 { color: #F5A623 }
|
||||
.color-orange-dark { color: #C26822 }
|
||||
.color-green { color: #42AE5E }
|
||||
.color-green2 { color: #00dc69 }
|
||||
.color-green-dark { color: #2C9848 }
|
||||
.color-red { color: #cc0000 }
|
||||
.color-red2 { color: #F5A623 }
|
||||
.color-red-lightest { color: rgba(204, 0, 0, 0.1) }
|
||||
.color-blue { color: #366CD9 }
|
||||
.color-blue2 { color: #0076FF }
|
||||
.color-active-blue { color: #F6F7FF }
|
||||
.color-active-dark-blue { color: #E2E4F6 }
|
||||
.color-bg-blue { color: #e3e6ff }
|
||||
.color-active-blue-border { color: #D0D4F2 }
|
||||
.color-pink { color: #ffb9b9 }
|
||||
.color-light-blue-bg { color: #E5F7F7 }
|
||||
.color-white { color: #fff }
|
||||
.color-borderColor-default { color: #DDDDDD }
|
||||
.color-borderColor-gray-light-shade { color: #EEEEEE }
|
||||
.color-borderColor-primary { color: #3490dc }
|
||||
.color-borderColor-transparent { color: transparent }
|
||||
.color-transparent { color: transparent }
|
||||
.color-cyan { color: #EBF4F5 }
|
||||
.color-figmaColors-accent-secondary { color: rgba(62, 170, 175, 1) }
|
||||
.color-figmaColors-main { color: rgba(57, 78, 255, 1) }
|
||||
.color-figmaColors-primary-outlined-hover-background { color: rgba(62, 170, 175, 0.08) }
|
||||
.color-figmaColors-primary-outlined-resting-border { color: rgba(62, 170, 175, 0.5) }
|
||||
.color-figmaColors-secondary-outlined-hover-background { color: rgba(63, 81, 181, 0.08) }
|
||||
.color-figmaColors-secondary-outlined-resting-border { color: rgba(63, 81, 181, 0.5) }
|
||||
.color-figmaColors-text-disabled { color: rgba(0,0,0, 0.38) }
|
||||
.color-figmaColors-text-primary { color: rgba(0,0,0, 0.87) }
|
||||
.color-figmaColors-outlined-border { color: rgba(0,0,0, 0.23) }
|
||||
.color-figmaColors-divider { color: rgba(0, 0, 0, 0.12) }
|
||||
.color-main { color: var(--color-main) }
|
||||
.color-gray-light-shade { color: var(--color-gray-light-shade) }
|
||||
.color-gray-lightest { color: var(--color-gray-lightest) }
|
||||
.color-gray-lighter { color: var(--color-gray-lighter) }
|
||||
.color-gray-light { color: var(--color-gray-light) }
|
||||
.color-gray-bg { color: var(--color-gray-bg) }
|
||||
.color-gray-medium { color: var(--color-gray-medium) }
|
||||
.color-gray-dark { color: var(--color-gray-dark) }
|
||||
.color-gray-darkest { color: var(--color-gray-darkest) }
|
||||
.color-gray-light-blue { color: var(--color-gray-light-blue) }
|
||||
.color-teal { color: var(--color-teal) }
|
||||
.color-teal-dark { color: var(--color-teal-dark) }
|
||||
.color-teal-light { color: var(--color-teal-light) }
|
||||
.color-tealx { color: var(--color-tealx) }
|
||||
.color-tealx-light { color: var(--color-tealx-light) }
|
||||
.color-tealx-light-border { color: var(--color-tealx-light-border) }
|
||||
.color-tealx-lightest { color: var(--color-tealx-lightest) }
|
||||
.color-orange { color: var(--color-orange) }
|
||||
.color-yellow { color: var(--color-yellow) }
|
||||
.color-yellow2 { color: var(--color-yellow2) }
|
||||
.color-orange-dark { color: var(--color-orange-dark) }
|
||||
.color-green { color: var(--color-green) }
|
||||
.color-green2 { color: var(--color-green2) }
|
||||
.color-green-dark { color: var(--color-green-dark) }
|
||||
.color-red { color: var(--color-red) }
|
||||
.color-red2 { color: var(--color-red2) }
|
||||
.color-red-lightest { color: var(--color-red-lightest) }
|
||||
.color-blue { color: var(--color-blue) }
|
||||
.color-blue2 { color: var(--color-blue2) }
|
||||
.color-active-blue { color: var(--color-active-blue) }
|
||||
.color-active-dark-blue { color: var(--color-active-dark-blue) }
|
||||
.color-bg-blue { color: var(--color-bg-blue) }
|
||||
.color-active-blue-border { color: var(--color-active-blue-border) }
|
||||
.color-pink { color: var(--color-pink) }
|
||||
.color-light-blue-bg { color: var(--color-light-blue-bg) }
|
||||
.color-white { color: var(--color-white) }
|
||||
.color-black { color: var(--color-black) }
|
||||
.color-gray-border { color: var(--color-gray-border) }
|
||||
.color-borderColor-default { color: var(--color-borderColor-default) }
|
||||
.color-borderColor-gray-light-shade { color: var(--color-borderColor-gray-light-shade) }
|
||||
.color-borderColor-primary { color: var(--color-borderColor-primary) }
|
||||
.color-borderColor-transparent { color: var(--color-borderColor-transparent) }
|
||||
.color-transparent { color: var(--color-transparent) }
|
||||
.color-cyan { color: var(--color-cyan) }
|
||||
.color-amber { color: var(--color-amber) }
|
||||
.color-figmaColors-accent-secondary { color: var(--color-figmaColors-accent-secondary) }
|
||||
.color-figmaColors-main { color: var(--color-figmaColors-main) }
|
||||
.color-figmaColors-primary-outlined-hover-background { color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.color-figmaColors-primary-outlined-resting-border { color: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.color-figmaColors-secondary-outlined-hover-background { color: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.color-figmaColors-secondary-outlined-resting-border { color: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.color-figmaColors-text-disabled { color: var(--color-figmaColors-text-disabled) }
|
||||
.color-figmaColors-text-primary { color: var(--color-figmaColors-text-primary) }
|
||||
.color-figmaColors-outlined-border { color: var(--color-figmaColors-outlined-border) }
|
||||
.color-figmaColors-divider { color: var(--color-figmaColors-divider) }
|
||||
|
||||
/* hover color */
|
||||
.hover-main:hover { color: #394EFF }
|
||||
.hover-gray-light-shade:hover { color: #EEEEEE }
|
||||
.hover-gray-lightest:hover { color: #f6f6f6 }
|
||||
.hover-gray-lighter:hover { color: #f1f1f1 }
|
||||
.hover-gray-light:hover { color: #ddd }
|
||||
.hover-gray-bg:hover { color: #CCC }
|
||||
.hover-gray-medium:hover { color: #888 }
|
||||
.hover-gray-dark:hover { color: #666 }
|
||||
.hover-gray-darkest:hover { color: #333 }
|
||||
.hover-gray-light-blue:hover { color: #F8F8FA }
|
||||
.hover-teal:hover { color: #394EFF }
|
||||
.hover-teal-dark:hover { color: #2331A8 }
|
||||
.hover-teal-light:hover { color: rgba(57, 78, 255, 0.1) }
|
||||
.hover-tealx:hover { color: #3EAAAF }
|
||||
.hover-tealx-light:hover { color: #E2F0EE }
|
||||
.hover-tealx-light-border:hover { color: #C6DCDA }
|
||||
.hover-tealx-lightest:hover { color: rgba(62, 170, 175, 0.1) }
|
||||
.hover-orange:hover { color: #E28940 }
|
||||
.hover-yellow:hover { color: #FFFBE5 }
|
||||
.hover-yellow2:hover { color: #F5A623 }
|
||||
.hover-orange-dark:hover { color: #C26822 }
|
||||
.hover-green:hover { color: #42AE5E }
|
||||
.hover-green2:hover { color: #00dc69 }
|
||||
.hover-green-dark:hover { color: #2C9848 }
|
||||
.hover-red:hover { color: #cc0000 }
|
||||
.hover-red2:hover { color: #F5A623 }
|
||||
.hover-red-lightest:hover { color: rgba(204, 0, 0, 0.1) }
|
||||
.hover-blue:hover { color: #366CD9 }
|
||||
.hover-blue2:hover { color: #0076FF }
|
||||
.hover-active-blue:hover { color: #F6F7FF }
|
||||
.hover-active-dark-blue:hover { color: #E2E4F6 }
|
||||
.hover-bg-blue:hover { color: #e3e6ff }
|
||||
.hover-active-blue-border:hover { color: #D0D4F2 }
|
||||
.hover-pink:hover { color: #ffb9b9 }
|
||||
.hover-light-blue-bg:hover { color: #E5F7F7 }
|
||||
.hover-white:hover { color: #fff }
|
||||
.hover-borderColor-default:hover { color: #DDDDDD }
|
||||
.hover-borderColor-gray-light-shade:hover { color: #EEEEEE }
|
||||
.hover-borderColor-primary:hover { color: #3490dc }
|
||||
.hover-borderColor-transparent:hover { color: transparent }
|
||||
.hover-transparent:hover { color: transparent }
|
||||
.hover-cyan:hover { color: #EBF4F5 }
|
||||
.hover-figmaColors-accent-secondary:hover { color: rgba(62, 170, 175, 1) }
|
||||
.hover-figmaColors-main:hover { color: rgba(57, 78, 255, 1) }
|
||||
.hover-figmaColors-primary-outlined-hover-background:hover { color: rgba(62, 170, 175, 0.08) }
|
||||
.hover-figmaColors-primary-outlined-resting-border:hover { color: rgba(62, 170, 175, 0.5) }
|
||||
.hover-figmaColors-secondary-outlined-hover-background:hover { color: rgba(63, 81, 181, 0.08) }
|
||||
.hover-figmaColors-secondary-outlined-resting-border:hover { color: rgba(63, 81, 181, 0.5) }
|
||||
.hover-figmaColors-text-disabled:hover { color: rgba(0,0,0, 0.38) }
|
||||
.hover-figmaColors-text-primary:hover { color: rgba(0,0,0, 0.87) }
|
||||
.hover-figmaColors-outlined-border:hover { color: rgba(0,0,0, 0.23) }
|
||||
.hover-figmaColors-divider:hover { color: rgba(0, 0, 0, 0.12) }
|
||||
.hover-main:hover { color: var(--color-main) }
|
||||
.hover-gray-light-shade:hover { color: var(--color-gray-light-shade) }
|
||||
.hover-gray-lightest:hover { color: var(--color-gray-lightest) }
|
||||
.hover-gray-lighter:hover { color: var(--color-gray-lighter) }
|
||||
.hover-gray-light:hover { color: var(--color-gray-light) }
|
||||
.hover-gray-bg:hover { color: var(--color-gray-bg) }
|
||||
.hover-gray-medium:hover { color: var(--color-gray-medium) }
|
||||
.hover-gray-dark:hover { color: var(--color-gray-dark) }
|
||||
.hover-gray-darkest:hover { color: var(--color-gray-darkest) }
|
||||
.hover-gray-light-blue:hover { color: var(--color-gray-light-blue) }
|
||||
.hover-teal:hover { color: var(--color-teal) }
|
||||
.hover-teal-dark:hover { color: var(--color-teal-dark) }
|
||||
.hover-teal-light:hover { color: var(--color-teal-light) }
|
||||
.hover-tealx:hover { color: var(--color-tealx) }
|
||||
.hover-tealx-light:hover { color: var(--color-tealx-light) }
|
||||
.hover-tealx-light-border:hover { color: var(--color-tealx-light-border) }
|
||||
.hover-tealx-lightest:hover { color: var(--color-tealx-lightest) }
|
||||
.hover-orange:hover { color: var(--color-orange) }
|
||||
.hover-yellow:hover { color: var(--color-yellow) }
|
||||
.hover-yellow2:hover { color: var(--color-yellow2) }
|
||||
.hover-orange-dark:hover { color: var(--color-orange-dark) }
|
||||
.hover-green:hover { color: var(--color-green) }
|
||||
.hover-green2:hover { color: var(--color-green2) }
|
||||
.hover-green-dark:hover { color: var(--color-green-dark) }
|
||||
.hover-red:hover { color: var(--color-red) }
|
||||
.hover-red2:hover { color: var(--color-red2) }
|
||||
.hover-red-lightest:hover { color: var(--color-red-lightest) }
|
||||
.hover-blue:hover { color: var(--color-blue) }
|
||||
.hover-blue2:hover { color: var(--color-blue2) }
|
||||
.hover-active-blue:hover { color: var(--color-active-blue) }
|
||||
.hover-active-dark-blue:hover { color: var(--color-active-dark-blue) }
|
||||
.hover-bg-blue:hover { color: var(--color-bg-blue) }
|
||||
.hover-active-blue-border:hover { color: var(--color-active-blue-border) }
|
||||
.hover-pink:hover { color: var(--color-pink) }
|
||||
.hover-light-blue-bg:hover { color: var(--color-light-blue-bg) }
|
||||
.hover-white:hover { color: var(--color-white) }
|
||||
.hover-black:hover { color: var(--color-black) }
|
||||
.hover-gray-border:hover { color: var(--color-gray-border) }
|
||||
.hover-borderColor-default:hover { color: var(--color-borderColor-default) }
|
||||
.hover-borderColor-gray-light-shade:hover { color: var(--color-borderColor-gray-light-shade) }
|
||||
.hover-borderColor-primary:hover { color: var(--color-borderColor-primary) }
|
||||
.hover-borderColor-transparent:hover { color: var(--color-borderColor-transparent) }
|
||||
.hover-transparent:hover { color: var(--color-transparent) }
|
||||
.hover-cyan:hover { color: var(--color-cyan) }
|
||||
.hover-amber:hover { color: var(--color-amber) }
|
||||
.hover-figmaColors-accent-secondary:hover { color: var(--color-figmaColors-accent-secondary) }
|
||||
.hover-figmaColors-main:hover { color: var(--color-figmaColors-main) }
|
||||
.hover-figmaColors-primary-outlined-hover-background:hover { color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.hover-figmaColors-primary-outlined-resting-border:hover { color: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.hover-figmaColors-secondary-outlined-hover-background:hover { color: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.hover-figmaColors-secondary-outlined-resting-border:hover { color: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.hover-figmaColors-text-disabled:hover { color: var(--color-figmaColors-text-disabled) }
|
||||
.hover-figmaColors-text-primary:hover { color: var(--color-figmaColors-text-primary) }
|
||||
.hover-figmaColors-outlined-border:hover { color: var(--color-figmaColors-outlined-border) }
|
||||
.hover-figmaColors-divider:hover { color: var(--color-figmaColors-divider) }
|
||||
|
||||
.border-main { border-color: #394EFF }
|
||||
.border-gray-light-shade { border-color: #EEEEEE }
|
||||
.border-gray-lightest { border-color: #f6f6f6 }
|
||||
.border-gray-lighter { border-color: #f1f1f1 }
|
||||
.border-gray-light { border-color: #ddd }
|
||||
.border-gray-bg { border-color: #CCC }
|
||||
.border-gray-medium { border-color: #888 }
|
||||
.border-gray-dark { border-color: #666 }
|
||||
.border-gray-darkest { border-color: #333 }
|
||||
.border-gray-light-blue { border-color: #F8F8FA }
|
||||
.border-teal { border-color: #394EFF }
|
||||
.border-teal-dark { border-color: #2331A8 }
|
||||
.border-teal-light { border-color: rgba(57, 78, 255, 0.1) }
|
||||
.border-tealx { border-color: #3EAAAF }
|
||||
.border-tealx-light { border-color: #E2F0EE }
|
||||
.border-tealx-light-border { border-color: #C6DCDA }
|
||||
.border-tealx-lightest { border-color: rgba(62, 170, 175, 0.1) }
|
||||
.border-orange { border-color: #E28940 }
|
||||
.border-yellow { border-color: #FFFBE5 }
|
||||
.border-yellow2 { border-color: #F5A623 }
|
||||
.border-orange-dark { border-color: #C26822 }
|
||||
.border-green { border-color: #42AE5E }
|
||||
.border-green2 { border-color: #00dc69 }
|
||||
.border-green-dark { border-color: #2C9848 }
|
||||
.border-red { border-color: #cc0000 }
|
||||
.border-red2 { border-color: #F5A623 }
|
||||
.border-red-lightest { border-color: rgba(204, 0, 0, 0.1) }
|
||||
.border-blue { border-color: #366CD9 }
|
||||
.border-blue2 { border-color: #0076FF }
|
||||
.border-active-blue { border-color: #F6F7FF }
|
||||
.border-active-dark-blue { border-color: #E2E4F6 }
|
||||
.border-bg-blue { border-color: #e3e6ff }
|
||||
.border-active-blue-border { border-color: #D0D4F2 }
|
||||
.border-pink { border-color: #ffb9b9 }
|
||||
.border-light-blue-bg { border-color: #E5F7F7 }
|
||||
.border-white { border-color: #fff }
|
||||
.border-borderColor-default { border-color: #DDDDDD }
|
||||
.border-borderColor-gray-light-shade { border-color: #EEEEEE }
|
||||
.border-borderColor-primary { border-color: #3490dc }
|
||||
.border-borderColor-transparent { border-color: transparent }
|
||||
.border-transparent { border-color: transparent }
|
||||
.border-cyan { border-color: #EBF4F5 }
|
||||
.border-figmaColors-accent-secondary { border-color: rgba(62, 170, 175, 1) }
|
||||
.border-figmaColors-main { border-color: rgba(57, 78, 255, 1) }
|
||||
.border-figmaColors-primary-outlined-hover-background { border-color: rgba(62, 170, 175, 0.08) }
|
||||
.border-figmaColors-primary-outlined-resting-border { border-color: rgba(62, 170, 175, 0.5) }
|
||||
.border-figmaColors-secondary-outlined-hover-background { border-color: rgba(63, 81, 181, 0.08) }
|
||||
.border-figmaColors-secondary-outlined-resting-border { border-color: rgba(63, 81, 181, 0.5) }
|
||||
.border-figmaColors-text-disabled { border-color: rgba(0,0,0, 0.38) }
|
||||
.border-figmaColors-text-primary { border-color: rgba(0,0,0, 0.87) }
|
||||
.border-figmaColors-outlined-border { border-color: rgba(0,0,0, 0.23) }
|
||||
.border-figmaColors-divider { border-color: rgba(0, 0, 0, 0.12) }
|
||||
/* border color */
|
||||
.border-main { border-color: var(--color-main) }
|
||||
.border-gray-light-shade { border-color: var(--color-gray-light-shade) }
|
||||
.border-gray-lightest { border-color: var(--color-gray-lightest) }
|
||||
.border-gray-lighter { border-color: var(--color-gray-lighter) }
|
||||
.border-gray-light { border-color: var(--color-gray-light) }
|
||||
.border-gray-bg { border-color: var(--color-gray-bg) }
|
||||
.border-gray-medium { border-color: var(--color-gray-medium) }
|
||||
.border-gray-dark { border-color: var(--color-gray-dark) }
|
||||
.border-gray-darkest { border-color: var(--color-gray-darkest) }
|
||||
.border-gray-light-blue { border-color: var(--color-gray-light-blue) }
|
||||
.border-teal { border-color: var(--color-teal) }
|
||||
.border-teal-dark { border-color: var(--color-teal-dark) }
|
||||
.border-teal-light { border-color: var(--color-teal-light) }
|
||||
.border-tealx { border-color: var(--color-tealx) }
|
||||
.border-tealx-light { border-color: var(--color-tealx-light) }
|
||||
.border-tealx-light-border { border-color: var(--color-tealx-light-border) }
|
||||
.border-tealx-lightest { border-color: var(--color-tealx-lightest) }
|
||||
.border-orange { border-color: var(--color-orange) }
|
||||
.border-yellow { border-color: var(--color-yellow) }
|
||||
.border-yellow2 { border-color: var(--color-yellow2) }
|
||||
.border-orange-dark { border-color: var(--color-orange-dark) }
|
||||
.border-green { border-color: var(--color-green) }
|
||||
.border-green2 { border-color: var(--color-green2) }
|
||||
.border-green-dark { border-color: var(--color-green-dark) }
|
||||
.border-red { border-color: var(--color-red) }
|
||||
.border-red2 { border-color: var(--color-red2) }
|
||||
.border-red-lightest { border-color: var(--color-red-lightest) }
|
||||
.border-blue { border-color: var(--color-blue) }
|
||||
.border-blue2 { border-color: var(--color-blue2) }
|
||||
.border-active-blue { border-color: var(--color-active-blue) }
|
||||
.border-active-dark-blue { border-color: var(--color-active-dark-blue) }
|
||||
.border-bg-blue { border-color: var(--color-bg-blue) }
|
||||
.border-active-blue-border { border-color: var(--color-active-blue-border) }
|
||||
.border-pink { border-color: var(--color-pink) }
|
||||
.border-light-blue-bg { border-color: var(--color-light-blue-bg) }
|
||||
.border-white { border-color: var(--color-white) }
|
||||
.border-black { border-color: var(--color-black) }
|
||||
.border-gray-border { border-color: var(--color-gray-border) }
|
||||
.border-borderColor-default { border-color: var(--color-borderColor-default) }
|
||||
.border-borderColor-gray-light-shade { border-color: var(--color-borderColor-gray-light-shade) }
|
||||
.border-borderColor-primary { border-color: var(--color-borderColor-primary) }
|
||||
.border-borderColor-transparent { border-color: var(--color-borderColor-transparent) }
|
||||
.border-transparent { border-color: var(--color-transparent) }
|
||||
.border-cyan { border-color: var(--color-cyan) }
|
||||
.border-amber { border-color: var(--color-amber) }
|
||||
.border-figmaColors-accent-secondary { border-color: var(--color-figmaColors-accent-secondary) }
|
||||
.border-figmaColors-main { border-color: var(--color-figmaColors-main) }
|
||||
.border-figmaColors-primary-outlined-hover-background { border-color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.border-figmaColors-primary-outlined-resting-border { border-color: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.border-figmaColors-secondary-outlined-hover-background { border-color: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.border-figmaColors-secondary-outlined-resting-border { border-color: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.border-figmaColors-text-disabled { border-color: var(--color-figmaColors-text-disabled) }
|
||||
.border-figmaColors-text-primary { border-color: var(--color-figmaColors-text-primary) }
|
||||
.border-figmaColors-outlined-border { border-color: var(--color-figmaColors-outlined-border) }
|
||||
.border-figmaColors-divider { border-color: var(--color-figmaColors-divider) }
|
||||
|
||||
/* background color */
|
||||
.bg-main { background-color: var(--color-main) }
|
||||
.bg-gray-light-shade { background-color: var(--color-gray-light-shade) }
|
||||
.bg-gray-lightest { background-color: var(--color-gray-lightest) }
|
||||
.bg-gray-lighter { background-color: var(--color-gray-lighter) }
|
||||
.bg-gray-light { background-color: var(--color-gray-light) }
|
||||
.bg-gray-bg { background-color: var(--color-gray-bg) }
|
||||
.bg-gray-medium { background-color: var(--color-gray-medium) }
|
||||
.bg-gray-dark { background-color: var(--color-gray-dark) }
|
||||
.bg-gray-darkest { background-color: var(--color-gray-darkest) }
|
||||
.bg-gray-light-blue { background-color: var(--color-gray-light-blue) }
|
||||
.bg-teal { background-color: var(--color-teal) }
|
||||
.bg-teal-dark { background-color: var(--color-teal-dark) }
|
||||
.bg-teal-light { background-color: var(--color-teal-light) }
|
||||
.bg-tealx { background-color: var(--color-tealx) }
|
||||
.bg-tealx-light { background-color: var(--color-tealx-light) }
|
||||
.bg-tealx-light-border { background-color: var(--color-tealx-light-border) }
|
||||
.bg-tealx-lightest { background-color: var(--color-tealx-lightest) }
|
||||
.bg-orange { background-color: var(--color-orange) }
|
||||
.bg-yellow { background-color: var(--color-yellow) }
|
||||
.bg-yellow2 { background-color: var(--color-yellow2) }
|
||||
.bg-orange-dark { background-color: var(--color-orange-dark) }
|
||||
.bg-green { background-color: var(--color-green) }
|
||||
.bg-green2 { background-color: var(--color-green2) }
|
||||
.bg-green-dark { background-color: var(--color-green-dark) }
|
||||
.bg-red { background-color: var(--color-red) }
|
||||
.bg-red2 { background-color: var(--color-red2) }
|
||||
.bg-red-lightest { background-color: var(--color-red-lightest) }
|
||||
.bg-blue { background-color: var(--color-blue) }
|
||||
.bg-blue2 { background-color: var(--color-blue2) }
|
||||
.bg-active-blue { background-color: var(--color-active-blue) }
|
||||
.bg-active-dark-blue { background-color: var(--color-active-dark-blue) }
|
||||
.bg-bg-blue { background-color: var(--color-bg-blue) }
|
||||
.bg-active-blue-border { background-color: var(--color-active-blue-border) }
|
||||
.bg-pink { background-color: var(--color-pink) }
|
||||
.bg-light-blue-bg { background-color: var(--color-light-blue-bg) }
|
||||
.bg-white { background-color: var(--color-white) }
|
||||
.bg-black { background-color: var(--color-black) }
|
||||
.bg-gray-border { background-color: var(--color-gray-border) }
|
||||
.bg-borderColor-default { background-color: var(--color-borderColor-default) }
|
||||
.bg-borderColor-gray-light-shade { background-color: var(--color-borderColor-gray-light-shade) }
|
||||
.bg-borderColor-primary { background-color: var(--color-borderColor-primary) }
|
||||
.bg-borderColor-transparent { background-color: var(--color-borderColor-transparent) }
|
||||
.bg-transparent { background-color: var(--color-transparent) }
|
||||
.bg-cyan { background-color: var(--color-cyan) }
|
||||
.bg-amber { background-color: var(--color-amber) }
|
||||
.bg-figmaColors-accent-secondary { background-color: var(--color-figmaColors-accent-secondary) }
|
||||
.bg-figmaColors-main { background-color: var(--color-figmaColors-main) }
|
||||
.bg-figmaColors-primary-outlined-hover-background { background-color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
.bg-figmaColors-primary-outlined-resting-border { background-color: var(--color-figmaColors-primary-outlined-resting-border) }
|
||||
.bg-figmaColors-secondary-outlined-hover-background { background-color: var(--color-figmaColors-secondary-outlined-hover-background) }
|
||||
.bg-figmaColors-secondary-outlined-resting-border { background-color: var(--color-figmaColors-secondary-outlined-resting-border) }
|
||||
.bg-figmaColors-text-disabled { background-color: var(--color-figmaColors-text-disabled) }
|
||||
.bg-figmaColors-text-primary { background-color: var(--color-figmaColors-text-primary) }
|
||||
.bg-figmaColors-outlined-border { background-color: var(--color-figmaColors-outlined-border) }
|
||||
.bg-figmaColors-divider { background-color: var(--color-figmaColors-divider) }
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
}
|
||||
|
||||
.border-gray-light {
|
||||
border: solid thin rgb(229 231 235 / var(--tw-text-opacity, 1))
|
||||
border: solid thin var(--color-gray-light)
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
|
|
@ -425,10 +425,6 @@ p {
|
|||
background-color: #ffff;
|
||||
}
|
||||
|
||||
.ant-menu-light .ant-menu-item-selected, :where(.css-dev-only-do-not-override).ant-menu-light>.ant-menu .ant-menu-item-selected{
|
||||
background-color: #E6E9FA;
|
||||
}
|
||||
|
||||
.pref-projects-menu .ant-menu-light .ant-menu-item-selected{
|
||||
background-color: #F6F7FF;
|
||||
color: rgba(0,0,0,.7);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ input.no-focus:focus {
|
|||
}
|
||||
|
||||
.widget-wrapper {
|
||||
@apply rounded-lg shadow-sm border bg-white;
|
||||
@apply rounded-lg shadow-sm border bg-white border-gray-light;
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
|||
|
|
@ -6,110 +6,17 @@
|
|||
}
|
||||
|
||||
* {
|
||||
border-color: #eeeeee;
|
||||
border-color: $gray-light;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.page-margin {
|
||||
padding-top: 81px;
|
||||
}
|
||||
|
||||
.container-fit {
|
||||
margin: 0 30px 0px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 30px 30px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1380px) {
|
||||
.container-70 {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1380px) {
|
||||
.container-70 {
|
||||
width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-70 {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-90 {
|
||||
width: 98%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
width: 250px;
|
||||
height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
padding-right: 20px;
|
||||
position: fixed;
|
||||
top: 81px;
|
||||
|
||||
&
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
&
|
||||
:hover {
|
||||
|
||||
&
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.side-menu-margined {
|
||||
margin-left: 250px;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
margin-bottom: 25px;
|
||||
/* border: dashed thin gray; */
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
margin-right: 15px;
|
||||
|
||||
&
|
||||
> span {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
&
|
||||
.title {
|
||||
margin-right: 15px;
|
||||
|
||||
&
|
||||
span {
|
||||
color: $ gray-medium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.page-title-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-hidden='true'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -135,15 +42,6 @@ label {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hover {
|
||||
|
||||
&
|
||||
:hover {
|
||||
background-color: $ active-blue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hover-teal:hover {
|
||||
background-color: $ active-blue;
|
||||
color: $ teal;
|
||||
|
|
@ -155,36 +53,6 @@ svg {
|
|||
|
||||
}
|
||||
|
||||
.note-hover {
|
||||
border: solid thin transparent;
|
||||
|
||||
&
|
||||
:hover {
|
||||
background-color: #FFFEF5;
|
||||
border-color: $ gray-lightest;
|
||||
}
|
||||
|
||||
}
|
||||
.note-hover-bg {
|
||||
|
||||
&
|
||||
:hover {
|
||||
background-color: #FFFEF5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.text-dotted-underline {
|
||||
text-decoration: underline dotted !important;
|
||||
}
|
||||
|
||||
|
||||
.no-scroll {
|
||||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.json-view {
|
||||
display: block;
|
||||
color: #4d4d4d;
|
||||
|
|
@ -416,3 +284,6 @@ svg {
|
|||
cursor: grab;
|
||||
}
|
||||
|
||||
.text-black {
|
||||
color: $black;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-dot"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-dot"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 284 B |
|
|
@ -1,6 +1,5 @@
|
|||
module.exports = {
|
||||
main: '#394EFF',
|
||||
|
||||
main: 'oklch(54.6% 0.245 262.881)',
|
||||
'gray-light-shade': '#EEEEEE',
|
||||
'gray-lightest': '#f6f6f6',
|
||||
'gray-lighter': '#f1f1f1',
|
||||
|
|
@ -10,17 +9,13 @@ module.exports = {
|
|||
'gray-dark': '#666',
|
||||
'gray-darkest': '#333',
|
||||
'gray-light-blue': '#F8F8FA',
|
||||
|
||||
teal: '#394EFF' /* blue */,
|
||||
'teal-dark': '#2331A8' /* "blue-dark" */,
|
||||
'teal-light': 'rgba(57, 78, 255, 0.1)' /* "blue-light" */,
|
||||
|
||||
teal: '#394EFF', /* blue */
|
||||
'teal-dark': '#2331A8', /* "blue-dark" */
|
||||
'teal-light': 'rgba(57, 78, 255, 0.1)', /* "blue-light" */
|
||||
tealx: '#3EAAAF',
|
||||
'tealx-light': '#E2F0EE',
|
||||
'tealx-light-border': '#C6DCDA',
|
||||
|
||||
'tealx-lightest': 'rgba(62, 170, 175, 0.1)',
|
||||
|
||||
orange: '#E28940',
|
||||
yellow: '#FFFBE5',
|
||||
yellow2: '#F5A623',
|
||||
|
|
@ -39,8 +34,9 @@ module.exports = {
|
|||
'active-blue-border': '#D0D4F2',
|
||||
pink: '#ffb9b9',
|
||||
'light-blue-bg': '#E5F7F7',
|
||||
|
||||
white: '#fff',
|
||||
black: 'black',
|
||||
'gray-border': '#999', // Added for border-gray shadow
|
||||
borderColor: {
|
||||
default: '#DDDDDD',
|
||||
'gray-light-shade': '#EEEEEE',
|
||||
|
|
@ -49,8 +45,8 @@ module.exports = {
|
|||
},
|
||||
transparent: 'transparent',
|
||||
cyan: '#EBF4F5',
|
||||
amber: 'oklch(98.7% 0.022 95.277)',
|
||||
|
||||
// actual theme colors - use this for new components
|
||||
figmaColors: {
|
||||
'accent-secondary': 'rgba(62, 170, 175, 1)',
|
||||
main: 'rgba(57, 78, 255, 1)',
|
||||
|
|
@ -63,4 +59,49 @@ module.exports = {
|
|||
'outlined-border': 'rgba(0,0,0, 0.23)',
|
||||
divider: 'rgba(0, 0, 0, 0.12)',
|
||||
},
|
||||
|
||||
dark: {
|
||||
// used as background in multiple places
|
||||
white: 'oklch(20.5% 0 0)',
|
||||
black: '#fff',
|
||||
teal: 'oklch(70.7% 0.165 254.624)',
|
||||
main: 'oklch(70.7% 0.165 254.624)',
|
||||
'text-primary': 'oklch(97% 0 0)',
|
||||
'text-disabled': 'rgba(255, 255, 255, 0.38)',
|
||||
'outlined-border': 'rgba(255, 255, 255, 0.23)',
|
||||
divider: 'rgba(255, 255, 255, 0.12)',
|
||||
'background': 'oklch(20.5% 0 0)',
|
||||
'surface': '#1E1E1E',
|
||||
amber: 'oklch(41.4% 0.112 45.904)',
|
||||
|
||||
'gray-light-shade': 'oklch(37.1% 0 0)',
|
||||
'gray-lightest': 'oklch(26.9% 0 0)',
|
||||
'gray-lighter': 'oklch(29.9% 0 0)',
|
||||
'gray-light': 'oklch(37.1% 0 0)',
|
||||
'gray-bg': 'oklch(37.1% 0 0)',
|
||||
'gray-medium': 'oklch(70.7% 0.022 261.325)',
|
||||
'gray-dark': 'oklch(87% 0 0)',
|
||||
'gray-darkest': 'oklch(97% 0 0)',
|
||||
'gray-light-blue': 'oklch(55.4% 0.046 257.417)',
|
||||
'gray-border': '#888',
|
||||
|
||||
'active-blue': 'oklch(43.2% 0.232 292.759)',
|
||||
'active-dark-blue': 'oklch(43.2% 0.232 292.759)',
|
||||
'bg-blue': 'oklch(35.9% 0.144 278.697)',
|
||||
'active-blue-border': 'oklch(45.7% 0.24 277.023)',
|
||||
'tealx': 'oklch(77.7% 0.152 181.912)',
|
||||
'tealx-light': 'oklch(38.6% 0.063 188.416)',
|
||||
'tealx-light-border': 'oklch(43.7% 0.078 188.216)',
|
||||
|
||||
'light-blue-bg': 'oklch(39.8% 0.07 227.392)',
|
||||
'disabled-text': 'rgba(255, 255, 255, 0.38)',
|
||||
|
||||
figmaColors: {
|
||||
'accent-secondary': 'rgba(82, 190, 195, 1)',
|
||||
'text-disabled': 'rgba(255, 255, 255, 0.38)',
|
||||
'text-primary': 'rgba(255, 255, 255, 0.87)',
|
||||
'outlined-border': 'rgba(255, 255, 255, 0.23)',
|
||||
divider: 'rgba(255, 255, 255, 0.12)',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,14 +2,39 @@ const path = require('path');
|
|||
const colors = require('./app/theme/colors');
|
||||
const cssnanoOptions = { zindex: false };
|
||||
|
||||
const transformColorsToCssVars = (colorsObj) => {
|
||||
const result = {};
|
||||
|
||||
for (const [key, value] of Object.entries(colorsObj)) {
|
||||
if (typeof value === 'object' && value !== null && key !== 'dark') {
|
||||
// Handle nested objects
|
||||
const transformedNested = {};
|
||||
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
||||
// Create CSS variable reference for nested values
|
||||
transformedNested[nestedKey] = `var(--color-${key}-${nestedKey})`;
|
||||
}
|
||||
result[key] = transformedNested;
|
||||
} else if (key !== 'dark') {
|
||||
// Create CSS variable reference for direct values
|
||||
result[key] = `var(--color-${key})`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const cssVarColors = transformColorsToCssVars(colors);
|
||||
|
||||
module.exports = ({ file, options, env }) => ({
|
||||
// parser: 'sugarss', // syntax check ?
|
||||
// parser: 'sugarss', // syntax check ?
|
||||
plugins: {
|
||||
'postcss-import': {
|
||||
path: path.join(__dirname, 'app/styles/import')
|
||||
},
|
||||
'postcss-mixins': {},
|
||||
'postcss-simple-vars': { variables: colors },
|
||||
'postcss-simple-vars': {
|
||||
variables: cssVarColors
|
||||
},
|
||||
'postcss-nesting': {},
|
||||
// 'postcss-inline-svg': {
|
||||
// path: path.join(__dirname, 'app/svg'),
|
||||
|
|
@ -20,4 +45,4 @@ module.exports = ({ file, options, env }) => ({
|
|||
//'postcss-preset-env': {}, //includes autoprefixer
|
||||
cssnano: env === 'production' ? cssnanoOptions : false,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,38 +1,40 @@
|
|||
const fs = require('fs');
|
||||
const colors = require('../app/theme/colors.js');
|
||||
|
||||
// Helper function to flatten the nested color objects
|
||||
const flattenColors = (colors) => {
|
||||
let flatColors = {};
|
||||
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value === 'object' && value !== null && key !== 'dark') {
|
||||
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
||||
flatColors[`${key}-${nestedKey}`] = nestedValue;
|
||||
flatColors[`${key}-${nestedKey}`] = `${key}-${nestedKey}`;
|
||||
}
|
||||
} else {
|
||||
flatColors[key] = value;
|
||||
} else if (key !== 'dark') {
|
||||
flatColors[key] = key;
|
||||
}
|
||||
}
|
||||
|
||||
return flatColors;
|
||||
};
|
||||
|
||||
const flatColors = flattenColors(colors);
|
||||
|
||||
const generatedCSS = `/* Auto-generated, DO NOT EDIT */
|
||||
/* Uses CSS variables (--color-*) generated by Tailwind config */
|
||||
|
||||
/* fill */
|
||||
${ Object.entries(flatColors).map(([name, value]) => `.fill-${ name.replace(/ /g, '-') } { fill: ${ value } }`).join('\n') }
|
||||
${ Object.entries(flatColors).map(([name, value]) => `.hover-fill-${ name.replace(/ /g, '-') }:hover svg { fill: ${ value } }`).join('\n') }
|
||||
${Object.entries(flatColors).map(([name, key]) => `.fill-${name.replace(/ /g, '-')} { fill: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
${Object.entries(flatColors).map(([name, key]) => `.hover-fill-${name.replace(/ /g, '-')}:hover svg { fill: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
|
||||
/* color */
|
||||
${ Object.entries(flatColors).map(([name, value]) => `.color-${ name.replace(/ /g, '-') } { color: ${ value } }`).join('\n') }
|
||||
${Object.entries(flatColors).map(([name, key]) => `.color-${name.replace(/ /g, '-')} { color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
|
||||
/* hover color */
|
||||
${ Object.entries(flatColors).map(([name, value]) => `.hover-${ name.replace(/ /g, '-') }:hover { color: ${ value } }`).join('\n') }
|
||||
${Object.entries(flatColors).map(([name, key]) => `.hover-${name.replace(/ /g, '-')}:hover { color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
|
||||
${ Object.entries(flatColors).map(([name, value]) => `.border-${ name.replace(/ /g, '-') } { border-color: ${ value } }`).join('\n') }
|
||||
/* border color */
|
||||
${Object.entries(flatColors).map(([name, key]) => `.border-${name.replace(/ /g, '-')} { border-color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
|
||||
/* background color */
|
||||
${Object.entries(flatColors).map(([name, key]) => `.bg-${name.replace(/ /g, '-')} { background-color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
|
||||
`;
|
||||
|
||||
// Write the generated CSS to a file
|
||||
|
|
|
|||
|
|
@ -1,29 +1,64 @@
|
|||
const colors = require('./app/theme/colors');
|
||||
const defaultColors = require('tailwindcss/colors');
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
const deprecatedDefaults = ['lightBlue', 'warmGray', 'trueGray', 'coolGray', 'blueGray']
|
||||
deprecatedDefaults.forEach(color => {
|
||||
delete defaultColors[color]
|
||||
})
|
||||
const deprecatedDefaults = [
|
||||
'lightBlue',
|
||||
'warmGray',
|
||||
'trueGray',
|
||||
'coolGray',
|
||||
'blueGray',
|
||||
];
|
||||
deprecatedDefaults.forEach((color) => {
|
||||
delete defaultColors[color];
|
||||
});
|
||||
|
||||
const cssVar = (name) => `var(--${name})`;
|
||||
|
||||
function createColorVariables(colors, darkColors) {
|
||||
const result = {};
|
||||
|
||||
// Process all colors
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
// Skip nested objects for now (we'll handle them separately)
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
result[key] = cssVar(`color-${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle nested color objects
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null && key !== 'dark') {
|
||||
result[key] = {};
|
||||
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
||||
result[key][nestedKey] = cssVar(`color-${key}-${nestedKey}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const variableBasedColors = createColorVariables(colors);
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
darkMode: 'class',
|
||||
content: ['./app/**/*.tsx', './app/**/*.js'],
|
||||
theme: {
|
||||
// Use variable references instead of hard-coded colors
|
||||
colors: {
|
||||
...defaultColors,
|
||||
...colors,
|
||||
...variableBasedColors,
|
||||
},
|
||||
extend: {
|
||||
keyframes: {
|
||||
'fade-in': {
|
||||
'0%': {
|
||||
opacity: '0',
|
||||
// transform: 'translateY(-10px)'
|
||||
},
|
||||
'100%': {
|
||||
opacity: '1',
|
||||
// transform: 'translateY(0)'
|
||||
},
|
||||
},
|
||||
'bg-spin': {
|
||||
|
|
@ -43,12 +78,12 @@ module.exports = {
|
|||
'bg-spin': 'bg-spin 1s ease infinite',
|
||||
},
|
||||
colors: {
|
||||
'disabled-text': 'rgba(0,0,0, 0.38)',
|
||||
'disabled-text': cssVar('color-disabled-text'),
|
||||
},
|
||||
boxShadow: {
|
||||
'border-blue': `0 0 0 1px ${colors['active-blue-border']}`,
|
||||
'border-main': `0 0 0 1px ${colors['main']}`,
|
||||
'border-gray': '0 0 0 1px #999',
|
||||
'border-blue': `0 0 0 1px ${cssVar('color-active-blue-border')}`,
|
||||
'border-main': `0 0 0 1px ${cssVar('color-main')}`,
|
||||
'border-gray': `0 0 0 1px ${cssVar('color-gray-border')}`,
|
||||
},
|
||||
button: {
|
||||
'background-color': 'red',
|
||||
|
|
@ -58,7 +93,59 @@ module.exports = {
|
|||
variants: {
|
||||
visibility: ['responsive', 'hover', 'focus', 'group-hover'],
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
plugin(function ({ addBase }) {
|
||||
const lightModeVars = {};
|
||||
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
if (typeof value !== 'object' || value === null || key === 'dark') {
|
||||
lightModeVars[`--color-${key}`] = value;
|
||||
}
|
||||
});
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null && key !== 'dark') {
|
||||
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
||||
lightModeVars[`--color-${key}-${nestedKey}`] = nestedValue;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const darkModeVars = {};
|
||||
|
||||
if (colors.dark) {
|
||||
// Process flat dark colors
|
||||
Object.entries(colors.dark).forEach(([key, value]) => {
|
||||
if (typeof value !== 'object') {
|
||||
// Find the corresponding light mode key
|
||||
const lightKey = key.replace('dark-', '');
|
||||
darkModeVars[`--color-${lightKey}`] = value;
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(colors.dark).forEach(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
||||
darkModeVars[`--color-${key}-${nestedKey}`] = nestedValue;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (colors['gray-light'] && colors.dark['gray-light']) {
|
||||
darkModeVars['--color-gray-light'] = colors.dark['gray-light'];
|
||||
}
|
||||
if (colors['gray-dark'] && colors.dark['gray-dark']) {
|
||||
darkModeVars['--color-gray-dark'] = colors.dark['gray-dark'];
|
||||
}
|
||||
darkModeVars['--color-disabled-text'] =
|
||||
colors.dark['text-disabled'] || 'rgba(255, 255, 255, 0.38)';
|
||||
}
|
||||
|
||||
addBase({
|
||||
':root': lightModeVars,
|
||||
'.dark': darkModeVars,
|
||||
});
|
||||
}),
|
||||
],
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ message 1, 'SessionStart', :tracker => false, :replayer => false do
|
|||
string 'UserID'
|
||||
end
|
||||
|
||||
# DEPRECATED; backend only (TODO: remove in the next release)
|
||||
message 3, 'SessionEndDeprecated', :tracker => false, :replayer => false do
|
||||
uint 'Timestamp'
|
||||
end
|
||||
|
||||
# DEPRECATED since 14.0.0 -> goto 122
|
||||
message 4, 'SetPageLocationDeprecated' do
|
||||
string 'URL'
|
||||
|
|
@ -129,6 +134,12 @@ message 24, 'PageRenderTiming', :replayer => false do
|
|||
uint 'VisuallyComplete'
|
||||
uint 'TimeToInteractive'
|
||||
end
|
||||
# DEPRECATED since 4.1.6 / 1.8.2 in favor of #78
|
||||
message 25, 'JSExceptionDeprecated', :replayer => false, :tracker => false do
|
||||
string 'Name'
|
||||
string 'Message'
|
||||
string 'Payload'
|
||||
end
|
||||
message 26, 'IntegrationEvent', :tracker => false, :replayer => false do
|
||||
uint 'Timestamp'
|
||||
string 'Source'
|
||||
|
|
@ -208,6 +219,29 @@ message 35, 'SetNodeAttributeDictGlobal' do
|
|||
uint 'Name'
|
||||
uint 'Value'
|
||||
end
|
||||
|
||||
# DEPRECATED since 4.0.2 in favor of AdoptedSSInsertRule + AdoptedSSAddOwner
|
||||
message 37, 'CSSInsertRule' do
|
||||
uint 'ID'
|
||||
string 'Rule'
|
||||
uint 'Index'
|
||||
end
|
||||
# DEPRECATED since 4.0.2
|
||||
message 38, 'CSSDeleteRule' do
|
||||
uint 'ID'
|
||||
uint 'Index'
|
||||
end
|
||||
|
||||
# DEPRECATED since 4.1.10 in favor of NetworkRequest
|
||||
message 39, 'Fetch', :replayer => :devtools do
|
||||
string 'Method'
|
||||
string 'URL'
|
||||
string 'Request'
|
||||
string 'Response'
|
||||
uint 'Status'
|
||||
uint 'Timestamp'
|
||||
uint 'Duration'
|
||||
end
|
||||
message 40, 'Profiler', :replayer => :devtools do
|
||||
string 'Name'
|
||||
uint 'Duration'
|
||||
|
|
@ -308,8 +342,9 @@ message 56, 'PerformanceTrackAggr', :tracker => false, :replayer => false do
|
|||
uint 'AvgUsedJSHeapSize'
|
||||
uint 'MaxUsedJSHeapSize'
|
||||
end
|
||||
|
||||
# Since 4.1.7 / 1.9.0
|
||||
message 57, 'LoadFontFace' do
|
||||
message 57, 'LoadFontFace' do
|
||||
uint 'ParentID'
|
||||
string 'Family'
|
||||
string 'Source'
|
||||
|
|
@ -319,6 +354,17 @@ end
|
|||
message 58, 'SetNodeFocus' do
|
||||
int 'ID'
|
||||
end
|
||||
|
||||
#DEPRECATED (since 3.0.?)
|
||||
message 59, 'LongTask' do
|
||||
uint 'Timestamp'
|
||||
uint 'Duration'
|
||||
uint 'Context'
|
||||
uint 'ContainerType'
|
||||
string 'ContainerSrc'
|
||||
string 'ContainerId'
|
||||
string 'ContainerName'
|
||||
end
|
||||
message 60, 'SetNodeAttributeURLBased' do
|
||||
uint 'ID'
|
||||
string 'Name'
|
||||
|
|
@ -331,6 +377,15 @@ message 61, 'SetCSSDataURLBased' do
|
|||
string 'Data'
|
||||
string 'BaseURL'
|
||||
end
|
||||
# DEPRECATED; backend only (TODO: remove in the next release)
|
||||
message 62, 'IssueEventDeprecated', :replayer => false, :tracker => false do
|
||||
uint 'MessageID'
|
||||
uint 'Timestamp'
|
||||
string 'Type'
|
||||
string 'ContextString'
|
||||
string 'Context'
|
||||
string 'Payload'
|
||||
end
|
||||
message 63, 'TechnicalInfo', :replayer => false do
|
||||
string 'Type'
|
||||
string 'Value'
|
||||
|
|
@ -343,6 +398,13 @@ end
|
|||
message 66, 'AssetCache', :replayer => false, :tracker => false do
|
||||
string 'URL'
|
||||
end
|
||||
message 67, 'CSSInsertRuleURLBased' do
|
||||
uint 'ID'
|
||||
string 'Rule'
|
||||
uint 'Index'
|
||||
string 'BaseURL'
|
||||
end
|
||||
|
||||
message 68, 'MouseClick' do
|
||||
uint 'ID'
|
||||
uint 'HesitationTime'
|
||||
|
|
@ -351,6 +413,7 @@ message 68, 'MouseClick' do
|
|||
uint 'NormalizedX'
|
||||
uint 'NormalizedY'
|
||||
end
|
||||
|
||||
message 69, 'MouseClickDeprecated' do
|
||||
uint 'ID'
|
||||
uint 'HesitationTime'
|
||||
|
|
@ -414,6 +477,13 @@ end
|
|||
|
||||
# Special one for Batch Metadata. Message id could define the version
|
||||
|
||||
# DEPRECATED since tracker 3.6.0 in favor of BatchMetadata
|
||||
message 80, 'BatchMeta', :replayer => false, :tracker => false do
|
||||
uint 'PageNo'
|
||||
uint 'FirstIndex'
|
||||
int 'Timestamp'
|
||||
end
|
||||
|
||||
# since tracker 3.6.0 TODO: for webworker only
|
||||
message 81, 'BatchMetadata', :replayer => false do
|
||||
uint 'Version'
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ usr=$(whoami)
|
|||
# Installing k3s
|
||||
function install_k8s() {
|
||||
echo "nameserver 1.1.1.1" | sudo tee /etc/k3s-resolv.conf
|
||||
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.31.5+k3s1' INSTALL_K3S_EXEC="--disable=traefik server --resolv-conf=/etc/k3s-resolv.conf" sh -
|
||||
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" K3S_RESOLV_CONF="/etc/k3s-resolv.conf" INSTALL_K3S_VERSION='v1.31.5+k3s1' INSTALL_K3S_EXEC="--disable=traefik" sh -
|
||||
[[ -d ~/.kube ]] || mkdir ~/.kube
|
||||
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
|
||||
sudo chmod 0644 ~/.kube/config
|
||||
|
|
|
|||
|
|
@ -9,68 +9,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
|||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
PARTITION BY toYYYYMM(_timestamp)
|
||||
ORDER BY (project_id, user_id, session_id)
|
||||
TTL _timestamp + INTERVAL 3 MONTH;
|
||||
|
||||
SET allow_experimental_json_type = 1;
|
||||
SET enable_json_type = 1;
|
||||
ALTER TABLE product_analytics.events
|
||||
MODIFY COLUMN `$properties` JSON(
|
||||
max_dynamic_paths=0,
|
||||
label String ,
|
||||
hesitation_time UInt32 ,
|
||||
name String ,
|
||||
payload String ,
|
||||
level Enum8 ('info'=0, 'error'=1),
|
||||
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
|
||||
message String ,
|
||||
error_id String ,
|
||||
duration UInt16,
|
||||
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
|
||||
url_host String ,
|
||||
url_path String ,
|
||||
url_hostpath String ,
|
||||
request_start UInt16 ,
|
||||
response_start UInt16 ,
|
||||
response_end UInt16 ,
|
||||
dom_content_loaded_event_start UInt16 ,
|
||||
dom_content_loaded_event_end UInt16 ,
|
||||
load_event_start UInt16 ,
|
||||
load_event_end UInt16 ,
|
||||
first_paint UInt16 ,
|
||||
first_contentful_paint_time UInt16 ,
|
||||
speed_index UInt16 ,
|
||||
visually_complete UInt16 ,
|
||||
time_to_interactive UInt16,
|
||||
ttfb UInt16,
|
||||
ttlb UInt16,
|
||||
response_time UInt16,
|
||||
dom_building_time UInt16,
|
||||
dom_content_loaded_event_time UInt16,
|
||||
load_event_time UInt16,
|
||||
min_fps UInt8,
|
||||
avg_fps UInt8,
|
||||
max_fps UInt8,
|
||||
min_cpu UInt8,
|
||||
avg_cpu UInt8,
|
||||
max_cpu UInt8,
|
||||
min_total_js_heap_size UInt64,
|
||||
avg_total_js_heap_size UInt64,
|
||||
max_total_js_heap_size UInt64,
|
||||
min_used_js_heap_size UInt64,
|
||||
avg_used_js_heap_size UInt64,
|
||||
max_used_js_heap_size UInt64,
|
||||
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
|
||||
status UInt16,
|
||||
success UInt8,
|
||||
request_body String,
|
||||
response_body String,
|
||||
transfer_size UInt32,
|
||||
selector String,
|
||||
normalized_x Float32,
|
||||
normalized_y Float32,
|
||||
message_id UInt64
|
||||
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events';
|
||||
ORDER BY (project_id, user_id, session_id);
|
||||
|
||||
DROP TABLE IF EXISTS product_analytics.all_events;
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.all_events
|
||||
|
|
@ -216,7 +155,8 @@ CREATE TABLE IF NOT EXISTS product_analytics.property_values_samples
|
|||
ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, property_name, is_event_property);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.property_values_sampler_mvREFRESHEVERY30HOURTOproduct_analytics.property_values_samples AS
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.property_values_sampler_mv
|
||||
REFRESH EVERY 30 HOUR TO product_analytics.property_values_samples AS
|
||||
SELECT project_id,
|
||||
property_name,
|
||||
TRUE AS is_event_property,
|
||||
|
|
@ -236,3 +176,93 @@ FROM product_analytics.events
|
|||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||
AND value != ''
|
||||
LIMIT 2 BY project_id,property_name;
|
||||
|
||||
|
||||
-- Autocomplete
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
_timestamp DateTime
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
|
||||
TO product_analytics.autocomplete_events AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
WHERE _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
|
||||
SELECT project_id,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_events
|
||||
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, value;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, event_name, property_name, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
|
||||
TO product_analytics.autocomplete_event_properties AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS event_name,
|
||||
property_name,
|
||||
JSONExtractString(toString(`$properties`), property_name) AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, event_name, property_name, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
|
||||
SELECT project_id,
|
||||
event_name,
|
||||
property_name,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_event_properties
|
||||
WHERE length(value) > 0
|
||||
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, event_name, property_name, value;
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
|
|||
sign Int8
|
||||
) ENGINE = CollapsingMergeTree(sign)
|
||||
PARTITION BY toYYYYMM(_timestamp)
|
||||
ORDER BY (project_id, user_id, session_id)
|
||||
TTL _timestamp + INTERVAL 3 MONTH;
|
||||
ORDER BY (project_id, user_id, session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
||||
(
|
||||
|
|
@ -160,8 +159,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
|
|||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
PARTITION BY toYYYYMM(_timestamp)
|
||||
ORDER BY (project_id, user_id, session_id)
|
||||
TTL _timestamp + INTERVAL 3 MONTH;
|
||||
ORDER BY (project_id, user_id, session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
|
||||
(
|
||||
|
|
@ -171,8 +169,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
|
|||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
PARTITION BY toYYYYMM(_timestamp)
|
||||
ORDER BY (project_id, user_id, error_id)
|
||||
TTL _timestamp + INTERVAL 3 MONTH;
|
||||
ORDER BY (project_id, user_id, error_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS experimental.issues
|
||||
(
|
||||
|
|
@ -330,62 +327,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
|
|||
"$source" LowCardinality(String) DEFAULT '' COMMENT 'the name of the integration that sent the event',
|
||||
"$duration_s" UInt16 DEFAULT 0 COMMENT 'the duration from session-start in seconds',
|
||||
properties JSON DEFAULT '{}',
|
||||
"$properties" JSON(
|
||||
max_dynamic_paths=0,
|
||||
label String ,
|
||||
hesitation_time UInt32 ,
|
||||
name String ,
|
||||
payload String ,
|
||||
level Enum8 ('info'=0, 'error'=1),
|
||||
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
|
||||
message String ,
|
||||
error_id String ,
|
||||
duration UInt16,
|
||||
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
|
||||
url_host String ,
|
||||
url_path String ,
|
||||
url_hostpath String ,
|
||||
request_start UInt16 ,
|
||||
response_start UInt16 ,
|
||||
response_end UInt16 ,
|
||||
dom_content_loaded_event_start UInt16 ,
|
||||
dom_content_loaded_event_end UInt16 ,
|
||||
load_event_start UInt16 ,
|
||||
load_event_end UInt16 ,
|
||||
first_paint UInt16 ,
|
||||
first_contentful_paint_time UInt16 ,
|
||||
speed_index UInt16 ,
|
||||
visually_complete UInt16 ,
|
||||
time_to_interactive UInt16,
|
||||
ttfb UInt16,
|
||||
ttlb UInt16,
|
||||
response_time UInt16,
|
||||
dom_building_time UInt16,
|
||||
dom_content_loaded_event_time UInt16,
|
||||
load_event_time UInt16,
|
||||
min_fps UInt8,
|
||||
avg_fps UInt8,
|
||||
max_fps UInt8,
|
||||
min_cpu UInt8,
|
||||
avg_cpu UInt8,
|
||||
max_cpu UInt8,
|
||||
min_total_js_heap_size UInt64,
|
||||
avg_total_js_heap_size UInt64,
|
||||
max_total_js_heap_size UInt64,
|
||||
min_used_js_heap_size UInt64,
|
||||
avg_used_js_heap_size UInt64,
|
||||
max_used_js_heap_size UInt64,
|
||||
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
|
||||
status UInt16,
|
||||
success UInt8,
|
||||
request_body String,
|
||||
response_body String,
|
||||
transfer_size UInt32,
|
||||
selector String,
|
||||
normalized_x Float32,
|
||||
normalized_y Float32,
|
||||
message_id UInt64
|
||||
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
|
||||
"$properties" JSON DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
|
||||
description String DEFAULT '',
|
||||
group_id1 Array(String) DEFAULT [],
|
||||
group_id2 Array(String) DEFAULT [],
|
||||
|
|
@ -767,3 +709,92 @@ FROM product_analytics.events
|
|||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||
AND value != ''
|
||||
LIMIT 2 BY project_id,property_name;
|
||||
|
||||
-- Autocomplete
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
_timestamp DateTime
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
|
||||
TO product_analytics.autocomplete_events AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
WHERE _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
value String COMMENT 'The $event_name',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
|
||||
SELECT project_id,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_events
|
||||
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, value;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY (project_id, event_name, property_name, value, _timestamp)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
|
||||
TO product_analytics.autocomplete_event_properties AS
|
||||
SELECT project_id,
|
||||
`$event_name` AS event_name,
|
||||
property_name,
|
||||
JSONExtractString(toString(`$properties`), property_name) AS value,
|
||||
_timestamp
|
||||
FROM product_analytics.events
|
||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
|
||||
(
|
||||
project_id UInt16,
|
||||
event_name String COMMENT 'The $event_name',
|
||||
property_name String,
|
||||
value String COMMENT 'The property-value as a string',
|
||||
data_count UInt16 COMMENT 'The number of appearance during the past month',
|
||||
_timestamp DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||
ORDER BY (project_id, event_name, property_name, value)
|
||||
TTL _timestamp + INTERVAL 1 MONTH;
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
|
||||
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
|
||||
SELECT project_id,
|
||||
event_name,
|
||||
property_name,
|
||||
value,
|
||||
count(1) AS data_count,
|
||||
max(_timestamp) AS _timestamp
|
||||
FROM product_analytics.autocomplete_event_properties
|
||||
WHERE length(value) > 0
|
||||
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
|
||||
GROUP BY project_id, event_name, property_name, value;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,165 +1,49 @@
|
|||
import orLogo from "~/assets/orSpot.svg";
|
||||
import micOff from "~/assets/mic-off-red.svg";
|
||||
import micOn from "~/assets/mic-on-dark.svg";
|
||||
import { createEffect, onMount } from "solid-js";
|
||||
import Login from "~/entrypoints/popup/Login";
|
||||
import Settings from "~/entrypoints/popup/Settings";
|
||||
import { createSignal, createEffect, onMount } from "solid-js";
|
||||
import Dropdown from "~/entrypoints/popup/Dropdown";
|
||||
import Button from "~/entrypoints/popup/Button";
|
||||
import {
|
||||
ChevronSvg,
|
||||
RecordDesktopSvg,
|
||||
RecordTabSvg,
|
||||
HomePageSvg,
|
||||
SlackSvg,
|
||||
SettingsSvg,
|
||||
} from "./Icons";
|
||||
|
||||
async function getAudioDevices() {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevices = devices
|
||||
.filter((device) => device.kind === "audioinput")
|
||||
.map((device) => ({ label: device.label, id: device.deviceId }));
|
||||
|
||||
return { granted: true, audioDevices };
|
||||
} catch (error) {
|
||||
console.error("Error accessing audio devices:", error);
|
||||
const msg = error.message ?? "";
|
||||
return {
|
||||
granted: false,
|
||||
denied: msg.includes("denied"),
|
||||
audioDevices: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const orSite = () => {
|
||||
window.open("https://openreplay.com", "_blank");
|
||||
};
|
||||
|
||||
function Header({ openSettings }: { openSettings: () => void }) {
|
||||
const openHomePage = async () => {
|
||||
const { settings } = await chrome.storage.local.get("settings");
|
||||
return window.open(`${settings.ingestPoint}/spots`, "_blank");
|
||||
};
|
||||
return (
|
||||
<div class={"flex items-center gap-1"}>
|
||||
<div
|
||||
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
|
||||
onClick={orSite}
|
||||
>
|
||||
<img src={orLogo} class="w-5" alt={"OpenReplay Spot"} />
|
||||
<div class={"text-neutral-600"}>
|
||||
<span class={"text-lg font-semibold text-black"}>OpenReplay Spot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={"ml-auto flex items-center gap-2"}>
|
||||
<div class="text-sm tooltip tooltip-bottom" data-tip="My Spots">
|
||||
<div onClick={openHomePage}>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<HomePageSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Get help on Slack"
|
||||
>
|
||||
<a
|
||||
href={
|
||||
"https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<SlackSvg />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Settings"
|
||||
onClick={openSettings}
|
||||
>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<SettingsSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const STATE = {
|
||||
empty: "empty",
|
||||
login: "login",
|
||||
ready: "ready",
|
||||
starting: "starting",
|
||||
recording: "recording",
|
||||
};
|
||||
import Header from "./components/Header";
|
||||
import RecordingControls from "./components/RecordingControls";
|
||||
import AudioPicker from "./components/AudioPicker";
|
||||
import { useAppState } from "./hooks/useAppState";
|
||||
import { useAudioDevices } from "./hooks/useAudioDevices";
|
||||
import { AppState } from "./types";
|
||||
|
||||
function App() {
|
||||
const [state, setState] = createSignal(STATE.empty);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
|
||||
const [mic, setMic] = createSignal(false);
|
||||
const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
|
||||
const [hasPermissions, setHasPermissions] = createSignal(false);
|
||||
const {
|
||||
state,
|
||||
isSettingsOpen,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
audioDevices,
|
||||
selectedAudioDevice,
|
||||
mic,
|
||||
hasPermissions,
|
||||
isChecking,
|
||||
checkAudioDevices,
|
||||
handleMicToggle,
|
||||
selectAudioDevice,
|
||||
} = useAudioDevices();
|
||||
|
||||
// Listen for mic status updates from background
|
||||
onMount(() => {
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:no-login") {
|
||||
setState(STATE.login);
|
||||
}
|
||||
if (message.type === "popup:login") {
|
||||
setState(STATE.ready);
|
||||
}
|
||||
if (message.type === "popup:stopped") {
|
||||
setState(STATE.ready);
|
||||
}
|
||||
if (message.type === "popup:started") {
|
||||
setState(STATE.recording);
|
||||
}
|
||||
if (message.type === "popup:mic-status") {
|
||||
setMic(message.status);
|
||||
}
|
||||
});
|
||||
void browser.runtime.sendMessage({ type: "popup:check-status" });
|
||||
});
|
||||
|
||||
const startRecording = async (reqTab: "tab" | "desktop") => {
|
||||
setState(STATE.starting);
|
||||
await browser.runtime.sendMessage({
|
||||
type: "popup:start",
|
||||
area: reqTab,
|
||||
mic: mic(),
|
||||
audioId: selectedAudioDevice(),
|
||||
permissions: hasPermissions(),
|
||||
});
|
||||
window.close();
|
||||
const handleStartRecording = (area: "tab" | "desktop") => {
|
||||
startRecording(area, mic(), selectedAudioDevice(), hasPermissions());
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:stop",
|
||||
mic: mic(),
|
||||
audioId: selectedAudioDevice(),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleMic = async () => {
|
||||
setMic(!mic());
|
||||
};
|
||||
|
||||
const openSettings = () => {
|
||||
setIsSettingsOpen(true);
|
||||
};
|
||||
const closeSettings = () => {
|
||||
setIsSettingsOpen(false);
|
||||
const handleStopRecording = () => {
|
||||
stopRecording(mic(), selectedAudioDevice());
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -167,58 +51,30 @@ function App() {
|
|||
{isSettingsOpen() ? (
|
||||
<Settings goBack={closeSettings} />
|
||||
) : (
|
||||
<div class={"flex flex-col gap-4 p-5"}>
|
||||
<div class="flex flex-col gap-4 p-5">
|
||||
<Header openSettings={openSettings} />
|
||||
|
||||
{state() === STATE.login ? (
|
||||
{state() === AppState.LOGIN ? (
|
||||
<Login />
|
||||
) : (
|
||||
<>
|
||||
{state() === STATE.recording ? (
|
||||
<Button
|
||||
name={"End Recording"}
|
||||
onClick={() => stopRecording()}
|
||||
/>
|
||||
) : null}
|
||||
{state() === STATE.starting ? (
|
||||
<div
|
||||
class={
|
||||
"flex flex-row items-center gap-2 w-full justify-center"
|
||||
}
|
||||
>
|
||||
<div class="py-4">Your recording is starting</div>
|
||||
</div>
|
||||
) : null}
|
||||
{state() === STATE.ready ? (
|
||||
<>
|
||||
<div class="flex flex-row items-center gap-2 w-full justify-center">
|
||||
<button
|
||||
class="btn bg-indigo-100 text-base hover:bg-primary hover:text-white w-6/12"
|
||||
name="Record Tab"
|
||||
onClick={() => startRecording("tab")}
|
||||
>
|
||||
<RecordTabSvg />
|
||||
Record Tab
|
||||
</button>
|
||||
<RecordingControls
|
||||
state={state()}
|
||||
startRecording={handleStartRecording}
|
||||
stopRecording={handleStopRecording}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn bg-teal-50 text-base hover:bg-primary hover:text-white"
|
||||
name={"Record Desktop"}
|
||||
onClick={() => startRecording("desktop")}
|
||||
>
|
||||
<RecordDesktopSvg />
|
||||
Record Desktop
|
||||
</button>
|
||||
</div>
|
||||
<AudioPicker
|
||||
mic={mic}
|
||||
toggleMic={toggleMic}
|
||||
selectedAudioDevice={selectedAudioDevice}
|
||||
setSelectedAudioDevice={setSelectedAudioDevice}
|
||||
setHasPermissions={setHasPermissions}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{state() === AppState.READY && (
|
||||
<AudioPicker
|
||||
mic={mic}
|
||||
audioDevices={audioDevices}
|
||||
selectedAudioDevice={selectedAudioDevice}
|
||||
isChecking={isChecking}
|
||||
onMicToggle={handleMicToggle}
|
||||
onCheckAudio={checkAudioDevices}
|
||||
onSelectDevice={selectAudioDevice}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -227,111 +83,4 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
interface IAudioPicker {
|
||||
mic: () => boolean;
|
||||
toggleMic: () => void;
|
||||
selectedAudioDevice: () => string;
|
||||
setSelectedAudioDevice: (value: string) => void;
|
||||
setHasPermissions: (value: boolean) => void;
|
||||
}
|
||||
function AudioPicker(props: IAudioPicker) {
|
||||
const [audioDevices, setAudioDevices] = createSignal(
|
||||
[] as { label: string; id: string }[],
|
||||
);
|
||||
const [checkedAudioDevices, setCheckedAudioDevices] = createSignal(0);
|
||||
|
||||
createEffect(() => {
|
||||
chrome.storage.local.get("audioPerm", (data) => {
|
||||
if (data.audioPerm && audioDevices().length === 0) {
|
||||
props.setHasPermissions(true);
|
||||
void checkAudioDevices();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const checkAudioDevices = async () => {
|
||||
const { granted, audioDevices, denied } = await getAudioDevices();
|
||||
if (!granted && !denied) {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:get-audio-perm",
|
||||
});
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:audio-perm") {
|
||||
void checkAudioDevices();
|
||||
}
|
||||
});
|
||||
} else if (audioDevices.length > 0) {
|
||||
chrome.storage.local.set({ audioPerm: granted });
|
||||
setAudioDevices(audioDevices);
|
||||
props.setSelectedAudioDevice(audioDevices[0]?.id || "");
|
||||
}
|
||||
};
|
||||
|
||||
const checkAudio = async () => {
|
||||
if (checkedAudioDevices() > 0) {
|
||||
return;
|
||||
}
|
||||
setCheckedAudioDevices(1);
|
||||
await checkAudioDevices();
|
||||
setCheckedAudioDevices(2);
|
||||
};
|
||||
const onSelect = (value) => {
|
||||
props.setSelectedAudioDevice(value);
|
||||
if (!props.mic()) {
|
||||
props.toggleMic();
|
||||
}
|
||||
};
|
||||
|
||||
const onMicToggle = async () => {
|
||||
if (!audioDevices().length) {
|
||||
return await checkAudioDevices();
|
||||
}
|
||||
if (!props.selectedAudioDevice() && audioDevices().length) {
|
||||
onSelect(audioDevices()[0].id);
|
||||
} else {
|
||||
props.toggleMic();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={"inline-flex items-center gap-1 text-xs"}>
|
||||
<div
|
||||
class={
|
||||
"p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-50 pointer-events-auto tooltip tooltip-right text-sm font-normal"
|
||||
}
|
||||
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
|
||||
onClick={onMicToggle}
|
||||
>
|
||||
<img
|
||||
src={props.mic() ? micOn : micOff}
|
||||
alt={props.mic() ? "microphone on" : "microphone off"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class={
|
||||
"flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
|
||||
}
|
||||
onClick={checkAudio}
|
||||
>
|
||||
{audioDevices().length === 0 ? (
|
||||
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
|
||||
{checkedAudioDevices() === 1
|
||||
? "Loading audio devices"
|
||||
: "Grant microphone access"}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
options={audioDevices()}
|
||||
selected={props.selectedAudioDevice()}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
)}
|
||||
<ChevronSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
57
spot/entrypoints/popup/components/AudioPicker.tsx
Normal file
57
spot/entrypoints/popup/components/AudioPicker.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, For } from "solid-js";
|
||||
import micOff from "~/assets/mic-off-red.svg";
|
||||
import micOn from "~/assets/mic-on-dark.svg";
|
||||
import Dropdown from "~/entrypoints/popup/Dropdown";
|
||||
import { ChevronSvg } from "../Icons";
|
||||
import { AudioDevice } from "../types";
|
||||
|
||||
interface AudioPickerProps {
|
||||
mic: () => boolean;
|
||||
audioDevices: () => AudioDevice[];
|
||||
selectedAudioDevice: () => string;
|
||||
isChecking: () => boolean;
|
||||
onMicToggle: () => void;
|
||||
onCheckAudio: () => void;
|
||||
onSelectDevice: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
const AudioPicker: Component<AudioPickerProps> = (props) => {
|
||||
return (
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<div
|
||||
class="p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-50 pointer-events-auto tooltip tooltip-right text-sm font-normal"
|
||||
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
|
||||
onClick={props.onMicToggle}
|
||||
>
|
||||
<img
|
||||
src={props.mic() ? micOn : micOff}
|
||||
alt={props.mic() ? "microphone on" : "microphone off"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
|
||||
onClick={props.onCheckAudio}
|
||||
>
|
||||
{props.audioDevices().length === 0 ? (
|
||||
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
|
||||
{props.isChecking()
|
||||
? "Loading audio devices"
|
||||
: "Grant microphone access"}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
options={props.audioDevices()}
|
||||
selected={props.selectedAudioDevice()}
|
||||
onChange={props.onSelectDevice}
|
||||
/>
|
||||
)}
|
||||
<ChevronSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioPicker;
|
||||
73
spot/entrypoints/popup/components/Header.tsx
Normal file
73
spot/entrypoints/popup/components/Header.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Component } from "solid-js";
|
||||
import orLogo from "~/assets/orSpot.svg";
|
||||
import {
|
||||
HomePageSvg,
|
||||
SlackSvg,
|
||||
SettingsSvg,
|
||||
} from "../Icons";
|
||||
|
||||
interface HeaderProps {
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
const Header: Component<HeaderProps> = (props) => {
|
||||
const openHomePage = async () => {
|
||||
const { settings } = await chrome.storage.local.get("settings");
|
||||
return window.open(`${settings.ingestPoint}/spots`, "_blank");
|
||||
};
|
||||
|
||||
const openOrSite = () => {
|
||||
window.open("https://openreplay.com", "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
|
||||
onClick={openOrSite}
|
||||
>
|
||||
<img src={orLogo} class="w-5" alt="OpenReplay Spot" />
|
||||
<div class="text-neutral-600">
|
||||
<span class="text-lg font-semibold text-black">OpenReplay Spot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="text-sm tooltip tooltip-bottom" data-tip="My Spots">
|
||||
<div onClick={openHomePage}>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<HomePageSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Get help on Slack"
|
||||
>
|
||||
<a
|
||||
href="https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<SlackSvg />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Settings"
|
||||
onClick={props.openSettings}
|
||||
>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<SettingsSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue