Merge branch 'dev' into adopted-style-sheets
13
LICENSE
|
|
@ -1,12 +1,21 @@
|
|||
Copyright (c) 2022 Asayer, Inc.
|
||||
|
||||
OpenReplay monorepo uses multiple licenses. Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "ee/" directory of this repository, is licensed under the license defined in "ee/LICENSE".
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "Elastic License 2.0 (ELv2)" license as defined below.
|
||||
- Some directories have a specific LICENSE file and are licensed under the "MIT" license, as defined below.
|
||||
- Content outside of the above mentioned directories or restrictions defaults to the "Elastic License 2.0 (ELv2)" license, as defined below.
|
||||
|
||||
Reach out (license@openreplay.com) if you have any questions regarding licenses.
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
Elastic License 2.0 (ELv2)
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ Check out our [roadmap](https://www.notion.so/openreplay/Roadmap-889d2c3d968b478
|
|||
|
||||
## License
|
||||
|
||||
This repo is under the Elastic License 2.0 (ELv2), with the exception of the `ee` directory.
|
||||
This monorepo uses several licenses. See [LICENSE](/LICENSE) for more details.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
|
|
|||
3
api/.gitignore
vendored
|
|
@ -174,4 +174,5 @@ logs*.txt
|
|||
SUBNETS.json
|
||||
|
||||
./chalicelib/.configs
|
||||
README/*
|
||||
README/*
|
||||
.local
|
||||
|
|
@ -19,7 +19,7 @@ RUN cd /work_tmp && npm install
|
|||
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/.
|
||||
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. && chmod 644 /mappings.wasm
|
||||
|
||||
RUN adduser -u 1001 openreplay -D
|
||||
USER 1001
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
|||
RUN apk add --no-cache build-base tini
|
||||
ARG envarg
|
||||
ENV APP_NAME=alerts \
|
||||
pg_minconn=2 \
|
||||
pg_minconn=1 \
|
||||
pg_maxconn=10 \
|
||||
ENTERPRISE_BUILD=${envarg}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||
from decouple import config
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from chalicelib.utils import helper
|
||||
|
|
@ -14,7 +15,7 @@ from routers.crons import core_dynamic_crons
|
|||
from routers.subs import dashboard, insights, metrics, v1_api
|
||||
|
||||
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
@app.middleware('http')
|
||||
async def or_middleware(request: Request, call_next):
|
||||
|
|
|
|||
|
|
@ -138,7 +138,10 @@ def send_by_email(notification, destination):
|
|||
|
||||
|
||||
def send_by_email_batch(notifications_list):
|
||||
if not helper.has_smtp():
|
||||
logging.info("no SMTP configuration for email notifications")
|
||||
if notifications_list is None or len(notifications_list) == 0:
|
||||
logging.info("no email notifications")
|
||||
return
|
||||
for n in notifications_list:
|
||||
send_by_email(notification=n.get("notification"), destination=n.get("destination"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import requests
|
||||
from decouple import config
|
||||
|
||||
from os.path import exists
|
||||
import schemas
|
||||
from chalicelib.core import projects
|
||||
|
||||
|
|
@ -158,3 +158,11 @@ def autocomplete(project_id, q: str, key: str = None):
|
|||
def get_ice_servers():
|
||||
return config("iceServers") if config("iceServers", default=None) is not None \
|
||||
and len(config("iceServers")) > 0 else None
|
||||
|
||||
|
||||
def get_raw_mob_by_id(project_id, session_id):
|
||||
path_to_file = config("FS_DIR") + "/" + str(session_id)
|
||||
|
||||
if exists(path_to_file):
|
||||
return path_to_file
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -43,16 +43,24 @@ def __create(tenant_id, name):
|
|||
|
||||
def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, stack_integrations=False):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(f"""\
|
||||
SELECT
|
||||
s.project_id, s.name, s.project_key, s.save_request_payloads
|
||||
{',s.gdpr' if gdpr else ''}
|
||||
{',COALESCE((SELECT TRUE FROM public.sessions WHERE sessions.project_id = s.project_id LIMIT 1), FALSE) AS recorded' if recorded else ''}
|
||||
{',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''}
|
||||
FROM public.projects AS s
|
||||
{'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''}
|
||||
WHERE s.deleted_at IS NULL
|
||||
ORDER BY s.project_id;""")
|
||||
recorded_q = ""
|
||||
if recorded:
|
||||
recorded_q = """, COALESCE((SELECT TRUE
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id = s.project_id
|
||||
AND sessions.start_ts >= (EXTRACT(EPOCH FROM s.created_at) * 1000 - 24 * 60 * 60 * 1000)
|
||||
AND sessions.start_ts <= %(now)s
|
||||
LIMIT 1), FALSE) AS recorded"""
|
||||
query = cur.mogrify(f"""SELECT
|
||||
s.project_id, s.name, s.project_key, s.save_request_payloads
|
||||
{',s.gdpr' if gdpr else ''}
|
||||
{recorded_q}
|
||||
{',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''}
|
||||
FROM public.projects AS s
|
||||
{'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''}
|
||||
WHERE s.deleted_at IS NULL
|
||||
ORDER BY s.project_id;""", {"now": TimeUTC.now()})
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
if recording_state:
|
||||
project_ids = [f'({r["project_id"]})' for r in rows]
|
||||
|
|
|
|||
|
|
@ -712,13 +712,13 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
event.value, value_key=e_k))
|
||||
elif event_type == events.event_type.ERROR.ui_type:
|
||||
event_from = event_from % f"{events.event_type.ERROR.table} AS main INNER JOIN public.errors AS main1 USING(error_id)"
|
||||
event.source = tuple(event.source)
|
||||
event.source = list(set(event.source))
|
||||
if not is_any and event.value not in [None, "*", ""]:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"(main1.message {op} %({e_k})s OR main1.name {op} %({e_k})s)",
|
||||
event.value, value_key=e_k))
|
||||
if event.source[0] not in [None, "*", ""]:
|
||||
event_where.append(_multiple_conditions(f"main1.source = %({s_k})s", event.value, value_key=s_k))
|
||||
event_where.append(_multiple_conditions(f"main1.source = %({s_k})s", event.source, value_key=s_k))
|
||||
|
||||
|
||||
# ----- IOS
|
||||
|
|
@ -877,7 +877,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._duration:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s::integer", f.value, value_key=e_k_f))
|
||||
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s::integer", f.value,
|
||||
value_key=e_k_f))
|
||||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._request_body:
|
||||
event_where.append(
|
||||
|
|
@ -885,7 +886,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._response_body:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.response_body {op} %({e_k_f})s::text", f.value, value_key=e_k_f))
|
||||
_multiple_conditions(f"main.response_body {op} %({e_k_f})s::text", f.value,
|
||||
value_key=e_k_f))
|
||||
apply = True
|
||||
else:
|
||||
print(f"undefined FETCH filter: {f.type}")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import base64
|
||||
import logging
|
||||
import re
|
||||
from email.header import Header
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from chalicelib.utils import helper, smtp
|
||||
from decouple import config
|
||||
|
||||
from chalicelib.utils import smtp
|
||||
|
||||
|
||||
def __get_subject(subject):
|
||||
return subject
|
||||
|
|
@ -64,11 +66,11 @@ def send_html(BODY_HTML, SUBJECT, recipient, bcc=None):
|
|||
if bcc is not None and len(bcc) > 0:
|
||||
r += [bcc]
|
||||
try:
|
||||
print(f"Email sending to: {r}")
|
||||
logging.info(f"Email sending to: {r}")
|
||||
s.sendmail(msg['FROM'], r, msg.as_string().encode('ascii'))
|
||||
except Exception as e:
|
||||
print("!!! Email error!")
|
||||
print(e)
|
||||
logging.error("!!! Email error!")
|
||||
logging.error(e)
|
||||
|
||||
|
||||
def send_text(recipients, text, subject):
|
||||
|
|
@ -82,8 +84,8 @@ def send_text(recipients, text, subject):
|
|||
try:
|
||||
s.sendmail(msg['FROM'], recipients, msg.as_string().encode('ascii'))
|
||||
except Exception as e:
|
||||
print("!! Text-email failed: " + subject),
|
||||
print(e)
|
||||
logging.error("!! Text-email failed: " + subject),
|
||||
logging.error(e)
|
||||
|
||||
|
||||
def __escape_text_html(text):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.utils.email_handler import __get_html_from_file, send_html, __escape_text_html
|
||||
from chalicelib.utils.email_handler import __get_html_from_file, send_html
|
||||
|
||||
|
||||
def send_team_invitation(recipient, client_id, sender_name, invitation_link):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class JiraManager:
|
|||
self._config = {"JIRA_PROJECT_ID": project_id, "JIRA_URL": url, "JIRA_USERNAME": username,
|
||||
"JIRA_PASSWORD": password}
|
||||
try:
|
||||
self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=1)
|
||||
self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=0, timeout=3)
|
||||
except Exception as e:
|
||||
print("!!! JIRA AUTH ERROR")
|
||||
print(e)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
import time
|
||||
from threading import Semaphore
|
||||
|
||||
|
|
@ -6,6 +7,9 @@ import psycopg2.extras
|
|||
from decouple import config
|
||||
from psycopg2 import pool
|
||||
|
||||
logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO))
|
||||
logging.getLogger('apscheduler').setLevel(config("LOGLEVEL", default=logging.INFO))
|
||||
|
||||
_PG_CONFIG = {"host": config("pg_host"),
|
||||
"database": config("pg_dbname"),
|
||||
"user": config("pg_user"),
|
||||
|
|
@ -44,31 +48,34 @@ RETRY = 0
|
|||
|
||||
|
||||
def make_pool():
|
||||
if not config('PG_POOL', cast=bool, default=True):
|
||||
return
|
||||
global postgreSQL_pool
|
||||
global RETRY
|
||||
if postgreSQL_pool is not None:
|
||||
try:
|
||||
postgreSQL_pool.closeall()
|
||||
except (Exception, psycopg2.DatabaseError) as error:
|
||||
print("Error while closing all connexions to PostgreSQL", error)
|
||||
logging.error("Error while closing all connexions to PostgreSQL", error)
|
||||
try:
|
||||
postgreSQL_pool = ORThreadedConnectionPool(config("pg_minconn", cast=int, default=20),
|
||||
config("pg_maxconn", cast=int, default=80),
|
||||
**PG_CONFIG)
|
||||
if (postgreSQL_pool):
|
||||
print("Connection pool created successfully")
|
||||
logging.info("Connection pool created successfully")
|
||||
except (Exception, psycopg2.DatabaseError) as error:
|
||||
print("Error while connecting to PostgreSQL", error)
|
||||
logging.error("Error while connecting to PostgreSQL", error)
|
||||
if RETRY < RETRY_MAX:
|
||||
RETRY += 1
|
||||
print(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}")
|
||||
logging.info(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}")
|
||||
time.sleep(RETRY_INTERVAL)
|
||||
make_pool()
|
||||
else:
|
||||
raise error
|
||||
|
||||
|
||||
make_pool()
|
||||
if config('PG_POOL', cast=bool, default=True):
|
||||
make_pool()
|
||||
|
||||
|
||||
class PostgresClient:
|
||||
|
|
@ -87,8 +94,14 @@ class PostgresClient:
|
|||
elif long_query:
|
||||
long_config = dict(_PG_CONFIG)
|
||||
long_config["application_name"] += "-LONG"
|
||||
long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5 * 60) * 1000}"
|
||||
long_config["options"] = f"-c statement_timeout=" \
|
||||
f"{config('pg_long_timeout', cast=int, default=5 * 60) * 1000}"
|
||||
self.connection = psycopg2.connect(**long_config)
|
||||
elif not config('PG_POOL', cast=bool, default=True):
|
||||
single_config = dict(_PG_CONFIG)
|
||||
single_config["application_name"] += "-NOPOOL"
|
||||
single_config["options"] = f"-c statement_timeout={config('pg_timeout', cast=int, default=3 * 60) * 1000}"
|
||||
self.connection = psycopg2.connect(**single_config)
|
||||
else:
|
||||
self.connection = postgreSQL_pool.getconn()
|
||||
|
||||
|
|
@ -104,14 +117,19 @@ class PostgresClient:
|
|||
if self.long_query or self.unlimited_query:
|
||||
self.connection.close()
|
||||
except Exception as error:
|
||||
print("Error while committing/closing PG-connection", error)
|
||||
if str(error) == "connection already closed" and not self.long_query and not self.unlimited_query:
|
||||
print("Recreating the connexion pool")
|
||||
logging.error("Error while committing/closing PG-connection", error)
|
||||
if str(error) == "connection already closed" \
|
||||
and not self.long_query \
|
||||
and not self.unlimited_query \
|
||||
and config('PG_POOL', cast=bool, default=True):
|
||||
logging.info("Recreating the connexion pool")
|
||||
make_pool()
|
||||
else:
|
||||
raise error
|
||||
finally:
|
||||
if not self.long_query:
|
||||
if config('PG_POOL', cast=bool, default=True) \
|
||||
and not self.long_query \
|
||||
and not self.unlimited_query:
|
||||
postgreSQL_pool.putconn(self.connection)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import logging
|
||||
import smtplib
|
||||
from smtplib import SMTPAuthenticationError
|
||||
|
||||
from decouple import config
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
|
||||
class EmptySMTP:
|
||||
def sendmail(self, from_addr, to_addrs, msg, mail_options=(), rcpt_options=()):
|
||||
print("!! CANNOT SEND EMAIL, NO VALID SMTP CONFIGURATION FOUND")
|
||||
logging.error("!! CANNOT SEND EMAIL, NO VALID SMTP CONFIGURATION FOUND")
|
||||
|
||||
|
||||
class SMTPClient:
|
||||
|
|
@ -30,7 +34,11 @@ class SMTPClient:
|
|||
self.server.starttls()
|
||||
# stmplib docs recommend calling ehlo() before & after starttls()
|
||||
self.server.ehlo()
|
||||
self.server.login(user=config("EMAIL_USER"), password=config("EMAIL_PASSWORD"))
|
||||
if len(config("EMAIL_USER", default="")) > 0 and len(config("EMAIL_PASSWORD", default="")) > 0:
|
||||
try:
|
||||
self.server.login(user=config("EMAIL_USER"), password=config("EMAIL_PASSWORD"))
|
||||
except SMTPAuthenticationError:
|
||||
raise HTTPException(401, "SMTP Authentication Error")
|
||||
return self.server
|
||||
|
||||
def __exit__(self, *args):
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pg_minconn=20
|
|||
pg_maxconn=50
|
||||
PG_RETRY_MAX=50
|
||||
PG_RETRY_INTERVAL=2
|
||||
PG_POOL=true
|
||||
put_S3_TTL=20
|
||||
sentryURL=
|
||||
sessions_bucket=mobs
|
||||
|
|
@ -47,4 +48,5 @@ sessions_region=us-east-1
|
|||
sourcemaps_bucket=sourcemaps
|
||||
sourcemaps_reader=http://127.0.0.1:9000/sourcemaps
|
||||
stage=default-foss
|
||||
version_number=1.4.0
|
||||
version_number=1.4.0
|
||||
FS_DIR=/mnt/efs
|
||||
|
|
@ -4,7 +4,7 @@ boto3==1.24.26
|
|||
pyjwt==2.4.0
|
||||
psycopg2-binary==2.9.3
|
||||
elasticsearch==8.3.1
|
||||
jira==3.3.0
|
||||
jira==3.3.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ boto3==1.24.26
|
|||
pyjwt==2.4.0
|
||||
psycopg2-binary==2.9.3
|
||||
elasticsearch==8.3.1
|
||||
jira==3.3.0
|
||||
jira==3.3.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from typing import Union, Optional
|
|||
|
||||
from decouple import config
|
||||
from fastapi import Depends, Body, BackgroundTasks, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from starlette import status
|
||||
|
||||
import schemas
|
||||
|
|
@ -183,8 +184,8 @@ def session_top_filter_values(projectId: int, context: schemas.CurrentContext =
|
|||
@app.get('/{projectId}/integrations', tags=["integrations"])
|
||||
def get_integrations_status(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = integrations_global.get_global_integrations_status(tenant_id=context.tenant_id,
|
||||
user_id=context.user_id,
|
||||
project_id=projectId)
|
||||
user_id=context.user_id,
|
||||
project_id=projectId)
|
||||
return {"data": data}
|
||||
|
||||
|
||||
|
|
@ -230,7 +231,7 @@ def delete_sentry(projectId: int, context: schemas.CurrentContext = Depends(OR_c
|
|||
|
||||
|
||||
@app.get('/{projectId}/integrations/sentry/events/{eventId}', tags=["integrations"])
|
||||
def proxy_sentry(projectId: int, eventId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
def proxy_sentry(projectId: int, eventId: str, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": log_tool_sentry.proxy_get(tenant_id=context.tenant_id, project_id=projectId, event_id=eventId)}
|
||||
|
||||
|
||||
|
|
@ -440,29 +441,47 @@ def get_integration_status(context: schemas.CurrentContext = Depends(OR_context)
|
|||
return {"data": integration.get_obfuscated()}
|
||||
|
||||
|
||||
@app.get('/integrations/jira', tags=["integrations"])
|
||||
def get_integration_status_jira(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id,
|
||||
user_id=context.user_id,
|
||||
tool=integration_jira_cloud.PROVIDER)
|
||||
if error is not None and integration is None:
|
||||
return error
|
||||
return {"data": integration.get_obfuscated()}
|
||||
|
||||
|
||||
@app.get('/integrations/github', tags=["integrations"])
|
||||
def get_integration_status_github(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id,
|
||||
user_id=context.user_id,
|
||||
tool=integration_github.PROVIDER)
|
||||
if error is not None and integration is None:
|
||||
return error
|
||||
return {"data": integration.get_obfuscated()}
|
||||
|
||||
|
||||
@app.post('/integrations/jira', tags=["integrations"])
|
||||
@app.put('/integrations/jira', tags=["integrations"])
|
||||
def add_edit_jira_cloud(data: schemas.JiraGithubSchema = Body(...),
|
||||
def add_edit_jira_cloud(data: schemas.JiraSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER,
|
||||
tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None and integration is None:
|
||||
return error
|
||||
data.provider = integration_jira_cloud.PROVIDER
|
||||
return {"data": integration.add_edit(data=data.dict())}
|
||||
|
||||
|
||||
@app.post('/integrations/github', tags=["integrations"])
|
||||
@app.put('/integrations/github', tags=["integrations"])
|
||||
def add_edit_github(data: schemas.JiraGithubSchema = Body(...),
|
||||
def add_edit_github(data: schemas.GithubSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER,
|
||||
tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
return error
|
||||
data.provider = integration_github.PROVIDER
|
||||
return {"data": integration.add_edit(data=data.dict())}
|
||||
|
||||
|
||||
|
|
@ -895,6 +914,17 @@ def get_live_session(projectId: int, sessionId: str, background_tasks: Backgroun
|
|||
return {'data': data}
|
||||
|
||||
|
||||
@app.get('/{projectId}/unprocessed/{sessionId}', tags=["assist"])
|
||||
@app.get('/{projectId}/assist/sessions/{sessionId}/replay', tags=["assist"])
|
||||
def get_live_session_replay_file(projectId: int, sessionId: str,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId)
|
||||
if path is None:
|
||||
return {"errors": ["Replay file not found"]}
|
||||
|
||||
return FileResponse(path=path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@app.post('/{projectId}/heatmaps/url', tags=["heatmaps"])
|
||||
def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
|
|
@ -1118,14 +1148,6 @@ def generate_new_user_token(context: schemas.CurrentContext = Depends(OR_context
|
|||
return {"data": users.generate_new_api_key(user_id=context.user_id)}
|
||||
|
||||
|
||||
@app.post('/account', tags=["account"])
|
||||
@app.put('/account', tags=["account"])
|
||||
def edit_account(data: schemas.EditUserSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data,
|
||||
editor_id=context.user_id)
|
||||
|
||||
|
||||
@app.post('/account/password', tags=["account"])
|
||||
@app.put('/account/password', tags=["account"])
|
||||
def change_client_password(data: schemas.EditUserPasswordSchema = Body(...),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)):
|
|||
}
|
||||
|
||||
|
||||
@app.post('/account', tags=["account"])
|
||||
@app.put('/account', tags=["account"])
|
||||
def edit_account(data: schemas.EditUserSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data,
|
||||
editor_id=context.user_id)
|
||||
|
||||
|
||||
@app.get('/projects/limit', tags=['projects'])
|
||||
def get_projects_limit(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": {
|
||||
|
|
@ -87,18 +95,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B
|
|||
changes={"name": data.name, "endpoint": data.url})}
|
||||
|
||||
|
||||
# this endpoint supports both jira & github based on `provider` attribute
|
||||
@app.post('/integrations/issues', tags=["integrations"])
|
||||
def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
provider = data.provider.upper()
|
||||
error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.add_edit(data=data.dict())}
|
||||
|
||||
|
||||
@app.post('/client/members', tags=["client"])
|
||||
@app.put('/client/members', tags=["client"])
|
||||
def add_member(background_tasks: BackgroundTasks, data: schemas.CreateMemberSchema = Body(...),
|
||||
|
|
|
|||
|
|
@ -100,10 +100,12 @@ class NotificationsViewSchema(BaseModel):
|
|||
endTimestamp: Optional[int] = Field(default=None)
|
||||
|
||||
|
||||
class JiraGithubSchema(BaseModel):
|
||||
provider: str = Field(...)
|
||||
username: str = Field(...)
|
||||
class GithubSchema(BaseModel):
|
||||
token: str = Field(...)
|
||||
|
||||
|
||||
class JiraSchema(GithubSchema):
|
||||
username: str = Field(...)
|
||||
url: HttpUrl = Field(...)
|
||||
|
||||
@validator('url')
|
||||
|
|
@ -560,6 +562,8 @@ class _SessionSearchEventRaw(__MixedSearchFilter):
|
|||
assert len(values["source"]) > 0 and isinstance(values["source"][0], int), \
|
||||
f"source of type int if required for {PerformanceEventType.time_between_events}"
|
||||
else:
|
||||
assert "source" in values, f"source is required for {values.get('type')}"
|
||||
assert isinstance(values["source"], list), f"source of type list is required for {values.get('type')}"
|
||||
for c in values["source"]:
|
||||
assert isinstance(c, int), f"source value should be of type int for {values.get('type')}"
|
||||
elif values.get("type") == EventType.error and values.get("source") is None:
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ ENV TZ=UTC \
|
|||
AWS_REGION_WEB=eu-central-1 \
|
||||
AWS_REGION_IOS=eu-west-1 \
|
||||
AWS_REGION_ASSETS=eu-central-1 \
|
||||
AWS_SKIP_SSL_VALIDATION=false \
|
||||
CACHE_ASSETS=true \
|
||||
ASSETS_SIZE_LIMIT=6291456 \
|
||||
ASSETS_HEADERS="{ \"Cookie\": \"ABv=3;\" }" \
|
||||
|
|
@ -56,7 +57,7 @@ ENV TZ=UTC \
|
|||
PARTITIONS_NUMBER=16 \
|
||||
QUEUE_MESSAGE_SIZE_LIMIT=1048576 \
|
||||
BEACON_SIZE_LIMIT=1000000 \
|
||||
USE_FAILOVER=false \
|
||||
USE_FAILOVER=true \
|
||||
GROUP_STORAGE_FAILOVER=failover \
|
||||
TOPIC_STORAGE_FAILOVER=storage-failover
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ func main() {
|
|||
continue
|
||||
}
|
||||
msg := iter.Message().Decode()
|
||||
log.Printf("process message, type: %d", iter.Type())
|
||||
|
||||
// Just save session data into db without additional checks
|
||||
if err := saver.InsertMessage(sessionID, msg); err != nil {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func main() {
|
|||
func(sessionID uint64, iter messages.Iterator, meta *types.Meta) {
|
||||
for iter.Next() {
|
||||
statsLogger.Collect(sessionID, meta)
|
||||
builderMap.HandleMessage(sessionID, iter.Message(), iter.Message().Meta().Index)
|
||||
builderMap.HandleMessage(sessionID, iter.Message().Decode(), iter.Message().Meta().Index)
|
||||
}
|
||||
},
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func main() {
|
|||
|
||||
// Filter message
|
||||
if !IsReplayerType(msg.TypeID()) {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
// If message timestamp is empty, use at least ts of session start
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ func main() {
|
|||
if iter.Type() == messages.MsgSessionEnd {
|
||||
msg := iter.Message().Decode().(*messages.SessionEnd)
|
||||
if err := srv.UploadKey(strconv.FormatUint(sessionID, 10), 5); err != nil {
|
||||
log.Printf("can't find session: %d", sessionID)
|
||||
sessionFinder.Find(sessionID, msg.Timestamp)
|
||||
}
|
||||
// Log timestamp of last processed session
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"math/rand"
|
||||
"net/http"
|
||||
"openreplay/backend/internal/http/uuid"
|
||||
"openreplay/backend/pkg/flakeid"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
|
@ -134,7 +135,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
UserUUID: userUUID,
|
||||
SessionID: strconv.FormatUint(tokenData.ID, 10),
|
||||
BeaconSizeLimit: e.cfg.BeaconSizeLimit,
|
||||
Timestamp: e.services.Flaker.ExtractTimestamp(tokenData.ID),
|
||||
StartTimestamp: int64(flakeid.ExtractTimestamp(tokenData.ID)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type StartSessionRequest struct {
|
|||
|
||||
type StartSessionResponse struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
StartTimestamp int64 `json:"startTimestamp"`
|
||||
Delay int64 `json:"delay"`
|
||||
Token string `json:"token"`
|
||||
UserUUID string `json:"userUUID"`
|
||||
|
|
|
|||
11
backend/pkg/env/aws.go
vendored
|
|
@ -1,7 +1,9 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
|
|
@ -20,6 +22,15 @@ func AWSSessionOnRegion(region string) *_session.Session {
|
|||
config.Endpoint = aws.String(AWS_ENDPOINT)
|
||||
config.DisableSSL = aws.Bool(true)
|
||||
config.S3ForcePathStyle = aws.Bool(true)
|
||||
|
||||
AWS_SKIP_SSL_VALIDATION := Bool("AWS_SKIP_SSL_VALIDATION")
|
||||
if AWS_SKIP_SSL_VALIDATION {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
config.HTTPClient = client
|
||||
}
|
||||
}
|
||||
aws_session, err := _session.NewSession(config)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ func NewIterator(data []byte) Iterator {
|
|||
|
||||
func (i *iteratorImpl) Next() bool {
|
||||
if i.canSkip {
|
||||
log.Printf("skip message, type: %d, size: %d", i.msgType, i.msgSize)
|
||||
if _, err := i.data.Seek(int64(i.msgSize), io.SeekCurrent); err != nil {
|
||||
log.Printf("seek err: %s", err)
|
||||
return false
|
||||
|
|
@ -49,7 +48,6 @@ func (i *iteratorImpl) Next() bool {
|
|||
log.Printf("can't read message type: %s", err)
|
||||
return false
|
||||
}
|
||||
log.Printf("message type: %d", i.msgType)
|
||||
|
||||
if i.version > 0 && messageHasSize(i.msgType) {
|
||||
// Read message size if it is a new protocol version
|
||||
|
|
@ -58,7 +56,6 @@ func (i *iteratorImpl) Next() bool {
|
|||
log.Printf("can't read message size: %s", err)
|
||||
return false
|
||||
}
|
||||
log.Println("message size:", i.msgSize)
|
||||
i.msg = &RawMessage{
|
||||
tp: i.msgType,
|
||||
size: i.msgSize,
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ func (m *RawMessage) Encode() []byte {
|
|||
if m.encoded {
|
||||
return m.data
|
||||
}
|
||||
m.data = make([]byte, m.size)
|
||||
m.data = make([]byte, m.size+1)
|
||||
m.data[0] = uint8(m.tp)
|
||||
m.encoded = true
|
||||
*m.skipped = false
|
||||
n, err := io.ReadFull(m.reader, m.data)
|
||||
n, err := io.ReadFull(m.reader, m.data[1:])
|
||||
if err != nil {
|
||||
log.Printf("message encode err: %s", err)
|
||||
return nil
|
||||
|
|
@ -51,10 +52,11 @@ func (m *RawMessage) Decode() Message {
|
|||
if !m.encoded {
|
||||
m.Encode()
|
||||
}
|
||||
msg, err := ReadMessage(m.tp, bytes.NewReader(m.data))
|
||||
msg, err := ReadMessage(m.tp, bytes.NewReader(m.data[1:]))
|
||||
if err != nil {
|
||||
log.Printf("decode err: %s", err)
|
||||
}
|
||||
msg.Meta().SetMeta(m.meta)
|
||||
return msg
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ RUN cd /work_tmp && npm install
|
|||
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/.
|
||||
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. && chmod 644 /mappings.wasm
|
||||
|
||||
RUN adduser -u 1001 openreplay -D
|
||||
USER 1001
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
|||
RUN apk add --no-cache build-base tini
|
||||
ARG envarg
|
||||
ENV APP_NAME=alerts \
|
||||
pg_minconn=2 \
|
||||
pg_minconn=1 \
|
||||
pg_maxconn=10 \
|
||||
ENTERPRISE_BUILD=${envarg}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ ENV APP_NAME=crons \
|
|||
pg_minconn=2 \
|
||||
pg_maxconn=10 \
|
||||
ENTERPRISE_BUILD=${envarg} \
|
||||
ACTION=""
|
||||
ACTION="" \
|
||||
PG_POOL=false
|
||||
|
||||
WORKDIR /work_tmp
|
||||
COPY requirements-crons.txt /work_tmp/requirements.txt
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||
from decouple import config
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from starlette import status
|
||||
from starlette.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from routers import core, core_dynamic, ee, saml
|
||||
from routers.subs import v1_api
|
||||
from routers.crons import core_crons
|
||||
from routers.crons import core_dynamic_crons
|
||||
from routers.subs import dashboard, insights, metrics, v1_api_ee
|
||||
from routers.subs import v1_api
|
||||
|
||||
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
|
|
|
|||
|
|
@ -52,30 +52,28 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
|
|||
AND users.tenant_id = %(tenant_id)s
|
||||
AND (roles.all_projects OR roles_projects.project_id = s.project_id)
|
||||
) AS role_project ON (TRUE)"""
|
||||
pre_select = ""
|
||||
recorded_q = ""
|
||||
if recorded:
|
||||
pre_select = """WITH recorded_p AS (SELECT DISTINCT projects.project_id
|
||||
FROM projects INNER JOIN sessions USING (project_id)
|
||||
WHERE tenant_id =%(tenant_id)s
|
||||
AND deleted_at IS NULL
|
||||
AND duration > 0)"""
|
||||
cur.execute(
|
||||
cur.mogrify(f"""\
|
||||
{pre_select}
|
||||
recorded_q = """, COALESCE((SELECT TRUE
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id = s.project_id
|
||||
AND sessions.start_ts >= (EXTRACT(EPOCH FROM s.created_at) * 1000 - 24 * 60 * 60 * 1000)
|
||||
AND sessions.start_ts <= %(now)s
|
||||
LIMIT 1), FALSE) AS recorded"""
|
||||
query = cur.mogrify(f"""\
|
||||
SELECT
|
||||
s.project_id, s.name, s.project_key, s.save_request_payloads
|
||||
{',s.gdpr' if gdpr else ''}
|
||||
{',EXISTS(SELECT 1 FROM recorded_p WHERE recorded_p.project_id = s.project_id) AS recorded' if recorded else ''}
|
||||
{recorded_q}
|
||||
{',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''}
|
||||
FROM public.projects AS s
|
||||
{'LEFT JOIN recorded_p USING (project_id)' if recorded else ''}
|
||||
{'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''}
|
||||
{role_query if user_id is not None else ""}
|
||||
WHERE s.tenant_id =%(tenant_id)s
|
||||
AND s.deleted_at IS NULL
|
||||
ORDER BY s.project_id;""",
|
||||
{"tenant_id": tenant_id, "user_id": user_id})
|
||||
)
|
||||
{"tenant_id": tenant_id, "user_id": user_id, "now": TimeUTC.now()})
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
if recording_state:
|
||||
project_ids = [f'({r["project_id"]})' for r in rows]
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ pg_minconn=20
|
|||
pg_maxconn=50
|
||||
PG_RETRY_MAX=50
|
||||
PG_RETRY_INTERVAL=2
|
||||
PG_POOL=true
|
||||
put_S3_TTL=20
|
||||
sentryURL=
|
||||
sessions_bucket=mobs
|
||||
|
|
@ -57,3 +58,4 @@ sourcemaps_bucket=sourcemaps
|
|||
sourcemaps_reader=http://127.0.0.1:9000/sourcemaps
|
||||
stage=default-ee
|
||||
version_number=1.0.0
|
||||
FS_DIR=/mnt/efs
|
||||
|
|
@ -4,7 +4,7 @@ boto3==1.24.26
|
|||
pyjwt==2.4.0
|
||||
psycopg2-binary==2.9.3
|
||||
elasticsearch==8.3.1
|
||||
jira==3.3.0
|
||||
jira==3.3.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ boto3==1.24.26
|
|||
pyjwt==2.4.0
|
||||
psycopg2-binary==2.9.3
|
||||
elasticsearch==8.3.1
|
||||
jira==3.3.0
|
||||
jira==3.3.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ boto3==1.24.26
|
|||
pyjwt==2.4.0
|
||||
psycopg2-binary==2.9.3
|
||||
elasticsearch==8.3.1
|
||||
jira==3.3.0
|
||||
jira==3.3.1
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)):
|
|||
}
|
||||
|
||||
|
||||
@app.post('/account', tags=["account"])
|
||||
@app.put('/account', tags=["account"])
|
||||
def edit_account(data: schemas_ee.EditUserSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data,
|
||||
editor_id=context.user_id)
|
||||
|
||||
|
||||
@app.get('/projects/limit', tags=['projects'])
|
||||
def get_projects_limit(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": {
|
||||
|
|
@ -90,18 +98,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B
|
|||
changes={"name": data.name, "endpoint": data.url})}
|
||||
|
||||
|
||||
# this endpoint supports both jira & github based on `provider` attribute
|
||||
@app.post('/integrations/issues', tags=["integrations"])
|
||||
def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
provider = data.provider.upper()
|
||||
error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.add_edit(data=data.dict())}
|
||||
|
||||
|
||||
@app.post('/client/members', tags=["client"])
|
||||
@app.put('/client/members', tags=["client"])
|
||||
def add_member(background_tasks: BackgroundTasks, data: schemas_ee.CreateMemberSchema = Body(...),
|
||||
|
|
|
|||
6
ee/utilities/package-lock.json
generated
|
|
@ -10,9 +10,9 @@
|
|||
"license": "Elastic License 2.0 (ELv2)",
|
||||
"dependencies": {
|
||||
"@maxmind/geoip2-node": "^3.4.0",
|
||||
"@socket.io/redis-adapter": "^7.1.0",
|
||||
"express": "^4.17.1",
|
||||
"redis": "^4.0.3",
|
||||
"@socket.io/redis-adapter": "^7.2.0",
|
||||
"express": "^4.18.1",
|
||||
"redis": "^4.2.0",
|
||||
"socket.io": "^4.5.1",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.10.0"
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
"homepage": "https://github.com/openreplay/openreplay#readme",
|
||||
"dependencies": {
|
||||
"@maxmind/geoip2-node": "^3.4.0",
|
||||
"@socket.io/redis-adapter": "^7.1.0",
|
||||
"express": "^4.17.1",
|
||||
"redis": "^4.0.3",
|
||||
"@socket.io/redis-adapter": "^7.2.0",
|
||||
"express": "^4.18.1",
|
||||
"redis": "^4.2.0",
|
||||
"socket.io": "^4.5.1",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.10.0"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ const {
|
|||
uniqueAutocomplete
|
||||
} = require('../utils/helper');
|
||||
const {
|
||||
extractSessionInfo
|
||||
IDENTITIES,
|
||||
EVENTS_DEFINITION,
|
||||
extractSessionInfo,
|
||||
socketConnexionTimeout
|
||||
} = require('../utils/assistHelper');
|
||||
const {
|
||||
extractProjectKeyFromRequest,
|
||||
|
|
@ -19,15 +22,6 @@ const {
|
|||
const {createAdapter} = require("@socket.io/redis-adapter");
|
||||
const {createClient} = require("redis");
|
||||
const wsRouter = express.Router();
|
||||
const UPDATE_EVENT = "UPDATE_SESSION";
|
||||
const IDENTITIES = {agent: 'agent', session: 'session'};
|
||||
const NEW_AGENT = "NEW_AGENT";
|
||||
const NO_AGENTS = "NO_AGENT";
|
||||
const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
|
||||
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
|
||||
const NO_SESSIONS = "SESSION_DISCONNECTED";
|
||||
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
|
||||
const SESSION_RECONNECTED = "SESSION_RECONNECTED";
|
||||
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const pubClient = createClient({url: REDIS_URL});
|
||||
const subClient = pubClient.duplicate();
|
||||
|
|
@ -289,26 +283,27 @@ module.exports = {
|
|||
createSocketIOServer(server, prefix);
|
||||
io.on('connection', async (socket) => {
|
||||
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
|
||||
socket._connectedAt = new Date();
|
||||
socket.peerId = socket.handshake.query.peerId;
|
||||
socket.identity = socket.handshake.query.identity;
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
if (c_sessions > 0) {
|
||||
debug && console.log(`session already connected, refusing new connexion`);
|
||||
io.to(socket.id).emit(SESSION_ALREADY_CONNECTED);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED);
|
||||
return socket.disconnect();
|
||||
}
|
||||
extractSessionInfo(socket);
|
||||
if (c_agents > 0) {
|
||||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(SESSION_RECONNECTED, socket.id);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
debug && console.log(`notifying new agent about no SESSIONS`);
|
||||
io.to(socket.id).emit(NO_SESSIONS);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
await io.of('/').adapter.remoteJoin(socket.id, socket.peerId);
|
||||
let rooms = await io.of('/').adapter.allRooms();
|
||||
|
|
@ -320,13 +315,13 @@ module.exports = {
|
|||
if (socket.handshake.query.agentInfo !== undefined) {
|
||||
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
|
||||
}
|
||||
socket.to(socket.peerId).emit(NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
}
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
}
|
||||
debug && console.log("checking for number of connected agents and sessions");
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
|
|
@ -335,25 +330,29 @@ module.exports = {
|
|||
}
|
||||
if (c_sessions === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
|
||||
socket.to(socket.peerId).emit(NO_SESSIONS);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
if (c_agents === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
|
||||
socket.to(socket.peerId).emit(NO_AGENTS);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(UPDATE_EVENT, async (...args) => {
|
||||
socket.on(EVENTS_DEFINITION.listen.UPDATE_EVENT, async (...args) => {
|
||||
debug && console.log(`${socket.id} sent update event.`);
|
||||
if (socket.identity !== IDENTITIES.session) {
|
||||
debug && console.log('Ignoring update event.');
|
||||
return
|
||||
}
|
||||
socket.handshake.query.sessionInfo = {...socket.handshake.query.sessionInfo, ...args[0]};
|
||||
socket.to(socket.peerId).emit(UPDATE_EVENT, args[0]);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
});
|
||||
|
||||
socket.onAny(async (eventName, ...args) => {
|
||||
if (Object.values(EVENTS_DEFINITION.listen).indexOf(eventName) >= 0) {
|
||||
debug && console.log(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
return
|
||||
}
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}`);
|
||||
socket.to(socket.peerId).emit(eventName, args[0]);
|
||||
|
|
@ -362,7 +361,7 @@ module.exports = {
|
|||
let socketId = await findSessionSocketId(io, socket.peerId);
|
||||
if (socketId === null) {
|
||||
debug && console.log(`session not found for:${socket.peerId}`);
|
||||
io.to(socket.id).emit(NO_SESSIONS);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
} else {
|
||||
debug && console.log("message sent");
|
||||
io.to(socketId).emit(eventName, socket.id, args[0]);
|
||||
|
|
@ -371,7 +370,7 @@ module.exports = {
|
|||
});
|
||||
|
||||
});
|
||||
console.log("WS server started")
|
||||
console.log("WS server started");
|
||||
setInterval(async (io) => {
|
||||
try {
|
||||
let rooms = await io.of('/').adapter.allRooms();
|
||||
|
|
@ -389,13 +388,16 @@ module.exports = {
|
|||
if (debug) {
|
||||
for (let item of validRooms) {
|
||||
let connectedSockets = await io.in(item).fetchSockets();
|
||||
console.log(`Room: ${item} connected: ${connectedSockets.length}`)
|
||||
console.log(`Room: ${item} connected: ${connectedSockets.length}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 20000, io);
|
||||
}, 30000, io);
|
||||
|
||||
socketConnexionTimeout(io);
|
||||
|
||||
Promise.all([pubClient.connect(), subClient.connect()])
|
||||
.then(() => {
|
||||
io.adapter(createAdapter(pubClient, subClient));
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ const {
|
|||
uniqueAutocomplete
|
||||
} = require('../utils/helper');
|
||||
const {
|
||||
extractSessionInfo
|
||||
IDENTITIES,
|
||||
EVENTS_DEFINITION,
|
||||
extractSessionInfo,
|
||||
socketConnexionTimeout
|
||||
} = require('../utils/assistHelper');
|
||||
const {
|
||||
extractProjectKeyFromRequest,
|
||||
|
|
@ -17,15 +20,6 @@ const {
|
|||
extractPayloadFromRequest,
|
||||
} = require('../utils/helper-ee');
|
||||
const wsRouter = express.Router();
|
||||
const UPDATE_EVENT = "UPDATE_SESSION";
|
||||
const IDENTITIES = {agent: 'agent', session: 'session'};
|
||||
const NEW_AGENT = "NEW_AGENT";
|
||||
const NO_AGENTS = "NO_AGENT";
|
||||
const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
|
||||
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
|
||||
const NO_SESSIONS = "SESSION_DISCONNECTED";
|
||||
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
|
||||
const SESSION_RECONNECTED = "SESSION_RECONNECTED";
|
||||
|
||||
let io;
|
||||
const debug = process.env.debug === "1" || false;
|
||||
|
|
@ -267,26 +261,27 @@ module.exports = {
|
|||
createSocketIOServer(server, prefix);
|
||||
io.on('connection', async (socket) => {
|
||||
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
|
||||
socket._connectedAt = new Date();
|
||||
socket.peerId = socket.handshake.query.peerId;
|
||||
socket.identity = socket.handshake.query.identity;
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
if (c_sessions > 0) {
|
||||
debug && console.log(`session already connected, refusing new connexion`);
|
||||
io.to(socket.id).emit(SESSION_ALREADY_CONNECTED);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED);
|
||||
return socket.disconnect();
|
||||
}
|
||||
extractSessionInfo(socket);
|
||||
if (c_agents > 0) {
|
||||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(SESSION_RECONNECTED, socket.id);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
debug && console.log(`notifying new agent about no SESSIONS`);
|
||||
io.to(socket.id).emit(NO_SESSIONS);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
socket.join(socket.peerId);
|
||||
if (io.sockets.adapter.rooms.get(socket.peerId)) {
|
||||
|
|
@ -296,13 +291,13 @@ module.exports = {
|
|||
if (socket.handshake.query.agentInfo !== undefined) {
|
||||
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
|
||||
}
|
||||
socket.to(socket.peerId).emit(NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
}
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
}
|
||||
debug && console.log("checking for number of connected agents and sessions");
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
|
|
@ -311,25 +306,29 @@ module.exports = {
|
|||
}
|
||||
if (c_sessions === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
|
||||
socket.to(socket.peerId).emit(NO_SESSIONS);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
if (c_agents === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
|
||||
socket.to(socket.peerId).emit(NO_AGENTS);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(UPDATE_EVENT, async (...args) => {
|
||||
socket.on(EVENTS_DEFINITION.listen.UPDATE_EVENT, async (...args) => {
|
||||
debug && console.log(`${socket.id} sent update event.`);
|
||||
if (socket.identity !== IDENTITIES.session) {
|
||||
debug && console.log('Ignoring update event.');
|
||||
return
|
||||
}
|
||||
socket.handshake.query.sessionInfo = {...socket.handshake.query.sessionInfo, ...args[0]};
|
||||
socket.to(socket.peerId).emit(UPDATE_EVENT, args[0]);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
});
|
||||
|
||||
socket.onAny(async (eventName, ...args) => {
|
||||
if (Object.values(EVENTS_DEFINITION.listen).indexOf(eventName) >= 0) {
|
||||
debug && console.log(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
return
|
||||
}
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}`);
|
||||
socket.to(socket.peerId).emit(eventName, args[0]);
|
||||
|
|
@ -338,7 +337,7 @@ module.exports = {
|
|||
let socketId = await findSessionSocketId(io, socket.peerId);
|
||||
if (socketId === null) {
|
||||
debug && console.log(`session not found for:${socket.peerId}`);
|
||||
io.to(socket.id).emit(NO_SESSIONS);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
} else {
|
||||
debug && console.log("message sent");
|
||||
io.to(socketId).emit(eventName, socket.id, args[0]);
|
||||
|
|
@ -347,13 +346,13 @@ module.exports = {
|
|||
});
|
||||
|
||||
});
|
||||
console.log("WS server started")
|
||||
console.log("WS server started");
|
||||
setInterval(async (io) => {
|
||||
try {
|
||||
let count = 0;
|
||||
console.log(` ====== Rooms: ${io.sockets.adapter.rooms.size} ====== `);
|
||||
const arr = Array.from(io.sockets.adapter.rooms)
|
||||
const filtered = arr.filter(room => !room[1].has(room[0]))
|
||||
const arr = Array.from(io.sockets.adapter.rooms);
|
||||
const filtered = arr.filter(room => !room[1].has(room[0]));
|
||||
for (let i of filtered) {
|
||||
let {projectKey, sessionId} = extractPeerId(i[0]);
|
||||
if (projectKey !== null && sessionId !== null) {
|
||||
|
|
@ -363,13 +362,15 @@ module.exports = {
|
|||
console.log(` ====== Valid Rooms: ${count} ====== `);
|
||||
if (debug) {
|
||||
for (let item of filtered) {
|
||||
console.log(`Room: ${item[0]} connected: ${item[1].size}`)
|
||||
console.log(`Room: ${item[0]} connected: ${item[1].size}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 20000, io);
|
||||
}, 30000, io);
|
||||
|
||||
socketConnexionTimeout(io);
|
||||
},
|
||||
handlers: {
|
||||
socketsList,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 150,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { fetchUserInfo } from 'Duck/user';
|
|||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
|
||||
import Header from 'Components/Header/Header';
|
||||
import { fetchList as fetchMetadata } from 'Duck/customField';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
|
|
@ -16,12 +15,13 @@ import { withStore } from 'App/mstore';
|
|||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
import { OB_DEFAULT_TAB } from 'App/routes';
|
||||
import { OB_DEFAULT_TAB, isRoute } from 'App/routes';
|
||||
import Signup from './components/Signup/Signup';
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { ModalProvider } from './components/Modal';
|
||||
import { GLOBAL_DESTINATION_PATH } from 'App/constants/storageKeys';
|
||||
import SupportCallout from 'Shared/SupportCallout';
|
||||
|
||||
const Login = lazy(() => import('Components/Login/Login'));
|
||||
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
|
||||
|
|
@ -55,6 +55,10 @@ const METRICS_PATH = routes.metrics();
|
|||
const METRICS_DETAILS = routes.metricDetails();
|
||||
const METRICS_DETAILS_SUB = routes.metricDetailsSub();
|
||||
|
||||
const ALERTS_PATH = routes.alerts();
|
||||
const ALERT_CREATE_PATH = routes.alertCreate();
|
||||
const ALERT_EDIT_PATH = routes.alertEdit();
|
||||
|
||||
const DASHBOARD_PATH = routes.dashboard();
|
||||
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
|
||||
const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
|
||||
|
|
@ -99,13 +103,13 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
|||
tenants: state.getIn(['user', 'tenants']),
|
||||
existingTenant: state.getIn(['user', 'authDetails', 'tenants']),
|
||||
onboarding: state.getIn(['user', 'onboarding']),
|
||||
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || state.getIn(['user', 'authDetails', 'edition']) === 'ee',
|
||||
};
|
||||
},
|
||||
{
|
||||
fetchUserInfo,
|
||||
fetchTenants,
|
||||
setSessionPath,
|
||||
fetchMetadata,
|
||||
fetchSiteList,
|
||||
fetchAnnouncements,
|
||||
fetchAlerts,
|
||||
|
|
@ -121,15 +125,11 @@ class Router extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
fetchInitialData = () => {
|
||||
Promise.all([
|
||||
this.props.fetchUserInfo().then(() => {
|
||||
this.props.fetchSiteList().then(() => {
|
||||
const { mstore } = this.props;
|
||||
mstore.initClient();
|
||||
});
|
||||
}),
|
||||
]);
|
||||
fetchInitialData = async () => {
|
||||
await this.props.fetchUserInfo(),
|
||||
await this.props.fetchSiteList()
|
||||
const { mstore } = this.props;
|
||||
mstore.initClient();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -167,9 +167,10 @@ class Router extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, existingTenant, onboarding } = this.props;
|
||||
const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, existingTenant, onboarding, isEnterprise } = this.props;
|
||||
const siteIdList = sites.map(({ id }) => id).toJS();
|
||||
const hideHeader = (location.pathname && location.pathname.includes('/session/')) || location.pathname.includes('/assist/');
|
||||
const isPlayer = isRoute(SESSION_PATH, location.pathname) || isRoute(LIVE_SESSION_PATH, location.pathname);
|
||||
|
||||
return isLoggedIn ? (
|
||||
<ModalProvider>
|
||||
|
|
@ -198,6 +199,9 @@ class Router extends React.Component {
|
|||
{onboarding && <Redirect to={withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />}
|
||||
|
||||
{/* DASHBOARD and Metrics */}
|
||||
<Route exact strict path={withSiteId(ALERTS_PATH, siteIdList)} component={Dashboard} />
|
||||
<Route exact strict path={withSiteId(ALERT_EDIT_PATH, siteIdList)} component={Dashboard} />
|
||||
<Route exact strict path={withSiteId(ALERT_CREATE_PATH, siteIdList)} component={Dashboard} />
|
||||
<Route exact strict path={withSiteId(METRICS_PATH, siteIdList)} component={Dashboard} />
|
||||
<Route exact strict path={withSiteId(METRICS_DETAILS, siteIdList)} component={Dashboard} />
|
||||
<Route exact strict path={withSiteId(METRICS_DETAILS_SUB, siteIdList)} component={Dashboard} />
|
||||
|
|
@ -223,6 +227,7 @@ class Router extends React.Component {
|
|||
</Switch>
|
||||
</Suspense>
|
||||
</Loader>
|
||||
{!isEnterprise && !isPlayer && <SupportCallout /> }
|
||||
</ModalProvider>
|
||||
) : (
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
|
|
@ -232,6 +237,7 @@ class Router extends React.Component {
|
|||
{!existingTenant && <Route exact strict path={SIGNUP_PATH} component={Signup} />}
|
||||
<Redirect to={LOGIN_PATH} />
|
||||
</Switch>
|
||||
{!isEnterprise && <SupportCallout /> }
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const siteIdRequiredPaths = [
|
|||
'/custom_metrics',
|
||||
'/dashboards',
|
||||
'/metrics',
|
||||
'/unprocessed',
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenReplay</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="slack-app-id" content="AA5LEB34M">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal-root"></div>
|
||||
<div id="app"><p style="color: #eee;text-align: center;height: 100%;padding: 25%;">Loading...</p></div>
|
||||
</body>
|
||||
<head>
|
||||
<title>OpenReplay</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="slack-app-id" content="AA5LEB34M" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
|
||||
<!-- <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" /> -->
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal-root"></div>
|
||||
<div id="app"><p style="color: #eee; text-align: center; height: 100%; padding: 25%">Loading...</p></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
5
frontend/app/assets/integrations/aws.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="111" height="66" viewBox="0 0 111 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.3319 24.0791C31.3319 25.4209 31.477 26.5088 31.7308 27.3066C32.0209 28.1044 32.3835 28.9747 32.8912 29.9176C33.0726 30.2077 33.1451 30.4978 33.1451 30.7517C33.1451 31.1143 32.9275 31.4769 32.4561 31.8396L30.1715 33.3627C29.8451 33.5802 29.5187 33.689 29.2286 33.689C28.866 33.689 28.5033 33.5077 28.1407 33.1813C27.633 32.6374 27.1978 32.0572 26.8352 31.4769C26.4726 30.8605 26.1099 30.1715 25.711 29.3374C22.8825 32.6737 19.3286 34.3418 15.0495 34.3418C12.0033 34.3418 9.57366 33.4715 7.79674 31.7308C6.01981 29.9901 5.11322 27.6693 5.11322 24.7682C5.11322 21.6857 6.20113 19.1835 8.41322 17.2978C10.6253 15.4121 13.5627 14.4693 17.2978 14.4693C18.5308 14.4693 19.8 14.578 21.1418 14.7594C22.4835 14.9407 23.8616 15.2308 25.3121 15.5572V12.9099C25.3121 10.1539 24.7319 8.23189 23.6077 7.10772C22.4473 5.98354 20.489 5.43958 17.6967 5.43958C16.4275 5.43958 15.122 5.58464 13.7803 5.91101C12.4385 6.23739 11.133 6.63629 9.86377 7.14398C9.28355 7.39783 8.84838 7.54288 8.59454 7.61541C8.34069 7.68794 8.15937 7.7242 8.01432 7.7242C7.50663 7.7242 7.25278 7.36156 7.25278 6.60002V4.8231C7.25278 4.24288 7.32531 3.80772 7.50663 3.55387C7.68795 3.30002 8.01432 3.04618 8.52201 2.79233C9.79124 2.13959 11.3143 1.59563 13.0912 1.16046C14.8682 0.689036 16.7539 0.471453 18.7484 0.471453C23.0638 0.471453 26.2187 1.45057 28.2495 3.40882C30.244 5.36706 31.2594 8.34068 31.2594 12.3297V24.0791H31.3319ZM16.6088 29.5912C17.8055 29.5912 19.0385 29.3737 20.344 28.9385C21.6495 28.5033 22.8099 27.7055 23.789 26.6176C24.3693 25.9286 24.8044 25.1671 25.022 24.2967C25.2396 23.4264 25.3846 22.3747 25.3846 21.1418V19.6187C24.333 19.3649 23.2088 19.1473 22.0484 19.0022C20.8879 18.8572 19.7638 18.7846 18.6396 18.7846C16.2099 18.7846 14.433 19.2561 13.2363 20.2352C12.0396 21.2143 11.4594 22.5923 11.4594 24.4055C11.4594 26.1099 11.8945 27.3791 12.8011 28.2495C13.6715 29.1561 14.9407 29.5912 16.6088 29.5912ZM45.7286 33.5077C45.0759 33.5077 44.6407 33.3989 44.3506 33.1451C44.0605 32.9275 43.8066 32.4198 43.589 31.7308L35.0671 3.69893C34.8495 2.97365 34.7407 2.50222 34.7407 2.24838C34.7407 1.66816 35.0308 1.34178 35.611 1.34178H39.1649C39.8539 1.34178 40.3253 1.45057 40.5792 1.70442C40.8693 1.922 41.0868 2.4297 41.3044 3.11871L47.3967 27.1253L53.0539 3.11871C53.2352 2.39343 53.4528 1.922 53.7429 1.70442C54.033 1.48684 54.5407 1.34178 55.1934 1.34178H58.0945C58.7835 1.34178 59.255 1.45057 59.5451 1.70442C59.8352 1.922 60.089 2.4297 60.2341 3.11871L65.9638 27.4154L72.2374 3.11871C72.455 2.39343 72.7088 1.922 72.9627 1.70442C73.2528 1.48684 73.7242 1.34178 74.377 1.34178H77.7495C78.3297 1.34178 78.6561 1.63189 78.6561 2.24838C78.6561 2.42969 78.6198 2.61101 78.5835 2.8286C78.5473 3.04618 78.4748 3.33629 78.3297 3.73519L69.5901 31.7671C69.3726 32.4923 69.1187 32.9638 68.8286 33.1813C68.5385 33.3989 68.0671 33.544 67.4506 33.544H64.3319C63.6429 33.544 63.1715 33.4352 62.8813 33.1813C62.5912 32.9275 62.3374 32.4561 62.1923 31.7308L56.5715 8.34068L50.9868 31.6945C50.8055 32.4198 50.5879 32.8912 50.2978 33.1451C50.0077 33.3989 49.5 33.5077 48.8473 33.5077H45.7286ZM92.3275 34.4868C90.4418 34.4868 88.5561 34.2693 86.7429 33.8341C84.9297 33.3989 83.5154 32.9275 82.5726 32.3835C81.9923 32.0572 81.5934 31.6945 81.4484 31.3682C81.3033 31.0418 81.2308 30.6791 81.2308 30.3528V28.5033C81.2308 27.7418 81.5209 27.3791 82.0649 27.3791C82.2824 27.3791 82.5 27.4154 82.7176 27.4879C82.9352 27.5605 83.2616 27.7055 83.6242 27.8506C84.8572 28.3945 86.1989 28.8297 87.6132 29.1198C89.0638 29.4099 90.478 29.555 91.9286 29.555C94.2132 29.555 95.9901 29.1561 97.2231 28.3583C98.4561 27.5605 99.1088 26.4 99.1088 24.9132C99.1088 23.8978 98.7824 23.0638 98.1297 22.3747C97.477 21.6857 96.244 21.0693 94.4671 20.489L89.2088 18.8572C86.5616 18.0231 84.6033 16.7901 83.4066 15.1583C82.2099 13.5627 81.5934 11.7857 81.5934 9.90002C81.5934 8.37695 81.9198 7.03519 82.5726 5.87475C83.2253 4.71431 84.0956 3.69893 85.1835 2.90112C86.2715 2.06706 87.5044 1.45057 88.955 1.01541C90.4055 0.580244 91.9286 0.398926 93.5242 0.398926C94.322 0.398926 95.1561 0.43519 95.9539 0.543981C96.7879 0.652772 97.5495 0.797827 98.311 0.942882C99.0363 1.1242 99.7253 1.30552 100.378 1.5231C101.031 1.74068 101.538 1.95827 101.901 2.17585C102.409 2.46596 102.771 2.75607 102.989 3.08244C103.207 3.37255 103.315 3.77145 103.315 4.27915V5.98354C103.315 6.74508 103.025 7.14398 102.481 7.14398C102.191 7.14398 101.72 6.99893 101.103 6.70882C99.0363 5.76596 96.7154 5.29453 94.1407 5.29453C92.0737 5.29453 90.4418 5.6209 89.3176 6.30991C88.1934 6.99893 87.6132 8.05057 87.6132 9.53739C87.6132 10.5528 87.9759 11.4231 88.7011 12.1121C89.4264 12.8011 90.7682 13.4901 92.6901 14.1066L97.8396 15.7385C100.451 16.5726 102.336 17.733 103.46 19.2198C104.585 20.7066 105.129 22.411 105.129 24.2967C105.129 25.8561 104.802 27.2704 104.186 28.5033C103.533 29.7363 102.663 30.8242 101.538 31.6945C100.414 32.6011 99.0726 33.2539 97.5132 33.7253C95.8813 34.233 94.177 34.4868 92.3275 34.4868Z" fill="#252F3E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M99.1813 52.1111C87.2505 60.9232 69.9165 65.6012 55.0121 65.6012C34.1242 65.6012 15.3033 57.877 1.0879 45.0397C-0.0362783 44.0243 0.979106 42.6463 2.32086 43.4441C17.6967 52.365 36.6626 57.7683 56.2813 57.7683C69.5176 57.7683 84.0593 55.0122 97.4407 49.3551C99.4352 48.4485 101.14 50.6606 99.1813 52.1111Z" fill="#FF9900"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.149 46.454C102.626 44.4957 94.0681 45.5111 90.1879 45.9825C89.0274 46.1276 88.8461 45.1122 89.8978 44.3507C96.7154 39.5639 107.921 40.9419 109.226 42.5375C110.532 44.1693 108.864 55.3748 102.481 60.7419C101.502 61.5759 100.559 61.1408 100.994 60.0529C102.445 56.4628 105.673 48.3759 104.149 46.454Z" fill="#FF9900"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
|
@ -1 +1,4 @@
|
|||
<svg width="2500" height="1719" viewBox="0 0 256 176" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M57.838 170.017c.151 1.663-.051 3.789-.14 5.436h56.864c.053-1.654.091-3.311.091-4.974 0-39.942-15.768-76.266-44.011-104.51C56.704 52.032 40.885 41.31 23.246 33.898L0 86.328c33.989 15.82 54.211 43.783 57.838 83.689zm69.197-1.644c.108 2.371-.062 4.732-.167 7.08h58.177c.077-2.355.13-4.714.13-7.08 0-28.826-5.66-56.82-16.82-83.207-10.767-25.456-26.169-48.306-45.778-67.915a216.421 216.421 0 0 0-15.686-14.218l-37.68 44.315c37.293 33.313 55.304 65.858 57.824 121.025zM235.263 64.39C226.595 41.785 213.935 19.521 198.727 0l-46.95 34.442c27.495 35.099 44.442 79.71 46.058 127.612.152 4.502-.164 8.969-.457 13.399h58.252c.226-4.448.447-8.916.344-13.399-.805-34.945-8.23-65.12-20.71-97.665z" fill="#3676A1"/></svg>
|
||||
<svg width="58" height="80" viewBox="0 0 58 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.9431 55.1235C30.7258 55.1235 32.1709 53.6784 32.1709 51.8957C32.1709 50.113 30.7258 48.6678 28.9431 48.6678C27.1604 48.6678 25.7153 50.113 25.7153 51.8957C25.7153 53.6784 27.1604 55.1235 28.9431 55.1235Z" fill="#303F9F"/>
|
||||
<path d="M28.9431 78.9612C21.7674 78.9532 14.8878 76.0991 9.81374 71.0251C4.7397 65.9511 1.8856 59.0715 1.87762 51.8957V38.4743C1.87762 37.9402 2.08961 37.428 2.46701 37.0502C2.8444 36.6724 3.35635 36.4598 3.89038 36.4592H13.4904L13.4579 5.38672L5.90313 10.036V27.7287C5.90313 28.2626 5.69107 28.7745 5.31361 29.152C4.93615 29.5294 4.42419 29.7415 3.89038 29.7415C3.35656 29.7415 2.84461 29.5294 2.46715 29.152C2.08968 28.7745 1.87762 28.2626 1.87762 27.7287V9.80643C1.87915 9.18865 2.03815 8.58147 2.33962 8.04224C2.64108 7.50301 3.07505 7.04955 3.60052 6.72469L11.9692 1.57455C12.5174 1.23696 13.1458 1.05179 13.7895 1.03816C14.4331 1.02454 15.0688 1.18295 15.6308 1.49703C16.1928 1.81112 16.6608 2.26951 16.9865 2.82488C17.3121 3.38025 17.4837 4.01247 17.4834 4.65629L17.5182 36.4592H28.9431C31.9963 36.4587 34.981 37.3637 37.5198 39.0596C40.0587 40.7555 42.0376 43.1662 43.2063 45.9868C44.375 48.8074 44.681 51.9113 44.0856 54.9058C43.4903 57.9003 42.0203 60.6511 39.8615 62.8102C37.7028 64.9692 34.9523 66.4396 31.9578 67.0355C28.9634 67.6313 25.8595 67.3257 23.0387 66.1574C20.2179 64.9891 17.8069 63.0106 16.1106 60.472C14.4143 57.9334 13.5089 54.9489 13.5089 51.8957L13.495 40.487H5.90313V51.8957C5.90313 56.4526 7.2544 60.9071 9.78607 64.696C12.3177 68.485 15.9161 71.4381 20.1261 73.1819C24.3361 74.9257 28.9687 75.382 33.438 74.493C37.9073 73.604 42.0127 71.4097 45.2349 68.1874C48.4571 64.9652 50.6514 60.8599 51.5404 56.3906C52.4294 51.9213 51.9732 47.2887 50.2293 43.0787C48.4855 38.8687 45.5324 35.2703 41.7435 32.7386C37.9546 30.207 33.5 28.8557 28.9431 28.8557H25.451C24.9171 28.8557 24.4052 28.6436 24.0277 28.2662C23.6503 27.8887 23.4382 27.3768 23.4382 26.843C23.4382 26.3091 23.6503 25.7972 24.0277 25.4197C24.4052 25.0423 24.9171 24.8302 25.451 24.8302H28.9431C36.1214 24.8302 43.0056 27.6817 48.0813 32.7575C53.1571 37.8333 56.0086 44.7175 56.0086 51.8957C56.0086 59.0739 53.1571 65.9581 48.0813 71.0339C43.0056 76.1097 36.1214 78.9612 28.9431 78.9612V78.9612ZM17.5228 40.487V51.8934C17.5224 54.1499 18.1911 56.3559 19.4444 58.2324C20.6978 60.1088 22.4794 61.5715 24.564 62.4353C26.6486 63.2992 28.9426 63.5254 31.1558 63.0855C33.3691 62.6455 35.4021 61.5591 36.9979 59.9637C38.5937 58.3683 39.6805 56.3354 40.1208 54.1223C40.5612 51.9092 40.3354 49.6151 39.472 47.5303C38.6086 45.4455 37.1463 43.6636 35.2701 42.4099C33.3939 41.1562 31.1881 40.487 28.9315 40.487H17.5228Z" fill="#303F9F"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 2.7 KiB |
6
frontend/app/assets/integrations/google-cloud.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="98" height="79" viewBox="0 0 98 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M61.7777 21.5287H64.7407L73.1851 13.0842L73.6 9.49903C68.7624 5.22927 62.9163 2.26322 56.6137 0.881054C50.3112 -0.501108 43.7603 -0.253748 37.58 1.59977C31.3996 3.45328 25.7938 6.85177 21.292 11.4742C16.7903 16.0966 13.5412 21.7903 11.8518 28.0176C12.7926 27.632 13.8347 27.5694 14.8148 27.8398L31.7037 25.0546C31.7037 25.0546 32.5629 23.6324 33.0074 23.7213C36.6261 19.7469 41.6271 17.3061 46.9867 16.8985C52.3462 16.4909 57.6588 18.1473 61.837 21.5287H61.7777Z" fill="#EA4335"/>
|
||||
<path d="M85.2149 28.0176C83.2739 20.8698 79.2887 14.4441 73.7482 9.52869L61.8964 21.3805C64.3664 23.3988 66.3459 25.9516 67.6855 28.8464C69.0251 31.7412 69.6899 34.9025 69.6297 38.0916V40.1954C71.0149 40.1954 72.3865 40.4682 73.6663 40.9983C74.946 41.5284 76.1089 42.3053 77.0884 43.2848C78.0678 44.2643 78.8448 45.4271 79.3749 46.7069C79.905 47.9867 80.1778 49.3583 80.1778 50.7435C80.1778 52.1287 79.905 53.5003 79.3749 54.7801C78.8448 56.0599 78.0678 57.2227 77.0884 58.2022C76.1089 59.1817 74.946 59.9586 73.6663 60.4887C72.3865 61.0188 71.0149 61.2916 69.6297 61.2916H48.5334L46.4297 63.425V76.0768L48.5334 78.1805H69.6297C75.5208 78.2264 81.2701 76.3749 86.0273 72.8999C90.7846 69.4248 94.2969 64.5109 96.0449 58.8849C97.7928 53.259 97.6835 47.2198 95.7331 41.6608C93.7826 36.1018 90.0947 31.3181 85.2149 28.0176V28.0176Z" fill="#4285F4"/>
|
||||
<path d="M27.4074 78.0621H48.5037V61.1733H27.4074C25.9044 61.1729 24.419 60.8496 23.0519 60.2251L20.0889 61.1436L11.5852 69.5881L10.8445 72.551C15.6132 76.1519 21.432 78.0881 27.4074 78.0621V78.0621Z" fill="#34A853"/>
|
||||
<path d="M27.4074 23.2768C21.6913 23.3109 16.1286 25.1295 11.4963 28.4786C6.86394 31.8276 3.39326 36.5399 1.56901 41.9571C-0.255244 47.3744 -0.341967 53.2262 1.32095 58.6951C2.98387 64.1641 6.31338 68.9771 10.8445 72.462L23.0815 60.2249C21.5264 59.5224 20.165 58.453 19.1141 57.1086C18.0632 55.7642 17.3541 54.1849 17.0477 52.5062C16.7413 50.8275 16.8468 49.0996 17.355 47.4706C17.8633 45.8416 18.7592 44.3603 19.9658 43.1537C21.1724 41.947 22.6537 41.0512 24.2827 40.5429C25.9117 40.0347 27.6396 39.9292 29.3183 40.2356C30.997 40.542 32.5763 41.251 33.9207 42.302C35.2651 43.3529 36.3345 44.7143 37.0371 46.2694L49.2741 34.0323C46.7056 30.6746 43.3953 27.9565 39.6019 26.0906C35.8085 24.2248 31.6349 23.2617 27.4074 23.2768V23.2768Z" fill="#FBBC05"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1 +1,12 @@
|
|||
<svg id="CMYK_-_square" data-name="CMYK - square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 681.02 551.55"><defs><style>.cls-1{fill:#0097a0;}.cls-2{fill:#5bc6cc;}.cls-3{fill:#231f20;}</style></defs><title>NewRelic-logo-square</title><g id="outlines"><path class="cls-1" d="M692.8,220.54C660.86,73.7,484.77-12.68,299.47,27.61s-309.63,192-277.7,338.83,208,233.22,393.32,192.93S724.72,367.37,692.8,220.54ZM344.87,476.79c-103.41,0-187.2-83.82-187.2-187.22s83.8-187.19,187.2-187.19,187.2,83.81,187.2,187.19S448.25,476.79,344.87,476.79Z" transform="translate(-16.78 -17.71)"/><path class="cls-2" d="M391.53,57.56c-132.32,0-239.61,107.28-239.61,239.6S259.21,536.78,391.53,536.78,631.15,429.49,631.15,297.16,523.85,57.56,391.53,57.56ZM344.87,473.78c-101.75,0-184.19-82.47-184.19-184.21S243.12,105.4,344.87,105.4,529,187.85,529,289.57,446.58,473.78,344.87,473.78Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M278.93,271.2l-20.19-42.33c-4.82-10-9.77-21.36-11.46-26.7l-.39.39c.65,7.55.78,17.06.91,25l.52,43.63H233.61V181.08h16.93l21.88,44a164.17,164.17,0,0,1,9.25,23.18l.39-.39c-.39-4.56-1.3-17.45-1.3-25.66l-.26-41.15h14.2V271.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M321.51,242.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.73a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.27,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H346C346,220.93,341.31,214.55,334.15,214.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M437,271.46H423.61l-8.07-30.34c-2.08-7.81-4.3-18-4.3-18H411s-1,6.51-4.3,18.62l-7.94,29.69H385.32l-18-65.25,14.2-2,7.16,31.91c1.82,8.2,3.39,17.32,3.39,17.32h.39a178.91,178.91,0,0,1,3.78-17.71l8.47-30.47h14.07L426.22,235c2.74,10.68,4.17,18.75,4.17,18.75h.39s1.56-10,3.26-17.71l6.77-30.74h14.85Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M267.62,387.2l-7.81-13.94c-6.25-11.07-10.42-17.32-15.37-22.27a7.64,7.64,0,0,0-5.86-2.73V387.2H223.86V297.08h27.48c20.19,0,29.3,11.72,29.3,25.79,0,12.89-8.33,24.75-22.4,24.75,3.26,1.69,9.25,10.42,13.93,18l13.28,21.62Zm-20.84-78h-8.21v28.52h7.68c7.81,0,12-1,14.72-3.78,2.47-2.47,4-6.25,4-10.94C265,313.88,260.06,309.19,246.78,309.19Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M305.12,358.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.72a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.28,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H329.6C329.6,336.93,324.92,330.55,317.75,330.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M371.28,388.63c-14.46,0-14.46-13-14.46-18.62V313.88a106.72,106.72,0,0,0-1.3-19.27l14.72-3.26c1,4,1.17,9.51,1.17,18.1v55.87c0,8.86.39,10.29,1.43,11.85a4,4,0,0,0,4.69,1l2.34,8.86A22.44,22.44,0,0,1,371.28,388.63Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M396.15,311.53A9.34,9.34,0,0,1,386.9,302a9.44,9.44,0,1,1,9.25,9.51ZM389,387.2V322.34l14.46-2.6V387.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M444.46,388.89c-18,0-28-12.63-28-33.86,0-24,14.33-35.42,29-35.42,7.16,0,12.37,1.69,18.23,7.16l-7.16,9.51c-3.91-3.52-7.29-5.08-11.07-5.08a11.2,11.2,0,0,0-10.42,6.64c-2,4-2.73,10.16-2.73,18.36,0,9,1.43,14.72,4.43,18A11.58,11.58,0,0,0,445.5,378c4.56,0,9-2.21,13.28-6.51l6.77,8.72C459.57,386.16,453.32,388.89,444.46,388.89Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M477.78,388.64A9.67,9.67,0,1,1,487.4,379,9.63,9.63,0,0,1,477.78,388.64Zm0-17.42a7.78,7.78,0,1,0,7.44,7.75A7.55,7.55,0,0,0,477.78,371.22Zm1.9,13.1c-.42-.73-.6-1-1-1.79-1.07-2-1.4-2.5-1.79-2.65a.72.72,0,0,0-.34-.08v4.52H474.4V373.48h4a3,3,0,0,1,3.2,3.17,2.78,2.78,0,0,1-2.42,3,2.47,2.47,0,0,1,.44.47c.62.78,2.6,4.21,2.6,4.21Zm-1.14-8.94a4.35,4.35,0,0,0-1.22-.16h-.78v2.94h.73c.94,0,1.35-.1,1.64-.36a1.53,1.53,0,0,0,.42-1.09A1.28,1.28,0,0,0,478.53,375.38Z" transform="translate(-16.78 -17.71)"/></g></svg>
|
||||
<svg width="71" height="81" viewBox="0 0 71 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2090)">
|
||||
<path d="M57.0479 28.0403V52.9611L35.3182 65.424V81.0001L70.6399 60.7517V20.2498L57.0479 28.0403Z" fill="#00AC69"/>
|
||||
<path d="M35.3215 15.5812L57.0512 28.039L70.6433 20.2484L35.3215 0L0 20.2484L13.5868 28.039L35.3215 15.5812Z" fill="#1CE783"/>
|
||||
<path d="M21.7348 48.2938V73.2146L35.3215 81.0001V40.5032L-6.10352e-05 20.2498V35.8309L21.7348 48.2938Z" fill="black" fill-opacity="0.87"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2090">
|
||||
<rect width="70.8739" height="81" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 633 B |
|
|
@ -1,20 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="rollbar-mark-color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 304 240" style="enable-background:new 0 0 304 240;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#3A4757;}
|
||||
.st1{fill:#F7941D;}
|
||||
.st2{fill:#BFD730;}
|
||||
.st3{fill:#00BAD9;}
|
||||
</style>
|
||||
<title>rollbar-logo-color-vertical</title>
|
||||
<g id="icon">
|
||||
<path class="st0" d="M303.8,239.1V25.7c-0.5-13.6-0.9-34.3-31.4-21.9C221.7,22.4,170.6,40.2,120.3,60C82.2,75,40.5,91.4,19,171.6
|
||||
c-5.6,21-13.4,46.4-19,67.5h49.4c4.6-17,10.2-38.4,14.8-55.4c15.4-57.4,45.6-69.3,73.2-80.1C176.9,88,217,73.8,257,59.1v179.9
|
||||
H303.8z"/>
|
||||
<path class="st1" d="M119,124.5c-5,2.8-9.8,6.1-14.1,9.9c-14.9,13.3-23,32.2-28,51.1l-14.1,51.9H119V124.5z"/>
|
||||
<path class="st2" d="M180.1,99.7c-12.7,4.7-25.3,9.6-38,14.5c-3.4,1.3-6.7,2.6-10,4v119.2H180L180.1,99.7z"/>
|
||||
<path class="st3" d="M243.8,237.4v-161c-16.8,6.1-33.7,12.2-50.5,18.4v142.6H243.8z"/>
|
||||
<svg width="88" height="71" viewBox="0 0 88 71" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2051)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M84.9393 2.09491C84.9382 1.95919 84.9245 1.82386 84.8988 1.69059C84.8988 1.65016 84.8785 1.61377 84.8664 1.57333C84.8543 1.5329 84.8138 1.39139 84.7815 1.30244L84.721 1.17306C84.6834 1.09588 84.64 1.02156 84.5916 0.950679C84.5632 0.906204 84.5351 0.86173 84.5027 0.817255L84.4459 0.740435C84.4136 0.704046 84.3773 0.675744 84.345 0.643397L84.264 0.546361L84.1993 0.501886C84.1401 0.451215 84.0781 0.403969 84.0134 0.360372L83.8556 0.259293C83.7862 0.222856 83.7146 0.190456 83.6415 0.162255L83.4717 0.0935209C83.3949 0.0692618 83.3139 0.0571321 83.2371 0.0409594L83.0633 0.00457045C82.9676 -0.00152348 82.8718 -0.00152348 82.7761 0.00457045H82.6144C81.531 0.101607 67.214 1.46821 51.122 8.64491C41.4586 12.9428 33.9786 19.5414 29.2724 27.5509L28.0596 28.0766C10.8314 35.7666 0.537354 50.7145 0.537354 68.0637V68.3549C0.537763 68.7543 0.652111 69.1453 0.866967 69.4819C1.08183 69.8185 1.38827 70.0867 1.75031 70.2553C2.02817 70.3826 2.33014 70.4488 2.63578 70.4493H59.8268C59.9381 70.4499 60.049 70.4404 60.1584 70.4209L60.3041 70.3847C60.3727 70.3644 60.4416 70.3523 60.5103 70.3281C60.5789 70.3039 60.6112 70.2795 60.6638 70.2553C60.7164 70.2311 60.7851 70.2069 60.8419 70.1746C60.9486 70.1114 61.0499 70.0397 61.1451 69.9602L84.1914 50.5286C84.427 50.3306 84.6158 50.0829 84.7444 49.8031C84.873 49.5235 84.9382 49.219 84.9353 48.9111V2.09491H84.9393ZM64.1005 61.9788L61.889 63.8427V22.501L80.7466 6.60713V47.9489L64.1005 61.9788ZM26.4503 51.0177H57.696V66.2567H8.36905L26.4503 51.0177ZM52.8241 12.4779C60.1892 9.23502 67.8935 6.82515 75.7937 5.29308L58.9375 19.505C51.1569 20.4543 43.5042 22.2563 36.1176 24.8784C40.2903 19.7839 45.9182 15.5588 52.8241 12.4779ZM32.1108 30.9068C40.3192 27.528 48.9123 25.1719 57.696 23.8918V46.825H27.825C28.0404 41.2644 29.5051 35.8237 32.1108 30.9068ZM26.3573 33.5835C24.5907 38.1675 23.6516 43.0286 23.5836 47.9408L4.98487 63.6365C6.39191 50.8844 13.9648 40.2183 26.3573 33.5835Z" fill="#3569F3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2051">
|
||||
<rect width="86.7778" height="71" fill="white" transform="translate(0.537354)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI';
|
||||
import { alertMetrics as metrics } from 'App/constants';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
|
|
@ -9,333 +9,322 @@ import DropdownChips from './DropdownChips';
|
|||
import { validateEmail } from 'App/validate';
|
||||
import cn from 'classnames';
|
||||
import { fetchTriggerOptions } from 'Duck/alerts';
|
||||
import Select from 'Shared/Select'
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const thresholdOptions = [
|
||||
{ label: '15 minutes', value: 15 },
|
||||
{ label: '30 minutes', value: 30 },
|
||||
{ label: '1 hour', value: 60 },
|
||||
{ label: '2 hours', value: 120 },
|
||||
{ label: '4 hours', value: 240 },
|
||||
{ label: '1 day', value: 1440 },
|
||||
{ label: '15 minutes', value: 15 },
|
||||
{ label: '30 minutes', value: 30 },
|
||||
{ label: '1 hour', value: 60 },
|
||||
{ label: '2 hours', value: 120 },
|
||||
{ label: '4 hours', value: 240 },
|
||||
{ label: '1 day', value: 1440 },
|
||||
];
|
||||
|
||||
const changeOptions = [
|
||||
{ label: 'change', value: 'change' },
|
||||
{ label: '% change', value: 'percent' },
|
||||
{ label: 'change', value: 'change' },
|
||||
{ label: '% change', value: 'percent' },
|
||||
];
|
||||
|
||||
const Circle = ({ text }) => (
|
||||
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">{text}</div>
|
||||
)
|
||||
const Circle = ({ text }) => <div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">{text}</div>;
|
||||
|
||||
const Section = ({ index, title, description, content }) => (
|
||||
<div className="w-full">
|
||||
<div className="flex items-start">
|
||||
<Circle text={index} />
|
||||
<div>
|
||||
<span className="font-medium">{title}</span>
|
||||
{ description && <div className="text-sm color-gray-medium">{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex items-start">
|
||||
<Circle text={index} />
|
||||
<div>
|
||||
<span className="font-medium">{title}</span>
|
||||
{description && <div className="text-sm color-gray-medium">{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10">
|
||||
{content}
|
||||
<div className="ml-10">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
|
||||
|
||||
const AlertForm = props => {
|
||||
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId, style={ width: '580px', height: '100vh' } } = props;
|
||||
const write = ({ target: { value, name } }) => props.edit({ [ name ]: value })
|
||||
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name }}) => props.edit({ [ name ]: checked })
|
||||
// const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked })
|
||||
// const onChangeCheck = (e) => { console.log(e) }
|
||||
const AlertForm = (props) => {
|
||||
const {
|
||||
instance,
|
||||
slackChannels,
|
||||
webhooks,
|
||||
loading,
|
||||
onDelete,
|
||||
deleting,
|
||||
triggerOptions,
|
||||
metricId,
|
||||
style = { width: '580px', height: '100vh' },
|
||||
} = props;
|
||||
const write = ({ target: { value, name } }) => props.edit({ [name]: value });
|
||||
const writeOption = (e, { name, value }) => props.edit({ [name]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name } }) => props.edit({ [name]: checked });
|
||||
// const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked })
|
||||
// const onChangeCheck = (e) => { console.log(e) }
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchTriggerOptions();
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
props.fetchTriggerOptions();
|
||||
}, []);
|
||||
|
||||
const writeQueryOption = (e, { name, value }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name] : value } });
|
||||
}
|
||||
const writeQueryOption = (e, { name, value }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const writeQuery = ({ target: { value, name } }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name] : value } });
|
||||
}
|
||||
const writeQuery = ({ target: { value, name } }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const metric = (instance && instance.query.left) ? triggerOptions.find(i => i.value === instance.query.left) : null;
|
||||
const unit = metric ? metric.unit : '';
|
||||
const isThreshold = instance.detectionMethod === 'threshold';
|
||||
const metric = instance && instance.query.left ? triggerOptions.find((i) => i.value === instance.query.left) : null;
|
||||
const unit = metric ? metric.unit : '';
|
||||
const isThreshold = instance.detectionMethod === 'threshold';
|
||||
|
||||
return (
|
||||
<Form className={ cn("p-6 pb-10", stl.wrapper)} style={style} onSubmit={() => props.onSubmit(instance)} id="alert-form">
|
||||
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus={ true }
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{ fontSize: '18px', padding: '10px', fontWeight: '600'}}
|
||||
value={ instance && instance.name }
|
||||
onChange={ write }
|
||||
placeholder="New Alert"
|
||||
id="name-field"
|
||||
/>
|
||||
<div className="mb-8" />
|
||||
<Section
|
||||
index="1"
|
||||
title={'What kind of alert do you want to set?'}
|
||||
content={
|
||||
<div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={ (e, { name, value }) => props.edit({ [ name ]: value }) }
|
||||
value={{ value: instance.detectionMethod }}
|
||||
list={ [
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
{ name: 'Change', value: 'change' },
|
||||
]}
|
||||
/>
|
||||
<div className="text-sm color-gray-medium">
|
||||
{isThreshold && 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
|
||||
{!isThreshold && 'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
|
||||
</div>
|
||||
<div className="my-4" />
|
||||
return (
|
||||
<Form className={cn('p-6 pb-10', stl.wrapper)} style={style} onSubmit={() => props.onSubmit(instance)} id="alert-form">
|
||||
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
|
||||
value={instance && instance.name}
|
||||
onChange={write}
|
||||
placeholder="Untiltled Alert"
|
||||
id="name-field"
|
||||
/>
|
||||
<div className="mb-8" />
|
||||
<Section
|
||||
index="1"
|
||||
title={'What kind of alert do you want to set?'}
|
||||
content={
|
||||
<div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={(e, { name, value }) => props.edit({ [name]: value })}
|
||||
value={{ value: instance.detectionMethod }}
|
||||
list={[
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
{ name: 'Change', value: 'change' },
|
||||
]}
|
||||
/>
|
||||
<div className="text-sm color-gray-medium">
|
||||
{isThreshold && 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
|
||||
{!isThreshold &&
|
||||
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
|
||||
</div>
|
||||
<div className="my-4" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="change"
|
||||
options={changeOptions}
|
||||
name="change"
|
||||
defaultValue={instance.change}
|
||||
onChange={({ value }) => writeOption(null, { name: 'change', value })}
|
||||
id="change-dropdown"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{isThreshold ? 'Trigger when' : 'of'}</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="Select Metric"
|
||||
isSearchable={true}
|
||||
options={triggerOptions}
|
||||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
|
||||
<div className="w-4/6 flex items-center">
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
options={conditions}
|
||||
name="operator"
|
||||
defaultValue={instance.query.operator}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'operator', value: value.value })}
|
||||
/>
|
||||
{unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px' }}
|
||||
// label={{ basic: true, content: unit }}
|
||||
// labelPosition='right'
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">{'test'}</span>
|
||||
</>
|
||||
)}
|
||||
{!unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="currentPeriod"
|
||||
defaultValue={instance.currentPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'compared to previous'}</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="previousPeriod"
|
||||
defaultValue={instance.previousPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center my-4">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={instance.slack}
|
||||
onClick={onChangeCheck}
|
||||
label="Slack"
|
||||
/>
|
||||
<Checkbox
|
||||
name="email"
|
||||
type="checkbox"
|
||||
checked={instance.email}
|
||||
onClick={onChangeCheck}
|
||||
className="mr-8"
|
||||
label="Email"
|
||||
/>
|
||||
<Checkbox name="webhook" type="checkbox" checked={instance.webhook} onClick={onChangeCheck} label="Webhook" />
|
||||
</div>
|
||||
|
||||
{instance.slack && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => props.edit({ slackInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.email && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
textFiled
|
||||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => props.edit({ emailInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.webhook && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => props.edit({ webhookInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="change"
|
||||
options={ changeOptions }
|
||||
name="change"
|
||||
defaultValue={ instance.change }
|
||||
onChange={ ({ value }) => writeOption(null , { name: 'change', value }) }
|
||||
id="change-dropdown"
|
||||
/>
|
||||
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
|
||||
<div className="flex items-center">
|
||||
<Button loading={loading} variant="primary" type="submit" disabled={loading || !instance.validate()} id="submit-button">
|
||||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1" />
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{isThreshold ? 'Trigger when' : 'of'}</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="Select Metric"
|
||||
isSearchable={true}
|
||||
options={ triggerOptions }
|
||||
name="left"
|
||||
value={ triggerOptions.find(i => i.value === instance.query.left) }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value: value.value }) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
|
||||
<div className="w-4/6 flex items-center">
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
options={ conditions }
|
||||
name="operator"
|
||||
defaultValue={ instance.query.operator }
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value: value.value }) }
|
||||
/>
|
||||
{ unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px'}}
|
||||
// label={{ basic: true, content: unit }}
|
||||
// labelPosition='right'
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">{'test'}</span>
|
||||
</>
|
||||
)}
|
||||
{ !unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={ instance.query.right }
|
||||
onChange={ writeQuery }
|
||||
placeholder="Specify Value"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button hover variant="text" loading={deleting} type="button" onClick={() => onDelete(instance)} id="trash-button">
|
||||
<Icon name="trash" color="gray-medium" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={ thresholdOptions }
|
||||
name="currentPeriod"
|
||||
defaultValue={ instance.currentPeriod }
|
||||
// onChange={ writeOption }
|
||||
onChange={ ({ value }) => writeOption(null, { name: 'currentPeriod', value }) }
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'compared to previous'}</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={ thresholdOptions }
|
||||
name="previousPeriod"
|
||||
defaultValue={ instance.previousPeriod }
|
||||
// onChange={ writeOption }
|
||||
onChange={ ({ value }) => writeOption(null, { name: 'previousPeriod', value }) }
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center my-4">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={ instance.slack }
|
||||
onClick={ onChangeCheck }
|
||||
label="Slack"
|
||||
/>
|
||||
<Checkbox
|
||||
name="email"
|
||||
type="checkbox"
|
||||
checked={ instance.email }
|
||||
onClick={ onChangeCheck }
|
||||
className="mr-8"
|
||||
label="Email"
|
||||
/>
|
||||
<Checkbox
|
||||
name="webhook"
|
||||
type="checkbox"
|
||||
checked={ instance.webhook }
|
||||
onClick={ onChangeCheck }
|
||||
label="Webhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ instance.slack && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => props.edit({ 'slackInput': selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.email && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
textFiled
|
||||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => props.edit({ 'emailInput': selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{instance.webhook && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => props.edit({ 'webhookInput': selected })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={loading || !instance.validate()}
|
||||
id="submit-button"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1" />
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</div>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
variant="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
>
|
||||
<Icon name="trash" color="gray-medium" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading'])
|
||||
}), { fetchTriggerOptions })(AlertForm)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
|
||||
}),
|
||||
{ fetchTriggerOptions }
|
||||
)(AlertForm);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal, IconButton } from 'UI';
|
||||
import { init, edit, save, remove } from 'Duck/alerts';
|
||||
import { fetchList as fetchWebhooks } from 'Duck/webhook';
|
||||
|
|
@ -9,93 +9,98 @@ import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
|||
import { confirm } from 'UI';
|
||||
|
||||
interface Props {
|
||||
showModal?: boolean;
|
||||
metricId?: number;
|
||||
onClose?: () => void;
|
||||
webhooks: any;
|
||||
fetchWebhooks: Function;
|
||||
save: Function;
|
||||
remove: Function;
|
||||
init: Function;
|
||||
edit: Function;
|
||||
showModal?: boolean;
|
||||
metricId?: number;
|
||||
onClose?: () => void;
|
||||
webhooks: any;
|
||||
fetchWebhooks: Function;
|
||||
save: Function;
|
||||
remove: Function;
|
||||
init: Function;
|
||||
edit: Function;
|
||||
}
|
||||
function AlertFormModal(props: Props) {
|
||||
const { metricId = null, showModal = false, webhooks } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const { metricId = null, showModal = false, webhooks } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
|
||||
const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, text: name }))
|
||||
.toJS();
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, text: name }))
|
||||
.toJS();
|
||||
|
||||
const saveAlert = instance => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toggleForm(null, false);
|
||||
}
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
})
|
||||
}
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toggleForm(null, false);
|
||||
}
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance)
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{ 'Create Alert' }</span>
|
||||
{/* <IconButton
|
||||
circle
|
||||
size="small"
|
||||
icon="plus"
|
||||
outline
|
||||
id="add-button"
|
||||
onClick={ () => toggleForm({}, true) }
|
||||
/> */}
|
||||
</div>
|
||||
const onDelete = async (instance) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`,
|
||||
})
|
||||
) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
isDisplayed={ showModal }
|
||||
onClose={props.onClose}
|
||||
size="medium"
|
||||
content={ showModal &&
|
||||
<AlertForm
|
||||
metricId={ metricId }
|
||||
edit={props.edit}
|
||||
slackChannels={slackChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
};
|
||||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance);
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{'Create Alert'}</span>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={showModal}
|
||||
onClose={props.onClose}
|
||||
onDelete={onDelete}
|
||||
style={{ width: '580px', height: '100vh - 200px' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
size="medium"
|
||||
content={
|
||||
showModal && (
|
||||
<AlertForm
|
||||
metricId={metricId}
|
||||
edit={props.edit}
|
||||
slackChannels={slackChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={props.onClose}
|
||||
onDelete={onDelete}
|
||||
style={{ width: '580px', height: '100vh - 200px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(AlertFormModal)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}),
|
||||
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
|
||||
)(AlertFormModal);
|
||||
|
|
|
|||
|
|
@ -10,95 +10,100 @@ import { setShowAlerts } from 'Duck/dashboard';
|
|||
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
const Alerts = props => {
|
||||
const { webhooks, setShowAlerts } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const Alerts = (props) => {
|
||||
const { webhooks, setShowAlerts } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS();
|
||||
const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS();
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
|
||||
const saveAlert = instance => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toast.success('New alert saved')
|
||||
toggleForm(null, false);
|
||||
} else {
|
||||
toast.success('Alert updated')
|
||||
}
|
||||
})
|
||||
}
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toast.success('New alert saved');
|
||||
toggleForm(null, false);
|
||||
} else {
|
||||
toast.success('Alert updated');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
const onDelete = async (instance) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`,
|
||||
})
|
||||
) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance)
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
}
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance);
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{ 'Alerts' }</span>
|
||||
<IconButton
|
||||
circle
|
||||
size="small"
|
||||
icon="plus"
|
||||
outline
|
||||
id="add-button"
|
||||
onClick={ () => toggleForm({}, true) }
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{'Alerts'}</span>
|
||||
<IconButton circle size="small" icon="plus" outline id="add-button" onClick={() => toggleForm({}, true)} />
|
||||
</div>
|
||||
}
|
||||
isDisplayed={true}
|
||||
onClose={() => {
|
||||
toggleForm({}, false);
|
||||
setShowAlerts(false);
|
||||
}}
|
||||
size="small"
|
||||
content={
|
||||
<AlertsList
|
||||
onEdit={(alert) => {
|
||||
toggleForm(alert, true);
|
||||
}}
|
||||
onClickCreate={() => toggleForm({}, true)}
|
||||
/>
|
||||
}
|
||||
detailContent={
|
||||
showForm && (
|
||||
<AlertForm
|
||||
edit={props.edit}
|
||||
slackChannels={slackChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={() => toggleForm({}, false)}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={ true }
|
||||
onClose={ () => {
|
||||
toggleForm({}, false);
|
||||
setShowAlerts(false);
|
||||
} }
|
||||
size="small"
|
||||
content={
|
||||
<AlertsList
|
||||
onEdit={alert => {
|
||||
toggleForm(alert, true)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
detailContent={
|
||||
showForm && (
|
||||
<AlertForm
|
||||
edit={props.edit}
|
||||
slackChannels={slackChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={ () => toggleForm({}, false) }
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(Alerts)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}),
|
||||
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
|
||||
)(Alerts);
|
||||
|
|
|
|||
|
|
@ -1,55 +1,58 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Loader, NoContent, Input } from 'UI';
|
||||
import AlertItem from './AlertItem'
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Loader, NoContent, Input, Button } from 'UI';
|
||||
import AlertItem from './AlertItem';
|
||||
import { fetchList, init } from 'Duck/alerts';
|
||||
import { connect } from 'react-redux';
|
||||
import { getRE } from 'App/utils';
|
||||
|
||||
const AlertsList = props => {
|
||||
const { loading, list, instance, onEdit } = props;
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList()
|
||||
}, [])
|
||||
const AlertsList = (props) => {
|
||||
const { loading, list, instance, onEdit } = props;
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const filterRE = getRE(query, 'i');
|
||||
const _filteredList = list.filter(({ name, query: { left } }) => filterRE.test(name) || filterRE.test(left));
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 w-full px-3">
|
||||
<Input
|
||||
name="searchQuery"
|
||||
placeholder="Search by Name or Metric"
|
||||
onChange={({ target: { value } }) => setQuery(value)}
|
||||
/>
|
||||
</div>
|
||||
<Loader loading={ loading }>
|
||||
<NoContent
|
||||
title="No data available."
|
||||
size="small"
|
||||
show={ list.size === 0 }
|
||||
>
|
||||
<div className="bg-white">
|
||||
{_filteredList.map(a => (
|
||||
<div className="border-b" key={a.key}>
|
||||
<AlertItem
|
||||
active={instance.alertId === a.alertId}
|
||||
alert={a}
|
||||
onEdit={() => onEdit(a.toData())}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const filterRE = getRE(query, 'i');
|
||||
const _filteredList = list.filter(({ name, query: { left } }) => filterRE.test(name) || filterRE.test(left));
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['alerts', 'list']).sort((a, b ) => b.createdAt - a.createdAt),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
loading: state.getIn(['alerts', 'loading'])
|
||||
}), { fetchList, init })(AlertsList)
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 w-full px-3">
|
||||
<Input name="searchQuery" placeholder="Search by Name or Metric" onChange={({ target: { value } }) => setQuery(value)} />
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title="No alerts have been setup yet."
|
||||
subtext={
|
||||
<div className="flex flex-col items-center">
|
||||
<div>Alerts helps your team stay up to date with the activity on your app.</div>
|
||||
<Button variant="primary" className="mt-4" icon="plus" onClick={props.onClickCreate}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.size === 0}
|
||||
>
|
||||
<div className="bg-white">
|
||||
{_filteredList.map((a) => (
|
||||
<div className="border-b" key={a.key}>
|
||||
<AlertItem active={instance.alertId === a.alertId} alert={a} onEdit={() => onEdit(a.toData())} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
loading: state.getIn(['alerts', 'loading']),
|
||||
}),
|
||||
{ fetchList, init }
|
||||
)(AlertsList);
|
||||
|
|
|
|||
|
|
@ -1,79 +1,66 @@
|
|||
import React from 'react'
|
||||
import React from 'react';
|
||||
import { Input, TagBadge } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const DropdownChips = ({
|
||||
textFiled = false,
|
||||
validate = null,
|
||||
placeholder = '',
|
||||
selected = [],
|
||||
options = [],
|
||||
badgeClassName = 'lowercase',
|
||||
onChange = () => null,
|
||||
...props
|
||||
const DropdownChips = ({
|
||||
textFiled = false,
|
||||
validate = null,
|
||||
placeholder = '',
|
||||
selected = [],
|
||||
options = [],
|
||||
badgeClassName = 'lowercase',
|
||||
onChange = () => null,
|
||||
...props
|
||||
}) => {
|
||||
const onRemove = id => {
|
||||
onChange(selected.filter(i => i !== id))
|
||||
}
|
||||
const onRemove = (id) => {
|
||||
onChange(selected.filter((i) => i !== id));
|
||||
};
|
||||
|
||||
const onSelect = ({ value }) => {
|
||||
const newSlected = selected.concat(value.value);
|
||||
onChange(newSlected)
|
||||
};
|
||||
const onSelect = ({ value }) => {
|
||||
const newSlected = selected.concat(value.value);
|
||||
onChange(newSlected);
|
||||
};
|
||||
|
||||
const onKeyPress = e => {
|
||||
const val = e.target.value;
|
||||
if (e.key !== 'Enter' || selected.includes(val)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (validate && !validate(val)) return;
|
||||
const onKeyPress = (e) => {
|
||||
const val = e.target.value;
|
||||
if (e.key !== 'Enter' || selected.includes(val)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (validate && !validate(val)) return;
|
||||
|
||||
const newSlected = selected.concat(val);
|
||||
e.target.value = '';
|
||||
onChange(newSlected);
|
||||
}
|
||||
const newSlected = selected.concat(val);
|
||||
e.target.value = '';
|
||||
onChange(newSlected);
|
||||
};
|
||||
|
||||
const _options = options.filter(item => !selected.includes(item.value))
|
||||
const _options = options.filter((item) => !selected.includes(item.value));
|
||||
|
||||
const renderBadge = (item) => {
|
||||
const val = typeof item === 'string' ? item : item.value;
|
||||
const text = typeof item === 'string' ? item : item.label;
|
||||
return <TagBadge className={badgeClassName} key={text} text={text} hashed={false} onRemove={() => onRemove(val)} outline={true} />;
|
||||
};
|
||||
|
||||
const renderBadge = item => {
|
||||
const val = typeof item === 'string' ? item : item.value;
|
||||
const text = typeof item === 'string' ? item : item.label;
|
||||
return (
|
||||
<TagBadge
|
||||
className={badgeClassName}
|
||||
key={ text }
|
||||
text={ text }
|
||||
hashed={false}
|
||||
onRemove={ () => onRemove(val) }
|
||||
outline={ true }
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="w-full">
|
||||
{textFiled ? (
|
||||
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
) : (
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
isSearchable={true}
|
||||
options={_options}
|
||||
name="webhookInput"
|
||||
value={null}
|
||||
onChange={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap mt-3">
|
||||
{textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{textFiled ? (
|
||||
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
) : (
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
isSearchable={true}
|
||||
options={ _options }
|
||||
name="webhookInput"
|
||||
value={null}
|
||||
onChange={ onSelect }
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap mt-3">
|
||||
{
|
||||
textFiled ?
|
||||
selected.map(renderBadge) :
|
||||
options.filter(i => selected.includes(i.value)).map(renderBadge)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropdownChips
|
||||
export default DropdownChips;
|
||||
|
|
|
|||
|
|
@ -64,15 +64,14 @@ class Announcements extends React.Component {
|
|||
content={
|
||||
<div className="mx-4">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="100" />
|
||||
</div>
|
||||
}
|
||||
subtext="There are no announcements to show."
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && announcements.size === 0 }
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_ANNOUNCEMENTS} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">No announcements to show.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={ !loading && announcements.size === 0 }
|
||||
>
|
||||
{
|
||||
announcements.map(item => (
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ interface Props {
|
|||
stream: LocalStream | null,
|
||||
endCall: () => void,
|
||||
videoEnabled: boolean,
|
||||
setVideoEnabled: (boolean) => void
|
||||
isPrestart?: boolean,
|
||||
setVideoEnabled: (isEnabled: boolean) => void
|
||||
}
|
||||
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props) {
|
||||
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled, isPrestart } : Props) {
|
||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
|
||||
const toggleAudio = () => {
|
||||
|
|
@ -25,6 +26,13 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props
|
|||
.then(setVideoEnabled)
|
||||
}
|
||||
|
||||
/** muting user if he is auto connected to the call */
|
||||
React.useEffect(() => {
|
||||
if (isPrestart) {
|
||||
audioEnabled && toggleAudio();
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import React, { useState, FC, useEffect } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import VideoContainer from '../components/VideoContainer'
|
||||
import cn from 'classnames'
|
||||
import Counter from 'App/components/shared/SessionItem/Counter'
|
||||
|
|
@ -8,23 +7,23 @@ import ChatControls from '../ChatControls/ChatControls'
|
|||
import Draggable from 'react-draggable';
|
||||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
|
||||
|
||||
export interface Props {
|
||||
incomeStream: MediaStream | null,
|
||||
incomeStream: MediaStream[] | null,
|
||||
localStream: LocalStream | null,
|
||||
userId: String,
|
||||
userId: string,
|
||||
isPrestart?: boolean;
|
||||
endCall: () => void
|
||||
}
|
||||
|
||||
const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localStream, endCall }) {
|
||||
function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: Props) {
|
||||
const [localVideoEnabled, setLocalVideoEnabled] = useState(false)
|
||||
const [remoteVideoEnabled, setRemoteVideoEnabled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!incomeStream) { return }
|
||||
if (!incomeStream || incomeStream.length === 0) { return }
|
||||
const iid = setInterval(() => {
|
||||
const settings = incomeStream.getVideoTracks()[0]?.getSettings()
|
||||
const isDummyVideoTrack = !!settings ? (settings.width === 2 || settings.frameRate === 0) : true
|
||||
const settings = incomeStream.map(stream => stream.getVideoTracks()[0]?.getSettings()).filter(Boolean)
|
||||
const isDummyVideoTrack = settings.length > 0 ? (settings.every(s => s.width === 2 || s.frameRate === 0 || s.frameRate === undefined)) : true
|
||||
const shouldBeEnabled = !isDummyVideoTrack
|
||||
if (shouldBeEnabled !== localVideoEnabled) {
|
||||
setRemoteVideoEnabled(shouldBeEnabled)
|
||||
|
|
@ -42,16 +41,20 @@ const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localS
|
|||
style={{ width: '280px' }}
|
||||
>
|
||||
<div className="handle flex items-center p-2 cursor-move select-none border-b">
|
||||
<div className={stl.headerTitle}><b>Talking to </b> {userId ? userId : 'Anonymous User'}</div>
|
||||
<div className={stl.headerTitle}>
|
||||
<b>Talking to </b> {userId ? userId : 'Anonymous User'}
|
||||
{incomeStream && incomeStream.length > 2 ? ' (+ other agents in the call)' : ''}
|
||||
</div>
|
||||
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
|
||||
</div>
|
||||
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
|
||||
<VideoContainer stream={ incomeStream } />
|
||||
{!incomeStream && <div className={stl.noVideo}>Error obtaining incoming streams</div>}
|
||||
{incomeStream && incomeStream.map(stream => <VideoContainer stream={ stream } />)}
|
||||
<div className="absolute bottom-0 right-0 z-50">
|
||||
<VideoContainer stream={ localStream ? localStream.stream : null } muted width={50} />
|
||||
</div>
|
||||
</div>
|
||||
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} />
|
||||
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} isPrestart={isPrestart} />
|
||||
</div>
|
||||
</Draggable>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Popup, Icon, Button, IconButton } from 'UI';
|
||||
import logger from 'App/logger';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { toggleChatWindow } from 'Duck/sessions';
|
||||
import { connectPlayer } from 'Player/store';
|
||||
import ChatWindow from '../../ChatWindow';
|
||||
import { callPeer, requestReleaseRemoteControl, toggleAnnotation } from 'Player';
|
||||
import { callPeer, setCallArgs, requestReleaseRemoteControl, toggleAnnotation } from 'Player';
|
||||
import { CallingState, ConnectionStatus, RemoteControlStatus } from 'Player/MessageDistributor/managers/AssistManager';
|
||||
import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
|
||||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
|
|
@ -14,15 +15,12 @@ import { toast } from 'react-toastify';
|
|||
import { confirm } from 'UI';
|
||||
import stl from './AassistActions.module.css';
|
||||
|
||||
function onClose(stream) {
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
|
||||
function onReject() {
|
||||
toast.info(`Call was rejected.`);
|
||||
}
|
||||
|
||||
function onError(e) {
|
||||
console.log(e)
|
||||
toast.error(typeof e === 'string' ? e : e.message);
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +33,8 @@ interface Props {
|
|||
remoteControlStatus: RemoteControlStatus;
|
||||
hasPermission: boolean;
|
||||
isEnterprise: boolean;
|
||||
isCallActive: boolean;
|
||||
agentIds: string[];
|
||||
}
|
||||
|
||||
function AssistActions({
|
||||
|
|
@ -46,14 +46,21 @@ function AssistActions({
|
|||
remoteControlStatus,
|
||||
hasPermission,
|
||||
isEnterprise,
|
||||
isCallActive,
|
||||
agentIds
|
||||
}: Props) {
|
||||
const [incomeStream, setIncomeStream] = useState<MediaStream | null>(null);
|
||||
const [isPrestart, setPrestart] = useState(false);
|
||||
const [incomeStream, setIncomeStream] = useState<MediaStream[] | null>([]);
|
||||
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
|
||||
const [callObject, setCallObject] = useState<{ end: () => void } | null>(null);
|
||||
|
||||
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
|
||||
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
|
||||
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
|
||||
|
||||
useEffect(() => {
|
||||
return callObject?.end();
|
||||
}, []);
|
||||
return callObject?.end()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (peerConnectionStatus == ConnectionStatus.Disconnected) {
|
||||
|
|
@ -61,15 +68,34 @@ function AssistActions({
|
|||
}
|
||||
}, [peerConnectionStatus]);
|
||||
|
||||
function call() {
|
||||
RequestLocalStream()
|
||||
.then((lStream) => {
|
||||
setLocalStream(lStream);
|
||||
setCallObject(callPeer(lStream, setIncomeStream, lStream.stop.bind(lStream), onReject, onError));
|
||||
})
|
||||
.catch(onError);
|
||||
const addIncomeStream = (stream: MediaStream) => {
|
||||
setIncomeStream(oldState => [...oldState, stream]);
|
||||
}
|
||||
|
||||
function call(agentIds?: string[]) {
|
||||
RequestLocalStream().then(lStream => {
|
||||
setLocalStream(lStream);
|
||||
setCallArgs(
|
||||
lStream,
|
||||
addIncomeStream,
|
||||
lStream.stop.bind(lStream),
|
||||
onReject,
|
||||
onError
|
||||
)
|
||||
setCallObject(callPeer());
|
||||
if (agentIds) {
|
||||
callPeer(agentIds)
|
||||
}
|
||||
}).catch(onError)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!onCall && isCallActive && agentIds) {
|
||||
setPrestart(true);
|
||||
call(agentIds)
|
||||
}
|
||||
}, [agentIds, isCallActive])
|
||||
|
||||
const confirmCall = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
|
|
@ -82,10 +108,6 @@ function AssistActions({
|
|||
}
|
||||
};
|
||||
|
||||
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
|
||||
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
|
||||
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{(onCall || remoteActive) && (
|
||||
|
|
@ -123,7 +145,7 @@ function AssistActions({
|
|||
</div>
|
||||
<div className={stl.divider} />
|
||||
|
||||
<Popup content={cannotCall ? 'You don’t have the permissions to perform this action.' : `Call ${userId ? userId : 'User'}`}>
|
||||
<Popup content={cannotCall ? `You don't have the permissions to perform this action.` : `Call ${userId ? userId : 'User'}`}>
|
||||
<div
|
||||
className={cn('cursor-pointer p-2 flex items-center', { [stl.disabled]: cannotCall })}
|
||||
onClick={onCall ? callObject?.end : confirmCall}
|
||||
|
|
@ -138,7 +160,7 @@ function AssistActions({
|
|||
|
||||
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
|
||||
{onCall && callObject && (
|
||||
<ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} />
|
||||
<ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} isPrestart={isPrestart} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const AssistTabs = (props: Props) => {
|
|||
<>
|
||||
<div
|
||||
className={stl.btnLink}
|
||||
onClick={() => showModal(<SessionList userId={props.userId} />, {})}
|
||||
onClick={() => showModal(<SessionList userId={props.userId} />, { right: true })}
|
||||
>
|
||||
Active Sessions
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { fetchLiveList } from 'Duck/sessions';
|
|||
import { Loader, NoContent, Label } from 'UI';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
|
|
@ -24,14 +25,26 @@ function SessionList(props: Props) {
|
|||
|
||||
return (
|
||||
<div style={{ width: '50vw' }}>
|
||||
<div className="border-r shadow h-screen overflow-y-auto" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}>
|
||||
<div
|
||||
className="border-r shadow h-screen overflow-y-auto"
|
||||
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
|
||||
</div>
|
||||
</div>
|
||||
<Loader loading={props.loading}>
|
||||
<NoContent show={!props.loading && props.list.size === 0} title="No live sessions.">
|
||||
<NoContent
|
||||
show={!props.loading && props.list.size === 0}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No live sessions found.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-4">
|
||||
{props.list.map((session: any) => (
|
||||
<div className="mb-6">
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class AutoComplete extends React.PureComponent {
|
|||
onInputChange = ({ target: { value } }) => {
|
||||
changed = true;
|
||||
this.setState({ query: value, updated: true })
|
||||
const _value = value.trim();
|
||||
const _value = value ? value.trim() : undefined;
|
||||
if (_value !== '' && _value !== ' ') {
|
||||
this.debouncedRequestValues(_value)
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ class AutoComplete extends React.PureComponent {
|
|||
value = pasted ? this.hiddenInput.value : value;
|
||||
const { onSelect, name } = this.props;
|
||||
if (value !== this.props.value) {
|
||||
const _value = value.trim();
|
||||
const _value = value ? value.trim() : undefined;
|
||||
onSelect(null, {name, value: _value});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import {
|
||||
fetchFavoriteList as fetchFavoriteSessionList
|
||||
} from 'Duck/sessions';
|
||||
import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions';
|
||||
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import SessionList from './SessionList';
|
||||
import stl from './bugFinder.module.css';
|
||||
import withLocationHandlers from "HOCs/withLocationHandlers";
|
||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||
import { fetch as fetchFilterVariables } from 'Duck/sources';
|
||||
import { fetchSources } from 'Duck/customField';
|
||||
import { setActiveTab } from 'Duck/search';
|
||||
|
|
@ -21,113 +19,113 @@ import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search'
|
|||
import { FilterKey } from 'Types/filter/filterType';
|
||||
|
||||
const weakEqual = (val1, val2) => {
|
||||
if (!!val1 === false && !!val2 === false) return true;
|
||||
if (!val1 !== !val2) return false;
|
||||
return `${ val1 }` === `${ val2 }`;
|
||||
}
|
||||
if (!!val1 === false && !!val2 === false) return true;
|
||||
if (!val1 !== !val2) return false;
|
||||
return `${val1}` === `${val2}`;
|
||||
};
|
||||
|
||||
const allowedQueryKeys = [
|
||||
'userOs',
|
||||
'userId',
|
||||
'userBrowser',
|
||||
'userDevice',
|
||||
'userCountry',
|
||||
'startDate',
|
||||
'endDate',
|
||||
'minDuration',
|
||||
'maxDuration',
|
||||
'referrer',
|
||||
'sort',
|
||||
'order',
|
||||
'userOs',
|
||||
'userId',
|
||||
'userBrowser',
|
||||
'userDevice',
|
||||
'userCountry',
|
||||
'startDate',
|
||||
'endDate',
|
||||
'minDuration',
|
||||
'maxDuration',
|
||||
'referrer',
|
||||
'sort',
|
||||
'order',
|
||||
];
|
||||
|
||||
@withLocationHandlers()
|
||||
@connect(state => ({
|
||||
filter: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
variables: state.getIn([ 'customFields', 'list' ]),
|
||||
sources: state.getIn([ 'customFields', 'sources' ]),
|
||||
filterValues: state.get('filterValues'),
|
||||
favoriteList: state.getIn([ 'sessions', 'favoriteList' ]),
|
||||
currentProjectId: state.getIn([ 'site', 'siteId' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
watchdogs: state.getIn(['watchdogs', 'list']),
|
||||
activeFlow: state.getIn([ 'filters', 'activeFlow' ]),
|
||||
sessions: state.getIn([ 'sessions', 'list' ]),
|
||||
}), {
|
||||
fetchFavoriteSessionList,
|
||||
applyFilter,
|
||||
addAttribute,
|
||||
fetchFilterVariables,
|
||||
fetchSources,
|
||||
clearEvents,
|
||||
setActiveTab,
|
||||
clearSearch,
|
||||
fetchSessions,
|
||||
addFilterByKeyAndValue,
|
||||
})
|
||||
@withPageTitle("Sessions - OpenReplay")
|
||||
@connect(
|
||||
(state) => ({
|
||||
filter: state.getIn(['filters', 'appliedFilter']),
|
||||
variables: state.getIn(['customFields', 'list']),
|
||||
sources: state.getIn(['customFields', 'sources']),
|
||||
filterValues: state.get('filterValues'),
|
||||
favoriteList: state.getIn(['sessions', 'favoriteList']),
|
||||
currentProjectId: state.getIn(['site', 'siteId']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
watchdogs: state.getIn(['watchdogs', 'list']),
|
||||
activeFlow: state.getIn(['filters', 'activeFlow']),
|
||||
sessions: state.getIn(['sessions', 'list']),
|
||||
}),
|
||||
{
|
||||
fetchFavoriteSessionList,
|
||||
applyFilter,
|
||||
addAttribute,
|
||||
fetchFilterVariables,
|
||||
fetchSources,
|
||||
clearEvents,
|
||||
setActiveTab,
|
||||
clearSearch,
|
||||
fetchSessions,
|
||||
addFilterByKeyAndValue,
|
||||
}
|
||||
)
|
||||
@withPageTitle('Sessions - OpenReplay')
|
||||
export default class BugFinder extends React.PureComponent {
|
||||
state = {showRehydratePanel: false}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = { showRehydratePanel: false };
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// TODO should cache the response
|
||||
// props.fetchSources().then(() => {
|
||||
// defaultFilters[6] = {
|
||||
// category: 'Collaboration',
|
||||
// type: 'CUSTOM',
|
||||
// keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
||||
// };
|
||||
// defaultFilters[7] = {
|
||||
// category: 'Logging Tools',
|
||||
// type: 'ERROR',
|
||||
// keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
||||
// };
|
||||
// });
|
||||
if (props.sessions.size === 0) {
|
||||
props.fetchSessions();
|
||||
// TODO should cache the response
|
||||
// props.fetchSources().then(() => {
|
||||
// defaultFilters[6] = {
|
||||
// category: 'Collaboration',
|
||||
// type: 'CUSTOM',
|
||||
// keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
||||
// };
|
||||
// defaultFilters[7] = {
|
||||
// category: 'Logging Tools',
|
||||
// type: 'ERROR',
|
||||
// keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS()
|
||||
// };
|
||||
// });
|
||||
// if (props.sessions.size === 0) {
|
||||
// props.fetchSessions();
|
||||
// }
|
||||
|
||||
const queryFilter = this.props.query.all(allowedQueryKeys);
|
||||
if (queryFilter.hasOwnProperty('userId')) {
|
||||
props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId);
|
||||
} else {
|
||||
if (props.sessions.size === 0) {
|
||||
props.fetchSessions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryFilter = this.props.query.all(allowedQueryKeys);
|
||||
if (queryFilter.hasOwnProperty('userId')) {
|
||||
props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId);
|
||||
} else {
|
||||
if (props.sessions.size === 0) {
|
||||
props.fetchSessions();
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleRehydratePanel = () => {
|
||||
this.setState({ showRehydratePanel: !this.state.showRehydratePanel });
|
||||
};
|
||||
|
||||
toggleRehydratePanel = () => {
|
||||
this.setState({ showRehydratePanel: !this.state.showRehydratePanel })
|
||||
}
|
||||
setActiveTab = (tab) => {
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
setActiveTab = tab => {
|
||||
this.props.setActiveTab(tab);
|
||||
}
|
||||
render() {
|
||||
const { showRehydratePanel } = this.state;
|
||||
|
||||
render() {
|
||||
const { showRehydratePanel } = this.state;
|
||||
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<div className="flex-1 flex">
|
||||
<div className="side-menu">
|
||||
<SessionsMenu
|
||||
onMenuItemClick={this.setActiveTab}
|
||||
toggleRehydratePanel={ this.toggleRehydratePanel }
|
||||
/>
|
||||
</div>
|
||||
<div className={cn("side-menu-margined", stl.searchWrapper) }>
|
||||
<NoSessionsMessage />
|
||||
<div className="mb-5">
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<div className="flex-1 flex">
|
||||
<div className="side-menu">
|
||||
<SessionsMenu onMenuItemClick={this.setActiveTab} toggleRehydratePanel={this.toggleRehydratePanel} />
|
||||
</div>
|
||||
<div className={cn('side-menu-margined', stl.searchWrapper)}>
|
||||
<NoSessionsMessage />
|
||||
<div className="mb-5">
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
</div>
|
||||
<SessionList onMenuItemClick={this.setActiveTab} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SessionList onMenuItemClick={this.setActiveTab} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) {
|
|||
const dateValues = period.toJSON();
|
||||
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
applyFilter(dateValues);
|
||||
// applyFilter(dateValues);
|
||||
}
|
||||
}, [label]);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@ function AuditList(props: Props) {
|
|||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
<AnimatedSVG name={ICONS.NO_AUDIT_TRAIL} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">No data available</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
<div className="px-2 grid grid-cols-12 gap-4 items-center py-3 font-medium">
|
||||
<div className="col-span-5">Name</div>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default class Client extends React.PureComponent {
|
|||
<div className={ styles.tabMenu }>
|
||||
<PreferencesMenu activeTab={activeTab} />
|
||||
</div>
|
||||
<div className={ styles.tabContent }>
|
||||
<div className="bg-white w-full rounded-lg mx-4 my-6 p-5 border">
|
||||
{ activeTab && this.renderActiveTab() }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,59 +4,74 @@ import { edit, save } from 'Duck/customField';
|
|||
import { Form, Input, Button, Message } from 'UI';
|
||||
import styles from './customFieldForm.module.css';
|
||||
|
||||
@connect(state => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
|
||||
}), {
|
||||
edit,
|
||||
save,
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
}
|
||||
)
|
||||
class CustomFieldForm extends React.PureComponent {
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value });
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [name]: value });
|
||||
|
||||
render() {
|
||||
const { field, errors} = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<Form className={ styles.wrapper }>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="key"
|
||||
value={ field.key }
|
||||
onChange={ this.write }
|
||||
placeholder="Field Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
render() {
|
||||
const { field, errors } = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="key"
|
||||
value={field.key}
|
||||
onChange={this.write}
|
||||
placeholder="Field Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{ errors &&
|
||||
<div className="mb-3">
|
||||
{ errors.map(error => <Message visible={ errors } size="mini" error key={ error } className={ styles.error }>{ error }</Message>) }
|
||||
</div>
|
||||
}
|
||||
{errors && (
|
||||
<div className="mb-3">
|
||||
{errors.map((error) => (
|
||||
<Message visible={errors} size="mini" error key={error} className={styles.error}>
|
||||
{error}
|
||||
</Message>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={ () => this.props.onSave(field) }
|
||||
disabled={ !field.validate() }
|
||||
loading={ this.props.saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ exists ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
<Button
|
||||
data-hidden={ !exists }
|
||||
onClick={ this.props.onClose }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => this.props.onSave(field)}
|
||||
disabled={!field.validate()}
|
||||
loading={this.props.saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={this.props.onClose}>
|
||||
{'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={this.props.onDelete}></Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFieldForm;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { IconButton, SlideModal, Loader, NoContent, Icon, TextLink } from 'UI';
|
||||
import { Button, Loader, NoContent, Icon } from 'UI';
|
||||
import { init, fetchList, save, remove } from 'Duck/customField';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import styles from './customFields.module.css';
|
||||
|
|
@ -10,121 +10,118 @@ import CustomFieldForm from './CustomFieldForm';
|
|||
import ListItem from './ListItem';
|
||||
import { confirm } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
@connect(state => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy(i => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
|
||||
}), {
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove,
|
||||
})
|
||||
@withPageTitle('Metadata - OpenReplay Preferences')
|
||||
class CustomFields extends React.Component {
|
||||
state = { showModal: false, currentSite: this.props.sites.get(0), deletingItem: null };
|
||||
function CustomFields(props) {
|
||||
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
|
||||
const [deletingItem, setDeletingItem] = React.useState(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
componentWillMount() {
|
||||
const activeSite = this.props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
this.props.fetchList(activeSite.id);
|
||||
}
|
||||
useEffect(() => {
|
||||
const activeSite = props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
save = (field) => {
|
||||
const { currentSite } = this.state;
|
||||
this.props.save(currentSite.id, field).then(() => {
|
||||
const { errors } = this.props;
|
||||
if (!errors || errors.size === 0) {
|
||||
return this.closeModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
props.fetchList(activeSite.id);
|
||||
}, []);
|
||||
|
||||
closeModal = () => this.setState({ showModal: false });
|
||||
init = (field) => {
|
||||
this.props.init(field);
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
|
||||
onChangeSelect = ({ value }) => {
|
||||
const site = this.props.sites.find(s => s.id === value.value);
|
||||
this.setState({ currentSite: site })
|
||||
this.props.fetchList(site.id);
|
||||
}
|
||||
|
||||
removeMetadata = async (field) => {
|
||||
if (await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`
|
||||
})) {
|
||||
const { currentSite } = this.state;
|
||||
this.setState({ deletingItem: field.index });
|
||||
this.props.remove(currentSite.id, field.index)
|
||||
.then(() => this.setState({ deletingItem: null }));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fields, field, loading } = this.props;
|
||||
const { showModal, currentSite, deletingItem } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title={ `${ (field.exists() ? 'Update' : 'Add') + ' Metadata Field' }` }
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ showModal && <CustomFieldForm onClose={ this.closeModal } onSave={ this.save } /> }
|
||||
onClose={ this.closeModal }
|
||||
/>
|
||||
<div className={ styles.tabHeader }>
|
||||
<h3 className={ cn(styles.tabTitle, "text-2xl") }>{ 'Metadata' }</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown
|
||||
value={ currentSite && currentSite.id }
|
||||
onChange={ this.onChangeSelect }
|
||||
/>
|
||||
</div>
|
||||
<IconButton circle icon="plus" outline onClick={ () => this.init() } />
|
||||
<TextLink
|
||||
icon="book"
|
||||
className="ml-auto color-gray-medium"
|
||||
href="https://docs.openreplay.com/installation/metadata"
|
||||
label="Documentation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Loader loading={ loading }>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
</div>
|
||||
const save = (field) => {
|
||||
props.save(currentSite.id, field).then(() => {
|
||||
const { errors } = props;
|
||||
if (!errors || errors.size === 0) {
|
||||
hideModal();
|
||||
}
|
||||
size="small"
|
||||
show={ fields.size === 0 }
|
||||
// animatedIcon="empty-state"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{ fields.filter(i => i.index).map(field => (
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={ field._key }
|
||||
field={ field }
|
||||
onEdit={ this.init }
|
||||
onDelete={ () => this.removeMetadata(field) }
|
||||
/>
|
||||
))}
|
||||
});
|
||||
};
|
||||
|
||||
const init = (field) => {
|
||||
props.init(field);
|
||||
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }) => {
|
||||
const site = props.sites.find((s) => s.id === value.value);
|
||||
setCurrentSite(site);
|
||||
props.fetchList(site.id);
|
||||
};
|
||||
|
||||
const removeMetadata = async (field) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`,
|
||||
})
|
||||
) {
|
||||
setDeletingItem(field.index);
|
||||
props
|
||||
.remove(currentSite.id, field.index)
|
||||
.then(() => {
|
||||
hideModal();
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingItem(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { fields, loading } = props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
<Button variant="primary" onClick={() => init()}>Add</Button>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex items-center mt-3">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
See additonal user information in sessions.
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_METADATA} size={80} />
|
||||
{/* <div className="mt-4" /> */}
|
||||
<div className="text-center text-gray-600 my-4">None added yet</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={fields.size === 0}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{fields
|
||||
.filter((i) => i.index)
|
||||
.map((field) => (
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={field._key}
|
||||
field={field}
|
||||
onEdit={init}
|
||||
// onDelete={ () => removeMetadata(field) }
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFields;
|
||||
export default connect(
|
||||
(state) => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove,
|
||||
}
|
||||
)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields));
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames'
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'UI';
|
||||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ field, onEdit, onDelete, disabled }) => {
|
||||
return (
|
||||
<div className={ cn(styles.wrapper, field.index === 0 ? styles.preDefined : '', { [styles.disabled] : disabled} ) } onClick={ () => field.index != 0 && onEdit(field) } >
|
||||
<span>{ field.key }</span>
|
||||
<div className={ styles.actions } data-hidden={ field.index === 0}>
|
||||
<div className={ styles.button } onClick={ (e) => { e.stopPropagation(); onDelete(field) } }>
|
||||
<Icon name="trash" color="teal" size="16" />
|
||||
const ListItem = ({ field, onEdit, disabled }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b group last:border-none hover:bg-active-blue flex items-center justify-between p-3 cursor-pointer',
|
||||
field.index === 0 ? styles.preDefined : '',
|
||||
{
|
||||
[styles.disabled]: disabled,
|
||||
}
|
||||
)}
|
||||
onClick={() => field.index != 0 && onEdit(field)}
|
||||
>
|
||||
<span>{field.key}</span>
|
||||
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={ styles.button }>
|
||||
<Icon name="edit" color="teal" size="18"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
/* margin-bottom: 25px; */
|
||||
|
||||
& .tabTitle {
|
||||
margin: 0 15px 0 0;
|
||||
|
|
|
|||
|
|
@ -1,59 +1,56 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import AssistScript from './AssistScript'
|
||||
import AssistNpm from './AssistNpm'
|
||||
import AssistScript from './AssistScript';
|
||||
import AssistNpm from './AssistNpm';
|
||||
import { Tabs } from 'UI';
|
||||
import { useState } from 'react';
|
||||
|
||||
const NPM = 'NPM'
|
||||
const SCRIPT = 'SCRIPT'
|
||||
const NPM = 'NPM';
|
||||
const SCRIPT = 'SCRIPT';
|
||||
const TABS = [
|
||||
{ key: SCRIPT, text: SCRIPT },
|
||||
{ key: NPM, text: NPM },
|
||||
]
|
||||
{ key: SCRIPT, text: SCRIPT },
|
||||
{ key: NPM, text: NPM },
|
||||
];
|
||||
|
||||
const AssistDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
const [activeTab, setActiveTab] = useState(SCRIPT)
|
||||
|
||||
const { projectKey } = props;
|
||||
const [activeTab, setActiveTab] = useState(SCRIPT);
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case SCRIPT:
|
||||
return <AssistScript projectKey={projectKey} />
|
||||
case NPM:
|
||||
return <AssistNpm projectKey={projectKey} />
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case SCRIPT:
|
||||
return <AssistScript projectKey={projectKey} />;
|
||||
case NPM:
|
||||
return <AssistNpm projectKey={projectKey} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Assist</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them
|
||||
without requiring any 3rd-party screen sharing software.
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.</div>
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-assist`}</Highlight>
|
||||
<div className="mb-4" />
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-assist`}
|
||||
</Highlight>
|
||||
<div className="mb-4" />
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={(tab) => setActiveTab(tab)} />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab } onClick={ (tab) => setActiveTab(tab) }
|
||||
/>
|
||||
<div className="py-5">{renderActiveTab()}</div>
|
||||
|
||||
<div className="py-5">
|
||||
{ renderActiveTab() }
|
||||
</div>
|
||||
|
||||
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssistDoc.displayName = "AssistDoc";
|
||||
AssistDoc.displayName = 'AssistDoc';
|
||||
|
||||
export default AssistDoc;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,46 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const AxiosDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-axios`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires axios@^0.21.2 as a peer dependency.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Axios</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-axios`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires
|
||||
axios@^0.21.2 as a peer dependency.
|
||||
</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
import trackerAxios from '@openreplay/tracker-axios';
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: '${projectKey}'
|
||||
});
|
||||
tracker.use(trackerAxios(options)); // check list of available options below
|
||||
tracker.start();`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerAxios from '@openreplay/tracker-axios/cjs';
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: '${projectKey}'
|
||||
|
|
@ -47,15 +53,16 @@ function MyApp() {
|
|||
}, [])
|
||||
//...
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AxiosDoc.displayName = "AxiosDoc";
|
||||
AxiosDoc.displayName = 'AxiosDoc';
|
||||
|
||||
export default AxiosDoc;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
import React from 'react';
|
||||
import { tokenRE } from 'Types/integrations/bugsnagConfig';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import ProjectListDropdown from './ProjectListDropdown';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const BugsnagForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Bugsnag</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="bugsnag"
|
||||
formFields={[
|
||||
{
|
||||
key: 'authorizationToken',
|
||||
label: 'Authorisation Token',
|
||||
},
|
||||
{
|
||||
key: 'bugsnagProjectId',
|
||||
label: 'Project',
|
||||
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
|
||||
component: ProjectListDropdown,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="bugsnag"
|
||||
formFields={[ {
|
||||
key: "authorizationToken",
|
||||
label: "Authorisation Token",
|
||||
}, {
|
||||
key: "bugsnagProjectId",
|
||||
label: "Project",
|
||||
checkIfDisplayed: config => tokenRE.test(config.authorizationToken),
|
||||
component: ProjectListDropdown,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
BugsnagForm.displayName = "BugsnagForm";
|
||||
BugsnagForm.displayName = 'BugsnagForm';
|
||||
|
||||
export default BugsnagForm;
|
||||
export default BugsnagForm;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,48 @@
|
|||
import React from 'react';
|
||||
import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import LogGroupDropdown from './LogGroupDropdown';
|
||||
import RegionDropdown from './RegionDropdown';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const CloudwatchForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Cloud Watch</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="cloudwatch"
|
||||
formFields={[
|
||||
{
|
||||
key: 'awsAccessKeyId',
|
||||
label: 'AWS Access Key ID',
|
||||
},
|
||||
{
|
||||
key: 'awsSecretAccessKey',
|
||||
label: 'AWS Secret Access Key',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
component: RegionDropdown,
|
||||
},
|
||||
{
|
||||
key: 'logGroupName',
|
||||
label: 'Log Group Name',
|
||||
component: LogGroupDropdown,
|
||||
checkIfDisplayed: (config) =>
|
||||
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
|
||||
config.region !== '' &&
|
||||
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="cloudwatch"
|
||||
formFields={[ {
|
||||
key: "awsAccessKeyId",
|
||||
label: "AWS Access Key ID",
|
||||
}, {
|
||||
key: "awsSecretAccessKey",
|
||||
label: "AWS Secret Access Key",
|
||||
}, {
|
||||
key: "region",
|
||||
label: "Region",
|
||||
component: RegionDropdown,
|
||||
}, {
|
||||
key: "logGroupName",
|
||||
label: "Log Group Name",
|
||||
component: LogGroupDropdown,
|
||||
checkIfDisplayed: config =>
|
||||
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
|
||||
config.region !== '' &&
|
||||
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
CloudwatchForm.displayName = "CloudwatchForm";
|
||||
CloudwatchForm.displayName = 'CloudwatchForm';
|
||||
|
||||
export default CloudwatchForm;
|
||||
export default CloudwatchForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const DatadogForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Datadog</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="datadog"
|
||||
formFields={[
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
key: 'applicationKey',
|
||||
label: 'Application Key',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="datadog"
|
||||
formFields={[ {
|
||||
key: "apiKey",
|
||||
label: "API Key",
|
||||
autoFocus: true,
|
||||
}, {
|
||||
key: "applicationKey",
|
||||
label: "Application Key",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
DatadogForm.displayName = "DatadogForm";
|
||||
DatadogForm.displayName = 'DatadogForm';
|
||||
|
||||
export default DatadogForm;
|
||||
|
|
|
|||
|
|
@ -1,75 +1,88 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import { withRequest } from 'HOCs';
|
||||
import { edit } from 'Duck/integrations/actions';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
@connect(state => ({
|
||||
config: state.getIn([ 'elasticsearch', 'instance' ])
|
||||
}), { edit })
|
||||
@connect(
|
||||
(state) => ({
|
||||
config: state.getIn(['elasticsearch', 'instance']),
|
||||
}),
|
||||
{ edit }
|
||||
)
|
||||
@withRequest({
|
||||
dataName: "isValid",
|
||||
initialData: false,
|
||||
dataWrapper: data => data.state,
|
||||
requestName: "validateConfig",
|
||||
endpoint: '/integrations/elasticsearch/test',
|
||||
method: 'POST',
|
||||
dataName: 'isValid',
|
||||
initialData: false,
|
||||
dataWrapper: (data) => data.state,
|
||||
requestName: 'validateConfig',
|
||||
endpoint: '/integrations/elasticsearch/test',
|
||||
method: 'POST',
|
||||
})
|
||||
export default class ElasticsearchForm extends React.PureComponent {
|
||||
componentWillReceiveProps(newProps) {
|
||||
const { config: { host, port, apiKeyId, apiKey } } = this.props;
|
||||
const { loading, config } = newProps;
|
||||
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
|
||||
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
|
||||
this.validateConfig(newProps);
|
||||
componentWillReceiveProps(newProps) {
|
||||
const {
|
||||
config: { host, port, apiKeyId, apiKey },
|
||||
} = this.props;
|
||||
const { loading, config } = newProps;
|
||||
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
|
||||
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
|
||||
this.validateConfig(newProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig = (newProps) => {
|
||||
const { config } = newProps;
|
||||
this.props.validateConfig({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
apiKeyId: config.apiKeyId,
|
||||
apiKey: config.apiKey,
|
||||
}).then((res) => {
|
||||
const { isValid } = this.props;
|
||||
this.props.edit('elasticsearch', { isValid: isValid })
|
||||
});
|
||||
}
|
||||
validateConfig = (newProps) => {
|
||||
const { config } = newProps;
|
||||
this.props
|
||||
.validateConfig({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
apiKeyId: config.apiKeyId,
|
||||
apiKey: config.apiKey,
|
||||
})
|
||||
.then((res) => {
|
||||
const { isValid } = this.props;
|
||||
this.props.edit('elasticsearch', { isValid: isValid });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="elasticsearch"
|
||||
formFields={[ {
|
||||
key: "host",
|
||||
label: "Host",
|
||||
}, {
|
||||
key: "apiKeyId",
|
||||
label: "API Key ID",
|
||||
}, {
|
||||
key: "apiKey",
|
||||
label: "API Key",
|
||||
}, {
|
||||
key: "indexes",
|
||||
label: "Indexes",
|
||||
}, {
|
||||
key: "port",
|
||||
label: "Port",
|
||||
type: "number",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Elasticsearch</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="elasticsearch"
|
||||
formFields={[
|
||||
{
|
||||
key: 'host',
|
||||
label: 'Host',
|
||||
},
|
||||
{
|
||||
key: 'apiKeyId',
|
||||
label: 'API Key ID',
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
},
|
||||
{
|
||||
key: 'indexes',
|
||||
label: 'Indexes',
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
label: 'Port',
|
||||
type: 'number',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const FetchDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-fetch --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Fetch</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-fetch --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
import trackerFetch from '@openreplay/tracker-fetch';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -34,11 +37,11 @@ tracker.start();
|
|||
export const fetch = tracker.use(trackerFetch(<options>)); // check list of available options below
|
||||
//...
|
||||
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerFetch from '@openreplay/tracker-fetch/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -54,15 +57,16 @@ export const fetch = tracker.use(trackerFetch(<options>)); // check list of avai
|
|||
//...
|
||||
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FetchDoc.displayName = "FetchDoc";
|
||||
FetchDoc.displayName = 'FetchDoc';
|
||||
|
||||
export default FetchDoc;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const GithubForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
|
||||
</div>
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Github</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject
|
||||
name="github"
|
||||
customPath="github"
|
||||
formFields={[
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
ignoreProject
|
||||
name="issues"
|
||||
customPath="github"
|
||||
formFields={[
|
||||
{
|
||||
key: "token",
|
||||
label: "Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
GithubForm.displayName = "GithubForm";
|
||||
GithubForm.displayName = 'GithubForm';
|
||||
|
||||
export default GithubForm;
|
||||
export default GithubForm;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,36 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
|
||||
const GraphQLDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<p>This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</p>
|
||||
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-graphql --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.</p>
|
||||
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">GraphQL</h3>
|
||||
<div className="p-5">
|
||||
<p>
|
||||
This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</p>
|
||||
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-graphql --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It
|
||||
returns result without changes.
|
||||
</p>
|
||||
|
||||
<div className="py-3" />
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerGraphQL from '@openreplay/tracker-graphql';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -33,11 +39,11 @@ const tracker = new OpenReplay({
|
|||
tracker.start();
|
||||
//...
|
||||
export const recordGraphQL = tracker.use(trackerGraphQL());`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerGraphQL from '@openreplay/tracker-graphql/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -51,15 +57,16 @@ function SomeFunctionalComponent() {
|
|||
}
|
||||
//...
|
||||
export const recordGraphQL = tracker.use(trackerGraphQL());`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GraphQLDoc.displayName = "GraphQLDoc";
|
||||
GraphQLDoc.displayName = 'GraphQLDoc';
|
||||
|
||||
export default GraphQLDoc;
|
||||
|
|
|
|||
|
|
@ -1,144 +1,147 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Input, Form, Button, Checkbox } from 'UI';
|
||||
import { Input, Form, Button, Checkbox, Loader } from 'UI';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import { save, init, edit, remove, fetchList } from 'Duck/integrations/actions';
|
||||
import { fetchIntegrationList } from 'Duck/integrations/integrations';
|
||||
|
||||
@connect((state, { name, customPath }) => ({
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
initialSiteId: state.getIn([ 'site', 'siteId' ]),
|
||||
list: state.getIn([ name, 'list' ]),
|
||||
config: state.getIn([ name, 'instance']),
|
||||
saving: state.getIn([ customPath || name, 'saveRequest', 'loading']),
|
||||
removing: state.getIn([ name, 'removeRequest', 'loading']),
|
||||
}), {
|
||||
save,
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
fetchList
|
||||
})
|
||||
@connect(
|
||||
(state, { name, customPath }) => ({
|
||||
sites: state.getIn(['site', 'list']),
|
||||
initialSiteId: state.getIn(['site', 'siteId']),
|
||||
list: state.getIn([name, 'list']),
|
||||
config: state.getIn([name, 'instance']),
|
||||
loading: state.getIn([name, 'fetchRequest', 'loading']),
|
||||
saving: state.getIn([customPath || name, 'saveRequest', 'loading']),
|
||||
removing: state.getIn([name, 'removeRequest', 'loading']),
|
||||
siteId: state.getIn(['integrations', 'siteId']),
|
||||
}),
|
||||
{
|
||||
save,
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
fetchList,
|
||||
fetchIntegrationList,
|
||||
}
|
||||
)
|
||||
export default class IntegrationForm extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const currentSiteId = this.props.initialSiteId;
|
||||
this.state = { currentSiteId };
|
||||
this.init(currentSiteId);
|
||||
}
|
||||
|
||||
write = ({ target: { value, name: key, type, checked } }) => {
|
||||
if (type === 'checkbox')
|
||||
this.props.edit(this.props.name, { [ key ]: checked })
|
||||
else
|
||||
this.props.edit(this.props.name, { [ key ]: value })
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// const currentSiteId = this.props.initialSiteId;
|
||||
// this.state = { currentSiteId };
|
||||
// this.init(currentSiteId);
|
||||
}
|
||||
|
||||
onChangeSelect = ({ value }) => {
|
||||
const { sites, list, name } = this.props;
|
||||
const site = sites.find(s => s.id === value.value);
|
||||
this.setState({ currentSiteId: site.id })
|
||||
this.init(value.value);
|
||||
}
|
||||
write = ({ target: { value, name: key, type, checked } }) => {
|
||||
if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked });
|
||||
else this.props.edit(this.props.name, { [key]: value });
|
||||
};
|
||||
|
||||
init = (siteId) => {
|
||||
const { list, name } = this.props;
|
||||
const config = (parseInt(siteId) > 0) ? list.find(s => s.projectId === siteId) : undefined;
|
||||
this.props.init(name, config ? config : list.first());
|
||||
}
|
||||
// onChangeSelect = ({ value }) => {
|
||||
// const { sites, list, name } = this.props;
|
||||
// const site = sites.find((s) => s.id === value.value);
|
||||
// this.setState({ currentSiteId: site.id });
|
||||
// this.init(value.value);
|
||||
// };
|
||||
|
||||
save = () => {
|
||||
const { config, name, customPath } = this.props;
|
||||
const isExists = config.exists();
|
||||
const { currentSiteId } = this.state;
|
||||
const { ignoreProject } = this.props;
|
||||
this.props.save(customPath || name, (!ignoreProject ? currentSiteId : null), config)
|
||||
.then(() => {
|
||||
this.props.fetchList(name)
|
||||
this.props.onClose();
|
||||
if (isExists) return;
|
||||
});
|
||||
}
|
||||
// init = (siteId) => {
|
||||
// const { list, name } = this.props;
|
||||
// const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined;
|
||||
// this.props.init(name, config ? config : list.first());
|
||||
// };
|
||||
|
||||
remove = () => {
|
||||
const { name, config, ignoreProject } = this.props;
|
||||
this.props.remove(name, !ignoreProject ? config.projectId : null).then(function() {
|
||||
this.props.onClose();
|
||||
this.props.fetchList(name)
|
||||
}.bind(this));
|
||||
}
|
||||
save = () => {
|
||||
const { config, name, customPath, ignoreProject } = this.props;
|
||||
const isExists = config.exists();
|
||||
// const { currentSiteId } = this.state;
|
||||
this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => {
|
||||
// this.props.fetchList(name);
|
||||
this.props.onClose();
|
||||
if (isExists) return;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props;
|
||||
const { currentSiteId } = this.state;
|
||||
remove = () => {
|
||||
const { name, config, ignoreProject } = this.props;
|
||||
this.props.remove(name, !ignoreProject ? config.projectId : null).then(
|
||||
function () {
|
||||
this.props.onClose();
|
||||
this.props.fetchList(name);
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ph-20">
|
||||
<Form>
|
||||
{!ignoreProject &&
|
||||
<Form.Field>
|
||||
<label>{ 'OpenReplay Project' }</label>
|
||||
<SiteDropdown
|
||||
value={ currentSiteId }
|
||||
onChange={ this.onChangeSelect }
|
||||
/>
|
||||
</Form.Field>
|
||||
}
|
||||
render() {
|
||||
const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props;
|
||||
// const { currentSiteId } = this.state;
|
||||
|
||||
{ formFields.map(({
|
||||
key,
|
||||
label,
|
||||
placeholder=label,
|
||||
component: Component = 'input',
|
||||
type = "text",
|
||||
checkIfDisplayed,
|
||||
autoFocus=false
|
||||
}) => (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
|
||||
((type === 'checkbox') ?
|
||||
<Form.Field key={ key }>
|
||||
<Checkbox
|
||||
label={label}
|
||||
name={ key }
|
||||
value={ config[ key ] }
|
||||
onChange={ this.write }
|
||||
placeholder={ placeholder }
|
||||
type={ Component === 'input' ? type : null }
|
||||
/>
|
||||
</Form.Field>
|
||||
:
|
||||
<Form.Field key={ key }>
|
||||
<label>{ label }</label>
|
||||
<Input
|
||||
name={ key }
|
||||
value={ config[ key ] }
|
||||
onChange={ this.write }
|
||||
placeholder={ placeholder }
|
||||
type={ Component === 'input' ? type : null }
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</Form.Field>
|
||||
)
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !config.validate() }
|
||||
loading={ saving || loading }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ config.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className="ph-20">
|
||||
<Form>
|
||||
{/* {!ignoreProject && (
|
||||
<Form.Field>
|
||||
<label>{'OpenReplay Project'}</label>
|
||||
<SiteDropdown value={currentSiteId} onChange={this.onChangeSelect} />
|
||||
</Form.Field>
|
||||
)} */}
|
||||
|
||||
{config.exists() && (
|
||||
<Button
|
||||
loading={ removing }
|
||||
onClick={ this.remove }
|
||||
>
|
||||
{ 'Delete' }
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{formFields.map(
|
||||
({
|
||||
key,
|
||||
label,
|
||||
placeholder = label,
|
||||
component: Component = 'input',
|
||||
type = 'text',
|
||||
checkIfDisplayed,
|
||||
autoFocus = false,
|
||||
}) =>
|
||||
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
|
||||
(type === 'checkbox' ? (
|
||||
<Form.Field key={key}>
|
||||
<Checkbox
|
||||
label={label}
|
||||
name={key}
|
||||
value={config[key]}
|
||||
onChange={this.write}
|
||||
placeholder={placeholder}
|
||||
type={Component === 'input' ? type : null}
|
||||
/>
|
||||
</Form.Field>
|
||||
) : (
|
||||
<Form.Field key={key}>
|
||||
<label>{label}</label>
|
||||
<Input
|
||||
name={key}
|
||||
value={config[key]}
|
||||
onChange={this.write}
|
||||
placeholder={placeholder}
|
||||
type={Component === 'input' ? type : null}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</Form.Field>
|
||||
))
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!config.validate()}
|
||||
loading={saving || loading}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{config.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
{config.exists() && (
|
||||
<Button loading={removing} onClick={this.remove}>
|
||||
{'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
|
||||
const onDocLinkClick = (e, link) => {
|
||||
e.stopPropagation();
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
|
||||
const IntegrationItem = ({
|
||||
deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false
|
||||
}) => {
|
||||
return (
|
||||
<div className={ cn(stl.wrapper, 'mb-4', { [stl.integrated] : integrated })} onClick={ e => onClick(e, url) }>
|
||||
{integrated && (
|
||||
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
|
||||
<Icon name="check" size="14" color="white" />
|
||||
</div>
|
||||
)}
|
||||
<img className="h-12 w-12" src={'/assets/' + icon + '.svg'} alt="integration" />
|
||||
<h4 className="my-2">{ title }</h4>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default IntegrationItem;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
integration: any;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
integrated?: boolean;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const IntegrationItem = (props: Props) => {
|
||||
const { integration, integrated, hide = false } = props;
|
||||
return hide ? <></> : (
|
||||
<div className={cn(stl.wrapper, 'mb-4', { [stl.integrated]: integrated })} onClick={(e) => props.onClick(e)}>
|
||||
{integrated && (
|
||||
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
|
||||
<Popup content="Integrated" delay={0}>
|
||||
<Icon name="check" size="14" color="white" />
|
||||
</Popup>
|
||||
</div>
|
||||
)}
|
||||
<img className="h-12 w-12" src={'/assets/' + integration.icon + '.svg'} alt="integration" />
|
||||
<div className="text-center mt-2">
|
||||
<h4 className="">{integration.title}</h4>
|
||||
{/* <p className="text-sm color-gray-medium m-0 p-0 h-3">{integration.subtitle && integration.subtitle}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: any, props: Props) => {
|
||||
const list = state.getIn([props.integration.slug, 'list']) || [];
|
||||
return {
|
||||
// integrated: props.integration.slug === 'issues' ? !!(list.first() && list.first().token) : list.size > 0,
|
||||
};
|
||||
})(IntegrationItem);
|
||||
173
frontend/app/components/Client/Integrations/Integrations.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React, { useEffect } from 'react';
|
||||
import BugsnagForm from './BugsnagForm';
|
||||
import CloudwatchForm from './CloudwatchForm';
|
||||
import DatadogForm from './DatadogForm';
|
||||
import ElasticsearchForm from './ElasticsearchForm';
|
||||
import GithubForm from './GithubForm';
|
||||
import IntegrationItem from './IntegrationItem';
|
||||
import JiraForm from './JiraForm';
|
||||
import NewrelicForm from './NewrelicForm';
|
||||
import RollbarForm from './RollbarForm';
|
||||
import SentryForm from './SentryForm';
|
||||
import SlackForm from './SlackForm';
|
||||
import StackdriverForm from './StackdriverForm';
|
||||
import SumoLogicForm from './SumoLogicForm';
|
||||
import { fetch, init } from 'Duck/integrations/actions';
|
||||
import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations';
|
||||
import { connect } from 'react-redux';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import ReduxDoc from './ReduxDoc';
|
||||
import VueDoc from './VueDoc';
|
||||
import GraphQLDoc from './GraphQLDoc';
|
||||
import NgRxDoc from './NgRxDoc';
|
||||
import MobxDoc from './MobxDoc';
|
||||
import FetchDoc from './FetchDoc';
|
||||
import ProfilerDoc from './ProfilerDoc';
|
||||
import AxiosDoc from './AxiosDoc';
|
||||
import AssistDoc from './AssistDoc';
|
||||
import { PageTitle, Loader } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
fetch: (name: string, siteId: string) => void;
|
||||
init: () => void;
|
||||
fetchIntegrationList: (siteId: any) => void;
|
||||
integratedList: any;
|
||||
initialSiteId: string;
|
||||
setSiteId: (siteId: string) => void;
|
||||
siteId: string;
|
||||
hideHeader?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
function Integrations(props: Props) {
|
||||
const { initialSiteId, hideHeader = false, loading = false } = props;
|
||||
const { showModal } = useModal();
|
||||
const [integratedList, setIntegratedList] = React.useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const list = props.integratedList.filter((item: any) => item.integrated).map((item: any) => item.name);
|
||||
setIntegratedList(list);
|
||||
}, [props.integratedList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.siteId) {
|
||||
props.setSiteId(initialSiteId);
|
||||
props.fetchIntegrationList(initialSiteId);
|
||||
} else {
|
||||
props.fetchIntegrationList(props.siteId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClick = (integration: any) => {
|
||||
if (integration.slug) {
|
||||
props.fetch(integration.slug, props.siteId);
|
||||
}
|
||||
showModal(integration.component, { right: true });
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }: any) => {
|
||||
props.setSiteId(value.value);
|
||||
props.fetchIntegrationList(value.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
|
||||
{integrations.map((cat: any) => (
|
||||
<div className="mb-2 border-b last:border-none py-3">
|
||||
<div className="flex items-center">
|
||||
<h2 className="font-medium text-lg">{cat.title}</h2>
|
||||
{cat.isProject && (
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-wrap mx-4">
|
||||
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
|
||||
</div>
|
||||
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="">{cat.description}</div>
|
||||
|
||||
<div className="flex flex-wrap mt-4">
|
||||
{/* <Loader loading={loading && cat.isProject}> */}
|
||||
{cat.integrations.map((integration: any) => (
|
||||
<IntegrationItem
|
||||
integrated={integratedList.includes(integration.slug)}
|
||||
key={integration.name}
|
||||
integration={integration}
|
||||
onClick={() => onClick(integration)}
|
||||
hide={
|
||||
(integration.slug === 'github' && integratedList.includes('jira')) ||
|
||||
(integration.slug === 'jira' && integratedList.includes('github'))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{/* </Loader> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
initialSiteId: state.getIn(['site', 'siteId']),
|
||||
integratedList: state.getIn(['integrations', 'list']) || [],
|
||||
loading: state.getIn(['integrations', 'fetchRequest', 'loading']),
|
||||
siteId: state.getIn(['integrations', 'siteId']),
|
||||
}),
|
||||
{ fetch, init, fetchIntegrationList, setSiteId }
|
||||
)(Integrations);
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
title: 'Issue Reporting and Collaborations',
|
||||
description: 'Seamlessly report issues or share issues with your team right from OpenReplay.',
|
||||
isProject: false,
|
||||
integrations: [
|
||||
{ title: 'Jira', slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: <JiraForm /> },
|
||||
{ title: 'Github', slug: 'github', category: 'Errors', icon: 'integrations/github', component: <GithubForm /> },
|
||||
{ title: 'Slack', category: 'Errors', icon: 'integrations/slack', component: <SlackForm /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Backend Logging',
|
||||
isProject: true,
|
||||
description: 'Sync your backend errors with sessions replays and see what happened front-to-back.',
|
||||
integrations: [
|
||||
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
|
||||
{ title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: <BugsnagForm /> },
|
||||
{ title: 'Rollbar', slug: 'rollbar', icon: 'integrations/rollbar', component: <RollbarForm /> },
|
||||
{ title: 'Elasticsearch', slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: <ElasticsearchForm /> },
|
||||
{ title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: <DatadogForm /> },
|
||||
{ title: 'Sumo Logic', slug: 'sumologic', icon: 'integrations/sumologic', component: <SumoLogicForm /> },
|
||||
{
|
||||
title: 'Stackdriver',
|
||||
slug: 'stackdriver',
|
||||
icon: 'integrations/google-cloud',
|
||||
component: <StackdriverForm />,
|
||||
},
|
||||
{ title: 'CloudWatch', slug: 'cloudwatch', icon: 'integrations/aws', component: <CloudwatchForm /> },
|
||||
{ title: 'Newrelic', slug: 'newrelic', icon: 'integrations/newrelic', component: <NewrelicForm /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
isProject: false,
|
||||
description:
|
||||
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
|
||||
integrations: [
|
||||
{ title: 'Redux', slug: '', icon: 'integrations/redux', component: <ReduxDoc /> },
|
||||
{ title: 'VueX', slug: '', icon: 'integrations/vuejs', component: <VueDoc /> },
|
||||
{ title: 'GraphQL', slug: '', icon: 'integrations/graphql', component: <GraphQLDoc /> },
|
||||
{ title: 'NgRx', slug: '', icon: 'integrations/ngrx', component: <NgRxDoc /> },
|
||||
{ title: 'MobX', slug: '', icon: 'integrations/mobx', component: <MobxDoc /> },
|
||||
{ title: 'Fetch', slug: '', icon: 'integrations/openreplay', component: <FetchDoc /> },
|
||||
{ title: 'Profiler', slug: '', icon: 'integrations/openreplay', component: <ProfilerDoc /> },
|
||||
{ title: 'Axios', slug: '', icon: 'integrations/openreplay', component: <AxiosDoc /> },
|
||||
{ title: 'Assist', slug: '', icon: 'integrations/openreplay', component: <AssistDoc /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,37 +1,41 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const JiraForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
||||
</div>
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Jira</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject={true}
|
||||
name="jira"
|
||||
customPath="jira"
|
||||
formFields={[
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
label: 'API Token',
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: 'JIRA URL',
|
||||
placeholder: 'E.x. https://myjira.atlassian.net',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
ignoreProject={true}
|
||||
name="issues"
|
||||
customPath="jira"
|
||||
formFields={[ {
|
||||
key: "username",
|
||||
label: "Username",
|
||||
autoFocus: true
|
||||
}, {
|
||||
key: "token",
|
||||
label: "API Token",
|
||||
}, {
|
||||
key: "url",
|
||||
label: "JIRA URL",
|
||||
placeholder: 'E.x. https://myjira.atlassian.net'
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
JiraForm.displayName = "JiraForm";
|
||||
JiraForm.displayName = 'JiraForm';
|
||||
|
||||
export default JiraForm;
|
||||
export default JiraForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,35 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const MobxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-mobx --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">MobX</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-mobx --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux
|
||||
chain.
|
||||
</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerMobX from '@openreplay/tracker-mobx';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -31,11 +37,11 @@ const tracker = new OpenReplay({
|
|||
});
|
||||
tracker.use(trackerMobX(<options>)); // check list of available options below
|
||||
tracker.start();`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerMobX from '@openreplay/tracker-mobx/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -48,15 +54,16 @@ function SomeFunctionalComponent() {
|
|||
tracker.start();
|
||||
}, [])
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate MobX" url="https://docs.openreplay.com/plugins/mobx" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate MobX" url="https://docs.openreplay.com/plugins/mobx" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MobxDoc.displayName = "MobxDoc";
|
||||
MobxDoc.displayName = 'MobxDoc';
|
||||
|
||||
export default MobxDoc;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,36 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const NewrelicForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate NewRelic" url="https://docs.openreplay.com/integrations/newrelic" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">New Relic</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate NewRelic" url="https://docs.openreplay.com/integrations/newrelic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="newrelic"
|
||||
formFields={[
|
||||
{
|
||||
key: 'applicationId',
|
||||
label: 'Application Id',
|
||||
},
|
||||
{
|
||||
key: 'xQueryKey',
|
||||
label: 'X-Query-Key',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'EU Region',
|
||||
type: 'checkbox',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="newrelic"
|
||||
formFields={[ {
|
||||
key: "applicationId",
|
||||
label: "Application Id",
|
||||
}, {
|
||||
key: "xQueryKey",
|
||||
label: "X-Query-Key",
|
||||
}, {
|
||||
key: 'region',
|
||||
label: 'EU Region',
|
||||
type: 'checkbox'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
NewrelicForm.displayName = "NewrelicForm";
|
||||
NewrelicForm.displayName = 'NewrelicForm';
|
||||
|
||||
export default NewrelicForm;
|
||||
export default NewrelicForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const NgRxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-ngrx --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Add the generated meta-reducer into your imports. See NgRx documentation for more details.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">NgRx</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-ngrx --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Add the generated meta-reducer into your imports. See NgRx documentation for more details.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
import { reducers } from './reducers';
|
||||
import OpenReplay from '@openreplay/tracker';
|
||||
import trackerNgRx from '@openreplay/tracker-ngrx';
|
||||
|
|
@ -39,11 +42,11 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
|
|||
imports: [StoreModule.forRoot(reducers, { metaReducers })]
|
||||
})
|
||||
export class AppModule {}`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
import { reducers } from './reducers';
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerNgRx from '@openreplay/tracker-ngrx/cjs';
|
||||
|
|
@ -64,15 +67,16 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
|
|||
})
|
||||
export class AppModule {}
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate NgRx" url="https://docs.openreplay.com/plugins/ngrx" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate NgRx" url="https://docs.openreplay.com/plugins/ngrx" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NgRxDoc.displayName = "NgRxDoc";
|
||||
NgRxDoc.displayName = 'NgRxDoc';
|
||||
|
||||
export default NgRxDoc;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const ProfilerDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-profiler --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Profiler</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function
|
||||
call.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-profiler --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerProfiler from '@openreplay/tracker-profiler';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -36,11 +39,11 @@ export const profiler = tracker.use(trackerProfiler());
|
|||
const fn = profiler('call_name')(() => {
|
||||
//...
|
||||
}, thisArg); // thisArg is optional`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerProfiler from '@openreplay/tracker-profiler/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -58,15 +61,16 @@ const fn = profiler('call_name')(() => {
|
|||
//...
|
||||
}, thisArg); // thisArg is optional
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Profiler" url="https://docs.openreplay.com/plugins/profiler" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Profiler" url="https://docs.openreplay.com/plugins/profiler" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilerDoc.displayName = "ProfilerDoc";
|
||||
ProfilerDoc.displayName = 'ProfilerDoc';
|
||||
|
||||
export default ProfilerDoc;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from '../../../shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const ReduxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-redux --save`}
|
||||
</Highlight>
|
||||
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Redux</h3>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>Initialize the tracker then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-redux --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>Initialize the tracker then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
import OpenReplay from '@openreplay/tracker';
|
||||
import trackerRedux from '@openreplay/tracker-redux';
|
||||
//...
|
||||
|
|
@ -35,11 +38,11 @@ const store = createStore(
|
|||
reducer,
|
||||
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
||||
);`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerRedux from '@openreplay/tracker-redux/cjs';
|
||||
//...
|
||||
|
|
@ -57,15 +60,16 @@ const store = createStore(
|
|||
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
||||
);
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Redux" url="https://docs.openreplay.com/plugins/redux" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Redux" url="https://docs.openreplay.com/plugins/redux" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReduxDoc.displayName = "ReduxDoc";
|
||||
ReduxDoc.displayName = 'ReduxDoc';
|
||||
|
||||
export default ReduxDoc;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const RollbarForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate Rollbar" url="https://docs.openreplay.com/integrations/rollbar" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Rollbar</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate Rollbar" url="https://docs.openreplay.com/integrations/rollbar" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="rollbar"
|
||||
formFields={[
|
||||
{
|
||||
key: 'accessToken',
|
||||
label: 'Access Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="rollbar"
|
||||
formFields={[ {
|
||||
key: "accessToken",
|
||||
label: "Access Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
RollbarForm.displayName = "RollbarForm";
|
||||
RollbarForm.displayName = 'RollbarForm';
|
||||
|
||||
export default RollbarForm;
|
||||
export default RollbarForm;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const SentryForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Sentry" url="https://docs.openreplay.com/integrations/sentry" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Sentry</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Sentry" url="https://docs.openreplay.com/integrations/sentry" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="sentry"
|
||||
formFields={[
|
||||
{
|
||||
key: 'organizationSlug',
|
||||
label: 'Organization Slug',
|
||||
},
|
||||
{
|
||||
key: 'projectSlug',
|
||||
label: 'Project Slug',
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="sentry"
|
||||
formFields={[ {
|
||||
key: "organizationSlug",
|
||||
label: "Organization Slug",
|
||||
}, {
|
||||
key: "projectSlug",
|
||||
label: "Project Slug",
|
||||
}, {
|
||||
key: "token",
|
||||
label: "Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
SentryForm.displayName = "SentryForm";
|
||||
SentryForm.displayName = 'SentryForm';
|
||||
|
||||
export default SentryForm;
|
||||
export default SentryForm;
|
||||
|
|
|
|||
|
|
@ -1,101 +1,91 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { edit, save, init, update } from 'Duck/integrations/slack'
|
||||
import { Form, Input, Button, Message } from 'UI'
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, save, init, update } from 'Duck/integrations/slack';
|
||||
import { Form, Input, Button, Message } from 'UI';
|
||||
import { confirm } from 'UI';
|
||||
import { remove } from 'Duck/integrations/slack'
|
||||
import { remove } from 'Duck/integrations/slack';
|
||||
|
||||
class SlackAddForm extends React.PureComponent {
|
||||
componentWillUnmount() {
|
||||
this.props.init({});
|
||||
}
|
||||
|
||||
save = () => {
|
||||
const instance = this.props.instance;
|
||||
if(instance.exists()) {
|
||||
this.props.update(this.props.instance)
|
||||
} else {
|
||||
this.props.save(this.props.instance)
|
||||
}
|
||||
}
|
||||
|
||||
remove = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
})) {
|
||||
this.props.remove(id);
|
||||
componentWillUnmount() {
|
||||
this.props.init({});
|
||||
}
|
||||
}
|
||||
|
||||
write = ({ target: { name, value } }) => this.props.edit({ [ name ]: value });
|
||||
|
||||
render() {
|
||||
const { instance, saving, errors, onClose } = this.props;
|
||||
return (
|
||||
<div className="p-5" style={{ minWidth: '300px'}}>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Name</label>
|
||||
<Input
|
||||
name="name"
|
||||
value={ instance.name }
|
||||
onChange={ this.write }
|
||||
placeholder="Enter any name"
|
||||
type="text"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>URL</label>
|
||||
<Input
|
||||
name="endpoint"
|
||||
value={ instance.endpoint }
|
||||
onChange={ this.write }
|
||||
placeholder="Slack webhook URL"
|
||||
type="text"
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !instance.validate() }
|
||||
loading={ saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ instance.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={ onClose }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={ () => this.remove(instance.webhookId) }
|
||||
disabled={ !instance.exists() }
|
||||
>
|
||||
{ 'Delete' }
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{ errors &&
|
||||
<div className="my-3">
|
||||
{ errors.map(error => <Message visible={ errors } size="mini" error key={ error } >{ error }</Message>) }
|
||||
</div>
|
||||
save = () => {
|
||||
const instance = this.props.instance;
|
||||
if (instance.exists()) {
|
||||
this.props.update(this.props.instance);
|
||||
} else {
|
||||
this.props.save(this.props.instance);
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
remove = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`,
|
||||
})
|
||||
) {
|
||||
this.props.remove(id);
|
||||
}
|
||||
};
|
||||
|
||||
write = ({ target: { name, value } }) => this.props.edit({ [name]: value });
|
||||
|
||||
render() {
|
||||
const { instance, saving, errors, onClose } = this.props;
|
||||
return (
|
||||
<div className="p-5" style={{ minWidth: '300px' }}>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Name</label>
|
||||
<Input name="name" value={instance.name} onChange={this.write} placeholder="Enter any name" type="text" />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>URL</label>
|
||||
<Input name="endpoint" value={instance.endpoint} onChange={this.write} placeholder="Slack webhook URL" type="text" />
|
||||
</Form.Field>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!instance.validate()}
|
||||
loading={saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose}>{'Cancel'}</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
|
||||
{'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{errors && (
|
||||
<div className="my-3">
|
||||
{errors.map((error) => (
|
||||
<Message visible={errors} size="mini" error key={error}>
|
||||
{error}
|
||||
</Message>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
instance: state.getIn(['slack', 'instance']),
|
||||
saving: state.getIn(['slack', 'saveRequest', 'loading']),
|
||||
errors: state.getIn([ 'slack', 'saveRequest', 'errors' ]),
|
||||
}), { edit, save, init, remove, update })(SlackAddForm)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
instance: state.getIn(['slack', 'instance']),
|
||||
saving: state.getIn(['slack', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['slack', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{ edit, save, init, remove, update }
|
||||
)(SlackAddForm);
|
||||
|
|
|
|||
|
|
@ -1,49 +1,51 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { NoContent } from 'UI';
|
||||
import { remove, edit } from 'Duck/integrations/slack'
|
||||
import { remove, edit, init } from 'Duck/integrations/slack';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
function SlackChannelList(props) {
|
||||
const { list } = props;
|
||||
const { list } = props;
|
||||
|
||||
const onEdit = (instance) => {
|
||||
props.edit(instance)
|
||||
props.onEdit()
|
||||
}
|
||||
const onEdit = (instance) => {
|
||||
props.edit(instance);
|
||||
props.onEdit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.</div>
|
||||
{/* <DocLink className="mt-4" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" /> */}
|
||||
<DocLink className="mt-4 text-base" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={ list.size === 0 }
|
||||
>
|
||||
{list.map(c => (
|
||||
<div
|
||||
key={c.webhookId}
|
||||
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer"
|
||||
onClick={() => onEdit(c)}
|
||||
>
|
||||
<div className="flex-grow-0" style={{ maxWidth: '90%'}}>
|
||||
<div>{c.name}</div>
|
||||
<div className="truncate test-xs color-gray-medium">
|
||||
{c.endpoint}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">
|
||||
Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.
|
||||
</div>
|
||||
<DocLink className="mt-4 text-base" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.size === 0}
|
||||
>
|
||||
{list.map((c) => (
|
||||
<div
|
||||
key={c.webhookId}
|
||||
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer hover:bg-active-blue"
|
||||
onClick={() => onEdit(c)}
|
||||
>
|
||||
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
|
||||
<div>{c.name}</div>
|
||||
<div className="truncate test-xs color-gray-medium">{c.endpoint}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['slack', 'list'])
|
||||
}), { remove, edit })(SlackChannelList)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['slack', 'list']),
|
||||
}),
|
||||
{ remove, edit, init }
|
||||
)(SlackChannelList);
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
|
||||
const SlackForm = (props) => {
|
||||
const { onEdit } = props;
|
||||
return (
|
||||
<>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SlackForm.displayName = "SlackForm";
|
||||
|
||||
export default SlackForm;
|
||||