Merge remote-tracking branch 'origin/dev' into api-v1.8.0
# Conflicts: # api/chalicelib/core/projects.py # api/chalicelib/core/sessions.py # api/chalicelib/utils/pg_client.py # ee/api/chalicelib/core/projects.py
This commit is contained in:
commit
4a079f1b41
342 changed files with 8109 additions and 4246 deletions
13
LICENSE
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ 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)",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 and not self.unlimited_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
|
||||
|
|
|
|||
|
|
@ -221,7 +221,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)}
|
||||
|
||||
|
||||
|
|
@ -1138,14 +1138,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": {
|
||||
|
|
|
|||
|
|
@ -564,6 +564,8 @@ class _SessionSearchEventRaw(__MixedSearchFilter):
|
|||
assert len(values["source"]) > 0 and isinstance(values["source"][0], int), \
|
||||
f"source of type int is 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:
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
UserUUID: userUUID,
|
||||
SessionID: strconv.FormatUint(tokenData.ID, 10),
|
||||
BeaconSizeLimit: e.cfg.BeaconSizeLimit,
|
||||
StartTimestamp: e.services.Flaker.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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
6
ee/utilities/package-lock.json
generated
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -198,6 +202,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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) {
|
|||
}, [label]);
|
||||
|
||||
const { startDate, endDate, rangeValue } = filter;
|
||||
console.log('startDate', startDate);
|
||||
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() });
|
||||
|
||||
const onDateChange = (e) => {
|
||||
|
|
@ -40,7 +41,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>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Button, Loader, NoContent, 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';
|
||||
|
|
@ -71,21 +71,21 @@ function CustomFields(props) {
|
|||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
<Button rounded={true} icon="plus" variant="outline" onClick={() => init()} />
|
||||
<TextLink
|
||||
icon="book"
|
||||
className="ml-auto color-gray-medium"
|
||||
href="https://docs.openreplay.com/installation/metadata"
|
||||
label="Documentation"
|
||||
/>
|
||||
<Button variant="primary" onClick={() => init()}>Add</Button>
|
||||
</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.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { Button } from 'UI';
|
||||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ field, onEdit, disabled }) => {
|
||||
|
|
@ -17,9 +17,7 @@ const ListItem = ({ field, onEdit, disabled }) => {
|
|||
>
|
||||
<span>{field.key}</span>
|
||||
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
|
||||
<div className={styles.button}>
|
||||
<Icon name="edit" color="teal" size="18" />
|
||||
</div>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
/* margin-bottom: 25px; */
|
||||
|
||||
& .tabTitle {
|
||||
margin: 0 15px 0 0;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function Integrations(props: Props) {
|
|||
<h2 className="font-medium text-lg">{cat.title}</h2>
|
||||
{cat.isProject && (
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-wrap ml-4">
|
||||
<div className="flex flex-wrap mx-4">
|
||||
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
|
||||
</div>
|
||||
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ function SlackChannelList(props) {
|
|||
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
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
import { fetchList } from 'Duck/integrations/slack';
|
||||
import { fetchList, init } from 'Duck/integrations/slack';
|
||||
import { connect } from 'react-redux';
|
||||
import SlackAddForm from './SlackAddForm';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { Button } from 'UI';
|
||||
|
||||
interface Props {
|
||||
onEdit: (integration: any) => void;
|
||||
istance: any;
|
||||
fetchList: any;
|
||||
init: any;
|
||||
}
|
||||
const SlackForm = (props: Props) => {
|
||||
const { istance } = props;
|
||||
|
|
@ -19,21 +21,29 @@ const SlackForm = (props: Props) => {
|
|||
setActive(true);
|
||||
};
|
||||
|
||||
const onNew = () => {
|
||||
setActive(true);
|
||||
props.init({});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '650px' : '350px' }}>
|
||||
<div style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Slack</h3>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
|
||||
{active && (
|
||||
<div className="border-l h-full">
|
||||
<div className="border-r h-full" style={{ width: '350px' }}>
|
||||
<SlackAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="shrink-0" style={{ width: '350px' }}>
|
||||
<div className="flex items-center p-5">
|
||||
<h3 className="text-2xl mr-3">Slack</h3>
|
||||
<Button rounded={true} icon="plus" variant="outline" onClick={onNew}/>
|
||||
</div>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -44,5 +54,5 @@ export default connect(
|
|||
(state: any) => ({
|
||||
istance: state.getIn(['slack', 'instance']),
|
||||
}),
|
||||
{ fetchList }
|
||||
{ fetchList, init }
|
||||
)(SlackForm);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
.left {
|
||||
padding: 40px;
|
||||
width: 320px;
|
||||
font-weight: 300;
|
||||
& .info {
|
||||
color: $gray-medium;
|
||||
font-weight: 300;
|
||||
|
|
|
|||
|
|
@ -1,156 +1,145 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { Loader, IconButton, Popup, NoContent, SlideModal } from 'UI'
|
||||
import { connect } from 'react-redux'
|
||||
import stl from './roles.module.css'
|
||||
import RoleForm from './components/RoleForm'
|
||||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Loader, Popup, NoContent, Button } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './roles.module.css';
|
||||
import RoleForm from './components/RoleForm';
|
||||
import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/roles';
|
||||
import RoleItem from './components/RoleItem'
|
||||
import RoleItem from './components/RoleItem';
|
||||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
init: (role?: any) => void,
|
||||
edit: (role: any) => void,
|
||||
instance: any,
|
||||
roles: any[],
|
||||
deleteRole: (id: any) => Promise<void>,
|
||||
fetchList: () => Promise<void>,
|
||||
account: any,
|
||||
permissionsMap: any,
|
||||
removeErrors: any,
|
||||
resetErrors: () => void,
|
||||
projectsMap: any,
|
||||
loading: boolean;
|
||||
init: (role?: any) => void;
|
||||
edit: (role: any) => void;
|
||||
instance: any;
|
||||
roles: any[];
|
||||
deleteRole: (id: any) => Promise<void>;
|
||||
fetchList: () => Promise<void>;
|
||||
account: any;
|
||||
permissionsMap: any;
|
||||
removeErrors: any;
|
||||
resetErrors: () => void;
|
||||
projectsMap: any;
|
||||
}
|
||||
|
||||
function Roles(props: Props) {
|
||||
const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props
|
||||
const [showModal, setShowmModal] = useState(false)
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props;
|
||||
// const [showModal, setShowmModal] = useState(false);
|
||||
const { showModal, hideModal } = useModal();
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (removeErrors && removeErrors.size > 0) {
|
||||
removeErrors.forEach(e => {
|
||||
toast.error(e)
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
props.resetErrors()
|
||||
}
|
||||
}, [removeErrors])
|
||||
useEffect(() => {
|
||||
if (removeErrors && removeErrors.size > 0) {
|
||||
removeErrors.forEach((e) => {
|
||||
toast.error(e);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
props.resetErrors();
|
||||
};
|
||||
}, [removeErrors]);
|
||||
|
||||
const closeModal = (showToastMessage) => {
|
||||
if (showToastMessage) {
|
||||
toast.success(showToastMessage)
|
||||
props.fetchList()
|
||||
}
|
||||
setShowmModal(false)
|
||||
setTimeout(() => {
|
||||
init()
|
||||
}, 100)
|
||||
}
|
||||
const closeModal = (showToastMessage) => {
|
||||
if (showToastMessage) {
|
||||
toast.success(showToastMessage);
|
||||
props.fetchList();
|
||||
}
|
||||
setShowmModal(false);
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const editHandler = role => {
|
||||
init(role)
|
||||
setShowmModal(true)
|
||||
}
|
||||
const editHandler = (role: any) => {
|
||||
init(role);
|
||||
showModal(<RoleForm closeModal={hideModal} permissionsMap={permissionsMap} deleteHandler={deleteHandler} />, { right: true });
|
||||
// setShowmModal(true);
|
||||
};
|
||||
|
||||
const deleteHandler = async (role) => {
|
||||
if (await confirm({
|
||||
header: 'Roles',
|
||||
confirmation: `Are you sure you want to remove this role?`
|
||||
})) {
|
||||
deleteRole(role.roleId)
|
||||
}
|
||||
}
|
||||
const deleteHandler = async (role: any) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Roles',
|
||||
confirmation: `Are you sure you want to remove this role?`,
|
||||
})
|
||||
) {
|
||||
deleteRole(role.roleId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Loader loading={ loading }>
|
||||
<SlideModal
|
||||
title={ instance.exists() ? "Edit Role" : "Create Role" }
|
||||
size="small"
|
||||
isDisplayed={showModal }
|
||||
content={ showModal && <RoleForm closeModal={closeModal} permissionsMap={permissionsMap} deleteHandler={deleteHandler} /> }
|
||||
onClose={ closeModal }
|
||||
/>
|
||||
<div className={ stl.wrapper }>
|
||||
<div className={ cn(stl.tabHeader, 'flex items-center') }>
|
||||
<div className="flex items-center mr-auto">
|
||||
<h3 className={ cn(stl.tabTitle, "text-2xl") }>Roles and Access</h3>
|
||||
<Popup
|
||||
content="You don’t have the permissions to perform this action."
|
||||
disabled={ isAdmin }
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
disabled={ !isAdmin }
|
||||
onClick={ () => setShowmModal(true) }
|
||||
/>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Loader loading={loading}>
|
||||
<div className={stl.wrapper}>
|
||||
<div className={cn(stl.tabHeader, 'flex items-center')}>
|
||||
<div className="flex items-center mr-auto">
|
||||
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
|
||||
<Popup content="You don’t have the permissions to perform this action." disabled={isAdmin}>
|
||||
<Button variant="primary" onClick={() => setShowmModal(true)}>Add</Button>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent title="No roles are available." size="small" show={false}>
|
||||
<div className={''}>
|
||||
<div className={cn('flex items-start py-3 border-b px-3 pr-20 font-medium')}>
|
||||
<div className="" style={{ width: '20%' }}>
|
||||
Title
|
||||
</div>
|
||||
<div className="" style={{ width: '30%' }}>
|
||||
Project Access
|
||||
</div>
|
||||
<div className="" style={{ width: '50%' }}>
|
||||
Feature Access
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{roles.map((role) => (
|
||||
<RoleItem
|
||||
role={role}
|
||||
isAdmin={isAdmin}
|
||||
permissions={permissionsMap}
|
||||
projects={projectsMap}
|
||||
editHandler={editHandler}
|
||||
deleteHandler={deleteHandler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent
|
||||
title="No roles are available."
|
||||
size="small"
|
||||
show={ false }
|
||||
icon
|
||||
>
|
||||
<div className={''}>
|
||||
<div className={cn('flex items-start py-3 border-b px-3 pr-20 font-medium')}>
|
||||
<div className="" style={{ width: '20%'}}>Title</div>
|
||||
<div className="" style={{ width: '30%'}}>Project Access</div>
|
||||
<div className="" style={{ width: '50%'}}>Feature Access</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{roles.map(role => (
|
||||
<RoleItem
|
||||
role={role}
|
||||
isAdmin={isAdmin}
|
||||
permissions={permissionsMap}
|
||||
projects={projectsMap}
|
||||
editHandler={editHandler}
|
||||
deleteHandler={deleteHandler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</Loader>
|
||||
</React.Fragment>
|
||||
)
|
||||
</Loader>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => {
|
||||
const permissions = state.getIn(['roles', 'permissions'])
|
||||
const permissionsMap = {}
|
||||
permissions.forEach(p => {
|
||||
permissionsMap[p.value] = p.text
|
||||
});
|
||||
const projects = state.getIn([ 'site', 'list' ])
|
||||
return {
|
||||
instance: state.getIn(['roles', 'instance']) || null,
|
||||
permissionsMap: permissionsMap,
|
||||
roles: state.getIn(['roles', 'list']),
|
||||
removeErrors: state.getIn(['roles', 'removeRequest', 'errors']),
|
||||
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
|
||||
account: state.getIn([ 'user', 'account' ]),
|
||||
projectsMap: projects.reduce((acc, p) => {
|
||||
acc[ p.get('id') ] = p.get('name')
|
||||
return acc
|
||||
}
|
||||
, {}),
|
||||
}
|
||||
}, { init, edit, fetchList, deleteRole, resetErrors })(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles))
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const permissions = state.getIn(['roles', 'permissions']);
|
||||
const permissionsMap = {};
|
||||
permissions.forEach((p: any) => {
|
||||
permissionsMap[p.value] = p.text;
|
||||
});
|
||||
const projects = state.getIn(['site', 'list']);
|
||||
return {
|
||||
instance: state.getIn(['roles', 'instance']) || null,
|
||||
permissionsMap: permissionsMap,
|
||||
roles: state.getIn(['roles', 'list']),
|
||||
removeErrors: state.getIn(['roles', 'removeRequest', 'errors']),
|
||||
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
|
||||
account: state.getIn(['user', 'account']),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[p.get('id')] = p.get('name');
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
{ init, edit, fetchList, deleteRole, resetErrors }
|
||||
)(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles));
|
||||
|
|
|
|||
|
|
@ -1,203 +1,195 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import stl from './roleForm.module.css'
|
||||
import { save, edit } from 'Duck/roles'
|
||||
import { Form, Input, Button, Checkbox, Icon } from 'UI'
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './roleForm.module.css';
|
||||
import { save, edit } from 'Duck/roles';
|
||||
import { Form, Input, Button, Checkbox, Icon } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
interface Permission {
|
||||
name: string,
|
||||
value: string
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
role: any,
|
||||
edit: (role: any) => void,
|
||||
save: (role: any) => Promise<void>,
|
||||
closeModal: (toastMessage?: string) => void,
|
||||
saving: boolean,
|
||||
permissions: Array<Permission>[]
|
||||
projectOptions: Array<any>[],
|
||||
permissionsMap: any,
|
||||
projectsMap: any,
|
||||
deleteHandler: (id: any) => Promise<void>,
|
||||
role: any;
|
||||
edit: (role: any) => void;
|
||||
save: (role: any) => Promise<void>;
|
||||
closeModal: (toastMessage?: string) => void;
|
||||
saving: boolean;
|
||||
permissions: Array<Permission>[];
|
||||
projectOptions: Array<any>[];
|
||||
permissionsMap: any;
|
||||
projectsMap: any;
|
||||
deleteHandler: (id: any) => Promise<void>;
|
||||
}
|
||||
|
||||
const RoleForm = (props: Props) => {
|
||||
const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props
|
||||
let focusElement = useRef<any>(null)
|
||||
const _save = () => {
|
||||
save(role).then(() => {
|
||||
closeModal(role.exists() ? "Role updated" : "Role created");
|
||||
})
|
||||
}
|
||||
const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props;
|
||||
let focusElement = useRef<any>(null);
|
||||
const _save = () => {
|
||||
save(role).then(() => {
|
||||
closeModal(role.exists() ? 'Role updated' : 'Role created');
|
||||
});
|
||||
};
|
||||
|
||||
const write = ({ target: { value, name } }) => edit({ [ name ]: value })
|
||||
const write = ({ target: { value, name } }) => edit({ [name]: value });
|
||||
|
||||
const onChangePermissions = (e) => {
|
||||
const { permissions } = role
|
||||
const index = permissions.indexOf(e)
|
||||
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e)
|
||||
edit({ permissions: _perms })
|
||||
}
|
||||
const onChangePermissions = (e) => {
|
||||
const { permissions } = role;
|
||||
const index = permissions.indexOf(e);
|
||||
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e);
|
||||
edit({ permissions: _perms });
|
||||
};
|
||||
|
||||
const onChangeProjects = (e) => {
|
||||
const { projects } = role
|
||||
const index = projects.indexOf(e)
|
||||
const _projects = index === -1 ? projects.push(e) : projects.remove(index)
|
||||
edit({ projects: _projects })
|
||||
}
|
||||
const onChangeProjects = (e) => {
|
||||
const { projects } = role;
|
||||
const index = projects.indexOf(e);
|
||||
const _projects = index === -1 ? projects.push(e) : projects.remove(index);
|
||||
edit({ projects: _projects });
|
||||
};
|
||||
|
||||
const writeOption = ({ name, value }: any) => {
|
||||
if (name === 'permissions') {
|
||||
onChangePermissions(value)
|
||||
} else if (name === 'projects') {
|
||||
onChangeProjects(value)
|
||||
}
|
||||
}
|
||||
const writeOption = ({ name, value }: any) => {
|
||||
if (name === 'permissions') {
|
||||
onChangePermissions(value);
|
||||
} else if (name === 'projects') {
|
||||
onChangeProjects(value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAllProjects = () => {
|
||||
const { allProjects } = role
|
||||
edit({ allProjects: !allProjects })
|
||||
}
|
||||
const toggleAllProjects = () => {
|
||||
const { allProjects } = role;
|
||||
edit({ allProjects: !allProjects });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
focusElement && focusElement.current && focusElement.current.focus()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
focusElement && focusElement.current && focusElement.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={ stl.form }>
|
||||
<Form onSubmit={ _save } >
|
||||
<Form.Field>
|
||||
<label>{ 'Title' }</label>
|
||||
<Input
|
||||
ref={ focusElement }
|
||||
name="name"
|
||||
value={ role.name }
|
||||
onChange={ write }
|
||||
className={ stl.input }
|
||||
id="name-field"
|
||||
placeholder="Ex. Admin"
|
||||
/>
|
||||
</Form.Field>
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{role.exists() ? 'Edit Role' : 'Create Role'}</h3>
|
||||
<div className="px-5">
|
||||
<Form onSubmit={_save}>
|
||||
<Form.Field>
|
||||
<label>{'Title'}</label>
|
||||
<Input
|
||||
ref={focusElement}
|
||||
name="name"
|
||||
value={role.name}
|
||||
onChange={write}
|
||||
className={stl.input}
|
||||
id="name-field"
|
||||
placeholder="Ex. Admin"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{ 'Project Access' }</label>
|
||||
<Form.Field>
|
||||
<label>{'Project Access'}</label>
|
||||
|
||||
<div className="flex my-3">
|
||||
<Checkbox
|
||||
name="allProjects"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={ role.allProjects }
|
||||
onClick={toggleAllProjects}
|
||||
label={''}
|
||||
/>
|
||||
<div className="cursor-pointer" onClick={toggleAllProjects}>
|
||||
<div>All Projects</div>
|
||||
<span className="text-xs text-gray-600">
|
||||
(Uncheck to select specific projects)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{ !role.allProjects && (
|
||||
<>
|
||||
<Select
|
||||
isSearchable
|
||||
name="projects"
|
||||
options={ projectOptions }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'projects', value: value.value }) }
|
||||
value={null}
|
||||
/>
|
||||
{ role.projects.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{ role.projects.map(p => (
|
||||
OptionLabel(projectsMap, p, onChangeProjects)
|
||||
)) }
|
||||
<div className="flex my-3">
|
||||
<Checkbox
|
||||
name="allProjects"
|
||||
className="font-medium mr-3"
|
||||
type="checkbox"
|
||||
checked={role.allProjects}
|
||||
onClick={toggleAllProjects}
|
||||
label={''}
|
||||
/>
|
||||
<div className="cursor-pointer leading-none select-none" onClick={toggleAllProjects}>
|
||||
<div>All Projects</div>
|
||||
<span className="text-xs text-gray-600">(Uncheck to select specific projects)</span>
|
||||
</div>
|
||||
</div>
|
||||
{!role.allProjects && (
|
||||
<>
|
||||
<Select
|
||||
isSearchable
|
||||
name="projects"
|
||||
options={projectOptions}
|
||||
onChange={({ value }: any) => writeOption({ name: 'projects', value: value.value })}
|
||||
value={null}
|
||||
/>
|
||||
{role.projects.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.projects.map((p) => OptionLabel(projectsMap, p, onChangeProjects))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Capability Access'}</label>
|
||||
<Select
|
||||
isSearchable
|
||||
name="permissions"
|
||||
options={permissions}
|
||||
onChange={({ value }: any) => writeOption({ name: 'permissions', value: value.value })}
|
||||
value={null}
|
||||
/>
|
||||
{role.permissions.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.permissions.map((p) => OptionLabel(permissionsMap, p, onChangePermissions))}
|
||||
</div>
|
||||
)}
|
||||
</Form.Field>
|
||||
</Form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button onClick={_save} disabled={!role.validate()} loading={saving} variant="primary" className="float-left mr-2">
|
||||
{role.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{role.exists() && <Button onClick={closeModal}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
{role.exists() && (
|
||||
<Button variant="text" onClick={() => props.deleteHandler(role)}>
|
||||
<Icon name="trash" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{ 'Capability Access' }</label>
|
||||
<Select
|
||||
isSearchable
|
||||
name="permissions"
|
||||
options={ permissions }
|
||||
onChange={ ({ value }: any) => writeOption({ name: 'permissions', value: value.value }) }
|
||||
value={null}
|
||||
/>
|
||||
{ role.permissions.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{ role.permissions.map(p => (
|
||||
OptionLabel(permissionsMap, p, onChangePermissions)
|
||||
)) }
|
||||
</div>
|
||||
)}
|
||||
</Form.Field>
|
||||
</Form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button
|
||||
onClick={ _save }
|
||||
disabled={ !role.validate() }
|
||||
loading={ saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ role.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
{ role.exists() && (
|
||||
<Button
|
||||
onClick={ closeModal }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{ role.exists() && (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={ () => props.deleteHandler(role) }
|
||||
>
|
||||
<Icon name="trash" size="18"/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: any) => {
|
||||
const role = state.getIn(['roles', 'instance'])
|
||||
const projects = state.getIn([ 'site', 'list' ])
|
||||
return {
|
||||
role,
|
||||
projectOptions: projects.map((p: any) => ({
|
||||
key: p.get('id'),
|
||||
value: p.get('id'),
|
||||
label: p.get('name'),
|
||||
// isDisabled: role.projects.includes(p.get('id')),
|
||||
})).filter(({ value }: any) => !role.projects.includes(value)).toJS(),
|
||||
permissions: state.getIn(['roles', 'permissions']).filter(({ value }: any) => !role.permissions.includes(value))
|
||||
.map(({ text, value }: any) => ({ label: text, value })).toJS(),
|
||||
saving: state.getIn([ 'roles', 'saveRequest', 'loading' ]),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[ p.get('id') ] = p.get('name')
|
||||
return acc
|
||||
}
|
||||
, {}),
|
||||
}
|
||||
}, { edit, save })(RoleForm);
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const role = state.getIn(['roles', 'instance']);
|
||||
const projects = state.getIn(['site', 'list']);
|
||||
return {
|
||||
role,
|
||||
projectOptions: projects
|
||||
.map((p: any) => ({
|
||||
key: p.get('id'),
|
||||
value: p.get('id'),
|
||||
label: p.get('name'),
|
||||
// isDisabled: role.projects.includes(p.get('id')),
|
||||
}))
|
||||
.filter(({ value }: any) => !role.projects.includes(value))
|
||||
.toJS(),
|
||||
permissions: state
|
||||
.getIn(['roles', 'permissions'])
|
||||
.filter(({ value }: any) => !role.permissions.includes(value))
|
||||
.map(({ text, value }: any) => ({ label: text, value }))
|
||||
.toJS(),
|
||||
saving: state.getIn(['roles', 'saveRequest', 'loading']),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[p.get('id')] = p.get('name');
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
{ edit, save }
|
||||
)(RoleForm);
|
||||
|
||||
function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) {
|
||||
return <div className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between">
|
||||
<div>{nameMap[p]}</div>
|
||||
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between">
|
||||
<div>{nameMap[p]}</div>
|
||||
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,58 @@
|
|||
import React from 'react'
|
||||
import { Icon, Link } from 'UI'
|
||||
import stl from './roleItem.module.css'
|
||||
import cn from 'classnames'
|
||||
import React from 'react';
|
||||
import { Icon, Link, Button } from 'UI';
|
||||
import stl from './roleItem.module.css';
|
||||
import cn from 'classnames';
|
||||
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
|
||||
|
||||
|
||||
function PermisionLabel({ label }: any) {
|
||||
return (
|
||||
<div className={cn(stl.label, 'mb-2')}>{ label }</div>
|
||||
);
|
||||
return <div className={cn(stl.label, 'mb-2')}>{label}</div>;
|
||||
}
|
||||
|
||||
function PermisionLabelLinked({ label, route }: any) {
|
||||
return (
|
||||
<Link to={route}><div className={cn(stl.label, 'mb-2 bg-active-blue color-teal')}>{ label }</div></Link>
|
||||
);
|
||||
return (
|
||||
<Link to={route}>
|
||||
<div className={cn(stl.label, 'mb-2 bg-active-blue color-teal')}>{label}</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
role: any,
|
||||
deleteHandler?: (role: any) => void,
|
||||
editHandler?: (role: any) => void,
|
||||
permissions: any,
|
||||
isAdmin: boolean,
|
||||
projects: any,
|
||||
role: any;
|
||||
deleteHandler?: (role: any) => void;
|
||||
editHandler?: (role: any) => void;
|
||||
permissions: any;
|
||||
isAdmin: boolean;
|
||||
projects: any;
|
||||
}
|
||||
function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions, projects }: Props) {
|
||||
return (
|
||||
<div className={cn('flex items-start relative py-4 hover border-b last:border-none px-3 pr-20 group')}>
|
||||
<div className="flex" style={{ width: '20%'}}>
|
||||
<Icon name="user-alt" size="16" marginRight="10" />
|
||||
{ role.name }
|
||||
</div>
|
||||
<div className="flex items-start flex-wrap" style={{ width: '30%'}}>
|
||||
{role.allProjects ? (
|
||||
<PermisionLabelLinked label="All projects" route={clientRoute(CLIENT_TABS.SITES)}/>
|
||||
) : (
|
||||
role.projects.map(p => (
|
||||
<PermisionLabel label={projects[p]} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start flex-wrap" style={{ width: '50%'}}>
|
||||
<div className="flex items-center flex-wrap">
|
||||
{role.permissions.map((permission: any) => (
|
||||
<PermisionLabel label={permissions[permission]} key={permission.id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={ cn(stl.actions, 'absolute right-0 top-0 bottom-0 mr-8 invisible group-hover:visible') }>
|
||||
{isAdmin && !!editHandler &&
|
||||
<div className={ cn(stl.button, {[stl.disabled] : role.protected }) } onClick={ () => editHandler(role) }>
|
||||
<Icon name="edit" size="16" color="teal"/>
|
||||
return (
|
||||
<div className={cn('flex items-start relative py-4 hover border-b last:border-none px-3 pr-20 group')}>
|
||||
<div className="flex" style={{ width: '20%' }}>
|
||||
<Icon name="user-alt" size="16" marginRight="10" />
|
||||
{role.name}
|
||||
</div>
|
||||
<div className="flex items-start flex-wrap" style={{ width: '30%' }}>
|
||||
{role.allProjects ? (
|
||||
<PermisionLabelLinked label="All projects" route={clientRoute(CLIENT_TABS.SITES)} />
|
||||
) : (
|
||||
role.projects.map((p) => <PermisionLabel label={projects[p]} />)
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start flex-wrap" style={{ width: '50%' }}>
|
||||
<div className="flex items-center flex-wrap">
|
||||
{role.permissions.map((permission: any) => (
|
||||
<PermisionLabel label={permissions[permission]} key={permission.id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.actions, 'absolute right-0 top-0 bottom-0 mr-8 invisible group-hover:visible')}>
|
||||
{isAdmin && !!editHandler && (
|
||||
<Button variant="text-primary" icon="pencil" disabled={role.protected} onClick={() => editHandler(role)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleItem;
|
||||
export default RoleItem;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
|
|||
};
|
||||
return (
|
||||
<Popup content={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}>
|
||||
<Button rounded={true} variant="outline" icon="plus" onClick={onClick} disabled={!canAddProject || !isAdmin}></Button>
|
||||
<Button variant="primary" onClick={onClick} disabled={!canAddProject || !isAdmin}>Add</Button>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function InstallButton(props: Props) {
|
|||
);
|
||||
};
|
||||
return (
|
||||
<Button size="small" variant="primary" onClick={onClick}>
|
||||
<Button size="small" variant="text-primary" onClick={onClick}>
|
||||
{'Installation Steps'}
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Loader, Button, Popup, TextLink } from 'UI';
|
||||
import { Loader, Button, Popup, TextLink, NoContent } from 'UI';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site';
|
||||
import stl from './sites.module.css';
|
||||
|
|
@ -13,6 +13,7 @@ import InstallButton from './InstallButton';
|
|||
import ProjectKey from './ProjectKey';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { getInitials } from 'App/utils';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const STATUS_MESSAGE_MAP = {
|
||||
[RED]: ' There seems to be an issue (please verify your installation)',
|
||||
|
|
@ -74,8 +75,18 @@ class Sites extends React.PureComponent {
|
|||
<SiteSearch onChange={(value) => this.setState({ searchQuery: value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={stl.list}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_AUDIT_TRAIL} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">No matching results.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={!loading && filteredSites.size === 0}
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-2 w-full items-center border-b px-2 py-3 font-medium">
|
||||
<div className="col-span-4">Project Name</div>
|
||||
<div className="col-span-4">Key</div>
|
||||
|
|
@ -115,6 +126,7 @@ class Sites extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
@ -130,5 +142,5 @@ function EditButton({ isAdmin, onClick }) {
|
|||
onClick();
|
||||
showModal(<NewSiteForm onClose={hideModal} />);
|
||||
};
|
||||
return <Button icon="edit" variant="text" disabled={!isAdmin} onClick={_onClick} />;
|
||||
return <Button icon="edit" variant="text-primary" disabled={!isAdmin} onClick={_onClick} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Popup, IconButton } from 'UI';
|
||||
import { Popup, IconButton, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
|
|
@ -14,7 +14,8 @@ function AddUserButton({ isAdmin = false, onClick }: any ) {
|
|||
<Popup
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : (!cannAddUser ? LIMIT_WARNING : 'Add team member') }` }
|
||||
>
|
||||
<IconButton
|
||||
<Button disabled={ !cannAddUser || !isAdmin } variant="primary" onClick={ onClick }>Add</Button>
|
||||
{/* <IconButton
|
||||
id="add-button"
|
||||
disabled={ !cannAddUser || !isAdmin }
|
||||
circle
|
||||
|
|
@ -22,7 +23,7 @@ function AddUserButton({ isAdmin = false, onClick }: any ) {
|
|||
outline
|
||||
onClick={ onClick }
|
||||
className="ml-4"
|
||||
/>
|
||||
/> */}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function UserForm(props: Props) {
|
|||
<Form.Field>
|
||||
<label htmlFor="role">{ 'Role' }</label>
|
||||
<Select
|
||||
placeholder="Selct Role"
|
||||
placeholder="Select Role"
|
||||
selection
|
||||
options={ roles }
|
||||
name="roleId"
|
||||
|
|
|
|||
|
|
@ -44,13 +44,14 @@ function UserList(props: Props) {
|
|||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!loading && 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 matching results.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={!loading && length === 0}
|
||||
>
|
||||
<div className="mt-3 rounded bg-white">
|
||||
<div className="grid grid-cols-12 p-3 border-b font-medium">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import styles from './listItem.module.css';
|
||||
import { Button } from 'UI';
|
||||
|
||||
const ListItem = ({ webhook, onEdit, onDelete }) => {
|
||||
return (
|
||||
|
|
@ -10,9 +11,7 @@ const ListItem = ({ webhook, onEdit, onDelete }) => {
|
|||
<div className={styles.endpoint}>{webhook.endpoint}</div>
|
||||
</div>
|
||||
<div className="invisible group-hover:visible">
|
||||
<div className={styles.button}>
|
||||
<Icon name="edit" color="teal" size="16" />
|
||||
</div>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Button, Loader, NoContent } from 'UI';
|
||||
import { Button, Loader, NoContent, Icon } from 'UI';
|
||||
import { init, fetchList, remove } from 'Duck/webhook';
|
||||
import WebhookForm from './WebhookForm';
|
||||
import ListItem from './ListItem';
|
||||
|
|
@ -13,71 +13,77 @@ import { toast } from 'react-toastify';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
function Webhooks(props) {
|
||||
const { webhooks, loading } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { webhooks, loading } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack');
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack');
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
const init = (v) => {
|
||||
props.init(v);
|
||||
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
|
||||
};
|
||||
const init = (v) => {
|
||||
props.init(v);
|
||||
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
|
||||
};
|
||||
|
||||
const removeWebhook = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`,
|
||||
})
|
||||
) {
|
||||
props.remove(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
const removeWebhook = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`,
|
||||
})
|
||||
) {
|
||||
props.remove(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
<Button rounded={true} icon="plus" variant="outline" onClick={() => init()} />
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
{/* <Button rounded={true} icon="plus" variant="outline" onClick={() => init()} /> */}
|
||||
<Button variant="primary" onClick={() => init()}>Add</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-base text-disabled-text flex items-center mt-3">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Leverage webhooks to push OpenReplay data to other systems.
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_WEBHOOKS} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">None added yet</div>
|
||||
</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 webhooks available.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={noSlackWebhooks.size === 0}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{noSlackWebhooks.map((webhook) => (
|
||||
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
size="small"
|
||||
show={noSlackWebhooks.size === 0}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{noSlackWebhooks.map((webhook) => (
|
||||
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
loading: state.getIn(['webhooks', 'loading']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
remove,
|
||||
}
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
loading: state.getIn(['webhooks', 'loading']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
remove,
|
||||
}
|
||||
)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
/* margin-bottom: 25px; */
|
||||
|
||||
& .tabTitle {
|
||||
margin: 0 15px 0 0;
|
||||
|
|
|
|||
|
|
@ -6,43 +6,44 @@ import DashboardSideMenu from './components/DashboardSideMenu';
|
|||
import { Loader } from 'UI';
|
||||
import DashboardRouter from './components/DashboardRouter';
|
||||
import cn from 'classnames';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<{}>) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
interface RouterProps {
|
||||
siteId: string;
|
||||
dashboardId: string;
|
||||
metricId: string;
|
||||
}
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<RouterProps>) {
|
||||
const { history, match: { params: { siteId, dashboardId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const isMetricDetails = history.location.pathname.includes('/metrics/') || history.location.pathname.includes('/metric/');
|
||||
const isDashboardDetails = history.location.pathname.includes('/dashboard/')
|
||||
const isAlertsDetails = history.location.pathname.includes('/alert/')
|
||||
|
||||
const shouldHideMenu = isMetricDetails || isDashboardDetails || isAlertsDetails;
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
}
|
||||
});
|
||||
if (!dashboardId && location.pathname.includes('dashboard')) {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
props.history.push(withSiteId(`/dashboard/${dashboardId}`, siteId));
|
||||
}, () => {
|
||||
props.history.push(withSiteId('/dashboard', siteId));
|
||||
})
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<Loader loading={loading} className="mt-12">
|
||||
<div className="page-margin container-90">
|
||||
<div className={cn("side-menu", { 'hidden' : isMetricDetails })}>
|
||||
<div className={cn("side-menu", { 'hidden' : shouldHideMenu })}>
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
<div
|
||||
className={cn({
|
||||
"side-menu-margined" : !isMetricDetails,
|
||||
"container-70" : isMetricDetails
|
||||
"side-menu-margined" : !shouldHideMenu,
|
||||
"container-70" : shouldHideMenu
|
||||
})}
|
||||
>
|
||||
<DashboardRouter siteId={siteId} />
|
||||
<DashboardRouter />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class BreakdownOfLoadedResources extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default class CallWithErrors extends React.PureComponent {
|
|||
</div>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ images.size === 0 }
|
||||
>
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class CallsErrors4xx extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class CallsErrors5xx extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export default class CpuLoad extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class Crashes extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function CustomMetricOverviewChart(props: Props) {
|
|||
// unit={unit && ' ' + unit}
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<NoContent size="small" title="No recordings found" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<ResponsiveContainer height={ 220 } width="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import { Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { NoContent } from 'UI';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { tableColumnName } from 'App/constants/filterOptions';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
|
|
@ -49,19 +49,29 @@ function CustomMetricTable(props: Props) {
|
|||
onClick(filters);
|
||||
}
|
||||
return (
|
||||
<div className="" style={{ maxHeight: '240px'}}>
|
||||
<NoContent show={data.values && data.values.length === 0} size="small">
|
||||
<Table
|
||||
small
|
||||
cols={ getColumns(metric) }
|
||||
rows={ rows }
|
||||
rowClass="group"
|
||||
onRowClick={ onClickHandler }
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
<div className="" style={{ height: 240 }}>
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
show={data.values && data.values.length === 0}
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No data for the selected time period
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
small
|
||||
cols={getColumns(metric)}
|
||||
rows={rows}
|
||||
rowClass="group"
|
||||
onRowClick={onClickHandler}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricTable;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import { Pagination, NoContent, Icon } from "UI";
|
||||
import ErrorListItem from "App/components/Dashboard/components/Errors/ErrorListItem";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { useModal } from "App/components/Modal";
|
||||
import ErrorDetailsModal from "App/components/Dashboard/components/Errors/ErrorDetailsModal";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
interface Props {
|
||||
metric: any;
|
||||
data: any;
|
||||
|
|
@ -18,7 +17,6 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
|
|||
const errorId = new URLSearchParams(props.location.search).get("errorId");
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { dashboardStore } = useStore();
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const onErrorClick = (e: any, error: any) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -46,9 +44,10 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
|
|||
|
||||
return (
|
||||
<NoContent
|
||||
title={`No errors found ${overPastString(period)}`}
|
||||
title={<div className="flex items-center"><Icon name="info-circle" size={18} className="mr-2" />No data for the selected time period</div>}
|
||||
show={!data.errors || data.errors.length === 0}
|
||||
size="small"
|
||||
style={{ minHeight: 220 }}
|
||||
>
|
||||
<div className="pb-4">
|
||||
{data.errors &&
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import SessionItem from "Shared/SessionItem";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -26,7 +26,13 @@ function CustomMetricTableSessions(props: Props) {
|
|||
data.sessions.length === 0
|
||||
}
|
||||
size="small"
|
||||
title={`No sessions found ${overPastString(period)}`}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No relevant sessions found for the selected time period.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="pb-4">
|
||||
{data.sessions &&
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ function CustomMetricWidget(props: Props) {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export default class DomBuildingTime extends React.PureComponent {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<React.Fragment>
|
||||
|
|
@ -60,6 +61,7 @@ export default class DomBuildingTime extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.size === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default class ErrorsByOrigin extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class ErrorsByType extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default class ErrorsPerDomain extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.size === 0 }
|
||||
>
|
||||
<div className="w-full pt-3" style={{ height: '240px' }}>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default class FPS extends React.PureComponent {
|
|||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class LastFeedbacks extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ sessions.size === 0 }
|
||||
>
|
||||
{ sessions.map(({
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default class MemoryConsumption extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export default class MostImpactfulErrors extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ errors.size === 0 }
|
||||
>
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function BreakdownOfLoadedResources(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
@ -46,4 +47,4 @@ function BreakdownOfLoadedResources(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default BreakdownOfLoadedResources;
|
||||
export default BreakdownOfLoadedResources;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function CPULoad(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -42,7 +43,7 @@ function CPULoad(props: Props) {
|
|||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -54,4 +55,4 @@ function CPULoad(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CPULoad;
|
||||
export default CPULoad;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function CallWithErrors(props: Props) {
|
|||
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function CallsErrors4xx(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -46,4 +47,4 @@ function CallsErrors4xx(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CallsErrors4xx;
|
||||
export default CallsErrors4xx;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function CallsErrors5xx(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -46,4 +47,4 @@ function CallsErrors5xx(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CallsErrors5xx;
|
||||
export default CallsErrors5xx;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function Crashes(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -40,7 +41,7 @@ function Crashes(props: Props) {
|
|||
name="Crashes"
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -52,4 +53,4 @@ function Crashes(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Crashes;
|
||||
export default Crashes;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function DomBuildingTime(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -66,7 +67,7 @@ function DomBuildingTime(props: Props) {
|
|||
type="monotone"
|
||||
// unit="%"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -87,4 +88,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(DomBuildingTime)
|
||||
})(DomBuildingTime)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { NoContent } from 'UI';
|
|||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
|
|
@ -13,10 +13,12 @@ interface Props {
|
|||
metric?: any
|
||||
}
|
||||
function ErrorsByOrigin(props: Props) {
|
||||
const { data, metric } = props;
|
||||
const { metric } = props;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -49,4 +51,4 @@ function ErrorsByOrigin(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsByOrigin;
|
||||
export default ErrorsByOrigin;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function ErrorsByType(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -48,4 +49,4 @@ function ErrorsByType(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsByType;
|
||||
export default ErrorsByType;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ErrorsPerDomain(props: Props) {
|
|||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{metric.data.chart.map((item, i) =>
|
||||
|
|
@ -34,4 +35,4 @@ function ErrorsPerDomain(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsPerDomain;
|
||||
export default ErrorsPerDomain;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function FPS(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -44,7 +45,7 @@ function FPS(props: Props) {
|
|||
name="Avg"
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -57,4 +58,4 @@ function FPS(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default FPS;
|
||||
export default FPS;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ function MemoryConsumption(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
@ -47,7 +48,7 @@ function MemoryConsumption(props: Props) {
|
|||
unit=" mb"
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -60,4 +61,4 @@ function MemoryConsumption(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default MemoryConsumption;
|
||||
export default MemoryConsumption;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ResourceLoadedVsResponseEnd(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 246 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
@ -50,8 +51,8 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
|
|||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Legend />
|
||||
<Bar minPointSize={1} yAxisId="right" name="Images" type="monotone" dataKey="types.img" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar yAxisId="right" name="Scripts" type="monotone" dataKey="types.script" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar minPointSize={1} yAxisId="right" name="Images" type="monotone" dataKey="types.img" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar yAxisId="right" name="Scripts" type="monotone" dataKey="types.script" stackId="a" fill={Styles.colors[3]} />
|
||||
<Bar yAxisId="right" name="CSS" type="monotone" dataKey="types.stylesheet" stackId="a" fill={Styles.colors[4]} />
|
||||
<Line
|
||||
yAxisId="left"
|
||||
|
|
@ -69,4 +70,4 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ function ResourceLoadingTime(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
|
|
@ -98,7 +99,7 @@ function ResourceLoadingTime(props: Props) {
|
|||
unit=" ms"
|
||||
type="monotone"
|
||||
dataKey="avg"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -119,4 +120,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResourceLoadingTime)
|
||||
})(ResourceLoadingTime)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function ResponseTime(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -67,7 +68,7 @@ function ResponseTime(props: Props) {
|
|||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
|
|
@ -88,4 +89,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResponseTime)
|
||||
})(ResponseTime)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ function ResponseTimeDistribution(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -125,4 +126,4 @@ function ResponseTimeDistribution(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResponseTimeDistribution;
|
||||
export default ResponseTimeDistribution;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ function SessionsAffectedByJSErrors(props: Props) {
|
|||
const { data, metric } = props;
|
||||
return (
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
|
|
@ -44,4 +45,4 @@ function SessionsAffectedByJSErrors(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SessionsAffectedByJSErrors;
|
||||
export default SessionsAffectedByJSErrors;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue