Merge remote-tracking branch 'origin/dev' into api-v1.9.5
This commit is contained in:
commit
f9498939d9
124 changed files with 3058 additions and 3573 deletions
33
.github/workflows/api-ee.yaml
vendored
33
.github/workflows/api-ee.yaml
vendored
|
|
@ -1,6 +1,11 @@
|
|||
# This action will push the chalice changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: 'Skip Security checks if there is a unfixable vuln or error. Value: true/false'
|
||||
required: false
|
||||
default: 'false'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
|
@ -46,8 +51,23 @@ jobs:
|
|||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd api
|
||||
PUSH_IMAGE=1 bash build.sh ee
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("chalice" "alerts")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
PUSH_IMAGE=1 bash -x ./build.sh ee
|
||||
- name: Creating old image input
|
||||
run: |
|
||||
#
|
||||
|
|
@ -94,6 +114,17 @@ jobs:
|
|||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
- name: Alert slack
|
||||
if: ${{ failure() }}
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: ee
|
||||
SLACK_TITLE: "Failed ${{ github.workflow }}"
|
||||
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
|
||||
SLACK_USERNAME: "OR Bot"
|
||||
SLACK_MESSAGE: 'Build failed :bomb:'
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
|
|
|
|||
33
.github/workflows/api.yaml
vendored
33
.github/workflows/api.yaml
vendored
|
|
@ -1,6 +1,11 @@
|
|||
# This action will push the chalice changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: 'Skip Security checks if there is a unfixable vuln or error. Value: true/false'
|
||||
required: false
|
||||
default: 'false'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
|
@ -45,8 +50,23 @@ jobs:
|
|||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd api
|
||||
PUSH_IMAGE=1 bash build.sh
|
||||
PUSH_IMAGE=0 bash -x ./build.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("chalice" "alerts")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
PUSH_IMAGE=1 bash -x ./build.sh
|
||||
- name: Creating old image input
|
||||
run: |
|
||||
#
|
||||
|
|
@ -93,6 +113,17 @@ jobs:
|
|||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
- name: Alert slack
|
||||
if: ${{ failure() }}
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: foss
|
||||
SLACK_TITLE: "Failed ${{ github.workflow }}"
|
||||
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
|
||||
SLACK_USERNAME: "OR Bot"
|
||||
SLACK_MESSAGE: 'Build failed :bomb:'
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
|
|
|
|||
30
.github/workflows/workers-ee.yaml
vendored
30
.github/workflows/workers-ee.yaml
vendored
|
|
@ -7,6 +7,10 @@ on:
|
|||
description: 'Name of a single service to build(in small letters). "all" to build everything'
|
||||
required: false
|
||||
default: 'false'
|
||||
skip_security_checks:
|
||||
description: 'Skip Security checks if there is a unfixable vuln or error. Value: true/false'
|
||||
required: false
|
||||
default: 'false'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
|
@ -61,6 +65,7 @@ jobs:
|
|||
#
|
||||
set -x
|
||||
touch /tmp/images_to_build.txt
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
tmp_param=${{ github.event.inputs.build_service }}
|
||||
build_param=${tmp_param:-'false'}
|
||||
case ${build_param} in
|
||||
|
|
@ -89,7 +94,18 @@ jobs:
|
|||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
echo "Bulding $image"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh ee $image
|
||||
PUSH_IMAGE=0 bash -x ./build.sh skip $image
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
./trivy image --exit-code 1 --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
PUSH_IMAGE=1 bash -x ./build.sh skip $image
|
||||
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
|
|
@ -140,6 +156,18 @@ jobs:
|
|||
# Deploy command
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -
|
||||
|
||||
- name: Alert slack
|
||||
if: ${{ failure() }}
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: ee
|
||||
SLACK_TITLE: "Failed ${{ github.workflow }}"
|
||||
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
|
||||
SLACK_USERNAME: "OR Bot"
|
||||
SLACK_MESSAGE: 'Build failed :bomb:'
|
||||
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
|
|
|
|||
10
.github/workflows/workers.yaml
vendored
10
.github/workflows/workers.yaml
vendored
|
|
@ -153,6 +153,16 @@ jobs:
|
|||
# Deploy command
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -
|
||||
|
||||
- name: Alert slack
|
||||
if: ${{ failure() }}
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: foss
|
||||
SLACK_TITLE: "Failed ${{ github.workflow }}"
|
||||
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
|
||||
SLACK_USERNAME: "OR Bot"
|
||||
SLACK_MESSAGE: 'Build failed :bomb:'
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
|
|
|
|||
17
api/build.sh
17
api/build.sh
|
|
@ -6,8 +6,16 @@
|
|||
# Default will be OSS build.
|
||||
|
||||
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
|
||||
set -e
|
||||
|
||||
# Helper function
|
||||
exit_err() {
|
||||
err_code=$1
|
||||
if [[ err_code != 0 ]]; then
|
||||
exit $err_code
|
||||
fi
|
||||
}
|
||||
|
||||
environment=$1
|
||||
git_sha1=${IMAGE_TAG:-$(git rev-parse HEAD)}
|
||||
envarg="default-foss"
|
||||
check_prereq() {
|
||||
|
|
@ -45,12 +53,13 @@ function build_api(){
|
|||
}
|
||||
|
||||
check_prereq
|
||||
build_api $1
|
||||
build_api $environment
|
||||
echo buil_complete
|
||||
IMAGE_TAG=$IMAGE_TAG PUSH_IMAGE=$PUSH_IMAGE DOCKER_REPO=$DOCKER_REPO bash build_alerts.sh $1
|
||||
|
||||
[[ $1 == "ee" ]] && {
|
||||
[[ $environment == "ee" ]] && {
|
||||
cp ../ee/api/build_crons.sh .
|
||||
IMAGE_TAG=$IMAGE_TAG PUSH_IMAGE=$PUSH_IMAGE DOCKER_REPO=$DOCKER_REPO bash build_crons.sh $1
|
||||
exit_err $?
|
||||
rm build_crons.sh
|
||||
}
|
||||
} || true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from os import access, R_OK
|
||||
from os.path import exists as path_exists
|
||||
from os.path import exists as path_exists, getsize
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
|
|
@ -207,9 +207,11 @@ def get_raw_mob_by_id(project_id, session_id):
|
|||
path_to_file = efs_path + "/" + __get_mob_path(project_id=project_id, session_id=session_id)
|
||||
if path_exists(path_to_file):
|
||||
if not access(path_to_file, R_OK):
|
||||
raise HTTPException(400, f"Replay file found under: {efs_path};"
|
||||
f" but it is not readable, please check permissions")
|
||||
|
||||
raise HTTPException(400, f"Replay file found under: {efs_path};" +
|
||||
f" but it is not readable, please check permissions")
|
||||
# getsize return size in bytes, UNPROCESSED_MAX_SIZE is in Kb
|
||||
if (getsize(path_to_file) / 1000) >= config("UNPROCESSED_MAX_SIZE", cast=int, default=200 * 1000):
|
||||
raise HTTPException(413, "Replay file too large")
|
||||
return path_to_file
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ def __frame_is_valid(f):
|
|||
|
||||
def __format_frame(f):
|
||||
f["context"] = [] # no context by default
|
||||
if "source" in f: f.pop("source")
|
||||
if "source" in f:
|
||||
f.pop("source")
|
||||
url = f.pop("fileName")
|
||||
f["absPath"] = url
|
||||
f["filename"] = urlparse(url).path
|
||||
|
|
@ -67,8 +68,13 @@ def format_payload(p, truncate_to_first=False):
|
|||
|
||||
|
||||
def url_exists(url):
|
||||
r = requests.head(url, allow_redirects=False)
|
||||
return r.status_code == 200 and r.headers.get("Content-Type") != "text/html"
|
||||
try:
|
||||
r = requests.head(url, allow_redirects=False)
|
||||
return r.status_code == 200 and r.headers.get("Content-Type") != "text/html"
|
||||
except Exception as e:
|
||||
print(f"!! Issue checking if URL exists: {url}")
|
||||
print(e)
|
||||
return False
|
||||
|
||||
|
||||
def get_traces_group(project_id, payload):
|
||||
|
|
@ -90,8 +96,8 @@ def get_traces_group(project_id, payload):
|
|||
continue
|
||||
|
||||
if key not in payloads:
|
||||
file_exists_in_bucket = s3.exists(config('sourcemaps_bucket'), key)
|
||||
if not file_exists_in_bucket:
|
||||
file_exists_in_bucket = len(file_url) > 0 and s3.exists(config('sourcemaps_bucket'), key)
|
||||
if len(file_url) > 0 and not file_exists_in_bucket:
|
||||
print(f"{u['absPath']} sourcemap (key '{key}') doesn't exist in S3 looking in server")
|
||||
if not file_url.endswith(".map"):
|
||||
file_url += '.map'
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ def login(data: schemas.UserLoginSchema = Body(...)):
|
|||
|
||||
|
||||
@app.post('/{projectId}/sessions/search', tags=["sessions"])
|
||||
@app.post('/{projectId}/sessions/search2', tags=["sessions"])
|
||||
def sessions_search(projectId: int, data: schemas.FlatSessionsSearchPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = sessions.search_sessions(data=data, project_id=projectId, user_id=context.user_id)
|
||||
|
|
@ -867,7 +868,7 @@ def delete_slack_integration(integrationId: int, context: schemas.CurrentContext
|
|||
return webhook.delete(context.tenant_id, integrationId)
|
||||
|
||||
|
||||
@app.post('/webhooks', tags=["webhooks"])
|
||||
@app.put('/webhooks', tags=["webhooks"])
|
||||
def add_edit_webhook(data: schemas.CreateEditWebhookSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": webhook.add_edit(tenant_id=context.tenant_id, data=data.dict(), replace_none=True)}
|
||||
|
|
|
|||
2
ee/api/.gitignore
vendored
2
ee/api/.gitignore
vendored
|
|
@ -248,7 +248,7 @@ Pipfile
|
|||
/routers/core.py
|
||||
/routers/crons/core_crons.py
|
||||
/db_changes.sql
|
||||
/Dockerfile.bundle
|
||||
/Dockerfile_bundle
|
||||
/entrypoint.bundle.sh
|
||||
/chalicelib/core/heatmaps.py
|
||||
/schemas.py
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ rm -rf ./routers/base.py
|
|||
rm -rf ./routers/core.py
|
||||
rm -rf ./routers/crons/core_crons.py
|
||||
rm -rf ./db_changes.sql
|
||||
rm -rf ./Dockerfile.bundle
|
||||
rm -rf ./Dockerfile_bundle
|
||||
rm -rf ./entrypoint.bundle.sh
|
||||
rm -rf ./chalicelib/core/heatmaps.py
|
||||
rm -rf ./schemas.py
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import stl from './notifications.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { fetchList, setViewed, clearAll } from 'Duck/notifications';
|
||||
import { setLastRead } from 'Duck/announcements';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
|
@ -29,7 +29,7 @@ function Notifications(props: Props) {
|
|||
}, []);
|
||||
|
||||
return useObserver(() => (
|
||||
<Popup content={`Alerts`}>
|
||||
<Tooltip title={`Alerts`}>
|
||||
<div
|
||||
className={stl.button}
|
||||
onClick={() => showModal(<AlertTriggersModal />, { right: true })}
|
||||
|
|
@ -39,7 +39,7 @@ function Notifications(props: Props) {
|
|||
</div>
|
||||
<Icon name="bell" size="18" color="gray-dark" />
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import stl from './announcements.module.css';
|
||||
import ListItem from './ListItem';
|
||||
import { connect } from 'react-redux';
|
||||
import { SlideModal, Icon, NoContent, Popup } from 'UI';
|
||||
import { SlideModal, Icon, NoContent, Tooltip } from 'UI';
|
||||
import { fetchList, setLastRead } from 'Duck/announcements';
|
||||
import withToggle from 'Components/hocs/withToggle';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
|
@ -45,14 +45,14 @@ class Announcements extends React.Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Popup content={ `Announcements` } >
|
||||
<Tooltip content={ `Announcements` } >
|
||||
<div className={ stl.button } onClick={ this.toggleModal } data-active={ visible }>
|
||||
<div className={ stl.counter } data-hidden={ unReadNotificationsCount === 0 }>
|
||||
{ unReadNotificationsCount }
|
||||
</div>
|
||||
<Icon name="bullhorn" size="18" />
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
|
||||
<SlideModal
|
||||
title="Announcements"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Popup, Button } from 'UI';
|
||||
import { Button, Tooltip } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { toggleChatWindow } from 'Duck/sessions';
|
||||
|
|
@ -19,7 +19,6 @@ import {
|
|||
} from 'Player/MessageDistributor/managers/AssistManager';
|
||||
import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
|
||||
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { toast } from 'react-toastify';
|
||||
import { confirm } from 'UI';
|
||||
import stl from './AassistActions.module.css';
|
||||
|
|
@ -196,8 +195,8 @@ function AssistActions({
|
|||
</Tooltip>
|
||||
<div className={stl.divider} />
|
||||
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
cannotCall
|
||||
? `You don't have the permissions to perform this action.`
|
||||
: `Call ${userId ? userId : 'User'}`
|
||||
|
|
@ -219,7 +218,7 @@ function AssistActions({
|
|||
{onCall ? 'End' : isPrestart ? 'Join Call' : 'Call'}
|
||||
</Button>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
|
||||
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
|
||||
{onCall && callObject && (
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
|
||||
export default class Tooltip extends React.PureComponent {
|
||||
state = {
|
||||
open: false,
|
||||
}
|
||||
mouseOver = false
|
||||
onMouseEnter = () => {
|
||||
this.mouseOver = true;
|
||||
setTimeout(() => {
|
||||
if (this.mouseOver) this.setState({ open: true });
|
||||
}, 1000)
|
||||
}
|
||||
onMouseLeave = () => {
|
||||
this.mouseOver = false;
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { trigger, tooltip } = this.props;
|
||||
const { open } = this.state;
|
||||
return (
|
||||
<Popup
|
||||
open={ open }
|
||||
content={ tooltip }
|
||||
inverted
|
||||
>
|
||||
<span
|
||||
onMouseEnter={ this.onMouseEnter }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
>
|
||||
{ trigger }
|
||||
</span>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { SideMenuitem, Popup } from 'UI'
|
||||
import { SideMenuitem, Tooltip } from 'UI'
|
||||
import stl from './sessionMenu.module.css';
|
||||
import { clearEvents } from 'Duck/filters';
|
||||
import { issues_types } from 'Types/session/issue'
|
||||
|
|
@ -24,12 +24,11 @@ function SessionsMenu(props) {
|
|||
<span>Sessions</span>
|
||||
</div>
|
||||
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>
|
||||
<Popup
|
||||
hideOnClick={true}
|
||||
content={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
|
||||
<Tooltip
|
||||
title={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
|
||||
>
|
||||
Settings
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ const IntegrationItem = (props: Props) => {
|
|||
<div className={cn(stl.wrapper, 'mb-4', { [stl.integrated]: integrated })} onClick={(e) => props.onClick(e)}>
|
||||
{integrated && (
|
||||
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
|
||||
<Popup content="Integrated" delay={0}>
|
||||
<Tooltip title="Integrated" delay={0}>
|
||||
<Icon name="check" size="14" color="white" />
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{integration.icon.length ? <img className="h-12 w-12" src={'/assets/' + integration.icon + '.svg'} alt="integration" /> : (
|
||||
|
|
@ -33,9 +33,4 @@ const IntegrationItem = (props: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default connect((state: any, props: Props) => {
|
||||
const list = state.getIn([props.integration.slug, 'list']) || [];
|
||||
return {
|
||||
// integrated: props.integration.slug === 'issues' ? !!(list.first() && list.first().token) : list.size > 0,
|
||||
};
|
||||
})(IntegrationItem);
|
||||
export default IntegrationItem;
|
||||
|
|
|
|||
|
|
@ -1,633 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import withPageTitle from "HOCs/withPageTitle";
|
||||
import { Loader, IconButton, SlideModal } from "UI";
|
||||
import { fetchList as fetchListSlack } from "Duck/integrations/slack";
|
||||
import { remove as removeIntegrationConfig } from "Duck/integrations/actions";
|
||||
import { fetchList, init } from "Duck/integrations/actions";
|
||||
import cn from "classnames";
|
||||
|
||||
import IntegrationItem from "./IntegrationItem";
|
||||
import SentryForm from "./SentryForm";
|
||||
import GithubForm from "./GithubForm";
|
||||
import SlackForm from "./SlackForm";
|
||||
import DatadogForm from "./DatadogForm";
|
||||
import StackdriverForm from "./StackdriverForm";
|
||||
import RollbarForm from "./RollbarForm";
|
||||
import NewrelicForm from "./NewrelicForm";
|
||||
import BugsnagForm from "./BugsnagForm";
|
||||
import CloudwatchForm from "./CloudwatchForm";
|
||||
import ElasticsearchForm from "./ElasticsearchForm";
|
||||
import SumoLogicForm from "./SumoLogicForm";
|
||||
import JiraForm from "./JiraForm";
|
||||
import styles from "./integrations.module.css";
|
||||
import ReduxDoc from "./ReduxDoc";
|
||||
import VueDoc from "./VueDoc";
|
||||
import GraphQLDoc from "./GraphQLDoc";
|
||||
import NgRxDoc from "./NgRxDoc/NgRxDoc";
|
||||
import SlackAddForm from "./SlackAddForm";
|
||||
import FetchDoc from "./FetchDoc";
|
||||
import MobxDoc from "./MobxDoc";
|
||||
import ProfilerDoc from "./ProfilerDoc";
|
||||
import AssistDoc from "./AssistDoc";
|
||||
import AxiosDoc from "./AxiosDoc/AxiosDoc";
|
||||
|
||||
const NONE = -1;
|
||||
const SENTRY = 0;
|
||||
const DATADOG = 1;
|
||||
const STACKDRIVER = 2;
|
||||
const ROLLBAR = 3;
|
||||
const NEWRELIC = 4;
|
||||
const BUGSNAG = 5;
|
||||
const CLOUDWATCH = 6;
|
||||
const ELASTICSEARCH = 7;
|
||||
const SUMOLOGIC = 8;
|
||||
const JIRA = 9;
|
||||
const GITHUB = 10;
|
||||
const REDUX = 11;
|
||||
const VUE = 12;
|
||||
const GRAPHQL = 13;
|
||||
const NGRX = 14;
|
||||
const SLACK = 15;
|
||||
const FETCH = 16;
|
||||
const MOBX = 17;
|
||||
const PROFILER = 18;
|
||||
const ASSIST = 19;
|
||||
const AXIOS = 20;
|
||||
|
||||
const TITLE = {
|
||||
[SENTRY]: "Sentry",
|
||||
[SLACK]: "Slack",
|
||||
[DATADOG]: "Datadog",
|
||||
[STACKDRIVER]: "Stackdriver",
|
||||
[ROLLBAR]: "Rollbar",
|
||||
[NEWRELIC]: "New Relic",
|
||||
[BUGSNAG]: "Bugsnag",
|
||||
[CLOUDWATCH]: "CloudWatch",
|
||||
[ELASTICSEARCH]: "Elastic Search",
|
||||
[SUMOLOGIC]: "Sumo Logic",
|
||||
[JIRA]: "Jira",
|
||||
[GITHUB]: "Github",
|
||||
[REDUX]: "Redux",
|
||||
[VUE]: "VueX",
|
||||
[GRAPHQL]: "GraphQL",
|
||||
[NGRX]: "NgRx",
|
||||
[FETCH]: "Fetch",
|
||||
[MOBX]: "MobX",
|
||||
[PROFILER]: "Profiler",
|
||||
[ASSIST]: "Assist",
|
||||
[AXIOS]: "Axios",
|
||||
};
|
||||
|
||||
const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER, ASSIST];
|
||||
|
||||
const integrations = [
|
||||
"sentry",
|
||||
"datadog",
|
||||
"stackdriver",
|
||||
"rollbar",
|
||||
"newrelic",
|
||||
"bugsnag",
|
||||
"cloudwatch",
|
||||
"elasticsearch",
|
||||
"sumologic",
|
||||
"issues",
|
||||
];
|
||||
|
||||
@connect(
|
||||
(state) => {
|
||||
const props = {};
|
||||
integrations.forEach((name) => {
|
||||
props[`${name}Integrated`] =
|
||||
name === "issues"
|
||||
? !!(
|
||||
state.getIn([name, "list"]).first() &&
|
||||
state.getIn([name, "list"]).first().token
|
||||
)
|
||||
: state.getIn([name, "list"]).size > 0;
|
||||
props.loading =
|
||||
props.loading || state.getIn([name, "fetchRequest", "loading"]);
|
||||
});
|
||||
const site = state.getIn(["site", "instance"]);
|
||||
return {
|
||||
...props,
|
||||
issues: state.getIn(["issues", "list"]).first() || {},
|
||||
slackChannelListExists: state.getIn(["slack", "list"]).size > 0,
|
||||
tenantId: state.getIn(["user", "account", "tenantId"]),
|
||||
jwt: state.get("jwt"),
|
||||
projectKey: site ? site.projectKey : "",
|
||||
};
|
||||
},
|
||||
{
|
||||
fetchList,
|
||||
init,
|
||||
fetchListSlack,
|
||||
removeIntegrationConfig,
|
||||
}
|
||||
)
|
||||
@withPageTitle("Integrations - OpenReplay Preferences")
|
||||
export default class Integrations extends React.PureComponent {
|
||||
state = {
|
||||
modalContent: NONE,
|
||||
showDetailContent: false,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
integrations.forEach((name) => this.props.fetchList(name));
|
||||
this.props.fetchListSlack();
|
||||
}
|
||||
|
||||
onClickIntegrationItem = (e, url) => {
|
||||
e.preventDefault();
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
closeModal = () =>
|
||||
this.setState({ modalContent: NONE, showDetailContent: false });
|
||||
|
||||
onOauthClick = (source) => {
|
||||
if (source === GITHUB) {
|
||||
const githubUrl = `https://auth.openreplay.com/oauth/login?provider=github&back_url=${document.location.href}`;
|
||||
const options = {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: new Headers({
|
||||
Authorization: "Bearer " + this.props.jwt.toString(),
|
||||
}),
|
||||
};
|
||||
fetch(githubUrl, options).then((resp) =>
|
||||
resp.text().then((txt) => window.open(txt, "_self"))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderDetailContent() {
|
||||
switch (this.state.modalContent) {
|
||||
case SLACK:
|
||||
return (
|
||||
<SlackAddForm
|
||||
onClose={() =>
|
||||
this.setState({ showDetailContent: false })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderModalContent() {
|
||||
const { projectKey } = this.props;
|
||||
|
||||
switch (this.state.modalContent) {
|
||||
case SENTRY:
|
||||
return <SentryForm onClose={this.closeModal} />;
|
||||
case GITHUB:
|
||||
return <GithubForm onClose={this.closeModal} />;
|
||||
case SLACK:
|
||||
return (
|
||||
<SlackForm
|
||||
onClose={this.closeModal}
|
||||
onEdit={() =>
|
||||
this.setState({ showDetailContent: true })
|
||||
}
|
||||
/>
|
||||
);
|
||||
case DATADOG:
|
||||
return <DatadogForm onClose={this.closeModal} />;
|
||||
case STACKDRIVER:
|
||||
return <StackdriverForm onClose={this.closeModal} />;
|
||||
case ROLLBAR:
|
||||
return <RollbarForm onClose={this.closeModal} />;
|
||||
case NEWRELIC:
|
||||
return <NewrelicForm onClose={this.closeModal} />;
|
||||
case BUGSNAG:
|
||||
return <BugsnagForm onClose={this.closeModal} />;
|
||||
case CLOUDWATCH:
|
||||
return <CloudwatchForm onClose={this.closeModal} />;
|
||||
case ELASTICSEARCH:
|
||||
return <ElasticsearchForm onClose={this.closeModal} />;
|
||||
case SUMOLOGIC:
|
||||
return <SumoLogicForm onClose={this.closeModal} />;
|
||||
case JIRA:
|
||||
return <JiraForm onClose={this.closeModal} />;
|
||||
case REDUX:
|
||||
return (
|
||||
<ReduxDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case VUE:
|
||||
return (
|
||||
<VueDoc onClose={this.closeModal} projectKey={projectKey} />
|
||||
);
|
||||
case GRAPHQL:
|
||||
return (
|
||||
<GraphQLDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case NGRX:
|
||||
return (
|
||||
<NgRxDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case FETCH:
|
||||
return (
|
||||
<FetchDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case MOBX:
|
||||
return (
|
||||
<MobxDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case PROFILER:
|
||||
return (
|
||||
<ProfilerDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case ASSIST:
|
||||
return (
|
||||
<AssistDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
case AXIOS:
|
||||
return (
|
||||
<AxiosDoc
|
||||
onClose={this.closeModal}
|
||||
projectKey={projectKey}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
deleteHandler = (name) => {
|
||||
this.props.removeIntegrationConfig(name, null).then(
|
||||
function () {
|
||||
this.props.fetchList(name);
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
showIntegrationConfig = (type) => {
|
||||
this.setState({ modalContent: type });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
sentryIntegrated,
|
||||
stackdriverIntegrated,
|
||||
datadogIntegrated,
|
||||
rollbarIntegrated,
|
||||
newrelicIntegrated,
|
||||
bugsnagIntegrated,
|
||||
cloudwatchIntegrated,
|
||||
elasticsearchIntegrated,
|
||||
sumologicIntegrated,
|
||||
hideHeader = false,
|
||||
plugins = false,
|
||||
jiraIntegrated,
|
||||
issuesIntegrated,
|
||||
tenantId,
|
||||
slackChannelListExists,
|
||||
issues,
|
||||
} = this.props;
|
||||
const { modalContent, showDetailContent } = this.state;
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4">{TITLE[modalContent]}</div>
|
||||
{modalContent === SLACK && (
|
||||
<IconButton
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
small="small"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
showDetailContent: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
isDisplayed={modalContent !== NONE}
|
||||
onClose={this.closeModal}
|
||||
size={
|
||||
DOCS.includes(this.state.modalContent)
|
||||
? "middle"
|
||||
: "small"
|
||||
}
|
||||
content={this.renderModalContent()}
|
||||
detailContent={
|
||||
showDetailContent && this.renderDetailContent()
|
||||
}
|
||||
/>
|
||||
|
||||
{!hideHeader && (
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, "text-2xl")}>
|
||||
{"Integrations"}
|
||||
</h3>
|
||||
<p className={styles.subText}>
|
||||
Power your workflow with your favourite tools.
|
||||
</p>
|
||||
<div className={styles.divider} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plugins && (
|
||||
<div className="">
|
||||
<div className="mb-4">
|
||||
Use plugins to better debug your application's
|
||||
store, monitor queries and track performance issues.
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<IntegrationItem
|
||||
title="Redux"
|
||||
icon="integrations/redux"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(REDUX)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="VueX"
|
||||
icon="integrations/vuejs"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() => this.showIntegrationConfig(VUE)}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="GraphQL"
|
||||
icon="integrations/graphql"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(GRAPHQL)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="NgRx"
|
||||
icon="integrations/ngrx"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() => this.showIntegrationConfig(NGRX)}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="MobX"
|
||||
icon="integrations/mobx"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() => this.showIntegrationConfig(MOBX)}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Fetch"
|
||||
icon="integrations/openreplay"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(FETCH)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Profiler"
|
||||
icon="integrations/openreplay"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(PROFILER)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Axios"
|
||||
icon="integrations/openreplay"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/plugins/axios"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(AXIOS)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Assist"
|
||||
icon="integrations/assist"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/installation/assist"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(ASSIST)
|
||||
}
|
||||
// integrated={ sentryIntegrated }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!plugins && (
|
||||
<Loader loading={loading}>
|
||||
<div className={styles.content}>
|
||||
<div className="">
|
||||
<div className="mb-4">
|
||||
How are you monitoring errors and crash
|
||||
reporting?
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
{(!issues.token ||
|
||||
issues.provider !== "github") && (
|
||||
<IntegrationItem
|
||||
title="Jira"
|
||||
description="Jira is a proprietary issue tracking product developed by Atlassian that allows bug tracking and agile project management."
|
||||
icon="integrations/jira"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/jira"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(JIRA)
|
||||
}
|
||||
integrated={issuesIntegrated}
|
||||
/>
|
||||
)}
|
||||
{(!issues.token ||
|
||||
issues.provider !== "jira") && (
|
||||
<IntegrationItem
|
||||
title="Github"
|
||||
description="Easily share issues on GitHub directly from any session replay."
|
||||
icon="integrations/github"
|
||||
url={`https://auth.openreplay.com/oauth/login?provider=github&back_url=${
|
||||
window.env.ORIGIN ||
|
||||
window.location.origin
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(
|
||||
GITHUB
|
||||
)
|
||||
}
|
||||
integrated={issuesIntegrated}
|
||||
deleteHandler={
|
||||
issuesIntegrated
|
||||
? () =>
|
||||
this.deleteHandler(
|
||||
"issues"
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IntegrationItem
|
||||
title="Slack"
|
||||
description="Error tracking that helps developers monitor and fix crashes in real time."
|
||||
icon="integrations/slack"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(SLACK)
|
||||
}
|
||||
integrated={sentryIntegrated}
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Sentry"
|
||||
description="Error tracking that helps developers monitor and fix crashes in real time."
|
||||
icon="integrations/sentry"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sentry"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(SENTRY)
|
||||
}
|
||||
integrated={sentryIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="Bugsnag"
|
||||
description="Bugsnag is an error-monitoring tool that allows your developers to identify, prioritize and replicate bugs in a time-efficient and enjoyable manner."
|
||||
icon="integrations/bugsnag"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/bugsnag"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(BUGSNAG)
|
||||
}
|
||||
integrated={bugsnagIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="Rollbar"
|
||||
description="Rollbar provides real-time error tracking & debugging tools for developers."
|
||||
icon="integrations/rollbar"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/rollbar"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(ROLLBAR)
|
||||
}
|
||||
integrated={rollbarIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="Elastic Search"
|
||||
description="Elasticsearch is a distributed, RESTful search and analytics engine capable of addressing a growing number of use cases."
|
||||
icon="integrations/elasticsearch"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/elastic"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(
|
||||
ELASTICSEARCH
|
||||
)
|
||||
}
|
||||
integrated={elasticsearchIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="Datadog"
|
||||
description="Monitoring service for cloud-scale applications, providing monitoring of servers, databases, tools, and services."
|
||||
icon="integrations/datadog"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/datadog"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(DATADOG)
|
||||
}
|
||||
integrated={datadogIntegrated}
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Sumo Logic"
|
||||
description="Sumo Logic to collaborate, develop, operate, and secure their applications at cloud scale."
|
||||
icon="integrations/sumologic"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/sumo"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(
|
||||
SUMOLOGIC
|
||||
)
|
||||
}
|
||||
integrated={sumologicIntegrated}
|
||||
/>
|
||||
<IntegrationItem
|
||||
title="Stackdriver"
|
||||
description="Monitoring and management for services, containers, applications, and infrastructure."
|
||||
icon="integrations/stackdriver"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/stackdriver"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(
|
||||
STACKDRIVER
|
||||
)
|
||||
}
|
||||
integrated={stackdriverIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="CloudWatch"
|
||||
description="Amazon CloudWatch is a monitoring and management service that provides data and actionable insights for AWS, hybrid, and on-premises applications and infrastructure resources."
|
||||
icon="integrations/cloudwatch"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/cloudwatch"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(
|
||||
CLOUDWATCH
|
||||
)
|
||||
}
|
||||
integrated={cloudwatchIntegrated}
|
||||
/>
|
||||
|
||||
<IntegrationItem
|
||||
title="New Relic"
|
||||
description="New Relic's application monitoring gives you detailed performance metrics for every aspect of your environment."
|
||||
icon="integrations/newrelic"
|
||||
url={null}
|
||||
dockLink="https://docs.openreplay.com/integrations/newrelic"
|
||||
onClick={() =>
|
||||
this.showIntegrationConfig(NEWRELIC)
|
||||
}
|
||||
integrated={newrelicIntegrated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import {
|
||||
Form, IconButton, SlideModal, Input, Button, Loader,
|
||||
NoContent, Popup, CopyButton } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member';
|
||||
import { fetchList as fetchRoles } from 'Duck/roles';
|
||||
import styles from './manageUsers.module.css';
|
||||
import UserItem from './UserItem';
|
||||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached users limit.';
|
||||
|
||||
@connect(state => ({
|
||||
account: state.getIn([ 'user', 'account' ]),
|
||||
members: state.getIn([ 'members', 'list' ]).filter(u => u.id),
|
||||
member: state.getIn([ 'members', 'instance' ]),
|
||||
errors: state.getIn([ 'members', 'saveRequest', 'errors' ]),
|
||||
loading: state.getIn([ 'members', 'loading' ]),
|
||||
saving: state.getIn([ 'members', 'saveRequest', 'loading' ]),
|
||||
roles: state.getIn(['roles', 'list']).filter(r => !r.protected).map(r => ({ label: r.name, value: r.roleId })).toJS(),
|
||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
||||
}), {
|
||||
init,
|
||||
save,
|
||||
edit,
|
||||
deleteMember,
|
||||
fetchList,
|
||||
generateInviteLink,
|
||||
fetchRoles
|
||||
})
|
||||
@withPageTitle('Team - OpenReplay Preferences')
|
||||
class ManageUsers extends React.PureComponent {
|
||||
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }
|
||||
|
||||
// writeOption = (e, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
onChange = ({ name, value }) => this.props.edit({ [ name ]: value.value });
|
||||
onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked });
|
||||
setFocus = () => this.focusElement && this.focusElement.focus();
|
||||
closeModal = () => this.setState({ showModal: false });
|
||||
componentWillMount = () => {
|
||||
this.props.fetchList();
|
||||
if (this.props.isEnterprise) {
|
||||
this.props.fetchRoles();
|
||||
}
|
||||
}
|
||||
|
||||
adminLabel = (user) => {
|
||||
if (user.superAdmin) return null;
|
||||
return user.admin ? 'Admin' : '';
|
||||
};
|
||||
|
||||
editHandler = user => {
|
||||
this.init(user)
|
||||
}
|
||||
|
||||
deleteHandler = async (user) => {
|
||||
if (await confirm({
|
||||
header: 'Users',
|
||||
confirmation: `Are you sure you want to remove this user?`
|
||||
})) {
|
||||
this.props.deleteMember(user.id).then(() => {
|
||||
const { remaining } = this.state;
|
||||
if (remaining <= 0) return;
|
||||
this.setState({ remaining: remaining - 1 })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
save = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.save(this.props.member)
|
||||
.then(() => {
|
||||
const { errors } = this.props;
|
||||
if (errors && errors.size > 0) {
|
||||
errors.forEach(e => {
|
||||
toast.error(e);
|
||||
})
|
||||
}
|
||||
this.setState({ invited: true })
|
||||
// this.closeModal()
|
||||
});
|
||||
}
|
||||
|
||||
formContent = () => {
|
||||
const { member, account, isEnterprise, roles } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.form }>
|
||||
<Form onSubmit={ this.save } >
|
||||
<div className={ styles.formGroup }>
|
||||
<label>{ 'Full Name' }</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="name"
|
||||
value={ member.name }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
id="name-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Field>
|
||||
<label>{ 'Email Address' }</label>
|
||||
<Input
|
||||
disabled={member.exists()}
|
||||
name="email"
|
||||
value={ member.email }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
/>
|
||||
</Form.Field>
|
||||
{ !account.smtp &&
|
||||
<div className={cn("mb-4 p-2", styles.smtpMessage)}>
|
||||
SMTP is not configured (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but you’d have to manually copy then send them the invitation link.
|
||||
</div>
|
||||
}
|
||||
<Form.Field>
|
||||
<label className={ styles.checkbox }>
|
||||
<input
|
||||
name="admin"
|
||||
type="checkbox"
|
||||
value={ member.admin }
|
||||
checked={ !!member.admin }
|
||||
onChange={ this.onChangeCheckbox }
|
||||
disabled={member.superAdmin}
|
||||
/>
|
||||
<span>{ 'Admin Privileges' }</span>
|
||||
</label>
|
||||
<div className={ styles.adminInfo }>{ 'Can manage Projects and team members.' }</div>
|
||||
</Form.Field>
|
||||
|
||||
{ isEnterprise && (
|
||||
<Form.Field>
|
||||
<label htmlFor="role">{ 'Role' }</label>
|
||||
<Select
|
||||
placeholder="Role"
|
||||
selection
|
||||
options={ roles }
|
||||
name="roleId"
|
||||
value={ roles.find(r => r.value === member.roleId) }
|
||||
onChange={ this.onChange }
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !member.validate() }
|
||||
loading={ this.props.saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ member.exists() ? 'Update' : 'Invite' }
|
||||
</Button>
|
||||
{member.exists() && (
|
||||
<Button
|
||||
onClick={ this.closeModal }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{ !member.joined && member.invitationLink &&
|
||||
<CopyButton
|
||||
content={member.invitationLink}
|
||||
className="link"
|
||||
btnText="Copy invite link"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
init = (v) => {
|
||||
const { roles } = this.props;
|
||||
this.props.init(v ? v : { roleId: roles[0] ? roles[0].value : null });
|
||||
this.setState({ showModal: true });
|
||||
setTimeout(this.setFocus, 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
members, loading, account, hideHeader = false
|
||||
} = this.props;
|
||||
const { showModal, remaining, invited } = this.state;
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
const canAddUsers = isAdmin && remaining !== 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Loader loading={ loading }>
|
||||
<SlideModal
|
||||
title="Invite People"
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ showModal && this.formContent() }
|
||||
onClose={ this.closeModal }
|
||||
/>
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ cn(styles.tabHeader, 'flex items-center') }>
|
||||
<div className="flex items-center mr-auto">
|
||||
{ !hideHeader && <h3 className={ cn(styles.tabTitle, "text-2xl") }>{ (isAdmin ? 'Manage ' : '') + `Users (${members.size})` }</h3> }
|
||||
{ hideHeader && <h3 className={ cn(styles.tabTitle, "text-xl") }>{ `Users (${members.size})` }</h3>}
|
||||
<Popup
|
||||
disabled={ canAddUsers }
|
||||
content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` }
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
disabled={ !canAddUsers }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ () => this.init() }
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={ members.size === 0 }
|
||||
// animatedIcon="empty-state"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{
|
||||
members.map(user => (
|
||||
<UserItem
|
||||
generateInviteLink={this.props.generateInviteLink}
|
||||
key={ user.id }
|
||||
user={ user }
|
||||
adminLabel={ this.adminLabel(user) }
|
||||
deleteHandler={ isAdmin && account.email !== user.email
|
||||
? this.deleteHandler
|
||||
: null
|
||||
}
|
||||
editHandler={ isAdmin ? this.editHandler : null }
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
{ !members.size > 0 &&
|
||||
<div>{ 'No Data.' }</div>
|
||||
}
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</Loader>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ManageUsers;
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon, CopyButton, Popup } from 'UI';
|
||||
import styles from './userItem.module.css';
|
||||
|
||||
const UserItem = ({ user, adminLabel, deleteHandler, editHandler, generateInviteLink }) => (
|
||||
<div className={ styles.wrapper } id="user-row">
|
||||
<Icon name="user-alt" size="16" marginRight="10" />
|
||||
<div id="user-name">{ user.name || user.email }</div>
|
||||
<div className="px-2"/>
|
||||
{ adminLabel && <div className={ styles.adminLabel }>{ adminLabel }</div>}
|
||||
{ user.roleName && <div className={ styles.adminLabel }>{ user.roleName }</div>}
|
||||
<div className={ styles.actions }>
|
||||
{ user.expiredInvitation && !user.joined &&
|
||||
<Popup
|
||||
content={ `Generate Invitation Link` }
|
||||
>
|
||||
<div className={ styles.button } onClick={ () => generateInviteLink(user) } id="trash">
|
||||
<Icon name="link-45deg" size="16" color="red"/>
|
||||
</div>
|
||||
</Popup>
|
||||
}
|
||||
{ !user.expiredInvitation && !user.joined && user.invitationLink &&
|
||||
<Popup
|
||||
content={ `Copy Invitation Link` }
|
||||
>
|
||||
<div className={ styles.button }>
|
||||
<CopyButton
|
||||
content={user.invitationLink}
|
||||
className="link"
|
||||
btnText={<Icon name="link-45deg" size="16" color="teal"/>}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
}
|
||||
{ !!deleteHandler &&
|
||||
<div className={ styles.button } onClick={ () => deleteHandler(user) } id="trash">
|
||||
<Icon name="trash" size="16" color="teal"/>
|
||||
</div>
|
||||
}
|
||||
{ !!editHandler &&
|
||||
<div className={ styles.button } onClick={ () => editHandler(user) }>
|
||||
<Icon name="edit" size="16" color="teal"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UserItem;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ManageUsers';
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
|
||||
& .tabTitle {
|
||||
margin: 0 15px 0 0;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 0 20px;
|
||||
|
||||
& .formGroup {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
& label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
& .input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& input[type=checkbox] {
|
||||
margin-right: 10px;
|
||||
height: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.adminInfo {
|
||||
font-size: 12px;
|
||||
color: $gray-medium;
|
||||
}
|
||||
|
||||
.smtpMessage {
|
||||
background-color: #faf6e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
@import 'mixins.css';
|
||||
|
||||
.wrapper {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: solid thin $gray-light-shade;
|
||||
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
transition: all 0.2s;
|
||||
& .actions {
|
||||
opacity: 1;
|
||||
transition: all 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
& .adminLabel {
|
||||
margin-left: 5px;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
background-color: $gray-lightest;
|
||||
font-size: 10px;
|
||||
border: solid thin $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
/* opacity: 0; */
|
||||
transition: all 0.4s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& .button {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
& svg {
|
||||
fill: $teal-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Loader, Popup, NoContent, Button } from 'UI';
|
||||
import { Loader, NoContent, Button, Tooltip } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './roles.module.css';
|
||||
import RoleForm from './components/RoleForm';
|
||||
|
|
@ -69,9 +69,9 @@ function Roles(props: Props) {
|
|||
<div className={cn(stl.tabHeader, 'flex items-center')}>
|
||||
<div className="flex items-center mr-auto px-5 pt-5">
|
||||
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
|
||||
<Popup content="You don’t have the permissions to perform this action." disabled={isAdmin}>
|
||||
<Tooltip title="You don’t have the permissions to perform this action." disabled={isAdmin}>
|
||||
<Button variant="primary" onClick={() => editHandler({})}>Add</Button>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Popup, Button, IconButton } from 'UI';
|
||||
import { Tooltip, Button, IconButton } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
|
|
@ -11,20 +11,27 @@ const PERMISSION_WARNING = 'You don’t have the permissions to perform this act
|
|||
const LIMIT_WARNING = 'You have reached site limit.';
|
||||
|
||||
function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
|
||||
const { userStore } = useStore();
|
||||
const { showModal, hideModal } = useModal();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0));
|
||||
const { userStore } = useStore();
|
||||
const { showModal, hideModal } = useModal();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const canAddProject = useObserver(
|
||||
() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)
|
||||
);
|
||||
|
||||
const onClick = () => {
|
||||
init();
|
||||
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
|
||||
};
|
||||
return (
|
||||
<Popup content={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}>
|
||||
<Button variant="primary" onClick={onClick} disabled={!canAddProject || !isAdmin}>Add Project</Button>
|
||||
</Popup>
|
||||
);
|
||||
const onClick = () => {
|
||||
init();
|
||||
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
|
||||
};
|
||||
return (
|
||||
<Tooltip
|
||||
title={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}
|
||||
disabled={isAdmin || canAddProject}
|
||||
>
|
||||
<Button variant="primary" onClick={onClick} disabled={!canAddProject || !isAdmin}>
|
||||
Add Project
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, { init, remove, fetchGDPR })(AddProjectButton);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { withCopy } from 'HOCs';
|
||||
import React from 'react';
|
||||
|
||||
function ProjectKey({ value, tooltip }: any) {
|
||||
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
|
||||
function ProjectKey({ value }: any) {
|
||||
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
|
||||
}
|
||||
|
||||
export default withCopy(ProjectKey);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Loader, Button, Popup, TextLink, NoContent } from 'UI';
|
||||
import { Loader, Button, Tooltip, 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';
|
||||
|
|
@ -101,7 +101,7 @@ class Sites extends React.PureComponent {
|
|||
>
|
||||
<div className="col-span-4">
|
||||
<div className="flex items-center">
|
||||
<Popup content={STATUS_MESSAGE_MAP[_site.status]} inverted>
|
||||
<Tooltip title={STATUS_MESSAGE_MAP[_site.status]}>
|
||||
<div className="relative flex items-center justify-center w-10 h-10">
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-10"
|
||||
|
|
@ -111,7 +111,7 @@ class Sites extends React.PureComponent {
|
|||
{getInitials(_site.name)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
<span className="ml-2">{_site.host}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Popup, IconButton, Button } from 'UI';
|
||||
import { Tooltip, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached users limit.';
|
||||
|
||||
function AddUserButton({ isAdmin = false, onClick }: any ) {
|
||||
const { userStore } = useStore();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const cannAddUser = useObserver(() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0));
|
||||
return (
|
||||
<Popup
|
||||
content={ `${ !isAdmin ? PERMISSION_WARNING : (!cannAddUser ? LIMIT_WARNING : 'Add team member') }` }
|
||||
>
|
||||
<Button disabled={ !cannAddUser || !isAdmin } variant="primary" onClick={ onClick }>Add Team Member</Button>
|
||||
</Popup>
|
||||
);
|
||||
function AddUserButton({ isAdmin = false, onClick }: any) {
|
||||
const { userStore } = useStore();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const cannAddUser = useObserver(
|
||||
() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0)
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
title={`${!isAdmin ? PERMISSION_WARNING : !cannAddUser ? LIMIT_WARNING : 'Add team member'}`}
|
||||
disabled={isAdmin || cannAddUser}
|
||||
>
|
||||
<Button disabled={!cannAddUser || !isAdmin} variant="primary" onClick={onClick}>
|
||||
Add Team Member
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddUserButton;
|
||||
export default AddUserButton;
|
||||
|
|
|
|||
|
|
@ -1,68 +1,108 @@
|
|||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, Popup } from 'UI';
|
||||
import { Button, Tooltip } from 'UI';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import cn from 'classnames';
|
||||
|
||||
const AdminPrivilegeLabel = ({ user }) => {
|
||||
return (
|
||||
<>
|
||||
{user.isAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>}
|
||||
{user.isSuperAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>}
|
||||
{!user.isAdmin && !user.isSuperAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Member</span>}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{user.isAdmin && (
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>
|
||||
)}
|
||||
{user.isSuperAdmin && (
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>
|
||||
)}
|
||||
{!user.isAdmin && !user.isSuperAdmin && (
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Member</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
interface Props {
|
||||
isOnboarding?: boolean;
|
||||
user: any;
|
||||
editHandler?: any;
|
||||
generateInvite?: any;
|
||||
copyInviteCode?: any;
|
||||
isEnterprise?: boolean;
|
||||
isOnboarding?: boolean;
|
||||
user: any;
|
||||
editHandler?: any;
|
||||
generateInvite?: any;
|
||||
copyInviteCode?: any;
|
||||
isEnterprise?: boolean;
|
||||
}
|
||||
function UserListItem(props: Props) {
|
||||
const { user, editHandler = () => {}, generateInvite = () => {}, copyInviteCode = () => {}, isEnterprise = false, isOnboarding = false } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group cursor-pointer" onClick={editHandler}>
|
||||
<div className="col-span-5">
|
||||
<span className="mr-2">{user.name}</span>
|
||||
{/* {isEnterprise && <AdminPrivilegeLabel user={user} />} */}
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
{!isEnterprise && <AdminPrivilegeLabel user={user} />}
|
||||
{isEnterprise && (
|
||||
<>
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">{user.roleName}</span>
|
||||
{ user.isSuperAdmin || user.isAdmin && <><span className="ml-2" /><AdminPrivilegeLabel user={user} /></> }
|
||||
</>)}
|
||||
</div>
|
||||
{!isOnboarding && (
|
||||
<div className="col-span-2">
|
||||
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
|
||||
</div>
|
||||
const {
|
||||
user,
|
||||
editHandler = () => {},
|
||||
generateInvite = () => {},
|
||||
copyInviteCode = () => {},
|
||||
isEnterprise = false,
|
||||
isOnboarding = false,
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group cursor-pointer"
|
||||
onClick={editHandler}
|
||||
>
|
||||
<div className="col-span-5">
|
||||
<span className="mr-2">{user.name}</span>
|
||||
{/* {isEnterprise && <AdminPrivilegeLabel user={user} />} */}
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
{!isEnterprise && <AdminPrivilegeLabel user={user} />}
|
||||
{isEnterprise && (
|
||||
<>
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">
|
||||
{user.roleName}
|
||||
</span>
|
||||
{user.isSuperAdmin ||
|
||||
(user.isAdmin && (
|
||||
<>
|
||||
<span className="ml-2" />
|
||||
<AdminPrivilegeLabel user={user} />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isOnboarding && (
|
||||
<div className="col-span-2">
|
||||
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn('justify-self-end invisible group-hover:visible', {
|
||||
'col-span-2': !isOnboarding,
|
||||
'col-span-4': isOnboarding,
|
||||
})}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3 items-center justify-end">
|
||||
<div>
|
||||
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (
|
||||
<Tooltip title="Copy Invite Code" hideOnClick={true}>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="link-45deg"
|
||||
className=""
|
||||
onClick={copyInviteCode}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className={cn('justify-self-end invisible group-hover:visible', { 'col-span-2': !isOnboarding, 'col-span-4': isOnboarding })}>
|
||||
<div className="grid grid-cols-2 gap-3 items-center justify-end">
|
||||
<div>
|
||||
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (
|
||||
<Popup delay={500} content="Copy Invite Code" hideOnClick={true}>
|
||||
<Button variant="text-primary" icon="link-45deg" className="" onClick={copyInviteCode} />
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{!user.isJoined && user.isExpiredInvite && (
|
||||
<Popup delay={500} arrow content="Generate Invite" hideOnClick={true}>
|
||||
<Button icon="link-45deg" variant="text-primary" className="" onClick={generateInvite} />
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
{!user.isJoined && user.isExpiredInvite && (
|
||||
<Tooltip title="Generate Invite" hideOnClick={true}>
|
||||
<Button
|
||||
icon="link-45deg"
|
||||
variant="text-primary"
|
||||
className=""
|
||||
onClick={generateInvite}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserListItem;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
import { Popup, Icon } from 'UI';
|
||||
import { Tooltip, Icon } from 'UI';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
const ImageInfo = ({ data }) => (
|
||||
<div className={ styles.name }>
|
||||
<Popup
|
||||
className={ styles.popup }
|
||||
content={ <img src={ `//${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
|
||||
<div className={styles.name}>
|
||||
<Tooltip
|
||||
className={styles.Tooltip}
|
||||
title={
|
||||
<img
|
||||
src={`//${data.url}`}
|
||||
className={styles.imagePreview}
|
||||
alt="One of the slowest images"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={ styles.imageWrapper }>
|
||||
<Icon name="camera-alt" size="18" color="gray-light" />
|
||||
<div className={ styles.label }>{ 'Preview' }</div>
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup content={ data.url } >
|
||||
<span>{ data.name }</span>
|
||||
</Popup>
|
||||
<div className={styles.imageWrapper}>
|
||||
<Icon name="camera-alt" size="18" color="gray-light" />
|
||||
<div className={styles.label}>{'Preview'}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={data.url}>
|
||||
<span>{data.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,35 @@
|
|||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, Icon, Popup } from 'UI';
|
||||
import { Loader, NoContent, Icon, Tooltip } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer } from 'recharts';
|
||||
import stl from './CustomMetricWidget.module.css';
|
||||
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||
import { init, edit, remove, setAlertMetricId, setActiveWidget, updateActiveState } from 'Duck/customMetrics';
|
||||
import {
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
setAlertMetricId,
|
||||
setActiveWidget,
|
||||
updateActiveState,
|
||||
} from 'Duck/customMetrics';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||
import CustomMetricPieChart from '../CustomMetricPieChart';
|
||||
import CustomMetricPercentage from '../CustomMetricPercentage';
|
||||
import CustomMetricTable from '../CustomMetricTable';
|
||||
import { NO_METRIC_DATA } from 'App/constants/messages'
|
||||
import { NO_METRIC_DATA } from 'App/constants/messages';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
const customParams = (rangeName) => {
|
||||
const params = { density: 70 };
|
||||
|
||||
// if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
// if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
// if (rangeName === YESTERDAY) params.density = 70
|
||||
// if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -43,12 +50,12 @@ interface Props {
|
|||
}
|
||||
function CustomMetricWidget(props: Props) {
|
||||
const { metric, period, isTemplate } = props;
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<any>([]);
|
||||
// const [seriesMap, setSeriesMap] = useState<any>([]);
|
||||
|
||||
const colors = Styles.customMetricColors;
|
||||
const params = customParams(period.rangeName)
|
||||
const params = customParams(period.rangeName);
|
||||
// const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart', startDate: period.start, endDate: period.end }
|
||||
const isLineChart = metric.viewType === 'lineChart';
|
||||
const isProgress = metric.viewType === 'progress';
|
||||
|
|
@ -61,17 +68,18 @@ function CustomMetricWidget(props: Props) {
|
|||
period: period,
|
||||
...period.toTimestamps(),
|
||||
filters,
|
||||
}
|
||||
};
|
||||
props.setActiveWidget(activeWidget);
|
||||
}
|
||||
};
|
||||
|
||||
const clickHandler = (event, index) => {
|
||||
if (event) {
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps = metric.metricType === 'timeseries' ?
|
||||
getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density) :
|
||||
period.toTimestamps();
|
||||
const periodTimestamps =
|
||||
metric.metricType === 'timeseries'
|
||||
? getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density)
|
||||
: period.toTimestamps();
|
||||
|
||||
const activeWidget = {
|
||||
widget: metric,
|
||||
|
|
@ -79,68 +87,81 @@ function CustomMetricWidget(props: Props) {
|
|||
...periodTimestamps,
|
||||
timestamp: payload.timestamp,
|
||||
index,
|
||||
}
|
||||
};
|
||||
|
||||
props.setActiveWidget(activeWidget);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateActiveState = (metricId, state) => {
|
||||
props.updateActiveState(metricId, state);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className="flex items-center p-2">
|
||||
<div className="font-medium">{metric.name}</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
{!isTable && !isPieChart && <WidgetIcon className="cursor-pointer mr-6" icon="bell-plus" tooltip="Set Alert" onClick={props.onAlertClick} /> }
|
||||
<WidgetIcon className="cursor-pointer mr-6" icon="pencil" tooltip="Edit Metric" onClick={() => props.init(metric)} />
|
||||
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={() => updateActiveState(metric.metricId, false)} />
|
||||
{!isTable && !isPieChart && (
|
||||
<WidgetIcon
|
||||
className="cursor-pointer mr-6"
|
||||
icon="bell-plus"
|
||||
tooltip="Set Alert"
|
||||
onClick={props.onAlertClick}
|
||||
/>
|
||||
)}
|
||||
<WidgetIcon
|
||||
className="cursor-pointer mr-6"
|
||||
icon="pencil"
|
||||
tooltip="Edit Metric"
|
||||
onClick={() => props.init(metric)}
|
||||
/>
|
||||
<WidgetIcon
|
||||
className="cursor-pointer"
|
||||
icon="close"
|
||||
tooltip="Hide Metric"
|
||||
onClick={() => updateActiveState(metric.metricId, false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title={NO_METRIC_DATA}
|
||||
show={ data.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<Loader loading={loading} size="small">
|
||||
<NoContent size="small" title={NO_METRIC_DATA} show={data.length === 0}>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<>
|
||||
{isLineChart && (
|
||||
<CustomMetriLineChart
|
||||
data={ data }
|
||||
params={ params }
|
||||
// seriesMap={ seriesMap }
|
||||
colors={ colors }
|
||||
onClick={ clickHandler }
|
||||
data={data}
|
||||
params={params}
|
||||
// seriesMap={ seriesMap }
|
||||
colors={colors}
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPieChart && (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={ data[0] }
|
||||
colors={ colors }
|
||||
onClick={ clickHandlerTable }
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
onClick={clickHandlerTable}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProgress && (
|
||||
<CustomMetricPercentage
|
||||
data={ data[0] }
|
||||
params={ params }
|
||||
colors={ colors }
|
||||
onClick={ clickHandler }
|
||||
data={data[0]}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTable && (
|
||||
<CustomMetricTable
|
||||
metric={ metric }
|
||||
data={ data[0] }
|
||||
onClick={ clickHandlerTable }
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
onClick={clickHandlerTable}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -153,26 +174,36 @@ function CustomMetricWidget(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
period: state.getIn(['dashboard', 'period']),
|
||||
}), {
|
||||
remove,
|
||||
setShowAlerts,
|
||||
setAlertMetricId,
|
||||
edit,
|
||||
setActiveWidget,
|
||||
updateActiveState,
|
||||
init,
|
||||
})(CustomMetricWidget);
|
||||
export default connect(
|
||||
(state) => ({
|
||||
period: state.getIn(['dashboard', 'period']),
|
||||
}),
|
||||
{
|
||||
remove,
|
||||
setShowAlerts,
|
||||
setAlertMetricId,
|
||||
edit,
|
||||
setActiveWidget,
|
||||
updateActiveState,
|
||||
init,
|
||||
}
|
||||
)(CustomMetricWidget);
|
||||
|
||||
|
||||
const WidgetIcon = ({ className = '', tooltip = '', icon, onClick }) => (
|
||||
<Popup
|
||||
size="small"
|
||||
content={tooltip}
|
||||
>
|
||||
const WidgetIcon = ({
|
||||
className = '',
|
||||
tooltip = '',
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
className: string;
|
||||
tooltip: string;
|
||||
icon: string;
|
||||
onClick: any;
|
||||
}) => (
|
||||
<Tooltip title={tooltip}>
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={icon} size="14" />
|
||||
{/* @ts-ignore */}
|
||||
<Icon name={icon} size="14" />
|
||||
</div>
|
||||
</Popup>
|
||||
)
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||
import { Map } from 'immutable';
|
||||
import cn from 'classnames';
|
||||
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Legend } from 'recharts';
|
||||
import { Loader, TextEllipsis, Popup, Icon } from 'UI';
|
||||
import { Loader, TextEllipsis, Tooltip } from 'UI';
|
||||
import { TYPES } from 'Types/resource';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, LAST_7_DAYS, LAST_30_DAYS } from 'Types/app/period';
|
||||
import { fetchPerformanseSearch } from 'Duck/dashboard';
|
||||
|
|
@ -101,7 +101,7 @@ export default class Performance extends React.PureComponent {
|
|||
|
||||
compare = () => this.setState({ comparing: true })
|
||||
|
||||
legendPopup = (component, trigger) => <Popup size="mini" content={ component }>{trigger}</Popup>
|
||||
legendPopup = (component, trigger) => <Tooltip size="mini" content={ component }>{trigger}</Tooltip>
|
||||
|
||||
legendFormatter = (value, entry, index) => {
|
||||
const { opacity } = this.state;
|
||||
|
|
@ -113,16 +113,15 @@ export default class Performance extends React.PureComponent {
|
|||
if (value.includes(BASE_KEY)) {
|
||||
const resourceIndex = Number.parseInt(value.substr(BASE_KEY.length));
|
||||
return (
|
||||
<Popup
|
||||
wide
|
||||
content={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
|
||||
<Tooltip
|
||||
title={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
|
||||
>
|
||||
<TextEllipsis
|
||||
maxWidth="200px"
|
||||
style={ { verticalAlign: 'middle' } }
|
||||
text={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
|
||||
/>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
|
|
@ -8,18 +8,24 @@ const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
|
|||
const ImageInfo = ({ data }) => {
|
||||
const canPreview = supportedTypes.includes(data.type);
|
||||
return (
|
||||
<div className={ styles.name }>
|
||||
<Popup
|
||||
className={ styles.popup }
|
||||
<div className={styles.name}>
|
||||
<Tooltip
|
||||
className={styles.popup}
|
||||
disabled={!canPreview}
|
||||
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
|
||||
title={
|
||||
<img
|
||||
src={`${data.url}`}
|
||||
className={styles.imagePreview}
|
||||
alt="One of the slowest images"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={cn({ [styles.hasPreview]: canPreview})}>
|
||||
<div className={ styles.label }>{data.name}</div>
|
||||
<div className={cn({ [styles.hasPreview]: canPreview })}>
|
||||
<div className={styles.label}>{data.name}</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ImageInfo.displayName = 'ImageInfo';
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Popup, Icon } from 'UI';
|
||||
import { Tooltip, Icon } from 'UI';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
const ImageInfo = ({ data }) => (
|
||||
<div className={ styles.name }>
|
||||
<Popup
|
||||
className={ styles.popup }
|
||||
content={ <img src={ `//${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
|
||||
<div className={styles.name}>
|
||||
<Tooltip
|
||||
className={styles.popup}
|
||||
title={
|
||||
<img
|
||||
src={`//${data.url}`}
|
||||
className={styles.imagePreview}
|
||||
alt="One of the slowest images"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={ styles.imageWrapper }>
|
||||
<div className={styles.imageWrapper}>
|
||||
<Icon name="camera-alt" size="18" color="gray-light" />
|
||||
<div className={ styles.label }>{ 'Preview' }</div>
|
||||
<div className={styles.label}>{'Preview'}</div>
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup
|
||||
disabled
|
||||
content={ data.url }
|
||||
>
|
||||
<span>{ data.name }</span>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
<Tooltip disabled content={data.url}>
|
||||
<span>{data.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
|
|
@ -8,18 +8,24 @@ const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
|
|||
const ImageInfo = ({ data }) => {
|
||||
const canPreview = supportedTypes.includes(data.type);
|
||||
return (
|
||||
<div className={ styles.name }>
|
||||
<Popup
|
||||
className={ styles.popup }
|
||||
<div className={styles.name}>
|
||||
<Tooltip
|
||||
className={styles.popup}
|
||||
disabled={!canPreview}
|
||||
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
|
||||
title={
|
||||
<img
|
||||
src={`${data.url}`}
|
||||
className={styles.imagePreview}
|
||||
alt="One of the slowest images"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={cn({ [styles.hasPreview]: canPreview})}>
|
||||
<div className={ styles.label }>{data.name}</div>
|
||||
<div className={cn({ [styles.hasPreview]: canPreview })}>
|
||||
<div className={styles.label}>{data.name}</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ImageInfo.displayName = 'ImageInfo';
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function Condition({
|
|||
isSearchable={true}
|
||||
options={triggerOptions}
|
||||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left) || ''}
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -75,7 +75,7 @@ function Condition({
|
|||
placeholder="Select Condition"
|
||||
options={conditions}
|
||||
name="operator"
|
||||
value={conditions.find(c => c.value === instance.query.operator)}
|
||||
value={conditions.find(c => c.value === instance.query.operator) || ''}
|
||||
onChange={({ value }) =>
|
||||
writeQueryOption(null, { name: 'operator', value: value.value })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import NotifyHooks from './AlertForm/NotifyHooks';
|
|||
import AlertListItem from './AlertListItem';
|
||||
import Condition from './AlertForm/Condition';
|
||||
|
||||
|
||||
const Circle = ({ text }: { text: string }) => (
|
||||
<div style={{ left: -14, height: 26, width: 26 }} className="circle rounded-full bg-gray-light flex items-center justify-center absolute top-0">
|
||||
<div
|
||||
style={{ left: -14, height: 26, width: 26 }}
|
||||
className="circle rounded-full bg-gray-light flex items-center justify-center absolute top-0"
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -54,7 +56,7 @@ interface IProps extends RouteComponentProps {
|
|||
loading: boolean;
|
||||
deleting: boolean;
|
||||
triggerOptions: any[];
|
||||
list: any,
|
||||
list: any;
|
||||
fetchTriggerOptions: () => void;
|
||||
edit: (query: any) => void;
|
||||
init: (alert?: Alert) => any;
|
||||
|
|
@ -83,6 +85,7 @@ const NewAlert = (props: IProps) => {
|
|||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
init({});
|
||||
if (list.size === 0) fetchList();
|
||||
props.fetchTriggerOptions();
|
||||
fetchWebhooks();
|
||||
|
|
@ -90,19 +93,22 @@ const NewAlert = (props: IProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (list.size > 0) {
|
||||
const alertId = location.pathname.split('/').pop()
|
||||
const currentAlert = list.toJS().find((alert: Alert) => alert.alertId === parseInt(alertId, 10));
|
||||
const alertId = location.pathname.split('/').pop();
|
||||
const currentAlert = list
|
||||
.toJS()
|
||||
.find((alert: Alert) => alert.alertId === parseInt(alertId, 10));
|
||||
init(currentAlert);
|
||||
}
|
||||
}, [list])
|
||||
|
||||
}, [list]);
|
||||
|
||||
const write = ({ target: { value, name } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.edit({ [name]: value });
|
||||
|
||||
const writeOption = (
|
||||
_: React.ChangeEvent,
|
||||
{ name, value }: { name: string; value: Record<string, any> }
|
||||
) => props.edit({ [name]: value.value });
|
||||
|
||||
const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.edit({ [name]: checked });
|
||||
|
||||
|
|
@ -115,39 +121,35 @@ const NewAlert = (props: IProps) => {
|
|||
})
|
||||
) {
|
||||
remove(instance.alertId).then(() => {
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
props.history.push(withSiteId(alerts(), siteId));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = (instance: Alert) => {
|
||||
const wasUpdating = instance.exists();
|
||||
save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toast.success('New alert saved');
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
props.history.push(withSiteId(alerts(), siteId));
|
||||
} else {
|
||||
toast.success('Alert updated');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
props.history.push(withSiteId(alerts(), siteId))
|
||||
}
|
||||
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
// @ts-ignore
|
||||
.toJS();
|
||||
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
// @ts-ignore
|
||||
.toJS();
|
||||
|
||||
|
||||
|
||||
const writeQueryOption = (
|
||||
e: React.ChangeEvent,
|
||||
{ name, value }: { name: string; value: string }
|
||||
|
|
@ -184,87 +186,84 @@ const NewAlert = (props: IProps) => {
|
|||
onSubmit={() => onSave(instance)}
|
||||
id="alert-form"
|
||||
>
|
||||
<div
|
||||
className={cn('px-6 py-4 flex justify-between items-center',
|
||||
)}
|
||||
>
|
||||
<div className={cn('px-6 py-4 flex justify-between items-center')}>
|
||||
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
|
||||
<WidgetName name={instance.name} onUpdate={(name) => write({ target: { value: name, name: 'name' }} as any)} canEdit />
|
||||
<WidgetName
|
||||
name={instance.name}
|
||||
onUpdate={(name) => write({ target: { value: name, name: 'name' } } as any)}
|
||||
canEdit
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="text-gray-600 w-full cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
<div className="text-gray-600 w-full cursor-pointer"></div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-3 flex flex-col">
|
||||
<Section
|
||||
index="1"
|
||||
title={'Alert based on'}
|
||||
content={
|
||||
<div className="">
|
||||
<SegmentSelection
|
||||
outline
|
||||
name="detectionMethod"
|
||||
className="my-3 w-1/4"
|
||||
onSelect={(e: any, { name, value }: any) => 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. When Threshold is above 1ms over the past 15mins, notify me through Slack #foss-notifications.'}
|
||||
{!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>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<Condition
|
||||
isThreshold={isThreshold}
|
||||
writeOption={writeOption}
|
||||
instance={instance}
|
||||
triggerOptions={triggerOptions}
|
||||
writeQueryOption={writeQueryOption}
|
||||
writeQuery={writeQuery}
|
||||
unit={unit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<NotifyHooks
|
||||
instance={instance}
|
||||
onChangeCheck={onChangeCheck}
|
||||
slackChannels={slackChannels}
|
||||
validateEmail={validateEmail}
|
||||
hooks={hooks}
|
||||
edit={edit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-6 border-t">
|
||||
<BottomButtons
|
||||
loading={loading}
|
||||
<div className="px-6 pb-3 flex flex-col">
|
||||
<Section
|
||||
index="1"
|
||||
title={'Alert based on'}
|
||||
content={
|
||||
<div className="">
|
||||
<SegmentSelection
|
||||
outline
|
||||
name="detectionMethod"
|
||||
className="my-3 w-1/4"
|
||||
onSelect={(e: any, { name, value }: any) => 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. When Threshold is above 1ms over the past 15mins, notify me through Slack #foss-notifications.'}
|
||||
{!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>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<Condition
|
||||
isThreshold={isThreshold}
|
||||
writeOption={writeOption}
|
||||
instance={instance}
|
||||
deleting={deleting}
|
||||
onDelete={onDelete}
|
||||
triggerOptions={triggerOptions}
|
||||
writeQueryOption={writeQueryOption}
|
||||
writeQuery={writeQuery}
|
||||
unit={unit}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<NotifyHooks
|
||||
instance={instance}
|
||||
onChangeCheck={onChangeCheck}
|
||||
slackChannels={slackChannels}
|
||||
validateEmail={validateEmail}
|
||||
hooks={hooks}
|
||||
edit={edit}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-6 border-t">
|
||||
<BottomButtons
|
||||
loading={loading}
|
||||
instance={instance}
|
||||
deleting={deleting}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="bg-white mt-4 border rounded mb-10">
|
||||
|
|
@ -276,21 +275,23 @@ const NewAlert = (props: IProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default withRouter(connect(
|
||||
(state) => ({
|
||||
export default withRouter(
|
||||
connect(
|
||||
(state) => ({
|
||||
// @ts-ignore
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
//@ts-ignore
|
||||
list: state.getIn(['alerts', 'list']),
|
||||
// @ts-ignore
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
// @ts-ignore
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
}),
|
||||
{ fetchTriggerOptions, init, edit, save, remove, fetchWebhooks, fetchList }
|
||||
// @ts-ignore
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
//@ts-ignore
|
||||
list: state.getIn(['alerts', 'list']),
|
||||
// @ts-ignore
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
// @ts-ignore
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
|
||||
// @ts-ignore
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
}),
|
||||
{ fetchTriggerOptions, init, edit, save, remove, fetchWebhooks, fetchList }
|
||||
// @ts-ignore
|
||||
)(NewAlert));
|
||||
)(NewAlert)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, PageTitle, Loader } from 'UI';
|
||||
import { Button, PageTitle, Loader, Tooltip, Popover } from 'UI';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
|
|
@ -15,7 +15,6 @@ import withPageTitle from 'HOCs/withPageTitle';
|
|||
import withReport from 'App/components/hocs/withReport';
|
||||
import DashboardOptions from '../DashboardOptions';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import AddMetricContainer from '../DashboardWidgetGrid/AddMetricContainer';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
|
|
@ -126,24 +125,25 @@ function DashboardView(props: Props) {
|
|||
<PageTitle
|
||||
title={
|
||||
// @ts-ignore
|
||||
<Tooltip delay={100} arrow title="Double click to rename">
|
||||
<Tooltip title="Double click to rename">
|
||||
{dashboard?.name}
|
||||
</Tooltip>
|
||||
}
|
||||
onDoubleClick={() => onEdit(true)}
|
||||
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
|
||||
actionButton={
|
||||
<OutsideClickDetectingDiv onClickOutside={() => setShowTooltip(false)}>
|
||||
<Tooltip
|
||||
open={showTooltip}
|
||||
interactive
|
||||
useContext
|
||||
// <OutsideClickDetectingDiv onClickOutside={() => setShowTooltip(false)}>
|
||||
<Popover
|
||||
// open={showTooltip}
|
||||
// interactive
|
||||
// useContext
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
hideDelay={0}
|
||||
duration={0}
|
||||
distance={20}
|
||||
html={
|
||||
// theme="nopadding"
|
||||
// hideDelay={0}
|
||||
// duration={0}
|
||||
// distance={20}
|
||||
placement="left"
|
||||
render={() => showTooltip && (
|
||||
<div style={{ padding: 0 }}>
|
||||
<AddMetricContainer
|
||||
onAction={() => setShowTooltip(false)}
|
||||
|
|
@ -151,13 +151,13 @@ function DashboardView(props: Props) {
|
|||
siteId={siteId}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Button variant="primary" onClick={() => setShowTooltip(true)}>
|
||||
Add Metric
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</OutsideClickDetectingDiv>
|
||||
</Popover>
|
||||
// </OutsideClickDetectingDiv>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,50 +1,59 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
|
||||
const MIN_WIDTH = '20px';
|
||||
interface Props {
|
||||
issue: any
|
||||
issue: any;
|
||||
}
|
||||
function FunnelIssueGraph(props: Props) {
|
||||
const { issue } = props;
|
||||
const { issue } = props;
|
||||
|
||||
return (
|
||||
<div className="flex rounded-sm" style={{ width: '600px' }}>
|
||||
<div style={{ width: issue.unaffectedSessionsPer + '%', minWidth: MIN_WIDTH }} className="relative">
|
||||
<Popup
|
||||
content={ `Unaffected sessions` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
>
|
||||
<div className="w-full relative rounded-tl-sm rounded-bl-sm" style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.unaffectedSessions}</div>
|
||||
</Popup>
|
||||
</div>
|
||||
<div style={{ width: issue.affectedSessionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
|
||||
<Popup
|
||||
content={ `Affected sessions` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
>
|
||||
<div className="w-full relative" style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.affectedSessions}</div>
|
||||
</Popup>
|
||||
</div>
|
||||
<div style={{ width: issue.lostConversionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
|
||||
<Popup
|
||||
content={ `Conversion lost` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
>
|
||||
<div className="w-full relative rounded-tr-sm rounded-br-sm" style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">{issue.lostConversions}</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex rounded-sm" style={{ width: '600px' }}>
|
||||
<div
|
||||
style={{ width: issue.unaffectedSessionsPer + '%', minWidth: MIN_WIDTH }}
|
||||
className="relative"
|
||||
>
|
||||
<Tooltip title={`Unaffected sessions`} placement="top">
|
||||
<div
|
||||
className="w-full relative rounded-tl-sm rounded-bl-sm"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
|
||||
{issue.unaffectedSessions}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: issue.affectedSessionsPer + '%', minWidth: MIN_WIDTH }}
|
||||
className="border-l relative"
|
||||
>
|
||||
<Tooltip title={`Affected sessions`} placement="top">
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
|
||||
{issue.affectedSessions}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: issue.lostConversionsPer + '%', minWidth: MIN_WIDTH }}
|
||||
className="border-l relative"
|
||||
>
|
||||
<Tooltip title={`Conversion lost`} placement="top">
|
||||
<div
|
||||
className="w-full relative rounded-tr-sm rounded-br-sm"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">
|
||||
{issue.lostConversions}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelIssueGraph;
|
||||
export default FunnelIssueGraph;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Icon, Link } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
|
|
@ -24,9 +23,7 @@ function MetricTypeIcon({ type }: any) {
|
|||
|
||||
return (
|
||||
<Tooltip
|
||||
html={<div className="capitalize">{type}</div>}
|
||||
position="top"
|
||||
arrow
|
||||
title={<div className="capitalize">{type}</div>}
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon()} size="16" color="tealx" />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useStore } from 'App/mstore';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import { Button, Icon, SegmentSelection } from 'UI'
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import { confirm, Popup } from 'UI';
|
||||
import { confirm, Tooltip } from 'UI';
|
||||
import Select from 'Shared/Select'
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
|
||||
|
||||
|
|
@ -200,8 +200,8 @@ function WidgetForm(props: Props) {
|
|||
</div>
|
||||
|
||||
<div className="form-groups flex items-center justify-between">
|
||||
<Popup
|
||||
content="Cannot save funnel metric without at least 2 events"
|
||||
<Tooltip
|
||||
title="Cannot save funnel metric without at least 2 events"
|
||||
disabled={!cannotSaveFunnel}
|
||||
>
|
||||
<Button
|
||||
|
|
@ -211,7 +211,7 @@ function WidgetForm(props: Props) {
|
|||
>
|
||||
{metric.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
<div className="flex items-center">
|
||||
{metric.exists() && (
|
||||
<Button variant="text-primary" onClick={onDelete}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
|
|
@ -65,7 +64,7 @@ function WidgetName(props: Props) {
|
|||
/>
|
||||
) : (
|
||||
// @ts-ignore
|
||||
<Tooltip delay={100} arrow title="Double click to rename" disabled={!canEdit}>
|
||||
<Tooltip title="Double click to rename" disabled={!canEdit}>
|
||||
<div
|
||||
onDoubleClick={() => setEditing(true)}
|
||||
className={
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import React from 'react';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
className: string
|
||||
onClick: () => void
|
||||
icon: string
|
||||
tooltip: string
|
||||
className: string;
|
||||
onClick: () => void;
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
}
|
||||
function WidgetIcon(props: Props) {
|
||||
const { className, onClick, icon, tooltip } = props;
|
||||
return (
|
||||
<Popup title={tooltip} >
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={icon} size="14" />
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
const { className, onClick, icon, tooltip } = props;
|
||||
return (
|
||||
<Tooltip title={tooltip}>
|
||||
<div className={className} onClick={onClick}>
|
||||
{/* @ts-ignore */}
|
||||
<Icon name={icon} size="14" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetIcon;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useRef } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { ItemMenu, Popup } from 'UI';
|
||||
import { ItemMenu, Tooltip } from 'UI';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import WidgetChart from '../WidgetChart';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -14,150 +14,160 @@ import { FilterKey } from 'App/types/filter/filterType';
|
|||
import LazyLoad from 'react-lazyload';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
widget?: any;
|
||||
index?: number;
|
||||
moveListItem?: any;
|
||||
isPreview?: boolean;
|
||||
isTemplate?: boolean
|
||||
dashboardId?: string;
|
||||
siteId?: string,
|
||||
active?: boolean;
|
||||
history?: any
|
||||
onClick?: () => void;
|
||||
isWidget?: boolean;
|
||||
hideName?: boolean;
|
||||
grid?: string;
|
||||
className?: string;
|
||||
widget?: any;
|
||||
index?: number;
|
||||
moveListItem?: any;
|
||||
isPreview?: boolean;
|
||||
isTemplate?: boolean;
|
||||
dashboardId?: string;
|
||||
siteId?: string;
|
||||
active?: boolean;
|
||||
history?: any;
|
||||
onClick?: () => void;
|
||||
isWidget?: boolean;
|
||||
hideName?: boolean;
|
||||
grid?: string;
|
||||
}
|
||||
function WidgetWrapper(props: Props & RouteComponentProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { isWidget = false, active = false, index = 0, moveListItem = null, isPreview = false, isTemplate = false, siteId, grid = "" } = props;
|
||||
const widget: any = props.widget;
|
||||
const isTimeSeries = widget.metricType === 'timeseries';
|
||||
const isPredefined = widget.metricType === 'predefined';
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const { dashboardStore } = useStore();
|
||||
const {
|
||||
isWidget = false,
|
||||
active = false,
|
||||
index = 0,
|
||||
moveListItem = null,
|
||||
isPreview = false,
|
||||
isTemplate = false,
|
||||
siteId,
|
||||
grid = '',
|
||||
} = props;
|
||||
const widget: any = props.widget;
|
||||
const isTimeSeries = widget.metricType === 'timeseries';
|
||||
const isPredefined = widget.metricType === 'predefined';
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
|
||||
const [{ isDragging }, dragRef] = useDrag({
|
||||
type: 'item',
|
||||
item: { index, grid },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
opacity: monitor.isDragging() ? 0.5 : 1,
|
||||
}),
|
||||
});
|
||||
const [{ isDragging }, dragRef] = useDrag({
|
||||
type: 'item',
|
||||
item: { index, grid },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
opacity: monitor.isDragging() ? 0.5 : 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ isOver, canDrop }, dropRef] = useDrop({
|
||||
accept: 'item',
|
||||
drop: (item: any) => {
|
||||
if (item.index === index || (item.grid !== grid)) return;
|
||||
moveListItem(item.index, index);
|
||||
},
|
||||
canDrop(item) {
|
||||
return item.grid === grid
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
})
|
||||
const [{ isOver, canDrop }, dropRef] = useDrop({
|
||||
accept: 'item',
|
||||
drop: (item: any) => {
|
||||
if (item.index === index || item.grid !== grid) return;
|
||||
moveListItem(item.index, index);
|
||||
},
|
||||
canDrop(item) {
|
||||
return item.grid === grid;
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
|
||||
const onDelete = async () => {
|
||||
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId, widget.widgetId);
|
||||
}
|
||||
const onDelete = async () => {
|
||||
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId, widget.widgetId);
|
||||
};
|
||||
|
||||
const onChartClick = () => {
|
||||
if (!isWidget || isPredefined) return;
|
||||
|
||||
props.history.push(withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId),siteId));
|
||||
}
|
||||
|
||||
const ref: any = useRef(null)
|
||||
const dragDropRef: any = dragRef(dropRef(ref))
|
||||
const addOverlay = isTemplate || (!isPredefined && isWidget && widget.metricOf !== FilterKey.ERRORS && widget.metricOf !== FilterKey.SESSIONS)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
"relative rounded bg-white border group",
|
||||
'col-span-' + widget.config.col,
|
||||
{ "hover:shadow-border-gray": !isTemplate && isWidget },
|
||||
{ "hover:shadow-border-main": isTemplate }
|
||||
)
|
||||
}
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
borderColor: (canDrop && isOver) || active ? '#394EFF' : (isPreview ? 'transparent' : '#EEEEEE'),
|
||||
}}
|
||||
ref={dragDropRef}
|
||||
onClick={props.onClick ? props.onClick : () => {}}
|
||||
id={`widget-${widget.widgetId}`}
|
||||
>
|
||||
{!isTemplate && isWidget && isPredefined &&
|
||||
<div
|
||||
className={cn(
|
||||
stl.drillDownMessage,
|
||||
'disabled text-gray text-sm invisible group-hover:visible')}
|
||||
>
|
||||
{'Cannot drill down system provided metrics'}
|
||||
</div>
|
||||
}
|
||||
{/* @ts-ignore */}
|
||||
<Popup
|
||||
hideOnClick={true}
|
||||
position="bottom"
|
||||
delay={300}
|
||||
followCursor
|
||||
disabled={!isTemplate}
|
||||
boundary="viewport"
|
||||
flip={["top"]}
|
||||
content={<span>Click to select</span>}
|
||||
>
|
||||
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
|
||||
<div
|
||||
className={cn("p-3 pb-4 flex items-center justify-between", { "cursor-move" : !isTemplate && isWidget })}
|
||||
>
|
||||
{!props.hideName ? <div className="capitalize-first w-full font-medium">{widget.name}</div> : null}
|
||||
{isWidget && (
|
||||
<div className="flex items-center" id="no-print">
|
||||
{!isPredefined && isTimeSeries && (
|
||||
<>
|
||||
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
|
||||
<div className='mx-2'/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTemplate && (
|
||||
<ItemMenu
|
||||
items={[
|
||||
{
|
||||
text: widget.metricType === 'predefined' ? 'Cannot edit system generated metrics' : 'Edit',
|
||||
onClick: onChartClick,
|
||||
disabled: widget.metricType === 'predefined',
|
||||
},
|
||||
{
|
||||
text: 'Hide',
|
||||
onClick: onDelete
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <LazyLoad height={!isTemplate ? 300 : 10} offset={!isTemplate ? 100 : 10} > */}
|
||||
<LazyLoad offset={!isTemplate ? 100 : 10} >
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart metric={widget} isTemplate={isTemplate} isWidget={isWidget} />
|
||||
</div>
|
||||
</LazyLoad>
|
||||
</Popup>
|
||||
</div>
|
||||
const onChartClick = () => {
|
||||
if (!isWidget || isPredefined) return;
|
||||
|
||||
props.history.push(
|
||||
withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId)
|
||||
);
|
||||
};
|
||||
|
||||
const ref: any = useRef(null);
|
||||
const dragDropRef: any = dragRef(dropRef(ref));
|
||||
const addOverlay =
|
||||
isTemplate ||
|
||||
(!isPredefined &&
|
||||
isWidget &&
|
||||
widget.metricOf !== FilterKey.ERRORS &&
|
||||
widget.metricOf !== FilterKey.SESSIONS);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded bg-white border group',
|
||||
'col-span-' + widget.config.col,
|
||||
{ 'hover:shadow-border-gray': !isTemplate && isWidget },
|
||||
{ 'hover:shadow-border-main': isTemplate }
|
||||
)}
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
borderColor:
|
||||
(canDrop && isOver) || active ? '#394EFF' : isPreview ? 'transparent' : '#EEEEEE',
|
||||
}}
|
||||
ref={dragDropRef}
|
||||
onClick={props.onClick ? props.onClick : () => {}}
|
||||
id={`widget-${widget.widgetId}`}
|
||||
>
|
||||
{!isTemplate && isWidget && isPredefined && (
|
||||
<div
|
||||
className={cn(
|
||||
stl.drillDownMessage,
|
||||
'disabled text-gray text-sm invisible group-hover:visible'
|
||||
)}
|
||||
>
|
||||
{'Cannot drill down system provided metrics'}
|
||||
</div>
|
||||
)}
|
||||
<Tooltip disabled={!isTemplate} title="Click to select">
|
||||
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
|
||||
<div
|
||||
className={cn('p-3 pb-4 flex items-center justify-between', {
|
||||
'cursor-move': !isTemplate && isWidget,
|
||||
})}
|
||||
>
|
||||
{!props.hideName ? (
|
||||
<div className="capitalize-first w-full font-medium">{widget.name}</div>
|
||||
) : null}
|
||||
{isWidget && (
|
||||
<div className="flex items-center" id="no-print">
|
||||
{!isPredefined && isTimeSeries && (
|
||||
<>
|
||||
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
|
||||
<div className="mx-2" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTemplate && (
|
||||
<ItemMenu
|
||||
items={[
|
||||
{
|
||||
text:
|
||||
widget.metricType === 'predefined'
|
||||
? 'Cannot edit system generated metrics'
|
||||
: 'Edit',
|
||||
onClick: onChartClick,
|
||||
disabled: widget.metricType === 'predefined',
|
||||
},
|
||||
{
|
||||
text: 'Hide',
|
||||
onClick: onDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LazyLoad offset={!isTemplate ? 100 : 600}>
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart metric={widget} isTemplate={isTemplate} isWidget={isWidget} />
|
||||
</div>
|
||||
</LazyLoad>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default withRouter(observer(WidgetWrapper));
|
||||
|
|
|
|||
|
|
@ -1,59 +1,59 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Popup, TextEllipsis } from 'UI';
|
||||
import { Tooltip, TextEllipsis } from 'UI';
|
||||
import { Styles } from '../../Dashboard/Widgets/common';
|
||||
import cls from './distributionBar.module.css';
|
||||
import { colorScale } from 'App/utils';
|
||||
|
||||
function DistributionBar({ className, title, partitions }) {
|
||||
if (partitions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (partitions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = Array(partitions.length)
|
||||
.fill()
|
||||
.map((element, index) => index + 0);
|
||||
const colors = colorScale(values, Styles.colors);
|
||||
const values = Array(partitions.length)
|
||||
.fill()
|
||||
.map((element, index) => index + 0);
|
||||
const colors = colorScale(values, Styles.colors);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<div className="capitalize">{title}</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px' }}>
|
||||
<TextEllipsis text={partitions[0].label} />
|
||||
</div>
|
||||
<div className="ml-2">{`${Math.round(partitions[0].prc)}% `}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('border-radius-3 overflow-hidden flex', cls.bar)}>
|
||||
{partitions.map((p, index) => (
|
||||
<Popup
|
||||
key={p.label}
|
||||
content={
|
||||
<div className="text-center">
|
||||
<span className="capitalize">{p.label}</span>
|
||||
<br />
|
||||
{`${Math.round(p.prc)}%`}
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
marginLeft: '1px',
|
||||
width: `${p.prc}%`,
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-tealx"
|
||||
style={{
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
))}
|
||||
</div>
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<div className="capitalize">{title}</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px' }}>
|
||||
<TextEllipsis text={partitions[0].label} />
|
||||
</div>
|
||||
<div className="ml-2">{`${Math.round(partitions[0].prc)}% `}</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
<div className={cn('border-radius-3 overflow-hidden flex', cls.bar)}>
|
||||
{partitions.map((p, index) => (
|
||||
<Tooltip
|
||||
key={p.label}
|
||||
title={
|
||||
<div className="text-center">
|
||||
<span className="capitalize">{p.label}</span>
|
||||
<br />
|
||||
{`${Math.round(p.prc)}%`}
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
marginLeft: '1px',
|
||||
width: `${p.prc}%`,
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-tealx"
|
||||
style={{
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DistributionBar.displayName = 'DistributionBar';
|
||||
|
|
|
|||
|
|
@ -1,119 +1,87 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import { errors as errorsRoute, error as errorRoute } from 'App/routes';
|
||||
import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI';
|
||||
import { error as errorRoute } from 'App/routes';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { fetch, fetchTrace } from 'Duck/errors';
|
||||
import MainSection from './MainSection';
|
||||
import SideSection from './SideSection';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
|
||||
list: state.getIn(['errors', 'instanceTrace']),
|
||||
loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']),
|
||||
errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
|
||||
}),
|
||||
{
|
||||
fetch,
|
||||
fetchTrace,
|
||||
}
|
||||
(state) => ({
|
||||
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
|
||||
list: state.getIn(['errors', 'instanceTrace']),
|
||||
loading:
|
||||
state.getIn(['errors', 'fetch', 'loading']) ||
|
||||
state.getIn(['errors', 'fetchTrace', 'loading']),
|
||||
errorOnFetch:
|
||||
state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
|
||||
}),
|
||||
{
|
||||
fetch,
|
||||
fetchTrace,
|
||||
}
|
||||
)
|
||||
@withSiteIdRouter
|
||||
export default class ErrorInfo extends React.PureComponent {
|
||||
ensureInstance() {
|
||||
const { errorId, loading, errorOnFetch } = this.props;
|
||||
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
|
||||
this.props.fetch(errorId);
|
||||
this.props.fetchTrace(errorId);
|
||||
}
|
||||
ensureInstance() {
|
||||
const { errorId, loading, errorOnFetch } = this.props;
|
||||
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
|
||||
this.props.fetch(errorId);
|
||||
this.props.fetchTrace(errorId);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
next = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const next = list.get(curIndex + 1);
|
||||
if (next != null) {
|
||||
this.props.history.push(errorRoute(next.errorId));
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.ensureInstance();
|
||||
};
|
||||
prev = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const prev = list.get(curIndex - 1);
|
||||
if (prev != null) {
|
||||
this.props.history.push(errorRoute(prev.errorId));
|
||||
}
|
||||
next = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const next = list.get(curIndex + 1);
|
||||
if (next != null) {
|
||||
this.props.history.push(errorRoute(next.errorId));
|
||||
}
|
||||
};
|
||||
prev = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const prev = list.get(curIndex - 1);
|
||||
if (prev != null) {
|
||||
this.props.history.push(errorRoute(prev.errorId));
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const { loading, errorIdInStore, list, errorId } = this.props;
|
||||
};
|
||||
render() {
|
||||
const { loading, errorIdInStore, list, errorId } = this.props;
|
||||
|
||||
let nextDisabled = true,
|
||||
prevDisabled = true;
|
||||
if (list.size > 0) {
|
||||
nextDisabled = loading || list.last().errorId === errorId;
|
||||
prevDisabled = loading || list.first().errorId === errorId;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 Error Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
// animatedIcon="no-results"
|
||||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
{/* <div className="w-9/12 mb-4 flex justify-between">
|
||||
<BackLink to={ errorsRoute() } label="Back" />
|
||||
<div />
|
||||
<div className="flex items-center">
|
||||
<Popup
|
||||
pinned
|
||||
content="Prev Error"
|
||||
>
|
||||
<IconButton
|
||||
outline
|
||||
compact
|
||||
size="small"
|
||||
icon="prev1"
|
||||
disabled={ prevDisabled }
|
||||
onClick={this.prev}
|
||||
/>
|
||||
</Popup>
|
||||
<div className="mr-3" />
|
||||
|
||||
<Popup
|
||||
pinned
|
||||
content="Next Error"
|
||||
>
|
||||
<IconButton
|
||||
outline
|
||||
compact
|
||||
size="small"
|
||||
icon="next1"
|
||||
disabled={ nextDisabled }
|
||||
onClick={this.next}
|
||||
/>
|
||||
</Popup>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex">
|
||||
<Loader loading={loading} className="w-9/12">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
let nextDisabled = true,
|
||||
prevDisabled = true;
|
||||
if (list.size > 0) {
|
||||
nextDisabled = loading || list.last().errorId === errorId;
|
||||
prevDisabled = loading || list.first().errorId === errorId;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 Error Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
<div className="flex">
|
||||
<Loader loading={loading} className="w-9/12">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Icon, Popup } from 'UI'
|
||||
import React, { useState } from 'react';
|
||||
import { Icon, Tooltip as AppTooltip } from 'UI';
|
||||
import { numberCompact } from 'App/utils';
|
||||
import { BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, LabelList, Label } from 'recharts';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LabelList,
|
||||
Label,
|
||||
} from 'recharts';
|
||||
import { connect } from 'react-redux';
|
||||
import { setActiveStages } from 'Duck/funnels';
|
||||
import { Styles } from '../../Dashboard/Widgets/common';
|
||||
import { numberWithCommas } from 'App/utils'
|
||||
import { truncate } from 'App/utils'
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import { truncate } from 'App/utils';
|
||||
|
||||
const MIN_BAR_HEIGHT = 20;
|
||||
|
||||
|
|
@ -14,27 +25,32 @@ function CustomTick(props) {
|
|||
const { x, y, payload } = props;
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text x={0} y={0} dy={16} fontSize={12} textAnchor="middle" fill="#666">{payload.value}</text>
|
||||
<text x={0} y={0} dy={16} fontSize={12} textAnchor="middle" fill="#666">
|
||||
{payload.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function FunnelGraph(props) {
|
||||
const { data, activeStages, funnelId, liveFilters } = props;
|
||||
const [activeIndex, setActiveIndex] = useState(activeStages)
|
||||
const { data, activeStages, funnelId, liveFilters } = props;
|
||||
const [activeIndex, setActiveIndex] = useState(activeStages);
|
||||
|
||||
const renderPercentage = (props) => {
|
||||
const {
|
||||
x, y, width, height, value,
|
||||
} = props;
|
||||
const { x, y, width, height, value } = props;
|
||||
const radius = 10;
|
||||
const _x = (x + width / 2) + 45;
|
||||
|
||||
const _x = x + width / 2 + 45;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<svg width="46px" height="21px" version="1.1">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M37.2387001,0.5 L45.3588127,10.5034561 L37.4215407,20.5 L0.5,20.5 L0.5,0.5 L37.2387001,0.5 Z" id="Rectangle" stroke="#AFACAC" fill="#FFFFFF"></path>
|
||||
<path
|
||||
d="M37.2387001,0.5 L45.3588127,10.5034561 L37.4215407,20.5 L0.5,20.5 L0.5,0.5 L37.2387001,0.5 Z"
|
||||
id="Rectangle"
|
||||
stroke="#AFACAC"
|
||||
fill="#FFFFFF"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<text x={x} y={70} fill="#000" textAnchor="middle" dominantBaseline="middle">
|
||||
|
|
@ -45,31 +61,38 @@ function FunnelGraph(props) {
|
|||
};
|
||||
|
||||
const renderCustomizedLabel = (props) => {
|
||||
const {
|
||||
x, y, width, height, value, textColor = '#fff'
|
||||
} = props;
|
||||
const { x, y, width, height, value, textColor = '#fff' } = props;
|
||||
const radius = 10;
|
||||
|
||||
if (value === 0) return;
|
||||
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={x + width / 2} y={(y - radius) + 20} fill={ textColor } font-size="12" textAnchor="middle" dominantBaseline="middle">
|
||||
<g>
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y - radius + 20}
|
||||
fill={textColor}
|
||||
font-size="12"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{numberCompact(value)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const handleClick= (data, index) => {
|
||||
if (activeStages.length === 1 && activeStages.includes(index)) { // selecting the same bar
|
||||
const handleClick = (data, index) => {
|
||||
if (activeStages.length === 1 && activeStages.includes(index)) {
|
||||
// selecting the same bar
|
||||
props.setActiveStages([], null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStages.length === 2) { // already having two bars
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStages.length === 2) {
|
||||
// already having two bars
|
||||
return;
|
||||
}
|
||||
|
||||
// new selection
|
||||
const arr = activeStages.concat([index]);
|
||||
|
|
@ -78,167 +101,180 @@ function FunnelGraph(props) {
|
|||
|
||||
const resetActiveSatges = () => {
|
||||
props.setActiveStages([], liveFilters, funnelId, true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDropLabel = ({ x, y, width, value }) => {
|
||||
if (value === 0) return;
|
||||
return (
|
||||
<text fill="#cc0000" x={x + width / 2} y={y - 5} textAnchor="middle" fontSize="12">{value}</text>
|
||||
)
|
||||
}
|
||||
<text fill="#cc0000" x={x + width / 2} y={y - 5} textAnchor="middle" fontSize="12">
|
||||
{value}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMainLabel = ({ x, y, width, value }) => {
|
||||
const renderMainLabel = ({ x, y, width, value }) => {
|
||||
return (
|
||||
<text fill="#FFFFFF" x={x + width / 2} y={y + 14} textAnchor="middle" fontSize="12">{numberWithCommas(value)}</text>
|
||||
)
|
||||
}
|
||||
<text fill="#FFFFFF" x={x + width / 2} y={y + 14} textAnchor="middle" fontSize="12">
|
||||
{numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const CustomBar = (props) => {
|
||||
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? (MIN_BAR_HEIGHT - 1): dropDueToIssues
|
||||
const CustomBar = (props) => {
|
||||
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
|
||||
const tmp = (height <= 20 ? 20 : height) - (TEMP[index].height > 20 ? 0 : TEMP[index].height);
|
||||
return (
|
||||
<svg >
|
||||
<svg>
|
||||
<rect x={x} y={y} width={width} height={tmp} fill={fill} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
const MainBar = (props) => {
|
||||
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues, hasSelection = false } = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? (MIN_BAR_HEIGHT - 1): dropDueToIssues
|
||||
);
|
||||
};
|
||||
const MainBar = (props) => {
|
||||
const {
|
||||
fill,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
sessionsCount,
|
||||
index,
|
||||
dropDueToIssues,
|
||||
hasSelection = false,
|
||||
} = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
|
||||
|
||||
TEMP[index] = {height,y};
|
||||
TEMP[index] = { height, y };
|
||||
|
||||
return (
|
||||
<svg style={{ cursor: hasSelection ? '' : 'pointer' }}>
|
||||
<rect x={x} y={y} width={width} height={height} fill={fill} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const renderDropPct = (props) => { // TODO
|
||||
const renderDropPct = (props) => {
|
||||
// TODO
|
||||
const { fill, x, y, width, height, value, totalBars } = props;
|
||||
const barW = x + ((730 / totalBars) / 2);
|
||||
|
||||
const barW = x + 730 / totalBars / 2;
|
||||
|
||||
return (
|
||||
<svg >
|
||||
<rect x={barW} y={80} width={width} height={20} fill='red' />
|
||||
<svg>
|
||||
<rect x={barW} y={80} width={width} height={20} fill="red" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTooltip = (props) => {
|
||||
const { payload } = props;
|
||||
if (payload.length === 0) return null;
|
||||
const { value, headerText } = payload[0].payload;
|
||||
|
||||
|
||||
// const value = payload[0].payload.value;
|
||||
if (!value) return null;
|
||||
return (
|
||||
<div className="rounded border bg-white p-2">
|
||||
<div>{headerText}</div>
|
||||
{value.map(i => (
|
||||
{value.map((i) => (
|
||||
<div className="text-sm ml-2">{truncate(i, 30)}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
// const CustomTooltip = ({ active, payload, msg = '' }) => {
|
||||
// const CustomTooltip = ({ active, payload, msg = '' }) => {
|
||||
// return (
|
||||
// <div className="rounded border bg-white p-2">
|
||||
// <div className="rounded border bg-white p-2">
|
||||
// <p className="text-sm">{msg}</p>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
const TEMP = {}
|
||||
const TEMP = {};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{ activeStages.length === 2 && (
|
||||
{activeStages.length === 2 && (
|
||||
<div
|
||||
className="absolute right-0 top-0 cursor-pointer z-10"
|
||||
style={{marginRight: '60px', marginTop: '0' }}
|
||||
className="absolute right-0 top-0 cursor-pointer z-10"
|
||||
style={{ marginRight: '60px', marginTop: '0' }}
|
||||
onClick={resetActiveSatges}
|
||||
>
|
||||
<Popup
|
||||
content={ `Reset Selection` }
|
||||
>
|
||||
<AppTooltip title={`Reset Selection`}>
|
||||
<Icon name="sync-alt" size="15" color="teal" />
|
||||
</Popup>
|
||||
</AppTooltip>
|
||||
</div>
|
||||
)}
|
||||
<BarChart width={800} height={190} data={data}
|
||||
margin={{top: 20, right: 20, left: 0, bottom: 0}}
|
||||
)}
|
||||
<BarChart
|
||||
width={800}
|
||||
height={190}
|
||||
data={data}
|
||||
margin={{ top: 20, right: 20, left: 0, bottom: 0 }}
|
||||
background={'transparent'}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="1 3" stroke="#BBB" vertical={false} />
|
||||
{/* {activeStages.length < 2 && <Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip msg={activeStages.length > 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */}
|
||||
<Tooltip cursor={{ fill: 'transparent' }} content={CustomTooltip} />
|
||||
<CartesianGrid strokeDasharray="1 3" stroke="#BBB" vertical={false} />
|
||||
{/* {activeStages.length < 2 && <Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip msg={activeStages.length > 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */}
|
||||
<Tooltip cursor={{ fill: 'transparent' }} content={CustomTooltip} />
|
||||
|
||||
<Bar
|
||||
dataKey="sessionsCount"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<MainBar hasSelection={activeStages.length === 2} />}
|
||||
cursor="pointer"
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
background={false}
|
||||
>
|
||||
<LabelList dataKey="sessionsCount" content={renderMainLabel} />
|
||||
{
|
||||
data.map((entry, index) => {
|
||||
const selected = activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
cursor="pointer"
|
||||
fill={selected ? '#394EFF' : (opacity === 1 ? '#3EAAAF' : '#CCC') }
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Bar>
|
||||
|
||||
<Bar
|
||||
hide={activeStages.length !== 2}
|
||||
dataKey="dropDueToIssues"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<CustomBar />}
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
>
|
||||
<LabelList dataKey="dropDueToIssues" content={renderDropLabel} />
|
||||
{
|
||||
data.map((entry, index) => {
|
||||
const selected = activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
opacity={opacity}
|
||||
cursor="pointer"
|
||||
fill={ activeStages[1] === index ? '#cc000040' : 'transparent' }
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Bar>
|
||||
|
||||
<XAxis
|
||||
stroke={0}
|
||||
dataKey="label"
|
||||
strokeWidth={0}
|
||||
interval={0}
|
||||
// tick ={{ fill: '#666', fontSize: 12 }}
|
||||
tick={<CustomTick />}
|
||||
xAxisId={0}
|
||||
/>
|
||||
{/* <XAxis
|
||||
<Bar
|
||||
dataKey="sessionsCount"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<MainBar hasSelection={activeStages.length === 2} />}
|
||||
cursor="pointer"
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
background={false}
|
||||
>
|
||||
<LabelList dataKey="sessionsCount" content={renderMainLabel} />
|
||||
{data.map((entry, index) => {
|
||||
const selected =
|
||||
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
cursor="pointer"
|
||||
fill={selected ? '#394EFF' : opacity === 1 ? '#3EAAAF' : '#CCC'}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
|
||||
<Bar
|
||||
hide={activeStages.length !== 2}
|
||||
dataKey="dropDueToIssues"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<CustomBar />}
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
>
|
||||
<LabelList dataKey="dropDueToIssues" content={renderDropLabel} />
|
||||
{data.map((entry, index) => {
|
||||
const selected =
|
||||
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
opacity={opacity}
|
||||
cursor="pointer"
|
||||
fill={activeStages[1] === index ? '#cc000040' : 'transparent'}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
|
||||
<XAxis
|
||||
stroke={0}
|
||||
dataKey="label"
|
||||
strokeWidth={0}
|
||||
interval={0}
|
||||
// tick ={{ fill: '#666', fontSize: 12 }}
|
||||
tick={<CustomTick />}
|
||||
xAxisId={0}
|
||||
/>
|
||||
{/* <XAxis
|
||||
stroke={0}
|
||||
xAxisId={1}
|
||||
dataKey="value"
|
||||
|
|
@ -248,13 +284,21 @@ function FunnelGraph(props) {
|
|||
tick ={{ fill: '#666', fontSize: 12 }}
|
||||
tickFormatter={val => '"' + val + '"'}
|
||||
/> */}
|
||||
<YAxis interval={ 0 } strokeWidth={0} tick ={{ fill: '#999999', fontSize: 11 }} tickFormatter={val => Styles.tickFormatter(val)} />
|
||||
</BarChart>
|
||||
</div>
|
||||
)
|
||||
<YAxis
|
||||
interval={0}
|
||||
strokeWidth={0}
|
||||
tick={{ fill: '#999999', fontSize: 11 }}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
}), { setActiveStages })(FunnelGraph)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
}),
|
||||
{ setActiveStages }
|
||||
)(FunnelGraph);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Icon, BackLink, IconButton, Dropdown, Popup, TextEllipsis, Button } from 'UI';
|
||||
import { remove as deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered } from 'Duck/funnels';
|
||||
import { editFilter, editFunnelFilter, refresh, addFilter } from 'Duck/funnels';
|
||||
import React, { useState } from 'react';
|
||||
import { Icon, BackLink, IconButton, Dropdown, Tooltip, TextEllipsis, Button } from 'UI';
|
||||
import {
|
||||
remove as deleteFunnel,
|
||||
fetch,
|
||||
fetchInsights,
|
||||
fetchIssuesFiltered,
|
||||
fetchSessionsFiltered,
|
||||
} from 'Duck/funnels';
|
||||
import { editFilter, editFunnelFilter, refresh } from 'Duck/funnels';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { connect } from 'react-redux';
|
||||
import { confirm } from 'UI';
|
||||
|
|
@ -10,84 +16,107 @@ import stl from './funnelHeader.module.css';
|
|||
|
||||
const Info = ({ label = '', value = '', className = 'mx-4' }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={className}>
|
||||
<span className="color-gray-medium">{label}</span>
|
||||
<span className="font-medium ml-2">{value}</span>
|
||||
<span className="font-medium ml-2">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const FunnelHeader = (props) => {
|
||||
const { funnel, insights, funnels, onBack, funnelId, showFilters = false, funnelFilters, renameHandler } = props;
|
||||
const [showSaveModal, setShowSaveModal] = useState(false)
|
||||
const {
|
||||
funnel,
|
||||
insights,
|
||||
funnels,
|
||||
onBack,
|
||||
funnelId,
|
||||
showFilters = false,
|
||||
funnelFilters,
|
||||
renameHandler,
|
||||
} = props;
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
props.redirect(value)
|
||||
props.fetch(value).then(() => props.refresh(value))
|
||||
}
|
||||
props.redirect(value);
|
||||
props.fetch(value).then(() => props.refresh(value));
|
||||
};
|
||||
|
||||
const deleteFunnel = async (e, funnel) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (await confirm({
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Delete Funnel',
|
||||
confirmButton: 'Delete',
|
||||
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`
|
||||
})) {
|
||||
props.deleteFunnel(funnel.funnelId).then(props.onBack);
|
||||
} else {}
|
||||
}
|
||||
|
||||
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`,
|
||||
})
|
||||
) {
|
||||
props.deleteFunnel(funnel.funnelId).then(props.onBack);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
const onDateChange = (e) => {
|
||||
props.editFunnelFilter(e, funnelId);
|
||||
}
|
||||
};
|
||||
|
||||
const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS();
|
||||
const selectedFunnel = funnels.filter(i => i.funnelId === parseInt(funnelId)).first() || {};
|
||||
const eventsCount = funnel.filter.filters.filter(i => i.isEvent).size;
|
||||
const selectedFunnel = funnels.filter((i) => i.funnelId === parseInt(funnelId)).first() || {};
|
||||
const eventsCount = funnel.filter.filters.filter((i) => i.isEvent).size;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-white border rounded flex items-center w-full relative group pr-2">
|
||||
<BackLink onClick={onBack} vertical className="absolute" style={{ left: '-50px', top: '8px' }} />
|
||||
<FunnelSaveModal
|
||||
show={showSaveModal}
|
||||
closeHandler={() => setShowSaveModal(false)}
|
||||
/>
|
||||
<BackLink
|
||||
onClick={onBack}
|
||||
vertical
|
||||
className="absolute"
|
||||
style={{ left: '-50px', top: '8px' }}
|
||||
/>
|
||||
<FunnelSaveModal show={showSaveModal} closeHandler={() => setShowSaveModal(false)} />
|
||||
<div className="flex items-center mr-auto relative">
|
||||
<Dropdown
|
||||
<Dropdown
|
||||
scrolling
|
||||
trigger={
|
||||
<div className="text-xl capitalize font-medium" style={{ maxWidth: '300px', overflow: 'hidden'}}>
|
||||
<div
|
||||
className="text-xl capitalize font-medium"
|
||||
style={{ maxWidth: '300px', overflow: 'hidden' }}
|
||||
>
|
||||
<TextEllipsis text={selectedFunnel.name} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
options={options}
|
||||
className={ stl.dropdown }
|
||||
className={stl.dropdown}
|
||||
name="funnel"
|
||||
value={ parseInt(funnelId) }
|
||||
value={parseInt(funnelId)}
|
||||
// icon={null}
|
||||
onChange={ writeOption }
|
||||
onChange={writeOption}
|
||||
selectOnBlur={false}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
|
||||
icon={
|
||||
<Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} />
|
||||
}
|
||||
/>
|
||||
<Info label="Events" value={eventsCount} />
|
||||
<span>-</span>
|
||||
<Button variant="text-primary" onClick={props.toggleFilters}>{ showFilters ? 'HIDE' : 'EDIT FUNNEL' }</Button>
|
||||
<Info label="Sessions" value={insights.sessionsCount} />
|
||||
<Button variant="text-primary" onClick={props.toggleFilters}>
|
||||
{showFilters ? 'HIDE' : 'EDIT FUNNEL'}
|
||||
</Button>
|
||||
<Info label="Sessions" value={insights.sessionsCount} />
|
||||
<Info label="Conversion" value={`${insights.conversions}%`} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center invisible group-hover:visible">
|
||||
<Popup
|
||||
content={ `Edit Funnel` }
|
||||
>
|
||||
<IconButton icon="edit" onClick={() => setShowSaveModal(true)} />
|
||||
</Popup>
|
||||
<Popup content={ `Remove Funnel` } >
|
||||
<IconButton icon="trash" onClick={(e) => deleteFunnel(e, funnel)} className="ml-2 mr-2" />
|
||||
</Popup>
|
||||
<Tooltip title={`Edit Funnel`}>
|
||||
<IconButton icon="edit" onClick={() => setShowSaveModal(true)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={`Remove Funnel`}>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
onClick={(e) => deleteFunnel(e, funnel)}
|
||||
className="ml-2 mr-2"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<DateRange
|
||||
rangeValue={funnelFilters.rangeValue}
|
||||
|
|
@ -99,10 +128,22 @@ const FunnelHeader = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
funnelFilters: state.getIn([ 'funnels', 'funnelFilters' ]).toJS(),
|
||||
funnel: state.getIn([ 'funnels', 'instance' ]),
|
||||
}), { editFilter, editFunnelFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered, refresh })(FunnelHeader)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
funnelFilters: state.getIn(['funnels', 'funnelFilters']).toJS(),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
}),
|
||||
{
|
||||
editFilter,
|
||||
editFunnelFilter,
|
||||
deleteFunnel,
|
||||
fetch,
|
||||
fetchInsights,
|
||||
fetchIssuesFiltered,
|
||||
fetchSessionsFiltered,
|
||||
refresh,
|
||||
}
|
||||
)(FunnelHeader);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import stl from './funnelMenuItem.module.css'
|
||||
import { Icon, ItemMenu, Popup } from 'UI'
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import stl from './funnelMenuItem.module.css';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
function FunnelMenuItem({
|
||||
iconName = 'info',
|
||||
|
|
@ -14,31 +14,34 @@ function FunnelMenuItem({
|
|||
}) {
|
||||
return (
|
||||
<div
|
||||
className={ cn(
|
||||
className,
|
||||
stl.menuItem,
|
||||
"flex items-center py-1 justify-between group",
|
||||
{ [stl.active] : active }
|
||||
)}
|
||||
className={cn(className, stl.menuItem, 'flex items-center py-1 justify-between group', {
|
||||
[stl.active]: active,
|
||||
})}
|
||||
onClick={disabled ? null : onClick}
|
||||
>
|
||||
<div className={ cn(stl.iconLabel, 'flex items-center', { [stl.disabled] : disabled })}>
|
||||
<div className={cn(stl.iconLabel, 'flex items-center', { [stl.disabled]: disabled })}>
|
||||
<div className="flex items-center justify-center w-8 h-8 flex-shrink-0">
|
||||
<Icon name={ iconName } size={ 16 } color={'gray-dark'} className="absolute" />
|
||||
<Icon name={iconName} size={16} color={'gray-dark'} className="absolute" />
|
||||
</div>
|
||||
<span className={cn(stl.title, 'cap-first')}>{ title }</span>
|
||||
<span className={cn(stl.title, 'cap-first')}>{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className={cn("mx-2", { 'invisible': !isPublic })}>
|
||||
<Popup content={ `Shared with team` } >
|
||||
<div className={cn("bg-gray-light h-8 w-8 rounded-full flex items-center justify-center", stl.teamIcon)} style={{ opacity: '0.3'}}>
|
||||
<Icon name="user-friends" color="gray-dark" size={16} />
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('mx-2', { invisible: !isPublic })}>
|
||||
<Tooltip title={`Shared with team`}>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-gray-light h-8 w-8 rounded-full flex items-center justify-center',
|
||||
stl.teamIcon
|
||||
)}
|
||||
style={{ opacity: '0.3' }}
|
||||
>
|
||||
<Icon name="user-friends" color="gray-dark" size={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelMenuItem
|
||||
export default FunnelMenuItem;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import FunnelStepText from './FunnelStepText';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
filter: any;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,45 @@
|
|||
import React from 'react'
|
||||
import { Popup } from 'UI'
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'UI';
|
||||
|
||||
function IssueGraph({ issue }) {
|
||||
return (
|
||||
<div className="flex rounded-sm" style={{ width: '600px' }}>
|
||||
<Popup content={ `Unaffected sessions` } >
|
||||
<div style={{ width: issue.unaffectedSessionsPer + '%' }} className="relative">
|
||||
<div className="w-full relative rounded-tl-sm rounded-bl-sm" style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.unaffectedSessions}</div>
|
||||
<div className="flex rounded-sm" style={{ width: '600px' }}>
|
||||
<Tooltip title={`Unaffected sessions`}>
|
||||
<div style={{ width: issue.unaffectedSessionsPer + '%' }} className="relative">
|
||||
<div
|
||||
className="w-full relative rounded-tl-sm rounded-bl-sm"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
|
||||
{issue.unaffectedSessions}
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup content={ `Affected sessions` } >
|
||||
<div style={{ width: issue.affectedSessionsPer + '%'}} className="border-l relative">
|
||||
<div className="w-full relative" style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.affectedSessions}</div>
|
||||
{/* <div className="absolute left-0 ml-1 text-xs">{issue.affectedSessionsPer}</div> */}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={`Affected sessions`}>
|
||||
<div style={{ width: issue.affectedSessionsPer + '%' }} className="border-l relative">
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
|
||||
{issue.affectedSessions}
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup content={ `Conversion lost` } >
|
||||
<div style={{ width: issue.lostConversionsPer + '%'}} className="border-l relative">
|
||||
<div className="w-full relative rounded-tr-sm rounded-br-sm" style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">{issue.lostConversions}</div>
|
||||
{/* <div className="absolute left-0 ml-1 text-xs">{issue.affectedSessionsPer}</div> */}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={`Conversion lost`}>
|
||||
<div style={{ width: issue.lostConversionsPer + '%' }} className="border-l relative">
|
||||
<div
|
||||
className="w-full relative rounded-tr-sm rounded-br-sm"
|
||||
style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }}
|
||||
/>
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">
|
||||
{issue.lostConversions}
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default IssueGraph
|
||||
export default IssueGraph;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom';
|
|||
import cn from 'classnames';
|
||||
import { client, CLIENT_DEFAULT_TAB } from 'App/routes';
|
||||
import { logout } from 'Duck/user';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import styles from './header.module.css';
|
||||
import OnboardingExplore from './OnboardingExplore/OnboardingExplore';
|
||||
import Notifications from '../Alerts/Notifications';
|
||||
|
|
@ -68,7 +68,7 @@ const Header = (props) => {
|
|||
|
||||
<Notifications />
|
||||
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
|
||||
<Popup content={`Preferences`} disabled>
|
||||
<Tooltip title={`Preferences`} disabled>
|
||||
<div className="flex items-center">
|
||||
<NavLink to={CLIENT_PATH}>
|
||||
<Icon name="gear" size="20" color="gray-dark" />
|
||||
|
|
@ -76,7 +76,7 @@ const Header = (props) => {
|
|||
|
||||
<SettingsMenu className="invisible group-hover:visible" account={account} />
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function SettingsMenu(props: RouteComponentProps<Props>) {
|
|||
};
|
||||
return (
|
||||
<div
|
||||
style={{ width: '150px', marginTop: '49px' }}
|
||||
style={{ width: '150px', marginTop: '35px' }}
|
||||
className={cn(className, 'absolute right-0 top-0 bg-white border p-2')}
|
||||
>
|
||||
{isAdmin && (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Popup, SlideModal } from 'UI';
|
||||
import { Tooltip, SlideModal } from 'UI';
|
||||
|
||||
import { NETWORK } from 'Player/ios/state';
|
||||
|
||||
|
|
@ -24,13 +24,13 @@ const COLUMNS = [
|
|||
label: 'url',
|
||||
width: 130,
|
||||
render: (r) => (
|
||||
<Popup
|
||||
content={<div className={cls.popupNameContent}>{r.url}</div>}
|
||||
<Tooltip
|
||||
title={<div className={cls.popupNameContent}>{r.url}</div>}
|
||||
size="mini"
|
||||
position="right center"
|
||||
>
|
||||
<div className={cls.popupNameTrigger}>{r.url}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import { CRASHES, EVENTS } from 'Player/ios/state';
|
||||
import TimeTracker from './TimeTracker';
|
||||
import PlayerTime from './PlayerTime';
|
||||
|
|
@ -28,12 +28,10 @@ export default function Timeline({ player }) {
|
|||
<div key={e.key} className={cls.event} style={{ left: `${e.time * scale}%` }} />
|
||||
))}
|
||||
{player.lists[CRASHES].list.map((e) => (
|
||||
<Popup
|
||||
<Tooltip
|
||||
key={e.key}
|
||||
offset="-19"
|
||||
pinned
|
||||
className="error"
|
||||
content={
|
||||
title={
|
||||
<div className={cls.popup}>
|
||||
<b>{`Crash ${e.name}:`}</b>
|
||||
<br />
|
||||
|
|
@ -46,7 +44,7 @@ export default function Timeline({ player }) {
|
|||
className={cn(cls.markup, cls.error)}
|
||||
style={{ left: `${e.time * scale}%` }}
|
||||
/>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<PlayerTime player={player} timeKey="endTime" />
|
||||
|
|
|
|||
|
|
@ -1,77 +1,83 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { setAutoplayValues } from 'Duck/sessions'
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { setAutoplayValues } from 'Duck/sessions';
|
||||
import { session as sessionRoute } from 'App/routes';
|
||||
import { Link, Icon, Toggler } from 'UI';
|
||||
import { Link, Icon, Toggler, Tooltip } from 'UI';
|
||||
import { connectPlayer } from 'Player/store';
|
||||
import { Controls as PlayerControls } from 'Player';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import cn from 'classnames';
|
||||
|
||||
function Autoplay(props) {
|
||||
const { previousId, nextId, autoplay, disabled } = props
|
||||
const { previousId, nextId, autoplay, disabled } = props;
|
||||
|
||||
useEffect(() => {
|
||||
props.setAutoplayValues()
|
||||
}, [])
|
||||
props.setAutoplayValues();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div onClick={props.toggleAutoplay} className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2">
|
||||
<Toggler
|
||||
name="sessionsLive"
|
||||
onChange={ props.toggleAutoplay }
|
||||
checked={ autoplay }
|
||||
/>
|
||||
<span className="ml-2">Auto-Play</span>
|
||||
<div
|
||||
onClick={props.toggleAutoplay}
|
||||
className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2"
|
||||
>
|
||||
<Toggler name="sessionsLive" onChange={props.toggleAutoplay} checked={autoplay} />
|
||||
<span className="ml-2 whitespace-nowrap">Auto-Play</span>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
position="bottom center"
|
||||
title="Play Previous Session"
|
||||
placement="bottom"
|
||||
title={<div className="whitespace-nowrap">Play Previous Session</div>}
|
||||
disabled={!previousId}
|
||||
className={cn(
|
||||
"p-1 bg-gray-bg group rounded-full color-gray-darkest font-medium",
|
||||
previousId && 'cursor-pointer',
|
||||
!disabled && nextId&& 'hover:bg-bg-blue'
|
||||
)}
|
||||
>
|
||||
<Link to={ sessionRoute(previousId) } disabled={!previousId}>
|
||||
<Icon name="prev1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
<Link to={sessionRoute(previousId)} disabled={!previousId}>
|
||||
<div
|
||||
className={cn(
|
||||
'p-1 bg-gray-bg group rounded-full color-gray-darkest font-medium',
|
||||
previousId && 'cursor-pointer',
|
||||
!disabled && nextId && 'hover:bg-bg-blue'
|
||||
)}
|
||||
>
|
||||
<Icon name="prev1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
</div>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
position="bottom center"
|
||||
title="Play Next Session"
|
||||
placement="bottom"
|
||||
title={<div className="whitespace-nowrap">Play Next Session</div>}
|
||||
disabled={!nextId}
|
||||
className={cn(
|
||||
"p-1 bg-gray-bg group ml-1 rounded-full color-gray-darkest font-medium",
|
||||
nextId && 'cursor-pointer',
|
||||
!disabled && nextId && 'hover:bg-bg-blue',
|
||||
)}
|
||||
>
|
||||
<Link to={ sessionRoute(nextId) } disabled={!nextId} >
|
||||
<Icon name="next1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
<Link to={sessionRoute(nextId)} disabled={!nextId}>
|
||||
<div
|
||||
className={cn(
|
||||
'p-1 bg-gray-bg group ml-1 rounded-full color-gray-darkest font-medium',
|
||||
nextId && 'cursor-pointer',
|
||||
!disabled && nextId && 'hover:bg-bg-blue'
|
||||
)}
|
||||
>
|
||||
<Icon name="next1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
</div>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const connectAutoplay = connect(state => ({
|
||||
previousId: state.getIn([ 'sessions', 'previousId' ]),
|
||||
nextId: state.getIn([ 'sessions', 'nextId' ]),
|
||||
}), { setAutoplayValues })
|
||||
const connectAutoplay = connect(
|
||||
(state) => ({
|
||||
previousId: state.getIn(['sessions', 'previousId']),
|
||||
nextId: state.getIn(['sessions', 'nextId']),
|
||||
}),
|
||||
{ setAutoplayValues }
|
||||
);
|
||||
|
||||
export default connectAutoplay(connectPlayer(state => ({
|
||||
autoplay: state.autoplay,
|
||||
}), {
|
||||
toggleAutoplay: PlayerControls.toggleAutoplay
|
||||
})(Autoplay))
|
||||
export default connectAutoplay(
|
||||
connectPlayer(
|
||||
(state) => ({
|
||||
autoplay: state.autoplay,
|
||||
}),
|
||||
{
|
||||
toggleAutoplay: PlayerControls.toggleAutoplay,
|
||||
}
|
||||
)(Autoplay)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Tooltip } from 'UI';
|
||||
|
||||
function ReportTitle() {
|
||||
const { bugReportStore } = useStore();
|
||||
|
|
@ -45,7 +45,7 @@ function ReportTitle() {
|
|||
/>
|
||||
) : (
|
||||
// @ts-ignore
|
||||
<Tooltip delay={100} arrow title="Double click to rename">
|
||||
<Tooltip title="Double click to rename">
|
||||
<div
|
||||
onDoubleClick={toggleEdit}
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import SectionTitle from './SectionTitle';
|
||||
import { Icon } from 'UI'
|
||||
import { Tooltip } from 'react-tippy'
|
||||
import { Icon, Tooltip } from 'UI'
|
||||
|
||||
export default function Session({ user, sessionUrl }: { user: string, sessionUrl: string }) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon, ItemMenu } from 'UI';
|
||||
import { Icon, ItemMenu, Tooltip } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Step as IStep } from '../../types';
|
||||
const STEP_NAMES = { CLICKRAGE: 'Multiple click', CLICK: 'Clicked', LOCATION: 'Visited' };
|
||||
|
|
@ -7,7 +7,6 @@ import { useStore } from 'App/mstore';
|
|||
import cn from 'classnames';
|
||||
import { ErrorComp, NetworkComp, NoteComp } from './SubModalItems';
|
||||
import { durationFromMs } from 'App/date';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
const SUBSTEP = {
|
||||
network: NetworkComp,
|
||||
|
|
@ -80,7 +79,7 @@ function Step({ step, ind, isDefault }: { step: IStep; ind: number; isDefault?:
|
|||
/>
|
||||
</Tooltip>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Delete Step">
|
||||
<Tooltip title="Delete Step" className="whitespace-nowrap">
|
||||
<div onClick={() => bugReportStore.removeStep(step)}>
|
||||
<Icon name="trash" size={16} className="cursor-pointer hover:fill-gray-darkest" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
import { Tooltip } from 'UI'
|
||||
|
||||
interface Props {
|
||||
pickRadius: number;
|
||||
|
|
@ -11,7 +11,7 @@ function StepRadius({ pickRadius, setRadius }: Props) {
|
|||
<div className="w-full flex items-center gap-4">
|
||||
<div className="border-b border-dotted border-gray-medium cursor-help">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip html={<span>Closest step to the selected timestamp ± {pickRadius}.</span>}>
|
||||
<Tooltip title={<span>Closest step to the selected timestamp ± {pickRadius}.</span>}>
|
||||
<span>± {pickRadius}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ import { countries } from 'App/constants';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { Avatar, TextEllipsis, CountryFlag, Icon } from 'UI';
|
||||
import { Avatar, TextEllipsis, CountryFlag, Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { withRequest } from 'HOCs';
|
||||
import SessionInfoItem from '../../SessionInfoItem';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserSessionsModal from 'Shared/UserSessionsModal';
|
||||
|
||||
|
|
@ -77,12 +76,13 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
</span>
|
||||
<span className="mx-1 font-bold text-xl">·</span>
|
||||
<Tooltip
|
||||
theme="light"
|
||||
// theme="light"
|
||||
delay={0}
|
||||
hideOnClick="persistent"
|
||||
arrow
|
||||
interactive
|
||||
html={
|
||||
// hideOnClick="persistent"
|
||||
// arrow
|
||||
// interactive
|
||||
className="!bg-white shadow border !color-gray-dark"
|
||||
title={
|
||||
<div className="text-left">
|
||||
<SessionInfoItem
|
||||
comp={<CountryFlag country={userCountry} />}
|
||||
|
|
@ -100,9 +100,9 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
{revId && <SessionInfoItem icon="info" label="Rev ID:" value={revId} isLast />}
|
||||
</div>
|
||||
}
|
||||
position="bottom center"
|
||||
hoverable
|
||||
disabled={false}
|
||||
position="bottom"
|
||||
// hoverable
|
||||
// disabled={false}
|
||||
on="hover"
|
||||
>
|
||||
<span className="color-teal cursor-pointer">More</span>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import stl from './issueListItem.module.css';
|
||||
|
||||
const IssueListItem = ({ issue, onClick, icon, user, active }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={ () => onClick(issue) }
|
||||
className={ cn(stl.wrapper, active ? 'active-bg' : '', 'flex flex-col justify-between cursor-pointer text-base text-gray-800')}
|
||||
onClick={() => onClick(issue)}
|
||||
className={cn(
|
||||
stl.wrapper,
|
||||
active ? 'active-bg' : '',
|
||||
'flex flex-col justify-between cursor-pointer text-base text-gray-800'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{ icon }
|
||||
{/* <img src={ icon } width="16" height="16" className="mr-3" /> */}
|
||||
<span>{ issue.id }</span>
|
||||
{icon}
|
||||
<span>{issue.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{ user &&
|
||||
<Popup content={ 'Assignee ' + user.name } >
|
||||
<img src={ user.avatarUrls['24x24'] } width="24" height="24" />
|
||||
</Popup>
|
||||
}
|
||||
{user && (
|
||||
<Tooltip title={'Assignee ' + user.name}>
|
||||
<img src={user.avatarUrls['24x24']} width="24" height="24" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={ stl.title }>{ issue.title }</div>
|
||||
<div className={stl.title}>{issue.title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Popup, Button, Icon } from 'UI';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { Icon, Popover } from 'UI';
|
||||
import IssuesModal from './IssuesModal';
|
||||
import { fetchProjects, fetchMeta } from 'Duck/assignments';
|
||||
import stl from './issues.module.css';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
|
|
@ -71,32 +69,25 @@ class Issues extends React.Component {
|
|||
return (
|
||||
<div className="relative h-full w-full p-3">
|
||||
<div className={stl.buttonWrapper}>
|
||||
<OutsideClickDetectingDiv onClickOutside={this.closeModal}>
|
||||
<Tooltip
|
||||
open={this.state.showModal}
|
||||
position="bottom"
|
||||
interactive
|
||||
trigger="click"
|
||||
unmountHTMLWhenHide
|
||||
useContext
|
||||
theme="light"
|
||||
arrow
|
||||
html={
|
||||
<div>
|
||||
<IssuesModal
|
||||
provider={provider}
|
||||
sessionId={sessionId}
|
||||
closeHandler={this.closeModal}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<Popover
|
||||
render={({ close }) => (
|
||||
<div>
|
||||
<IssuesModal
|
||||
provider={provider}
|
||||
sessionId={sessionId}
|
||||
closeHandler={close}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center"
|
||||
disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}
|
||||
>
|
||||
<div className="flex items-center" onClick={this.handleOpen} disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}>
|
||||
<Icon name={ `integrations/${ provider === 'jira' ? 'jira' : 'github'}` } size="16" />
|
||||
<span className="ml-2">Create Issue</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</OutsideClickDetectingDiv>
|
||||
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size="16" />
|
||||
<span className="ml-2">Create Issue</span>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connectPlayer, jump, pause } from 'Player';
|
||||
import { Popup, Button, TextEllipsis } from 'UI';
|
||||
import { Tooltip, Button, TextEllipsis } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import { TYPES } from 'Types/session/resource';
|
||||
import stl from './network.module.css';
|
||||
|
|
@ -29,12 +29,12 @@ const TAB_TO_TYPE_MAP = {
|
|||
export function renderName(r) {
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<Popup
|
||||
<Tooltip
|
||||
style={{ maxWidth: '75%' }}
|
||||
content={<div className={stl.popupNameContent}>{r.url}</div>}
|
||||
title={<div className={stl.popupNameContent}>{r.url}</div>}
|
||||
>
|
||||
<TextEllipsis>{r.name}</TextEllipsis>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -56,9 +56,9 @@ export function renderDuration(r) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Popup content={tooltipText}>
|
||||
<Tooltip title={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
// import { connectPlayer } from 'Player';
|
||||
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
|
||||
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import { TYPES } from 'Types/session/resource';
|
||||
import { formatBytes } from 'App/utils';
|
||||
|
|
@ -40,17 +40,17 @@ const LOAD_TIME_COLOR = 'red';
|
|||
|
||||
export function renderType(r) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
|
||||
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.type}</div>}>
|
||||
<div className={stl.popupNameTrigger}>{r.type}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderName(r) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
|
||||
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.url}</div>}>
|
||||
<div className={stl.popupNameTrigger}>{r.name}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,6 @@ const renderXHRText = () => (
|
|||
<span className="flex items-center">
|
||||
{XHR}
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
content={
|
||||
<>
|
||||
Use our{' '}
|
||||
|
|
@ -129,9 +128,9 @@ function renderSize(r) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={content}>
|
||||
<Tooltip style={{ width: '100%' }} content={content}>
|
||||
<div>{triggerText}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -152,9 +151,9 @@ export function renderDuration(r) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={tooltipText}>
|
||||
<Tooltip style={{ width: '100%' }} content={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +243,7 @@ export default class NetworkContent extends React.PureComponent {
|
|||
<BottomBlock.Content>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div>
|
||||
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
|
||||
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
|
||||
</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point label={filtered.length} value=" requests" />
|
||||
|
|
|
|||
|
|
@ -1,66 +1,87 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { getTimelinePosition } from 'App/utils';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import PerformanceGraph from '../PerformanceGraph';
|
||||
interface Props {
|
||||
list?: any[];
|
||||
title: string;
|
||||
message?: string;
|
||||
className?: string;
|
||||
endTime?: number;
|
||||
renderElement?: (item: any) => React.ReactNode;
|
||||
isGraph?: boolean;
|
||||
zIndex?: number;
|
||||
noMargin?: boolean;
|
||||
|
||||
list?: any[];
|
||||
title: string;
|
||||
message?: string;
|
||||
className?: string;
|
||||
endTime?: number;
|
||||
renderElement?: (item: any) => React.ReactNode;
|
||||
isGraph?: boolean;
|
||||
zIndex?: number;
|
||||
noMargin?: boolean;
|
||||
}
|
||||
const EventRow = React.memo((props: Props) => {
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
|
||||
const scale = 100 / endTime;
|
||||
const _list =
|
||||
!isGraph &&
|
||||
React.useMemo(() => {
|
||||
return list.map((item: any, _index: number) => {
|
||||
const spread = item.toJS ? { ...item.toJS() } : { ...item }
|
||||
return {
|
||||
...spread,
|
||||
left: getTimelinePosition(item.time, scale),
|
||||
};
|
||||
});
|
||||
}, [list]);
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
|
||||
const scale = 100 / endTime;
|
||||
const _list =
|
||||
!isGraph &&
|
||||
React.useMemo(() => {
|
||||
return list.map((item: any, _index: number) => {
|
||||
const spread = item.toJS ? { ...item.toJS() } : { ...item };
|
||||
return {
|
||||
...spread,
|
||||
left: getTimelinePosition(item.time, scale),
|
||||
};
|
||||
});
|
||||
}, [list]);
|
||||
|
||||
return (
|
||||
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: isGraph ? 60 : 50 }}>
|
||||
<div className={cn("uppercase color-gray-medium text-sm flex items-center py-1", props.noMargin ? '' : 'ml-4' )}>
|
||||
<div style={{ zIndex: props.zIndex ? props.zIndex : undefined }} className="mr-2 leading-none">{title}</div>
|
||||
{message ? <RowInfo zIndex={props.zIndex} message={message} /> : null}
|
||||
</div>
|
||||
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
|
||||
{isGraph ? (
|
||||
<PerformanceGraph list={list} />
|
||||
) : (
|
||||
_list.length > 0 ? _list.map((item: any, index: number) => {
|
||||
return (
|
||||
<div key={index} className="absolute" style={{ left: `clamp(0%, calc(${item.left}% - 7px), calc(100% - 14px))`, zIndex: props.zIndex ? props.zIndex : undefined }}>
|
||||
{props.renderElement ? props.renderElement(item) : null}
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div className={cn("color-gray-medium text-sm pt-2", props.noMargin ? '' : 'ml-4')}>None captured.</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={cn('w-full flex flex-col py-2', className)}
|
||||
style={{ height: isGraph ? 60 : 50 }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'uppercase color-gray-medium text-sm flex items-center py-1',
|
||||
props.noMargin ? '' : 'ml-4'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{ zIndex: props.zIndex ? props.zIndex : undefined }}
|
||||
className="mr-2 leading-none"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
{message ? <RowInfo zIndex={props.zIndex} message={message} /> : null}
|
||||
</div>
|
||||
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
|
||||
{isGraph ? (
|
||||
<PerformanceGraph list={list} />
|
||||
) : _list.length > 0 ? (
|
||||
_list.map((item: any, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `clamp(0%, calc(${item.left}% - 7px), calc(100% - 14px))`,
|
||||
zIndex: props.zIndex ? props.zIndex : undefined,
|
||||
}}
|
||||
>
|
||||
{props.renderElement ? props.renderElement(item) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={cn('color-gray-medium text-sm pt-2', props.noMargin ? '' : 'ml-4')}>
|
||||
None captured.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default EventRow;
|
||||
|
||||
function RowInfo({ message, zIndex } : any) {
|
||||
return (
|
||||
<Popup content={message} delay={0} style={{ zIndex: zIndex ? zIndex : undefined }}>
|
||||
<Icon name="info-circle" color="gray-medium"/>
|
||||
</Popup>
|
||||
)
|
||||
function RowInfo({ message, zIndex }: any) {
|
||||
return (
|
||||
<Tooltip title={message} delay={0} style={{ zIndex: zIndex ? zIndex : undefined }}>
|
||||
<Icon name="info-circle" color="gray-medium" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Checkbox, Popup } from 'UI';
|
||||
import { Checkbox, Tooltip } from 'UI';
|
||||
|
||||
const NETWORK = 'NETWORK';
|
||||
const ERRORS = 'ERRORS';
|
||||
|
|
@ -8,48 +8,48 @@ const CLICKRAGE = 'CLICKRAGE';
|
|||
const PERFORMANCE = 'PERFORMANCE';
|
||||
|
||||
export const HELP_MESSAGE: any = {
|
||||
NETWORK: 'Network requests made in this session',
|
||||
EVENTS: 'Visualizes the events that takes place in the DOM',
|
||||
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
|
||||
CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded',
|
||||
PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline',
|
||||
}
|
||||
NETWORK: 'Network requests made in this session',
|
||||
EVENTS: 'Visualizes the events that takes place in the DOM',
|
||||
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
|
||||
CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded',
|
||||
PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
list: any[];
|
||||
updateList: any;
|
||||
list: any[];
|
||||
updateList: any;
|
||||
}
|
||||
function FeatureSelection(props: Props) {
|
||||
const { list } = props;
|
||||
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
|
||||
const disabled = list.length >= 3;
|
||||
const { list } = props;
|
||||
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
|
||||
const disabled = list.length >= 3;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{features.map((feature, index) => {
|
||||
const checked = list.includes(feature);
|
||||
const _disabled = disabled && !checked;
|
||||
return (
|
||||
<Popup content="X-RAY supports up to 3 views" disabled={!_disabled} delay={0}>
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={feature}
|
||||
checked={checked}
|
||||
className="mx-4"
|
||||
disabled={_disabled}
|
||||
onClick={() => {
|
||||
if (checked) {
|
||||
props.updateList(list.filter((item: any) => item !== feature));
|
||||
} else {
|
||||
props.updateList([...list, feature]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{features.map((feature, index) => {
|
||||
const checked = list.includes(feature);
|
||||
const _disabled = disabled && !checked;
|
||||
return (
|
||||
<Tooltip title="X-RAY supports up to 3 views" disabled={!_disabled} delay={0}>
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={feature}
|
||||
checked={checked}
|
||||
className="mx-4"
|
||||
disabled={_disabled}
|
||||
onClick={() => {
|
||||
if (checked) {
|
||||
props.updateList(list.filter((item: any) => item !== feature));
|
||||
} else {
|
||||
props.updateList([...list, feature]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeatureSelection;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import React from 'react';
|
||||
import { connectPlayer, Controls } from 'App/player';
|
||||
import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player';
|
||||
import { Controls } from 'App/player';
|
||||
import { NETWORK, EXCEPTIONS } from 'Duck/components/player';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { Icon, ErrorDetails, Popup } from 'UI';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { TYPES as EVENT_TYPES } from 'Types/session/event';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import StackEventModal from '../StackEventModal';
|
||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
import FetchDetails from 'Shared/FetchDetailsModal';
|
||||
|
|
@ -46,54 +44,58 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
const renderNetworkElement = (item: any) => {
|
||||
const name = item.name || '';
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
|
||||
<br />
|
||||
{name.length > 200 ? name.slice(0, 100) + ' ... ' + name.slice(-50) : name.length > 200 ? (item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)) : item.name}
|
||||
{name.length > 200
|
||||
? name.slice(0, 100) + ' ... ' + name.slice(-50)
|
||||
: name.length > 200
|
||||
? item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)
|
||||
: item.name}
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
placement="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
|
||||
<div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm">
|
||||
<span>!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderClickRageElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{'Click Rage'}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
placement="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
|
||||
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStackEventElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{'Stack Event'}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
onClick={createEventClickHandler(item, 'EVENT')}
|
||||
|
|
@ -101,20 +103,20 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
>
|
||||
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPerformanceElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{item.type}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
onClick={createEventClickHandler(item, EXCEPTIONS)}
|
||||
|
|
@ -122,14 +124,14 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
>
|
||||
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderExceptionElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{'Exception'}</b>
|
||||
<br />
|
||||
|
|
@ -137,14 +139,14 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
placement="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
|
||||
<div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm">
|
||||
<span>!</span>
|
||||
</div>
|
||||
<span>!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import LiveTag from 'Shared/LiveTag';
|
||||
import { jumpToLive } from 'Player';
|
||||
|
||||
import { Icon } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { toggleInspectorMode } from 'Player';
|
||||
import {
|
||||
fullscreenOn,
|
||||
|
|
@ -33,7 +33,6 @@ import ControlButton from './ControlButton';
|
|||
import PlayerControls from './components/PlayerControls';
|
||||
|
||||
import styles from './controls.module.css';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import XRayButton from 'Shared/XRayButton';
|
||||
|
||||
const SKIP_INTERVALS = {
|
||||
|
|
@ -227,11 +226,7 @@ export default class Controls extends React.Component {
|
|||
|
||||
return (
|
||||
<Tooltip
|
||||
delay={0}
|
||||
position="top"
|
||||
title={label}
|
||||
interactive
|
||||
hideOnClick="persistent"
|
||||
className="mr-4"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Icon } from 'UI';
|
||||
import { Icon, Tooltip, Popover } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { ReduxTime } from '../Time';
|
||||
|
|
@ -49,7 +48,7 @@ function PlayerControls(props: Props) {
|
|||
const speedRef = React.useRef(null);
|
||||
const arrowBackRef = React.useRef(null);
|
||||
const arrowForwardRef = React.useRef(null);
|
||||
const skipRef = React.useRef<HTMLDivElement>()
|
||||
const skipRef = React.useRef<HTMLDivElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyboard = (e: KeyboardEvent) => {
|
||||
|
|
@ -77,11 +76,12 @@ function PlayerControls(props: Props) {
|
|||
setShowTooltip(!showTooltip);
|
||||
};
|
||||
const handleClickOutside = () => {
|
||||
setShowTooltip(false)
|
||||
}
|
||||
setShowTooltip(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{playButton}
|
||||
<div className="mx-1" />
|
||||
{!live && (
|
||||
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
|
||||
{/* @ts-ignore */}
|
||||
|
|
@ -94,7 +94,7 @@ function PlayerControls(props: Props) {
|
|||
|
||||
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Rewind 10s" delay={0} position="top">
|
||||
<Tooltip title="Rewind 10s" position="top">
|
||||
<button
|
||||
ref={arrowBackRef}
|
||||
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
|
||||
|
|
@ -109,17 +109,17 @@ function PlayerControls(props: Props) {
|
|||
</button>
|
||||
</Tooltip>
|
||||
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border">
|
||||
<OutsideClickDetectingDiv onClickOutside={handleClickOutside}>
|
||||
<Tooltip
|
||||
open={showTooltip}
|
||||
interactive
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
html={
|
||||
<OutsideClickDetectingDiv onClickOutside={handleClickOutside}>
|
||||
<Popover
|
||||
// open={showTooltip}
|
||||
// interactive
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={() => (
|
||||
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">
|
||||
Jump <span className="text-disabled-text">(Secs)</span>
|
||||
|
|
@ -141,19 +141,19 @@ function PlayerControls(props: Props) {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip disabled={showTooltip} title="Set default skip duration">
|
||||
{currentInterval}s
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip disabled={showTooltip} title="Set default skip duration">
|
||||
{currentInterval}s
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Popover>
|
||||
</OutsideClickDetectingDiv>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Forward 10s" delay={0} position="top">
|
||||
<Tooltip title="Forward 10s" position="top">
|
||||
<button
|
||||
ref={arrowForwardRef}
|
||||
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
|
||||
|
|
@ -173,7 +173,7 @@ function PlayerControls(props: Props) {
|
|||
<div className="flex items-center">
|
||||
<div className="mx-2" />
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Control play back speed (↑↓)" delay={0} position="top">
|
||||
<Tooltip title="Control play back speed (↑↓)" placement="top">
|
||||
<button
|
||||
ref={speedRef}
|
||||
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
|
||||
|
|
@ -185,10 +185,10 @@ function PlayerControls(props: Props) {
|
|||
</Tooltip>
|
||||
<div className="mx-2" />
|
||||
<button
|
||||
className={cn(
|
||||
styles.skipIntervalButton,
|
||||
{ [styles.withCheckIcon]: skip, [styles.active]: skip },
|
||||
)}
|
||||
className={cn(styles.skipIntervalButton, {
|
||||
[styles.withCheckIcon]: skip,
|
||||
[styles.active]: skip,
|
||||
})}
|
||||
onClick={toggleSkip}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
import cn from 'classnames';
|
||||
import stl from './Marker.module.css';
|
||||
import { activeTarget } from 'Player';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
target: MarkedTarget;
|
||||
|
|
@ -17,23 +16,17 @@ export default function Marker({ target, active }: Props) {
|
|||
left: `${target.boundingRect.left}px`,
|
||||
width: `${target.boundingRect.width}px`,
|
||||
height: `${target.boundingRect.height}px`,
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={cn(stl.marker, { [stl.active]: active })} style={style} onClick={() => activeTarget(target.index)}>
|
||||
<div
|
||||
className={cn(stl.marker, { [stl.active]: active })}
|
||||
style={style}
|
||||
onClick={() => activeTarget(target.index)}
|
||||
>
|
||||
<div className={stl.index}>{target.index + 1}</div>
|
||||
{/* @ts-expect-error Tooltip doesn't have children property */}
|
||||
<Tooltip
|
||||
open={active}
|
||||
arrow
|
||||
sticky
|
||||
distance={15}
|
||||
html={(
|
||||
<div>{target.count} Clicks</div>
|
||||
)}
|
||||
trigger="mouseenter"
|
||||
>
|
||||
<Tooltip open={active} delay={0} title={<div>{target.count} Clicks</div>}>
|
||||
<div className="absolute inset-0"></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
export default function SessionInfoItem(props: Props) {
|
||||
const { label, icon, value, comp, isLast = false } = props
|
||||
return (
|
||||
<div className={cn("flex items-center w-full py-2", {'border-b' : !isLast})}>
|
||||
<div className={cn("flex items-center w-full py-2 color-gray-dark", {'border-b' : !isLast})}>
|
||||
<div className="px-2 capitalize" style={{ width: '30px' }}>
|
||||
{ icon && <Icon name={icon} size="16" /> }
|
||||
{ comp && comp }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
selectStorageListNow,
|
||||
selectStorageList,
|
||||
} from 'Player/store';
|
||||
import { JSONTree, NoContent } from 'UI';
|
||||
import { JSONTree, NoContent, Tooltip } from 'UI';
|
||||
import { formatMs } from 'App/date';
|
||||
import { diff } from 'deep-diff';
|
||||
import { jump } from 'Player';
|
||||
|
|
@ -17,7 +17,6 @@ import BottomBlock from '../BottomBlock/index';
|
|||
import DiffRow from './DiffRow';
|
||||
import cn from 'classnames';
|
||||
import stl from './storage.module.css';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
|
||||
// const STATE = 'STATE';
|
||||
// const DIFF = 'DIFF';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Icon, Button } from 'UI';
|
||||
import { Icon, Tooltip, Button } from 'UI';
|
||||
import Autoplay from './Autoplay';
|
||||
import Bookmark from 'Shared/Bookmark';
|
||||
import SharePopup from '../shared/SharePopup/SharePopup';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import Issues from './Issues/Issues';
|
||||
import NotePopup from './components/NotePopup';
|
||||
import { connectPlayer, pause } from 'Player';
|
||||
|
|
@ -46,14 +45,7 @@ function SubHeader(props) {
|
|||
}}
|
||||
>
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
hideOnClick={false}
|
||||
position="bottom center"
|
||||
title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}
|
||||
>
|
||||
<Tooltip title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}>
|
||||
{location}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import { percentOf } from 'App/utils';
|
||||
import styles from './barRow.module.css'
|
||||
import styles from './barRow.module.css';
|
||||
import tableStyles from './timeTable.module.css';
|
||||
import React from 'react';
|
||||
|
||||
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
|
||||
const formatTime = (time) => (time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`);
|
||||
|
||||
interface Props {
|
||||
resource: {
|
||||
time: number
|
||||
ttfb?: number
|
||||
duration?: number
|
||||
key: string
|
||||
}
|
||||
popup?: boolean
|
||||
timestart: number
|
||||
timewidth: number
|
||||
time: number;
|
||||
ttfb?: number;
|
||||
duration?: number;
|
||||
key: string;
|
||||
};
|
||||
popup?: boolean;
|
||||
timestart: number;
|
||||
timewidth: number;
|
||||
}
|
||||
|
||||
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
|
||||
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
|
||||
const BarRow = ({
|
||||
resource: { time, ttfb = 0, duration = 200, key },
|
||||
popup = false,
|
||||
timestart = 0,
|
||||
timewidth,
|
||||
}: Props) => {
|
||||
const timeOffset = time - timestart;
|
||||
ttfb = ttfb || 0;
|
||||
const trigger = (
|
||||
|
|
@ -28,7 +33,7 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
style={{
|
||||
left: `${percentOf(timeOffset, timewidth)}%`,
|
||||
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
|
||||
minWidth: '5px'
|
||||
minWidth: '5px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -41,23 +46,28 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
className={styles.downloadBar}
|
||||
style={{
|
||||
width: `${percentOf(duration - ttfb, duration)}%`,
|
||||
minWidth: '5px'
|
||||
minWidth: '5px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
|
||||
if (!popup)
|
||||
return (
|
||||
<div key={key} className={tableStyles.row}>
|
||||
{' '}
|
||||
{trigger}{' '}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} className={tableStyles.row} >
|
||||
<Popup
|
||||
basic
|
||||
content={
|
||||
<div key={key} className={tableStyles.row}>
|
||||
<Tooltip
|
||||
title={
|
||||
<React.Fragment>
|
||||
{ttfb != null &&
|
||||
{ttfb != null && (
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title}>{'Waiting (TTFB)'}</div>
|
||||
<div className={styles.popupBarWrapper} >
|
||||
<div className={styles.popupBarWrapper}>
|
||||
<div
|
||||
className={styles.ttfbBar}
|
||||
style={{
|
||||
|
|
@ -66,11 +76,11 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.time} >{formatTime(ttfb)}</div>
|
||||
<div className={styles.time}>{formatTime(ttfb)}</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title} >{'Content Download'}</div>
|
||||
<div className={styles.title}>{'Content Download'}</div>
|
||||
<div className={styles.popupBarWrapper}>
|
||||
<div
|
||||
className={styles.downloadBar}
|
||||
|
|
@ -86,11 +96,13 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
}
|
||||
size="mini"
|
||||
position="top center"
|
||||
/>
|
||||
>
|
||||
{trigger}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BarRow.displayName = "BarRow";
|
||||
BarRow.displayName = 'BarRow';
|
||||
|
||||
export default BarRow;
|
||||
export default BarRow;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Button } from 'UI';
|
|||
import { connectPlayer, pause } from 'Player';
|
||||
import { connect } from 'react-redux';
|
||||
import { setCreateNoteTooltip } from 'Duck/sessions';
|
||||
import GuidePopup, { FEATURE_KEYS } from 'Shared/GuidePopup';
|
||||
|
||||
function NotePopup({
|
||||
setCreateNoteTooltip,
|
||||
|
|
@ -24,9 +25,18 @@ function NotePopup({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Button icon="quotes" variant="text" disabled={tooltipActive} onClick={toggleNotePopup}>
|
||||
Add Note
|
||||
</Button>
|
||||
<GuidePopup
|
||||
title={
|
||||
<div className="color-gray-dark">
|
||||
Introducing <span className={''}>Notes</span>
|
||||
</div>
|
||||
}
|
||||
description={'Annotate session replays and share your feedback with the rest of your team.'}
|
||||
>
|
||||
<Button icon="quotes" variant="text" disabled={tooltipActive} onClick={toggleNotePopup}>
|
||||
Add Note
|
||||
</Button>
|
||||
</GuidePopup>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +46,7 @@ const NotePopupPl = connectPlayer(
|
|||
)(React.memo(NotePopup));
|
||||
|
||||
const NotePopupComp = connect(
|
||||
(state) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }),
|
||||
(state: any) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }),
|
||||
{ setCreateNoteTooltip }
|
||||
)(NotePopupPl);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Tooltip } from 'UI';
|
||||
|
||||
const withCopy = (WrappedComponent: React.ComponentType) => {
|
||||
const ComponentWithCopy = (props: any) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const { value, tooltip } = props;
|
||||
const copyToClipboard = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
return (
|
||||
<div onClick={() => copyToClipboard(value)} className="w-fit">
|
||||
<Tooltip delay={0} arrow animation="fade" hideOnClick={false} title={copied ? tooltip : 'Click to copy'}>
|
||||
<WrappedComponent {...props} copyToClipboard={copyToClipboard} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
const ComponentWithCopy = (props: any) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const { value, tooltip } = props;
|
||||
const copyToClipboard = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
return ComponentWithCopy;
|
||||
return (
|
||||
<div onClick={() => copyToClipboard(value)} className="w-fit cursor-pointer">
|
||||
<Tooltip title={copied ? tooltip : 'Click to copy'} delay={0}>
|
||||
<WrappedComponent {...props} copyToClipboard={copyToClipboard} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return ComponentWithCopy;
|
||||
};
|
||||
|
||||
export default withCopy;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Popup, Button, Icon } from 'UI';
|
||||
import { Tooltip, Button, Icon } from 'UI';
|
||||
import { toggleFavorite } from 'Duck/sessions';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface Props {
|
||||
toggleFavorite: (sessionId: string) => Promise<void>;
|
||||
favorite: Boolean;
|
||||
favorite: boolean;
|
||||
sessionId: any;
|
||||
isEnterprise: Boolean;
|
||||
isEnterprise: boolean;
|
||||
noMargin?: boolean;
|
||||
}
|
||||
function Bookmark(props: Props) {
|
||||
|
|
@ -37,12 +37,7 @@ function Bookmark(props: Props) {
|
|||
|
||||
return (
|
||||
<div onClick={toggleFavorite} className="w-full">
|
||||
<Popup
|
||||
delay={500}
|
||||
content={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}
|
||||
hideOnClick={true}
|
||||
distance={20}
|
||||
>
|
||||
<Tooltip title={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}>
|
||||
{noMargin ? (
|
||||
<div className="flex items-center cursor-pointer h-full w-full p-3">
|
||||
<Icon
|
||||
|
|
@ -62,13 +57,13 @@ function Bookmark(props: Props) {
|
|||
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
</Button>
|
||||
)}
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
(state: any) => ({
|
||||
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
|
||||
favorite: state.getIn(['sessions', 'current', 'favorite']),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Tooltip } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -17,14 +17,7 @@ function CopyText(props: Props) {
|
|||
setTimeout(() => setIsCopied(false), 5000);
|
||||
};
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Tooltip
|
||||
delay={0}
|
||||
arrow
|
||||
animation="fade"
|
||||
hideOnClick={false}
|
||||
title={isCopied ? afterLabel : label}
|
||||
>
|
||||
<Tooltip delay={0} title={isCopied ? afterLabel : label}>
|
||||
<span onClick={onClick}>{children}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
onClick: any;
|
||||
|
|
@ -8,7 +8,7 @@ interface Props {
|
|||
function JumpButton(props: Props) {
|
||||
const { tooltip = '' } = props;
|
||||
return (
|
||||
<Popup content={tooltip} disabled={!!tooltip}>
|
||||
<Tooltip title={tooltip} disabled={!!tooltip}>
|
||||
<div
|
||||
className="mr-2 border cursor-pointer invisible group-hover:visible rounded-lg bg-active-blue text-xs flex items-center px-2 py-1 color-teal absolute right-0 top-0 bottom-0 hover:shadow h-6 my-auto"
|
||||
onClick={(e: any) => {
|
||||
|
|
@ -19,7 +19,7 @@ function JumpButton(props: Props) {
|
|||
<Icon name="caret-right-fill" size="12" color="teal" />
|
||||
<span>JUMP</span>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
// import { connectPlayer } from 'Player';
|
||||
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
|
||||
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import Resource, { TYPES } from 'Types/session/resource';
|
||||
import { formatBytes } from 'App/utils';
|
||||
|
|
@ -10,12 +8,10 @@ import { formatMs } from 'App/date';
|
|||
import TimeTable from '../TimeTable';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
// import stl from './network.module.css';
|
||||
import { Duration } from 'luxon';
|
||||
import { connectPlayer, jump, pause } from 'Player';
|
||||
import { connectPlayer, jump } from 'Player';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||
import { sort } from 'App/duck/sessions';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const XHR = 'xhr';
|
||||
|
|
@ -49,17 +45,17 @@ function compare(a: any, b: any, key: string) {
|
|||
|
||||
export function renderType(r: any) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div>{r.type}</div>}>
|
||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||
<div>{r.type}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderName(r: any) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div>{r.url}</div>}>
|
||||
<Tooltip style={{ width: '100%' }} title={<div>{r.url}</div>}>
|
||||
<div>{r.name}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +71,6 @@ const renderXHRText = () => (
|
|||
<span className="flex items-center">
|
||||
{XHR}
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
content={
|
||||
<>
|
||||
Use our{' '}
|
||||
|
|
@ -128,9 +123,9 @@ function renderSize(r: any) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={content}>
|
||||
<Tooltip style={{ width: '100%' }} title={content}>
|
||||
<div>{triggerText}</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,9 +146,9 @@ export function renderDuration(r: any) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={tooltipText}>
|
||||
<Tooltip style={{ width: '100%' }} title={tooltipText}>
|
||||
<div> {text} </div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +216,7 @@ function NetworkPanel(props: Props) {
|
|||
({ type, name, status, success }: any) =>
|
||||
(!!filter ? filterRE.test(status) || filterRE.test(name) || filterRE.test(type) : true) &&
|
||||
(activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]) &&
|
||||
(showOnlyErrors ? (parseInt(status) >= 400 || !success) : true)
|
||||
(showOnlyErrors ? parseInt(status) >= 400 || !success : true)
|
||||
);
|
||||
return list;
|
||||
}, [filter, sortBy, sortAscending, showOnlyErrors, activeTab]);
|
||||
|
|
@ -374,7 +369,7 @@ function NetworkPanel(props: Props) {
|
|||
dataKey: 'decodedBodySize',
|
||||
render: renderSize,
|
||||
onClick: handleSort,
|
||||
hidden: activeTab === XHR
|
||||
hidden: activeTab === XHR,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import { percentOf } from 'App/utils';
|
||||
import styles from './barRow.module.css'
|
||||
import styles from './barRow.module.css';
|
||||
import tableStyles from './timeTable.module.css';
|
||||
import React from 'react';
|
||||
|
||||
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
|
||||
const formatTime = (time) => (time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`);
|
||||
|
||||
interface Props {
|
||||
resource: {
|
||||
time: number
|
||||
ttfb?: number
|
||||
duration?: number
|
||||
key: string
|
||||
}
|
||||
popup?: boolean
|
||||
timestart: number
|
||||
timewidth: number
|
||||
time: number;
|
||||
ttfb?: number;
|
||||
duration?: number;
|
||||
key: string;
|
||||
};
|
||||
popup?: boolean;
|
||||
timestart: number;
|
||||
timewidth: number;
|
||||
}
|
||||
|
||||
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
|
||||
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
|
||||
const BarRow = ({
|
||||
resource: { time, ttfb = 0, duration = 200, key },
|
||||
popup = false,
|
||||
timestart = 0,
|
||||
timewidth,
|
||||
}: Props) => {
|
||||
const timeOffset = time - timestart;
|
||||
ttfb = ttfb || 0;
|
||||
const trigger = (
|
||||
|
|
@ -28,7 +33,7 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
style={{
|
||||
left: `${percentOf(timeOffset, timewidth)}%`,
|
||||
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
|
||||
minWidth: '5px'
|
||||
minWidth: '5px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -41,23 +46,28 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
className={styles.downloadBar}
|
||||
style={{
|
||||
width: `${percentOf(duration - ttfb, duration)}%`,
|
||||
minWidth: '5px'
|
||||
minWidth: '5px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
|
||||
if (!popup)
|
||||
return (
|
||||
<div key={key} className={tableStyles.row}>
|
||||
{' '}
|
||||
{trigger}{' '}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} className={tableStyles.row} >
|
||||
<Popup
|
||||
basic
|
||||
content={
|
||||
<div key={key} className={tableStyles.row}>
|
||||
<Tooltip
|
||||
title={
|
||||
<React.Fragment>
|
||||
{ttfb != null &&
|
||||
{ttfb != null && (
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title}>{'Waiting (TTFB)'}</div>
|
||||
<div className={styles.popupBarWrapper} >
|
||||
<div className={styles.popupBarWrapper}>
|
||||
<div
|
||||
className={styles.ttfbBar}
|
||||
style={{
|
||||
|
|
@ -66,11 +76,11 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.time} >{formatTime(ttfb)}</div>
|
||||
<div className={styles.time}>{formatTime(ttfb)}</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title} >{'Content Download'}</div>
|
||||
<div className={styles.title}>{'Content Download'}</div>
|
||||
<div className={styles.popupBarWrapper}>
|
||||
<div
|
||||
className={styles.downloadBar}
|
||||
|
|
@ -84,13 +94,14 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
|
|||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
size="mini"
|
||||
position="top center"
|
||||
/>
|
||||
placement="top"
|
||||
>
|
||||
{trigger}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BarRow.displayName = "BarRow";
|
||||
BarRow.displayName = 'BarRow';
|
||||
|
||||
export default BarRow;
|
||||
export default BarRow;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,40 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Popup } from 'UI'
|
||||
import { resendEmailVerification } from 'Duck/user'
|
||||
import { Tooltip } from 'UI';
|
||||
import { resendEmailVerification } from 'Duck/user';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function EmailVerificationMessage(props) {
|
||||
const [sent, setSent] = useState(false);
|
||||
const { email } = props;
|
||||
const send = () => {
|
||||
props.resendEmailVerification(email).then(function() {
|
||||
props.resendEmailVerification(email).then(function () {
|
||||
toast.success(`Verification email sent to ${email}`);
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
`We've sent a verification email to "${email}" please follow the instructions in it to use OpenReplay uninterruptedly.`
|
||||
}
|
||||
setSent(true);
|
||||
});
|
||||
};
|
||||
return !sent ? (
|
||||
<Tooltip
|
||||
title={`We've sent a verification email to "${email}" please follow the instructions in it to use OpenReplay uninterruptedly.`}
|
||||
>
|
||||
<div
|
||||
className="mt-3 px-3 rounded-2xl font-medium"
|
||||
style={{ paddingTop: '3px', height: '28px', backgroundColor: 'rgba(255, 239, 239, 1)', border: 'solid thin rgba(221, 181, 181, 1)' }}
|
||||
className="mt-3 px-3 rounded-2xl font-medium"
|
||||
style={{
|
||||
paddingTop: '3px',
|
||||
height: '28px',
|
||||
backgroundColor: 'rgba(255, 239, 239, 1)',
|
||||
border: 'solid thin rgba(221, 181, 181, 1)',
|
||||
}}
|
||||
>
|
||||
<span>Please, verify your email.</span> <a href="#" className="link" onClick={send}>Resend</a>
|
||||
<span>Please, verify your email.</span>{' '}
|
||||
<a href="#" className="link" onClick={send}>
|
||||
Resend
|
||||
</a>
|
||||
</div>
|
||||
</Popup>
|
||||
)
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, { resendEmailVerification })(EmailVerificationMessage)
|
||||
export default connect(null, { resendEmailVerification })(EmailVerificationMessage);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import FilterItem from '../FilterItem';
|
||||
import { SegmentSelection, Popup } from 'UI';
|
||||
import { SegmentSelection, Tooltip } from 'UI';
|
||||
import { List } from 'immutable';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
|
|
@ -24,31 +24,34 @@ function FilterList(props: Props) {
|
|||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
props.onRemoveFilter(filterIndex);
|
||||
}
|
||||
};
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="flex flex-col">
|
||||
{ hasEvents && (
|
||||
{hasEvents && (
|
||||
<>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
{ !hideEventsOrder && (
|
||||
{!hideEventsOrder && (
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
|
||||
<Popup
|
||||
content={ `Select the operator to be applied between events in your search.` }
|
||||
<div
|
||||
className="mr-2 color-gray-medium text-sm"
|
||||
style={{ textDecoration: 'underline dotted' }}
|
||||
>
|
||||
<Tooltip
|
||||
title={`Select the operator to be applied between events in your search.`}
|
||||
>
|
||||
<div>Events Order</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
onSelect={props.onChangeEventsOrder}
|
||||
value={{ value: filter.eventsOrder }}
|
||||
list={ [
|
||||
list={[
|
||||
{ name: 'THEN', value: 'then' },
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
|
|
@ -57,38 +60,42 @@ function FilterList(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{filters.map((filter: any, filterIndex: any) => filter.isEvent ? (
|
||||
<FilterItem
|
||||
key={`${filter.key}-${filterIndex}`}
|
||||
filterIndex={rowIndex++}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
/>
|
||||
): null)}
|
||||
<div className='mb-2' />
|
||||
{filters.map((filter: any, filterIndex: any) =>
|
||||
filter.isEvent ? (
|
||||
<FilterItem
|
||||
key={`${filter.key}-${filterIndex}`}
|
||||
filterIndex={rowIndex++}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
<div className="mb-2" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasFilters && (
|
||||
<>
|
||||
{hasEvents && <div className='border-t -mx-5 mb-4' />}
|
||||
{hasEvents && <div className="border-t -mx-5 mb-4" />}
|
||||
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
|
||||
{filters.map((filter: any, filterIndex: any) => !filter.isEvent ? (
|
||||
<FilterItem
|
||||
key={filterIndex}
|
||||
isFilter={true}
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
): null)}
|
||||
{filters.map((filter: any, filterIndex: any) =>
|
||||
!filter.isEvent ? (
|
||||
<FilterItem
|
||||
key={filterIndex}
|
||||
isFilter={true}
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default FilterList;
|
||||
export default FilterList;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,71 @@
|
|||
import React from 'react';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Controls as Player } from 'Player';
|
||||
import { Tooltip } from 'UI';
|
||||
import { INDEXES } from 'App/constants/zindex';
|
||||
|
||||
export const FEATURE_KEYS = {
|
||||
XRAY: 'featureViewed'
|
||||
}
|
||||
XRAY: 'featureViewed',
|
||||
NOTES: 'notesFeatureViewed',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode
|
||||
title: React.ReactNode
|
||||
description: React.ReactNode
|
||||
key?: keyof typeof FEATURE_KEYS
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
key?: keyof typeof FEATURE_KEYS;
|
||||
}
|
||||
|
||||
export default function GuidePopup({ children, title, description }: IProps) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Tooltip
|
||||
html={
|
||||
<div>
|
||||
<div className="font-bold">
|
||||
{title}
|
||||
const [showGuide, setShowGuide] = useState(!localStorage.getItem(FEATURE_KEYS.NOTES));
|
||||
useEffect(() => {
|
||||
if (!showGuide) {
|
||||
return;
|
||||
}
|
||||
Player.pause();
|
||||
}, []);
|
||||
|
||||
const onClick = () => {
|
||||
setShowGuide(false);
|
||||
localStorage.setItem(FEATURE_KEYS.NOTES, 'true');
|
||||
};
|
||||
|
||||
return showGuide ? (
|
||||
<div>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-gray-darkest fixed inset-0 z-10 w-full h-screen cursor-pointer"
|
||||
style={{ zIndex: INDEXES.POPUP_GUIDE_BG, opacity: '0.7' }}
|
||||
></div>
|
||||
<Tooltip
|
||||
offset={30}
|
||||
className="!bg-white rounded text-center shadow !p-6"
|
||||
title={
|
||||
<div className="relative">
|
||||
<div className="font-bold">{title}</div>
|
||||
<div className="color-gray-medium w-80">{description}</div>
|
||||
<div className="w-10 h-10 bg-white rotate-45 absolute right-0 left-0 m-auto" style={{ top: '-38px'}} />
|
||||
</div>
|
||||
<div className="color-gray-medium">
|
||||
{description}
|
||||
}
|
||||
open={true}
|
||||
>
|
||||
<div className="relative pointer-events-none">
|
||||
<div className="" style={{ zIndex: INDEXES.POPUP_GUIDE_BTN, position: 'inherit' }}>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className="absolute bg-white top-0 left-0"
|
||||
style={{
|
||||
zIndex: INDEXES.POPUP_GUIDE_BG,
|
||||
width: '120px',
|
||||
height: '40px',
|
||||
borderRadius: '30px',
|
||||
margin: '-2px -10px',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
distance={30}
|
||||
theme={'light'}
|
||||
open={true}
|
||||
arrow={true}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,39 @@
|
|||
import React from 'react';
|
||||
import LiveSessionSearchField from 'Shared/LiveSessionSearchField';
|
||||
import { Button, Popup } from 'UI';
|
||||
import { Button, Tooltip } from 'UI';
|
||||
import { clearSearch } from 'Duck/liveSearch';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
clearSearch: () => void;
|
||||
appliedFilter: any;
|
||||
clearSearch: () => void;
|
||||
appliedFilter: any;
|
||||
}
|
||||
const LiveSearchBar = (props: Props) => {
|
||||
const { appliedFilter } = props;
|
||||
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div style={{ width: "60%", marginRight: "10px"}}>
|
||||
<LiveSessionSearchField />
|
||||
</div>
|
||||
<div className="flex items-center" style={{ width: "40%"}}>
|
||||
<Popup content={'Clear Steps'} >
|
||||
<Button
|
||||
variant="text-primary"
|
||||
disabled={!hasFilters}
|
||||
className="ml-auto font-medium"
|
||||
onClick={() => props.clearSearch()}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Popup>
|
||||
</div>
|
||||
<div style={{ width: '60%', marginRight: '10px' }}>
|
||||
<LiveSessionSearchField />
|
||||
</div>
|
||||
<div className="flex items-center" style={{ width: '40%' }}>
|
||||
<Tooltip title={'Clear Steps'}>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
disabled={!hasFilters}
|
||||
className="ml-auto font-medium"
|
||||
onClick={() => props.clearSearch()}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default connect(state => ({
|
||||
);
|
||||
};
|
||||
export default connect(
|
||||
(state) => ({
|
||||
appliedFilter: state.getIn(['liveSearch', 'instance']),
|
||||
}), { clearSearch })(LiveSearchBar);
|
||||
}),
|
||||
{ clearSearch }
|
||||
)(LiveSearchBar);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { CircularLoader, Icon, Popup } from 'UI';
|
||||
import { CircularLoader, Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -12,13 +12,11 @@ interface Props {
|
|||
export default function ReloadButton(props: Props) {
|
||||
const { loading, onClick, iconSize = '20', iconName = 'arrow-repeat', className = '' } = props;
|
||||
return (
|
||||
<Popup content="Refresh">
|
||||
<div
|
||||
className={cn('h-5 w-6 flex items-center justify-center', className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Tooltip title="Refresh">
|
||||
<div className={cn('h-5 w-6 flex items-center justify-center', className)} onClick={onClick}>
|
||||
{/* @ts-ignore */}
|
||||
{loading ? <CircularLoader className="ml-1" /> : <Icon name={iconName} size={iconSize} />}
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { MouseEvent, useState } from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon, Input } from 'UI';
|
||||
import { List } from 'immutable';
|
||||
import { confirm, Popup } from 'UI';
|
||||
import { confirm, Tooltip } from 'UI';
|
||||
import { applySavedSearch, remove, editSavedSearch } from 'Duck/search';
|
||||
import { connect } from 'react-redux';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
|
@ -18,9 +18,10 @@ interface ITooltipIcon {
|
|||
function TooltipIcon(props: ITooltipIcon) {
|
||||
return (
|
||||
<div onClick={(e) => props.onClick(e)}>
|
||||
<Popup content={props.title} hideOnClick={true}>
|
||||
<Tooltip title={props.title}>
|
||||
{/* @ts-ignore */}
|
||||
<Icon size="16" name={props.name} color="main" />
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,30 @@
|
|||
import React from 'react'
|
||||
import { Popup } from 'UI'
|
||||
import MetaItem from '../MetaItem'
|
||||
import React from 'react';
|
||||
import { Popover, Button } from 'UI';
|
||||
import MetaItem from '../MetaItem';
|
||||
|
||||
interface Props {
|
||||
list: any[],
|
||||
maxLength: number,
|
||||
list: any[];
|
||||
maxLength: number;
|
||||
}
|
||||
export default function MetaMoreButton(props: Props) {
|
||||
const { list, maxLength } = props
|
||||
return (
|
||||
<Popup
|
||||
className="p-0"
|
||||
theme="light"
|
||||
content={
|
||||
<div className="text-sm grid grid-col p-4 gap-3" style={{ maxHeight: '200px', overflowY: 'auto'}}>
|
||||
{list.slice(maxLength).map(({ label, value }, index) => (
|
||||
<MetaItem key={index} label={label} value={value} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
on="click"
|
||||
position="center center"
|
||||
const { list, maxLength } = props;
|
||||
return (
|
||||
<Popover
|
||||
render={() => (
|
||||
<div
|
||||
className="text-sm grid grid-col p-4 gap-3 bg-white"
|
||||
style={{ maxHeight: '200px', overflowY: 'auto' }}
|
||||
>
|
||||
<div className=" flex items-center">
|
||||
<span className="rounded bg-active-blue color-teal p-2 color-gray-dark cursor-pointer whitespace-nowrap">
|
||||
+{list.length - maxLength} More
|
||||
</span>
|
||||
</div>
|
||||
</Popup>
|
||||
)
|
||||
{list.slice(maxLength).map(({ label, value }, index) => (
|
||||
<MetaItem key={index} label={label} value={value} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Button variant="text-primary">+{list.length - maxLength} More</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { CountryFlag, Avatar, TextEllipsis, Label, Icon } from 'UI';
|
||||
import { CountryFlag, Avatar, TextEllipsis, Label, Icon, Tooltip } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
|
|
@ -12,7 +12,6 @@ import PlayLink from './PlayLink';
|
|||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from 'App/routes';
|
||||
import { capitalize } from 'App/utils';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession();
|
||||
|
|
@ -132,8 +131,8 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
)}
|
||||
<div style={{ width: compact ? '40%' : '20%' }} className="px-2 flex flex-col justify-between">
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip
|
||||
delay={0}
|
||||
title={`${formatTimeOrDate(startedAt, timezone, true)} ${timezone.label}`}
|
||||
className="w-fit !block"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
import React from 'react'
|
||||
import { Popup } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import MetaItem from '../MetaItem';
|
||||
import MetaMoreButton from '../MetaMoreButton';
|
||||
|
||||
interface Props {
|
||||
className?: string,
|
||||
metaList: any[],
|
||||
maxLength?: number,
|
||||
className?: string;
|
||||
metaList: any[];
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export default function SessionMetaList(props: Props) {
|
||||
const { className = '', metaList, maxLength = 4 } = props
|
||||
const { className = '', metaList, maxLength = 4 } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("text-sm flex items-center", className)}>
|
||||
<div className={cn('text-sm flex items-center', className)}>
|
||||
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
|
||||
<MetaItem key={index} label={label} value={''+value} className="mr-3" />
|
||||
<MetaItem key={index} label={label} value={'' + value} className="mr-3" />
|
||||
))}
|
||||
|
||||
{metaList.length > maxLength && (
|
||||
<MetaMoreButton list={metaList} maxLength={maxLength} />
|
||||
)}
|
||||
{metaList.length > maxLength && <MetaMoreButton list={metaList} maxLength={maxLength} />}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React from 'react';
|
||||
import SessionSettings from 'Shared/SessionSettings';
|
||||
import { Button } from 'UI';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { Button, Tooltip } from 'UI';
|
||||
|
||||
function SessionSettingButton(props: any) {
|
||||
const { showModal } = useModal();
|
||||
|
|
@ -13,8 +12,7 @@ function SessionSettingButton(props: any) {
|
|||
|
||||
return (
|
||||
<div className="cursor-pointer ml-4" onClick={handleClick}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Session Settings" unmountHTMLWhenHide>
|
||||
<Tooltip title="Session Settings">
|
||||
<Button icon="sliders" variant="text" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Icon, Toggler, Button, Input, Loader, Popup } from 'UI';
|
||||
import { Icon, Toggler, Button, Input, Loader, Tooltip } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { connect } from 'react-redux';
|
||||
|
|
@ -43,17 +43,17 @@ function CaptureRate({ isAdmin = false }) {
|
|||
<Loader loading={loading}>
|
||||
<h3 className="text-lg">Capture Rate</h3>
|
||||
<div className="my-1">The percentage of session you want to capture</div>
|
||||
<Popup content="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn('mt-2 mb-4 mr-1 flex items-center', { disabled: !isAdmin })}>
|
||||
<Toggler checked={captureAll} name="test" onChange={toggleRate} />
|
||||
<span className="ml-2" style={{ color: captureAll ? '#000000' : '#999' }}>
|
||||
100%
|
||||
</span>
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
{!captureAll && (
|
||||
<div className="flex items-center">
|
||||
<Popup content="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn("relative", { 'disabled' : !isAdmin })}>
|
||||
<Input
|
||||
type="number"
|
||||
|
|
@ -66,7 +66,7 @@ function CaptureRate({ isAdmin = false }) {
|
|||
/>
|
||||
<Icon className="absolute right-0 mr-6 top-0 bottom-0 m-auto" name="percent" color="gray-medium" size="18" />
|
||||
</div>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
<span className="mx-3">of the sessions</span>
|
||||
<Button
|
||||
disabled={!changed}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connectPlayer } from 'Player';
|
||||
import withRequest from 'HOCs/withRequest';
|
||||
import { Icon, Button } from 'UI';
|
||||
import { Icon, Button, Popover } from 'UI';
|
||||
import styles from './sharePopup.module.css';
|
||||
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
|
||||
import SessionCopyLink from './SessionCopyLink';
|
||||
import Select from 'Shared/Select';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import cn from 'classnames';
|
||||
import { fetchList, init } from 'Duck/integrations/slack';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { fetchList } from 'Duck/integrations/slack';
|
||||
|
||||
// @connectPlayer((state) => ({
|
||||
// time: state.time,
|
||||
// }))
|
||||
@connect(
|
||||
(state) => ({
|
||||
channels: state.getIn(['slack', 'list']),
|
||||
|
|
@ -75,84 +69,67 @@ export default class SharePopup extends React.PureComponent {
|
|||
const options = channels
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
return (
|
||||
<OutsideClickDetectingDiv
|
||||
className={cn('relative flex items-center w-full')}
|
||||
onClickOutside={() => {
|
||||
this.setState({ isOpen: false });
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
open={isOpen}
|
||||
theme="light"
|
||||
interactive
|
||||
position="bottom"
|
||||
unmountHTMLWhenHide
|
||||
useContext
|
||||
arrow
|
||||
trigger="click"
|
||||
shown={this.handleOpen}
|
||||
className="w-full"
|
||||
// beforeHidden={this.handleClose}
|
||||
html={
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div>
|
||||
</div>
|
||||
{options.length === 0 ? (
|
||||
<>
|
||||
<div className={styles.body}>
|
||||
<IntegrateSlackButton />
|
||||
</div>
|
||||
{showCopyLink && (
|
||||
<div className={styles.footer}>
|
||||
<SessionCopyLink />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.body}>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
cols="30"
|
||||
rows="4"
|
||||
resize="none"
|
||||
onChange={this.editMessage}
|
||||
value={comment}
|
||||
placeholder="Add Message (Optional)"
|
||||
className="p-4"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Select
|
||||
options={options}
|
||||
defaultValue={channelId}
|
||||
onChange={this.changeChannel}
|
||||
className="mr-4"
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={this.share} variant="primary">
|
||||
<div className="flex items-center">
|
||||
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
|
||||
{loading ? 'Sending...' : 'Send'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Popover
|
||||
render={() => (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div>
|
||||
</div>
|
||||
{options.length === 0 ? (
|
||||
<>
|
||||
<div className={styles.body}>
|
||||
<IntegrateSlackButton />
|
||||
</div>
|
||||
{showCopyLink && (
|
||||
<div className={styles.footer}>
|
||||
<SessionCopyLink />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.body}>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
cols="30"
|
||||
rows="4"
|
||||
resize="none"
|
||||
onChange={this.editMessage}
|
||||
value={comment}
|
||||
placeholder="Add Message (Optional)"
|
||||
className="p-4"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Select
|
||||
options={options}
|
||||
defaultValue={channelId}
|
||||
onChange={this.changeChannel}
|
||||
className="mr-4"
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={this.share} variant="primary">
|
||||
<div className="flex items-center">
|
||||
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
|
||||
{loading ? 'Sending...' : 'Send'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div onClick={this.onClickHandler} className="h-full w-full p-3">{trigger}</div>
|
||||
</Tooltip>
|
||||
</OutsideClickDetectingDiv>
|
||||
<div className={styles.footer}>
|
||||
<SessionCopyLink />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="p-3 w-full">{trigger}</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,40 @@
|
|||
import React from 'react'
|
||||
import { Icon, Popup } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
sortOrder: string,
|
||||
onChange?: (sortOrder: string) => void,
|
||||
sortOrder: string;
|
||||
onChange?: (sortOrder: string) => void;
|
||||
}
|
||||
export default React.memo(function SortOrderButton(props: Props) {
|
||||
const { sortOrder, onChange = () => null } = props
|
||||
const isAscending = sortOrder === 'asc'
|
||||
const { sortOrder, onChange = () => null } = props;
|
||||
const isAscending = sortOrder === 'asc';
|
||||
|
||||
return (
|
||||
<div className="flex items-center border">
|
||||
<Popup content={'Ascending'} >
|
||||
<div
|
||||
className={cn("p-2 hover:bg-active-blue", { 'cursor-pointer bg-white' : !isAscending, 'bg-active-blue pointer-events-none' : isAscending })}
|
||||
onClick={() => onChange('asc')}
|
||||
>
|
||||
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
<Popup content={'Descending'} >
|
||||
<div
|
||||
className={cn("p-2 hover:bg-active-blue border-l", { 'cursor-pointer bg-white' : isAscending, 'bg-active-blue pointer-events-none' : !isAscending })}
|
||||
onClick={() => onChange('desc')}
|
||||
>
|
||||
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
|
||||
</div>
|
||||
</Popup>
|
||||
return (
|
||||
<div className="flex items-center border">
|
||||
<Tooltip title={'Ascending'}>
|
||||
<div
|
||||
className={cn('p-2 hover:bg-active-blue', {
|
||||
'cursor-pointer bg-white': !isAscending,
|
||||
'bg-active-blue pointer-events-none': isAscending,
|
||||
})}
|
||||
onClick={() => onChange('asc')}
|
||||
>
|
||||
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Descending'}>
|
||||
<div
|
||||
className={cn('p-2 hover:bg-active-blue border-l', {
|
||||
'cursor-pointer bg-white': isAscending,
|
||||
'bg-active-blue pointer-events-none': !isAscending,
|
||||
})}
|
||||
onClick={() => onChange('desc')}
|
||||
>
|
||||
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
// import SlackIcon from '../../../svg/slack-help.svg';
|
||||
import { Popup, Icon } from 'UI';
|
||||
import { Icon } from 'UI';
|
||||
import SupportList from './components/SupportList';
|
||||
|
||||
function SupportCallout() {
|
||||
|
|
@ -10,11 +9,9 @@ function SupportCallout() {
|
|||
<SupportList />
|
||||
</div>
|
||||
<div className="fixed z-50 left-0 bottom-0 m-4">
|
||||
{/* <Popup content="OpenReplay community" delay={0}> */}
|
||||
<div className="w-12 h-12 cursor-pointer bg-white border rounded-full flex items-center justify-center group-hover:shadow-lg group-hover:!bg-active-blue">
|
||||
<Icon name="question-lg" size={30} color="teal" />
|
||||
</div>
|
||||
{/* </Popup> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import stl from './xrayButton.module.css';
|
||||
import cn from 'classnames';
|
||||
import { Popup } from 'UI';
|
||||
import { Tooltip } from 'UI';
|
||||
import GuidePopup, { FEATURE_KEYS } from 'Shared/GuidePopup';
|
||||
import { Controls as Player } from 'Player';
|
||||
import { INDEXES } from 'App/constants/zindex';
|
||||
|
|
@ -39,10 +39,10 @@ function XRayButton(props: Props) {
|
|||
)}
|
||||
<div className="relative">
|
||||
{showGuide ? (
|
||||
<GuidePopup
|
||||
title={<>Introducing <span className={stl.text}>X-Ray</span></>}
|
||||
description={"Get a quick overview on the issues in this session."}
|
||||
>
|
||||
// <GuidePopup
|
||||
// title={<div className="color-gray-dark">Introducing <span className={stl.text}>X-Ray</span></div>}
|
||||
// description={"Get a quick overview on the issues in this session."}
|
||||
// >
|
||||
<button
|
||||
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
|
||||
onClick={onClick}
|
||||
|
|
@ -51,26 +51,26 @@ function XRayButton(props: Props) {
|
|||
<span className="z-1">X-RAY</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="absolute bg-white top-0 left-0 z-0"
|
||||
style={{
|
||||
zIndex: INDEXES.POPUP_GUIDE_BG,
|
||||
width: '100px',
|
||||
height: '50px',
|
||||
borderRadius: '30px',
|
||||
margin: '-10px -16px',
|
||||
}}
|
||||
></div>
|
||||
</GuidePopup>
|
||||
// <div
|
||||
// className="absolute bg-white top-0 left-0 z-0"
|
||||
// style={{
|
||||
// zIndex: INDEXES.POPUP_GUIDE_BG,
|
||||
// width: '100px',
|
||||
// height: '50px',
|
||||
// borderRadius: '30px',
|
||||
// margin: '-10px -16px',
|
||||
// }}
|
||||
// ></div>
|
||||
// </GuidePopup>
|
||||
) : (
|
||||
<Popup content="Get a quick overview on the issues in this session." disabled={isActive}>
|
||||
<Tooltip title="Get a quick overview on the issues in this session." disabled={isActive}>
|
||||
<button
|
||||
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="z-1">X-RAY</span>
|
||||
</button>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,23 +2,39 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import { avatarIconName } from 'App/iconNames';
|
||||
import stl from './avatar.module.css';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
const Avatar = ({ isActive = false, isAssist = false, width = '38px', height = '38px', iconSize = 26, seed }) => {
|
||||
var iconName = avatarIconName(seed);
|
||||
return (
|
||||
<Popup content={isActive ? 'Active user' : 'User might be inactive'} disabled={!isAssist}>
|
||||
<div className={cn(stl.wrapper, 'p-2 border flex items-center justify-center rounded-full relative')} style={{ width, height }}>
|
||||
<Icon name={iconName} size={iconSize} color="tealx" />
|
||||
{isAssist && (
|
||||
<div
|
||||
className={cn('w-2 h-2 rounded-full absolute right-0 bottom-0', { 'bg-green': isActive, 'bg-orange': !isActive })}
|
||||
style={{ marginRight: '3px', marginBottom: '3px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
const Avatar = ({
|
||||
isActive = false,
|
||||
isAssist = false,
|
||||
width = '38px',
|
||||
height = '38px',
|
||||
iconSize = 26,
|
||||
seed,
|
||||
}) => {
|
||||
var iconName = avatarIconName(seed);
|
||||
return (
|
||||
<Tooltip title={isActive ? 'Active user' : 'User might be inactive'} disabled={!isAssist}>
|
||||
<div
|
||||
className={cn(
|
||||
stl.wrapper,
|
||||
'p-2 border flex items-center justify-center rounded-full relative'
|
||||
)}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<Icon name={iconName} size={iconSize} color="tealx" />
|
||||
{isAssist && (
|
||||
<div
|
||||
className={cn('w-2 h-2 rounded-full absolute right-0 bottom-0', {
|
||||
'bg-green': isActive,
|
||||
'bg-orange': !isActive,
|
||||
})}
|
||||
style={{ marginRight: '3px', marginBottom: '3px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
|
|
|||
|
|
@ -1,95 +1,104 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { CircularLoader, Icon, Popup } from 'UI';
|
||||
import { CircularLoader, Icon, Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green'
|
||||
loading?: boolean;
|
||||
icon?: string;
|
||||
iconSize?: number;
|
||||
rounded?: boolean;
|
||||
tooltip?: any;
|
||||
[x: string]: any;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green';
|
||||
loading?: boolean;
|
||||
icon?: string;
|
||||
iconSize?: number;
|
||||
rounded?: boolean;
|
||||
tooltip?: any;
|
||||
[x: string]: any;
|
||||
}
|
||||
export default (props: Props) => {
|
||||
const {
|
||||
icon = '',
|
||||
iconSize = 18,
|
||||
className = '',
|
||||
variant = 'default', // 'default|primary|text|text-primary|text-red|outline',
|
||||
type = 'button',
|
||||
size = '',
|
||||
disabled = false,
|
||||
children,
|
||||
loading = false,
|
||||
rounded = false,
|
||||
tooltip = null,
|
||||
...rest
|
||||
} = props;
|
||||
const {
|
||||
icon = '',
|
||||
iconSize = 18,
|
||||
className = '',
|
||||
variant = 'default', // 'default|primary|text|text-primary|text-red|outline',
|
||||
type = 'button',
|
||||
size = '',
|
||||
disabled = false,
|
||||
children,
|
||||
loading = false,
|
||||
rounded = false,
|
||||
tooltip = null,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
let classes = ['relative flex items-center h-10 px-3 rounded tracking-wide whitespace-nowrap'];
|
||||
let iconColor = variant === 'text' || variant === 'default' ? 'gray-dark' : 'teal';
|
||||
let classes = ['relative flex items-center h-10 px-3 rounded tracking-wide whitespace-nowrap'];
|
||||
let iconColor = variant === 'text' || variant === 'default' ? 'gray-dark' : 'teal';
|
||||
|
||||
if (variant === 'default') {
|
||||
classes.push('bg-white hover:bg-gray-light border border-gray-light');
|
||||
}
|
||||
if (variant === 'default') {
|
||||
classes.push('bg-white hover:bg-gray-light border border-gray-light');
|
||||
}
|
||||
|
||||
if (variant === 'primary') {
|
||||
classes.push('bg-teal color-white hover:bg-teal-dark');
|
||||
}
|
||||
if (variant === 'primary') {
|
||||
classes.push('bg-teal color-white hover:bg-teal-dark');
|
||||
}
|
||||
|
||||
if (variant === 'green') {
|
||||
classes.push('bg-green color-white hover:bg-green-dark');
|
||||
iconColor = 'white';
|
||||
}
|
||||
if (variant === 'green') {
|
||||
classes.push('bg-green color-white hover:bg-green-dark');
|
||||
iconColor = 'white';
|
||||
}
|
||||
|
||||
if (variant === 'text') {
|
||||
classes.push('bg-transparent color-gray-dark hover:bg-gray-light-shade');
|
||||
}
|
||||
if (variant === 'text') {
|
||||
classes.push('bg-transparent color-gray-dark hover:bg-gray-light-shade');
|
||||
}
|
||||
|
||||
if (variant === 'text-primary') {
|
||||
classes.push('bg-transparent color-teal hover:bg-teal-light hover:color-teal-dark');
|
||||
}
|
||||
if (variant === 'text-primary') {
|
||||
classes.push('bg-transparent color-teal hover:bg-teal-light hover:color-teal-dark');
|
||||
}
|
||||
|
||||
if (variant === 'text-red') {
|
||||
classes.push('bg-transparent color-red hover:bg-teal-light');
|
||||
}
|
||||
if (variant === 'text-red') {
|
||||
classes.push('bg-transparent color-red hover:bg-teal-light');
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
classes.push('bg-white color-teal border border-teal hover:bg-teal-light');
|
||||
}
|
||||
if (variant === 'outline') {
|
||||
classes.push('bg-white color-teal border border-teal hover:bg-teal-light');
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
classes.push('opacity-40 pointer-events-none');
|
||||
}
|
||||
if (disabled) {
|
||||
classes.push('opacity-40 pointer-events-none');
|
||||
}
|
||||
|
||||
if (variant === 'primary') {
|
||||
iconColor = 'white';
|
||||
}
|
||||
if (variant === 'text-red') {
|
||||
iconColor = 'red';
|
||||
}
|
||||
if (variant === 'primary') {
|
||||
iconColor = 'white';
|
||||
}
|
||||
if (variant === 'text-red') {
|
||||
iconColor = 'red';
|
||||
}
|
||||
|
||||
if (rounded) {
|
||||
classes = classes.map((c) => c.replace('rounded', 'rounded-full h-10 w-10 justify-center'));
|
||||
}
|
||||
if (rounded) {
|
||||
classes = classes.map((c) => c.replace('rounded', 'rounded-full h-10 w-10 justify-center'));
|
||||
}
|
||||
|
||||
const render = () => (
|
||||
<button {...rest} type={type} className={cn(classes, className)}>
|
||||
{icon && <Icon className={cn({ 'mr-2': children })} name={icon} color={iconColor} size={iconSize} />}
|
||||
{loading && (
|
||||
<div className="absolute flex items-center justify-center inset-0 z-1 rounded">
|
||||
<CircularLoader />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn({ 'opacity-0': loading }, 'flex items-center')}>{children}</div>
|
||||
</button>
|
||||
);
|
||||
const render = () => (
|
||||
<button {...rest} type={type} className={cn(classes, className)}>
|
||||
{icon && (
|
||||
// @ts-ignore
|
||||
<Icon className={cn({ 'mr-2': children })} name={icon} color={iconColor} size={iconSize} />
|
||||
)}
|
||||
{loading && (
|
||||
<div className="absolute flex items-center justify-center inset-0 z-1 rounded">
|
||||
<CircularLoader />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn({ 'opacity-0': loading }, 'flex items-center')}>{children}</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
return tooltip ? <Popup content={tooltip.title} {...tooltip}>{render()}</Popup> : render();
|
||||
return tooltip ? (
|
||||
<Tooltip title={tooltip.title} {...tooltip}>
|
||||
{render()}
|
||||
</Tooltip>
|
||||
) : (
|
||||
render()
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
|
||||
import React from 'react'
|
||||
import { Icon, Popup } from 'UI'
|
||||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
||||
interface Props {
|
||||
text: string,
|
||||
className?: string,
|
||||
position?: string,
|
||||
text: string;
|
||||
className?: string;
|
||||
position?: string;
|
||||
}
|
||||
export default function HelpText(props: Props) {
|
||||
const { text, className = '', position = 'top center' } = props
|
||||
return (
|
||||
<div>
|
||||
<Popup content={text} >
|
||||
<div className={className}><Icon name="question-circle" size={16} /></div>
|
||||
</Popup>
|
||||
const { text, className = '', position = 'top center' } = props;
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title={text}>
|
||||
<div className={className}>
|
||||
<Icon name="question-circle" size={16} />
|
||||
</div>
|
||||
)
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,81 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { CircularLoader, Icon, Popup } from 'UI';
|
||||
import { CircularLoader, Icon, Tooltip } from 'UI';
|
||||
import stl from './iconButton.module.css';
|
||||
|
||||
const IconButton = React.forwardRef(({
|
||||
icon,
|
||||
label = false,
|
||||
active,
|
||||
onClick,
|
||||
plain = false,
|
||||
shadow = false,
|
||||
red = false,
|
||||
primary = false,
|
||||
primaryText = false,
|
||||
redText = false,
|
||||
outline = false,
|
||||
loading = false,
|
||||
roundedOutline = false,
|
||||
hideLoader = false,
|
||||
circle = false,
|
||||
size = 'default',
|
||||
marginRight,
|
||||
buttonSmall,
|
||||
className = '',
|
||||
style,
|
||||
name,
|
||||
disabled = false,
|
||||
tooltip = false,
|
||||
tooltipPosition = 'top center',
|
||||
compact = false,
|
||||
...rest
|
||||
}, ref) => (
|
||||
<Popup
|
||||
content={tooltip}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<button
|
||||
ref={ ref }
|
||||
name={ name }
|
||||
className={ cn(stl.button, className, {
|
||||
[ stl.plain ]: plain,
|
||||
[ stl.active ]: active,
|
||||
[ stl.shadow ]: shadow,
|
||||
[ stl.primary ]: primary,
|
||||
[ stl.red ]: red,
|
||||
[ stl.primaryText ]: primaryText,
|
||||
[ stl.redText ]: redText,
|
||||
[ stl.outline ]: outline,
|
||||
[ stl.circle ]: circle,
|
||||
[ stl.roundedOutline ]: roundedOutline,
|
||||
[ stl.buttonSmall ]: buttonSmall,
|
||||
[ stl.small ]: size === 'small',
|
||||
[ stl.tiny ]: size === 'tiny',
|
||||
[ stl.marginRight ]: marginRight,
|
||||
[ stl.compact ]: compact,
|
||||
[ stl.hasLabel]: !!label
|
||||
}) }
|
||||
onClick={ onClick }
|
||||
disabled={ disabled || loading }
|
||||
const IconButton = React.forwardRef(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
label = false,
|
||||
active,
|
||||
onClick,
|
||||
plain = false,
|
||||
shadow = false,
|
||||
red = false,
|
||||
primary = false,
|
||||
primaryText = false,
|
||||
redText = false,
|
||||
outline = false,
|
||||
loading = false,
|
||||
roundedOutline = false,
|
||||
hideLoader = false,
|
||||
circle = false,
|
||||
size = 'default',
|
||||
marginRight,
|
||||
buttonSmall,
|
||||
className = '',
|
||||
style,
|
||||
name,
|
||||
disabled = false,
|
||||
tooltip = false,
|
||||
tooltipPosition = 'top center',
|
||||
compact = false,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<Tooltip title={tooltip} position={tooltipPosition}>
|
||||
<button
|
||||
ref={ref}
|
||||
name={name}
|
||||
className={cn(stl.button, className, {
|
||||
[stl.plain]: plain,
|
||||
[stl.active]: active,
|
||||
[stl.shadow]: shadow,
|
||||
[stl.primary]: primary,
|
||||
[stl.red]: red,
|
||||
[stl.primaryText]: primaryText,
|
||||
[stl.redText]: redText,
|
||||
[stl.outline]: outline,
|
||||
[stl.circle]: circle,
|
||||
[stl.roundedOutline]: roundedOutline,
|
||||
[stl.buttonSmall]: buttonSmall,
|
||||
[stl.small]: size === 'small',
|
||||
[stl.tiny]: size === 'tiny',
|
||||
[stl.marginRight]: marginRight,
|
||||
[stl.compact]: compact,
|
||||
[stl.hasLabel]: !!label,
|
||||
})}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
style={style}
|
||||
{ ...rest }
|
||||
{...rest}
|
||||
>
|
||||
{ !hideLoader && <CircularLoader loading={ loading } /> }
|
||||
{ icon &&
|
||||
{!hideLoader && <CircularLoader loading={loading} />}
|
||||
{icon && (
|
||||
<Icon
|
||||
color="teal"
|
||||
name={ icon }
|
||||
data-hidden={ loading }
|
||||
size={ size === 'tiny' || size === 'small' || buttonSmall ? '14' : '16' }
|
||||
name={icon}
|
||||
data-hidden={loading}
|
||||
size={size === 'tiny' || size === 'small' || buttonSmall ? '14' : '16'}
|
||||
/>
|
||||
}
|
||||
{ label && <span className={ cn(stl.label, icon || loading ? 'ml-2' : '') }>{ label }</span> }
|
||||
)}
|
||||
{label && <span className={cn(stl.label, icon || loading ? 'ml-2' : '')}>{label}</span>}
|
||||
</button>
|
||||
</Popup>
|
||||
));
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
|
||||
IconButton.displayName = "IconButton";
|
||||
IconButton.displayName = 'IconButton';
|
||||
export default IconButton;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue