Merge remote-tracking branch 'origin/dev' into api-v1.9.5

This commit is contained in:
Taha Yassine Kraiem 2022-11-15 16:29:34 +01:00
commit f9498939d9
124 changed files with 3058 additions and 3573 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
export { default } from './ManageUsers';

View file

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

View file

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

View file

@ -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 dont have the permissions to perform this action." disabled={isAdmin}>
<Tooltip title="You dont have the permissions to perform this action." disabled={isAdmin}>
<Button variant="primary" onClick={() => editHandler({})}>Add</Button>
</Popup>
</Tooltip>
</div>
</div>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import withPageTitle from 'HOCs/withPageTitle';
import { Loader, Button, Popup, TextLink, 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &plusmn; {pickRadius}.</span>}>
<Tooltip title={<span>Closest step to the selected timestamp &plusmn; {pickRadius}.</span>}>
<span>&plusmn; {pickRadius}</span>
</Tooltip>
</div>

View file

@ -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">&#183;</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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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