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:
Taha Yassine Kraiem 2022-08-18 15:34:43 +01:00
commit 4a079f1b41
342 changed files with 8109 additions and 4246 deletions

13
LICENSE
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(...),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"tabWidth": 4,
"tabWidth": 2,
"useTabs": false,
"printWidth": 150,
"printWidth": 100,
"singleQuote": true
}

View file

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

View file

@ -25,6 +25,7 @@ const siteIdRequiredPaths = [
'/custom_metrics',
'/dashboards',
'/metrics',
'/unprocessed',
// '/custom_metrics/sessions',
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
.tabHeader {
display: flex;
align-items: center;
margin-bottom: 25px;
/* margin-bottom: 25px; */
& .tabTitle {
margin: 0 15px 0 0;

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
.left {
padding: 40px;
width: 320px;
font-weight: 300;
& .info {
color: $gray-medium;
font-weight: 300;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
.tabHeader {
display: flex;
align-items: center;
margin-bottom: 25px;
/* margin-bottom: 25px; */
& .tabTitle {
margin: 0 15px 0 0;

View file

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

View file

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

View file

@ -64,6 +64,7 @@ export default class CallWithErrors extends React.PureComponent {
</div>
<NoContent
size="small"
title="No recordings found"
show={ images.size === 0 }
>
<Table

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,6 +61,7 @@ function CallWithErrors(props: Props) {
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={{ height: '240px'}}
>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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