Merge pull request #953 from openreplay/ui-ee-tooltip

change(ui) - ee tooltips
This commit is contained in:
Shekar Siri 2023-01-23 11:41:44 +01:00 committed by GitHub
commit 14f79832ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 205 additions and 140 deletions

View file

@ -21,7 +21,7 @@ function Assist(props: Props) {
const redirect = (path: string) => {
history.push(withSiteId(path, siteId));
};
if (isEnterprise) {
// if (isEnterprise) {
return (
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
@ -39,6 +39,8 @@ function Assist(props: Props) {
title="Recordings"
iconName="record-circle"
onClick={() => redirect(recordings())}
disabled={!isEnterprise}
tooltipTitle="This feature requires an enterprise license."
/>
</div>
<div className="side-menu-margined w-full">
@ -47,13 +49,13 @@ function Assist(props: Props) {
</div>
</div>
);
}
// }
return (
<div className="page-margin container-90 flex relative">
<AssistRouter />
</div>
)
// return (
// <div className="page-margin container-90 flex relative">
// <AssistRouter />
// </div>
// )
}
const Cont = connect((state: any) => ({

View file

@ -181,7 +181,7 @@ function AssistActions({
)}
{/* @ts-ignore wtf? */}
{isEnterprise ? <ScreenRecorder /> : null}
<ScreenRecorder />
<div className={stl.divider} />
{/* @ts-ignore */}

View file

@ -16,10 +16,8 @@ function DashboardOptions(props: Props) {
{ icon: 'text-paragraph', text: `${!isTitlePresent ? 'Add' : 'Edit'} Description`, onClick: () => editHandler(false) },
{ icon: 'users', text: 'Visibility & Access', onClick: editHandler },
{ icon: 'trash', text: 'Delete', onClick: deleteHandler },
{ icon: 'pdf-download', text: 'Download Report', onClick: renderReport, disabled: !isEnterprise, tooltipTitle: 'This feature requires an enterprise license.' }
]
if (isEnterprise) {
menuItems.unshift({ icon: 'pdf-download', text: 'Download Report', onClick: renderReport });
}
return (
<ItemMenu

View file

@ -1,12 +1,15 @@
import { IconNames } from 'App/components/ui/SVG';
import React from 'react';
import { Icon } from 'UI';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
export interface MetricType {
title: string;
icon?: IconNames;
description: string;
slug: string;
disabled?: boolean;
tooltipTitle?: string;
}
interface Props {
@ -16,23 +19,28 @@ interface Props {
function MetricTypeItem(props: Props) {
const {
metric: { title, icon, description, slug },
metric: { title, icon, description, slug, disabled },
onClick = () => {},
} = props;
return (
<div
className="rounded color-gray-darkest flex items-start border border-transparent p-4 hover:bg-active-blue hover:!border-active-blue-border cursor-pointer group hover-color-teal"
onClick={onClick}
>
<div className="pr-4 pt-1">
{/* @ts-ignore */}
<Icon name={icon} size="20" color="gray-dark" />
<Tooltip disabled={!disabled} title="This feature requires an enterprise license." delay={0}>
<div
className={cn(
'rounded color-gray-darkest flex items-start border border-transparent p-4 hover:bg-active-blue hover:!border-active-blue-border cursor-pointer group hover-color-teal',
{ 'opacity-30 pointer-events-none': disabled }
)}
onClick={onClick}
>
<div className="pr-4 pt-1">
{/* @ts-ignore */}
<Icon name={icon} size="20" color="gray-dark" />
</div>
<div className="flex flex-col items-start text-left">
<div className="text-base">{title}</div>
<div className="text-sm color-gray-medium font-normal">{description}</div>
</div>
</div>
<div className="flex flex-col items-start text-left">
<div className="text-base">{title}</div>
<div className="text-sm color-gray-medium font-normal">{description}</div>
</div>
</div>
</Tooltip>
);
}

View file

@ -2,27 +2,44 @@ import { useModal } from 'App/components/Modal';
import React from 'react';
import MetricsLibraryModal from '../MetricsLibraryModal';
import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem';
import { TYPES, LIBRARY } from 'App/constants/card';
import { TYPES, LIBRARY, INSIGHTS } from 'App/constants/card';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { dashboardMetricCreate, withSiteId } from 'App/routes';
import { useStore } from 'App/mstore';
import { connect } from 'react-redux';
interface Props extends RouteComponentProps {
dashboardId: number;
siteId: string;
isEnterprise: boolean;
}
function MetricTypeList(props: Props) {
const { dashboardId, siteId, history } = props;
const { dashboardId, siteId, history, isEnterprise } = props;
const { metricStore } = useStore();
const { hideModal } = useModal();
const list = React.useMemo(() => {
return TYPES.map((metric: MetricType) => {
const disabled = metric.slug === INSIGHTS && !isEnterprise;
return {
...metric,
disabled: metric.slug === INSIGHTS && !isEnterprise,
tooltipTitle: disabled ? 'This feature requires an enterprise license.' : '',
};
});
}, []);
const { showModal } = useModal();
const onClick = ({ slug }: MetricType) => {
hideModal();
if (slug === LIBRARY) {
return showModal(<MetricsLibraryModal siteId={siteId} dashboardId={dashboardId} />, { right: true, width: 800, onClose: () => {
metricStore.updateKey('metricsSearch', '')
} });
return showModal(<MetricsLibraryModal siteId={siteId} dashboardId={dashboardId} />, {
right: true,
width: 800,
onClose: () => {
metricStore.updateKey('metricsSearch', '');
},
});
}
// TODO redirect to card builder with metricType query param
@ -30,17 +47,19 @@ function MetricTypeList(props: Props) {
const queryString = new URLSearchParams({ type: slug }).toString();
history.push({
pathname: path,
search: `?${queryString}`
search: `?${queryString}`,
});
};
return (
<>
{TYPES.map((metric: MetricType) => (
{list.map((metric: MetricType) => (
<MetricTypeItem metric={metric} onClick={() => onClick(metric)} />
))}
</>
);
}
export default withRouter(MetricTypeList);
export default connect((state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}))(withRouter(MetricTypeList));

View file

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import withPageTitle from 'HOCs/withPageTitle';
import { Icon, Loader, Button, Link, Input, Form } from 'UI';
import { Icon, Loader, Button, Link, Input, Form, Popover, Tooltip } from 'UI';
import { login } from 'Duck/user';
import { forgotPassword, signup } from 'App/routes';
import ReCAPTCHA from 'react-google-recaptcha';
@ -16,12 +16,12 @@ const recaptchaRef = React.createRef();
@connect(
(state, props) => ({
errors: state.getIn([ 'user', 'loginRequest', 'errors' ]),
loading: state.getIn([ 'user', 'loginRequest', 'loading' ]),
errors: state.getIn(['user', 'loginRequest', 'errors']),
loading: state.getIn(['user', 'loginRequest', 'loading']),
authDetails: state.getIn(['user', 'authDetails']),
params: new URLSearchParams(props.location.search)
params: new URLSearchParams(props.location.search),
}),
{ login, setJwt },
{ login, setJwt }
)
@withPageTitle('Login - OpenReplay')
@withRouter
@ -34,7 +34,7 @@ export default class Login extends React.Component {
componentDidMount() {
const { params } = this.props;
const jwt = params.get('jwt')
const jwt = params.get('jwt');
if (jwt) {
this.props.setJwt(jwt);
window.location.href = '/';
@ -44,110 +44,139 @@ export default class Login extends React.Component {
handleSubmit = (token) => {
const { email, password } = this.state;
this.props.login({ email: email.trim(), password, 'g-recaptcha-response': token }).then(() => {
const { errors } = this.props;
})
}
const { errors } = this.props;
});
};
onSubmit = (e) => {
onSubmit = (e) => {
e.preventDefault();
const { CAPTCHA_ENABLED } = this.state;
if (CAPTCHA_ENABLED && recaptchaRef.current) {
recaptchaRef.current.execute();
recaptchaRef.current.execute();
} else if (!CAPTCHA_ENABLED) {
this.handleSubmit();
}
}
};
write = ({ target: { value, name } }) => this.setState({ [ name ]: value })
write = ({ target: { value, name } }) => this.setState({ [name]: value });
render() {
const { errors, loading, authDetails } = this.props;
const { CAPTCHA_ENABLED } = this.state;
return (
<div className="flex flex-col md:flex-row" style={{ height: '100vh'}}>
<div className={cn("md:w-6/12", stl.left)}>
<div className="flex flex-col md:flex-row" style={{ height: '100vh' }}>
<div className={cn('md:w-6/12', stl.left)}>
<div className="px-6 pt-10">
<img src="/assets/logo-white.svg" />
</div>
<div className="color-white text-lg flex items-center">
<div className="flex items-center justify-center w-full" style={{ height: 'calc(100vh - 130px)'}}>
<div
className="flex items-center justify-center w-full"
style={{ height: 'calc(100vh - 130px)' }}
>
<div className="text-4xl">Welcome Back!</div>
</div>
</div>
</div>
<div className="md:w-6/12 flex items-center justify-center py-10">
<div className="">
<Form onSubmit={ this.onSubmit } className="flex items-center justify-center flex-col">
<Form onSubmit={this.onSubmit} className="flex items-center justify-center flex-col">
<div className="mb-8">
<h2 className="text-center text-3xl mb-6">Login to OpenReplay</h2>
{ !authDetails.tenants && <div className="text-center text-xl">Don't have an account? <span className="link"><Link to={ SIGNUP_ROUTE }>Sign up</Link></span></div> }
{!authDetails.tenants && (
<div className="text-center text-xl">
Don't have an account?{' '}
<span className="link">
<Link to={SIGNUP_ROUTE}>Sign up</Link>
</span>
</div>
)}
</div>
<Loader loading={ loading }>
{ CAPTCHA_ENABLED && (
<Loader loading={loading}>
{CAPTCHA_ENABLED && (
<ReCAPTCHA
ref={ recaptchaRef }
ref={recaptchaRef}
size="invisible"
sitekey={ window.env.CAPTCHA_SITE_KEY }
onChange={ token => this.handleSubmit(token) }
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => this.handleSubmit(token)}
/>
)}
<div style={{ width: '350px'}}>
)}
<div style={{ width: '350px' }}>
<div className="mb-6">
<label>Email</label>
<Input
autoFocus={true}
autoComplete="username"
type="text"
placeholder="Email"
name="email"
onChange={ this.write }
required="true"
icon="user-alt"
/>
<Input
autoFocus={true}
autoComplete="username"
type="text"
placeholder="Email"
name="email"
onChange={this.write}
required="true"
icon="user-alt"
/>
</div>
<div className="mb-6">
<label className="mb-2">Password</label>
<Input
autoComplete="current-password"
type="password"
placeholder="Password"
name="password"
onChange={ this.write }
required="true"
icon="lock-alt"
/>
<Input
autoComplete="current-password"
type="password"
placeholder="Password"
name="password"
onChange={this.write}
required="true"
icon="lock-alt"
/>
</div>
</div>
</Loader>
{ errors.length ?
(<div className={ stl.errors }>
{ errors.map(error => (
{errors.length ? (
<div className={stl.errors}>
{errors.map((error) => (
<div className={stl.errorItem}>
<Icon name="info" color="red" size="20"/>
<span className="color-red ml-2">{ error }<br /></span>
<Icon name="info" color="red" size="20" />
<span className="color-red ml-2">
{error}
<br />
</span>
</div>
)) }
</div>) : null
}
{/* <div className={ stl.formFooter }> */}
<Button className="mt-2" type="submit" variant="primary" >{ 'Login' }</Button>
<div className={ cn(stl.links, 'text-lg') }>
<Link to={ FORGOT_PASSWORD }>{'Forgot your password?'}</Link>
))}
</div>
) : null}
{/* <div className={ stl.formFooter }> */}
<Button className="mt-2" type="submit" variant="primary">
{'Login'}
</Button>
<div className={cn(stl.links, 'text-lg')}>
<Link to={FORGOT_PASSWORD}>{'Forgot your password?'}</Link>
</div>
{/* </div> */}
</Form>
{ authDetails.sso && (
<div className={cn(stl.sso, "py-2 flex flex-col items-center")}>
<div className="mb-4">or</div>
<a href="/api/sso/saml2" rel="noopener noreferrer">
<Button variant="outline" type="submit" >{ `Login with SSO (${authDetails.ssoProvider})` }</Button>
</a>
<div className={cn(stl.sso, 'py-2 flex flex-col items-center')}>
<div className="mb-4">or</div>
<div>
<Tooltip
delay={0}
disabled={authDetails.sso}
title={<div>This feature requires an enterprise license.</div>}
placement="top"
// open={true}
>
<a
href="/api/sso/saml2"
rel="noopener noreferrer"
className={cn({ 'pointer-events-none opacity-30': !authDetails.sso })}
>
<Button variant="outline" type="submit">
{`Login with SSO ${authDetails.ssoProvider ? `(${authDetails.ssoProvider})` : ''}`}
</Button>
</a>
</Tooltip>
</div>
)}
</div>
</div>
</div>
</div>

View file

@ -35,9 +35,11 @@ const supportedMessage = `Supported Browsers: ${supportedBrowsers.join(', ')}`;
function ScreenRecorder({
siteId,
sessionId,
isEnterprise,
}: {
siteId: string;
sessionId: string;
isEnterprise: boolean;
}) {
const { player, store } = React.useContext(PlayerContext)
const recordingState = store.get().recordingState
@ -93,11 +95,11 @@ function ScreenRecorder({
player.assistManager.requestRecording()
};
if (!isSupported()) {
if (!isSupported() || !isEnterprise) {
return (
<div className="p-2">
{/* @ts-ignore */}
<Tooltip title={supportedMessage}>
<Tooltip title={isEnterprise ? supportedMessage : 'This feature requires an enterprise license.'}>
<Button icon="record-circle" disabled variant="text-primary">
Record Activity
</Button>
@ -119,6 +121,7 @@ function ScreenRecorder({
}
export default connect((state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
siteId: state.getIn(['site', 'siteId']),
sessionId: state.getIn(['sessions', 'current']).sessionId,
}))(observer(ScreenRecorder))

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Icon, Popover } from 'UI';
import { Icon, Popover, Tooltip } from 'UI';
import styles from './itemMenu.module.css';
import cn from 'classnames';
@ -9,6 +9,7 @@ interface Item {
onClick: (args: any) => void;
hidden?: boolean;
disabled?: boolean;
tooltipTitle?: string;
}
interface Props {
@ -66,23 +67,25 @@ export default class ItemMenu extends React.PureComponent<Props> {
>
{items
.filter(({ hidden }) => !hidden)
.map(({ onClick, text, icon, disabled = false }) => (
<div
key={text}
onClick={!disabled ? this.onClick(onClick) : () => {}}
className={disabled ? 'cursor-not-allowed' : ''}
role="menuitem"
>
<div className={cn(styles.menuItem, 'text-neutral-700', { disabled: disabled })}>
{icon && (
<div className={styles.iconWrapper}>
{/* @ts-ignore */}
<Icon name={icon} size="13" color="gray-dark" />
</div>
)}
<div>{text}</div>
.map(({ onClick, text, icon, disabled = false, tooltipTitle = '' }) => (
<Tooltip disabled={!disabled} title={tooltipTitle}>
<div
key={text}
onClick={!disabled ? this.onClick(onClick) : () => {}}
className={disabled ? 'cursor-not-allowed' : ''}
role="menuitem"
>
<div className={cn(styles.menuItem, 'text-neutral-700', { disabled: disabled })}>
{icon && (
<div className={styles.iconWrapper}>
{/* @ts-ignore */}
<Icon name={icon} size="13" color="gray-dark" />
</div>
)}
<div>{text}</div>
</div>
</div>
</div>
</Tooltip>
))}
</div>
)}
@ -90,7 +93,7 @@ export default class ItemMenu extends React.PureComponent<Props> {
<div
// onClick={this.toggleMenu}
className={cn(
'flex items-center cursor-pointer select-none hover rounded-full',
'flex items-center cursor-pointer select-none hover',
!this.props.flat ? parentStyles : '',
{ 'bg-gray-light': !this.props.flat && displayed && label }
)}

View file

@ -11,7 +11,7 @@
}
width: 36px;
height: 36px;
border-radius: 18px;
/* border-radius: 18px; */
border: 1px solid transparent;
transition: all 0.2s;
margin: 0 auto;

View file

@ -12,6 +12,7 @@ function SideMenuitem({
title,
active = false,
disabled = false,
tooltipTitle = '',
onClick,
deleteHandler = null,
leading = null,
@ -20,8 +21,8 @@ function SideMenuitem({
return (
<Tooltip
disabled={ !disabled }
title={ 'No recordings' }
placement="left"
title={ tooltipTitle }
placement="top"
>
<div
className={ cn(

View file

@ -211,24 +211,24 @@ export const TYPES: CardType[] = [
},
],
},
{
title: 'User Path',
icon: 'signpost-split',
description: 'Discover user journeys between 2 points.',
slug: USER_PATH,
},
{
title: 'Retention',
icon: 'arrow-repeat',
description: 'Retension graph of users / features over a period of time.',
slug: RETENTION,
},
{
title: 'Feature Adoption',
icon: 'card-checklist',
description: 'Find the adoption of your all features in your app.',
slug: FEATURE_ADOPTION,
},
// {
// title: 'User Path',
// icon: 'signpost-split',
// description: 'Discover user journeys between 2 points.',
// slug: USER_PATH,
// },
// {
// title: 'Retention',
// icon: 'arrow-repeat',
// description: 'Retension graph of users / features over a period of time.',
// slug: RETENTION,
// },
// {
// title: 'Feature Adoption',
// icon: 'card-checklist',
// description: 'Find the adoption of your all features in your app.',
// slug: FEATURE_ADOPTION,
// },
{
title: 'Insights',
icon: 'lightbulb',

View file

@ -212,6 +212,8 @@ export default class Widget {
);
} else {
if (data.hasOwnProperty('chart')) {
_data['value'] = data.value;
_data['unit'] = data.unit;
_data['chart'] = getChartFormatter(period)(data.chart);
_data['namesMap'] = data.chart
.map((i: any) => Object.keys(i))