remote pull and resolved conflicts
This commit is contained in:
commit
b763a1ebab
125 changed files with 1339 additions and 592 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 150,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ function AssistActions({
|
|||
|
||||
React.useEffect(() => {
|
||||
if (!onCall && isCallActive && agentIds) {
|
||||
logger.log('joinig the party', agentIds)
|
||||
setPrestart(true);
|
||||
call(agentIds)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,40 +9,41 @@ import cn from 'classnames';
|
|||
import { withSiteId } from 'App/routes';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<{}>) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
interface RouterProps {
|
||||
siteId: string;
|
||||
dashboardId: string;
|
||||
metricId: string;
|
||||
}
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<RouterProps>) {
|
||||
const { history, match: { params: { siteId, dashboardId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const isMetricDetails = history.location.pathname.includes('/metrics/') || history.location.pathname.includes('/metric/');
|
||||
const isDashboardDetails = history.location.pathname.includes('/dashboard/')
|
||||
|
||||
const shouldHideMenu = isMetricDetails || isDashboardDetails;
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
}
|
||||
});
|
||||
if (!dashboardId && location.pathname.includes('dashboard')) {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
props.history.push(withSiteId(`/dashboard/${dashboardId}`, siteId));
|
||||
}, () => {
|
||||
props.history.push(withSiteId('/dashboard', siteId));
|
||||
})
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<div className="page-margin container-90">
|
||||
<div className={cn("side-menu", { 'hidden' : isMetricDetails })}>
|
||||
<div className={cn("side-menu", { 'hidden' : shouldHideMenu })}>
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
<div
|
||||
className={cn({
|
||||
"side-menu-margined" : !isMetricDetails,
|
||||
"container-70" : isMetricDetails
|
||||
"side-menu-margined" : !shouldHideMenu,
|
||||
"container-70" : shouldHideMenu
|
||||
})}
|
||||
>
|
||||
<DashboardRouter siteId={siteId} />
|
||||
<DashboardRouter />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class BreakdownOfLoadedResources extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default class CallWithErrors extends React.PureComponent {
|
|||
</div>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ images.size === 0 }
|
||||
>
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class CallsErrors4xx extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class CallsErrors5xx extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export default class CpuLoad extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class Crashes extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<NoContent size="small" title="No recordings found" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<ResponsiveContainer height={ 220 } width="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import { Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { NoContent } from 'UI';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { tableColumnName } from 'App/constants/filterOptions';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
|
|
@ -49,8 +49,8 @@ function CustomMetricTable(props: Props) {
|
|||
onClick(filters);
|
||||
}
|
||||
return (
|
||||
<div className="" style={{ maxHeight: '240px'}}>
|
||||
<NoContent show={data.values && data.values.length === 0} size="small">
|
||||
<div className="" style={{ height: 240 }}>
|
||||
<NoContent show={data.values && data.values.length === 0} size="small" title={<div className="flex items-center"><Icon name="info-circle" className="mr-2" size="18" /> No data for the selected time period</div>}>
|
||||
<Table
|
||||
small
|
||||
cols={ getColumns(metric) }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import { Pagination, NoContent, Icon } from "UI";
|
||||
import ErrorListItem from "App/components/Dashboard/components/Errors/ErrorListItem";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { useModal } from "App/components/Modal";
|
||||
import ErrorDetailsModal from "App/components/Dashboard/components/Errors/ErrorDetailsModal";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
interface Props {
|
||||
metric: any;
|
||||
data: any;
|
||||
|
|
@ -18,7 +17,6 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
|
|||
const errorId = new URLSearchParams(props.location.search).get("errorId");
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { dashboardStore } = useStore();
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const onErrorClick = (e: any, error: any) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -46,9 +44,9 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
|
|||
|
||||
return (
|
||||
<NoContent
|
||||
title={`No errors found ${overPastString(period)}`}
|
||||
show={!data.errors || data.errors.length === 0}
|
||||
size="small"
|
||||
title={<div className="flex items-center"><Icon name="info-circle" size={18} className="mr-2" />No data for the selected time period</div>}
|
||||
show={!data.errors || data.errors.length === 0}
|
||||
size="small"
|
||||
>
|
||||
<div className="pb-4">
|
||||
{data.errors &&
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import SessionItem from "Shared/SessionItem";
|
||||
import { Pagination, NoContent } from "UI";
|
||||
import { useStore } from "App/mstore";
|
||||
import { overPastString } from "App/dateRange";
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -26,7 +26,13 @@ function CustomMetricTableSessions(props: Props) {
|
|||
data.sessions.length === 0
|
||||
}
|
||||
size="small"
|
||||
title={`No sessions found ${overPastString(period)}`}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No relevant sessions found for the selected time period.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="pb-4">
|
||||
{data.sessions &&
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ function CustomMetricWidget(props: Props) {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export default class DomBuildingTime extends React.PureComponent {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<React.Fragment>
|
||||
|
|
@ -60,6 +61,7 @@ export default class DomBuildingTime extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.size === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default class ErrorsByOrigin extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class ErrorsByType extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default class ErrorsPerDomain extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.size === 0 }
|
||||
>
|
||||
<div className="w-full pt-3" style={{ height: '240px' }}>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default class FPS extends React.PureComponent {
|
|||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class LastFeedbacks extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ sessions.size === 0 }
|
||||
>
|
||||
{ sessions.map(({
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default class MemoryConsumption extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export default class MostImpactfulErrors extends React.PureComponent {
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ errors.size === 0 }
|
||||
>
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function BreakdownOfLoadedResources(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
@ -46,4 +47,4 @@ function BreakdownOfLoadedResources(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default BreakdownOfLoadedResources;
|
||||
export default BreakdownOfLoadedResources;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function CPULoad(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -54,4 +55,4 @@ function CPULoad(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CPULoad;
|
||||
export default CPULoad;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function CallWithErrors(props: Props) {
|
|||
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function CallsErrors4xx(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -46,4 +47,4 @@ function CallsErrors4xx(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CallsErrors4xx;
|
||||
export default CallsErrors4xx;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function CallsErrors5xx(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -46,4 +47,4 @@ function CallsErrors5xx(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CallsErrors5xx;
|
||||
export default CallsErrors5xx;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function Crashes(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -52,4 +53,4 @@ function Crashes(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Crashes;
|
||||
export default Crashes;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function DomBuildingTime(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -87,4 +88,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(DomBuildingTime)
|
||||
})(DomBuildingTime)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ErrorsByOrigin(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -49,4 +50,4 @@ function ErrorsByOrigin(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsByOrigin;
|
||||
export default ErrorsByOrigin;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function ErrorsByType(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -48,4 +49,4 @@ function ErrorsByType(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsByType;
|
||||
export default ErrorsByType;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ErrorsPerDomain(props: Props) {
|
|||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{metric.data.chart.map((item, i) =>
|
||||
|
|
@ -34,4 +35,4 @@ function ErrorsPerDomain(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ErrorsPerDomain;
|
||||
export default ErrorsPerDomain;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function FPS(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -57,4 +58,4 @@ function FPS(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default FPS;
|
||||
export default FPS;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ function MemoryConsumption(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
|
|
@ -60,4 +61,4 @@ function MemoryConsumption(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default MemoryConsumption;
|
||||
export default MemoryConsumption;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function ResourceLoadedVsResponseEnd(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 246 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
@ -69,4 +70,4 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ function ResourceLoadingTime(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
|
|
@ -119,4 +120,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResourceLoadingTime)
|
||||
})(ResourceLoadingTime)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function ResponseTime(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
|
|
@ -88,4 +89,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResponseTime)
|
||||
})(ResponseTime)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ function ResponseTimeDistribution(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
>
|
||||
|
|
@ -125,4 +126,4 @@ function ResponseTimeDistribution(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResponseTimeDistribution;
|
||||
export default ResponseTimeDistribution;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ function SessionsAffectedByJSErrors(props: Props) {
|
|||
const { data, metric } = props;
|
||||
return (
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={ { height: '240px' } }
|
||||
|
|
@ -44,4 +45,4 @@ function SessionsAffectedByJSErrors(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SessionsAffectedByJSErrors;
|
||||
export default SessionsAffectedByJSErrors;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function SessionsImpactedBySlowRequests(props: Props) {
|
|||
|
||||
return (
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
|
|
@ -52,4 +53,4 @@ function SessionsImpactedBySlowRequests(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SessionsImpactedBySlowRequests;
|
||||
export default SessionsImpactedBySlowRequests;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function SessionsPerBrowser(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
|
|
@ -38,4 +39,4 @@ function SessionsPerBrowser(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SessionsPerBrowser;
|
||||
export default SessionsPerBrowser;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function SlowestDomains(props: Props) {
|
|||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ maxHeight: '240px' }}
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{metric.data.chart.map((item, i) =>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function SpeedIndexByLocation(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<NoContent size="small" show={false} style={{ height: '240px' }}>
|
||||
<NoContent size="small" show={false} style={{ height: '240px' }} title="No recordings found">
|
||||
<div className="absolute right-0 mr-4 top=0 w-full flex justify-end">
|
||||
<AvgLabel text="Avg" count={Math.round(metric.data.value)} unit="ms" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ function TimeToRender(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
|
|
@ -88,4 +89,4 @@ export default withRequest({
|
|||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(TimeToRender)
|
||||
})(TimeToRender)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export default class ResourceLoadedVsResponseEnd extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 247 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default class ResourceLoadedVsVisuallyComplete extends React.PureComponen
|
|||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.size === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export default class ResourceLoadingTime extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<React.Fragment>
|
||||
<div className="flex items-center mb-3">
|
||||
|
|
@ -96,6 +97,7 @@ export default class ResourceLoadingTime extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export default class ResponseTime extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export default class ResponseTimeDistribution extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" unit="ms" className="ml-3" count={data.avg} />
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default class SessionsAffectedByJSErrors extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<ComposedChart
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export default class SessionsImpactedBySlowRequests extends React.PureComponent
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default class SessionsPerBrowser extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="w-full pt-3" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
|
|
@ -40,4 +41,4 @@ export default class SessionsPerBrowser extends React.PureComponent {
|
|||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export default class ResponseTime extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.partition && data.partition.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<div className="w-full pt-3" style={{ height: '240px' }}>
|
||||
{data.partition && data.partition.map((item, i) =>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export default class SlowestImages extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ images.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<Table
|
||||
cols={ cols }
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export default class SlowestResources extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.size === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<Table cols={ cols } rows={ data } isTemplate={isTemplate} rowClass="group" compare={compare} />
|
||||
</NoContent>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export default class TimeToRender extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default class TopDomains extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { NoContent, Pagination, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { filterList } from 'App/utils';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import DashboardListItem from './DashboardListItem';
|
||||
|
||||
function DashboardList() {
|
||||
const { dashboardStore } = useStore();
|
||||
const [shownDashboards, setDashboards] = React.useState([]);
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const dashboardsSearch = dashboardStore.dashboardsSearch;
|
||||
|
||||
React.useEffect(() => {
|
||||
setDashboards(filterList(dashboards, dashboardsSearch, ['name', 'owner', 'description']))
|
||||
}, [dashboardsSearch])
|
||||
|
||||
const list = dashboardsSearch !== '' ? shownDashboards : dashboards;
|
||||
const lenth = list.length;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
show={lenth === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="mt-3 text-xl">You haven't created any dashboards yet</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border-b">
|
||||
<div className="grid grid-cols-12 py-2 font-medium">
|
||||
<div className="col-span-8">Title</div>
|
||||
<div className="col-span-2">Visibility</div>
|
||||
<div className="col-span-2 text-right">Created</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map((dashboard: any) => (
|
||||
<React.Fragment key={dashboard.dashboardId}>
|
||||
<DashboardListItem dashboard={dashboard} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4">
|
||||
<div className="text-disabled-text">
|
||||
Showing <span className="font-semibold">{Math.min(list.length, dashboardStore.pageSize)}</span> out of <span className="font-semibold">{list.length}</span> Dashboards
|
||||
</div>
|
||||
<Pagination
|
||||
page={dashboardStore.page}
|
||||
totalPages={Math.ceil(lenth / dashboardStore.pageSize)}
|
||||
onPageChange={(page) => dashboardStore.updateKey('page', page)}
|
||||
limit={dashboardStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DashboardList);
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { IDashboard } from 'App/mstore/types/dashboard';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { withSiteId, dashboardSelected } from 'App/routes';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
dashboard: IDashboard;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
function DashboardListItem(props: Props) {
|
||||
const { dashboard, siteId, history } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
const onItemClick = () => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), siteId);
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div className="hover:bg-active-blue cursor-pointer" onClick={onItemClick}>
|
||||
<div className="grid grid-cols-12 py-4 border-t select-none">
|
||||
<div className="col-span-8 flex items-start">
|
||||
<div className="flex items-center capitalize-first">
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name="columns-gap" size="16" color="tealx" />
|
||||
</div>
|
||||
<div className="color-blue capitalize-first">{dashboard.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div><Label className="capitalize">{metric.metricType}</Label></div> */}
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center">
|
||||
<Icon name={dashboard.isPublic ? 'user-friends' : 'person-fill'} className="mr-2" />
|
||||
<span>{dashboard.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-right">{checkForRecent(dashboard.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
{dashboard.description ? <div className="text-disabled-text px-4 pb-2">{dashboard.description}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
export default connect((state) => ({ siteId: state.getIn(['site', 'siteId']) }))(withRouter(DashboardListItem));
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
let debounceUpdate: any = () => {}
|
||||
|
||||
function DashboardSearch() {
|
||||
const { dashboardStore } = useStore();
|
||||
const [query, setQuery] = useState(dashboardStore.dashboardsSearch);
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((key: string, value: any) => dashboardStore.updateKey(key, value), 500);
|
||||
}, [])
|
||||
|
||||
// @ts-ignore
|
||||
const write = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate('dashboardsSearch', value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="dashboardsSearch"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DashboardSearch);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon } from 'UI';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
import DashboardList from './DashboardList';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
|
||||
function DashboardsView({ history, siteId }: { history: any, siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
dashboardStore
|
||||
.save(dashboardStore.dashboardInstance)
|
||||
.then(async (syncedDashboard) => {
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 px-6 border">
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>Create Dashboard</Button>
|
||||
<div className="ml-auto w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex items-center">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
A dashboard is a custom visualization using your OpenReplay data.
|
||||
</div>
|
||||
<DashboardList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('Dashboards - OpenReplay')(DashboardsView);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardsView';
|
||||
|
|
@ -6,9 +6,16 @@ import cn from 'classnames';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { Loader } from 'UI';
|
||||
|
||||
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds }) {
|
||||
interface IWiProps {
|
||||
category: Record<string, any>
|
||||
onClick: (category: Record<string, any>) => void
|
||||
isSelected: boolean
|
||||
selectedWidgetIds: string[]
|
||||
}
|
||||
|
||||
export function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds }: IWiProps) {
|
||||
const selectedCategoryWidgetsCount = useObserver(() => {
|
||||
return category.widgets.filter(widget => selectedWidgetIds.includes(widget.metricId)).length;
|
||||
return category.widgets.filter((widget: any) => selectedWidgetIds.includes(widget.metricId)).length;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,23 +3,22 @@ import { useObserver } from 'mobx-react-lite';
|
|||
import DashboardMetricSelection from '../DashboardMetricSelection';
|
||||
import DashboardForm from '../DashboardForm';
|
||||
import { Button } from 'UI';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId, dashboardSelected } from 'App/routes';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
|
||||
interface Props {
|
||||
interface Props extends RouteComponentProps {
|
||||
history: any
|
||||
siteId?: string
|
||||
dashboardId?: string
|
||||
onMetricAdd?: () => void;
|
||||
}
|
||||
function DashboardModal(props) {
|
||||
function DashboardModal(props: Props) {
|
||||
const { history, siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const selectedWidgetsCount = useObserver(() => dashboardStore.selectedWidgets.length);
|
||||
const { hideModal } = useModal();
|
||||
const loadingTemplates = useObserver(() => dashboardStore.loadingTemplates);
|
||||
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
|
||||
const loading = useObserver(() => dashboardStore.isSaving);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
metrics,
|
||||
|
|
@ -16,19 +16,20 @@ import DashboardView from '../DashboardView';
|
|||
import MetricsView from '../MetricsView';
|
||||
import WidgetView from '../WidgetView';
|
||||
import WidgetSubDetailsView from '../WidgetSubDetailsView';
|
||||
import DashboardsView from '../DashboardList';
|
||||
|
||||
function DashboardViewSelected({ siteId, dashboardId }) {
|
||||
function DashboardViewSelected({ siteId, dashboardId }: { siteId: string, dashboardId: string }) {
|
||||
return (
|
||||
<DashboardView siteId={siteId} dashboardId={dashboardId} />
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
history: any
|
||||
interface Props extends RouteComponentProps {
|
||||
match: any
|
||||
}
|
||||
function DashboardRouter(props: Props) {
|
||||
const { match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { match: { params: { siteId, dashboardId } }, history } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
|
|
@ -44,8 +45,8 @@ function DashboardRouter(props: Props) {
|
|||
<WidgetSubDetailsView siteId={siteId} {...props} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboard(), siteId)}>
|
||||
<DashboardView siteId={siteId} dashboardId={dashboardId} />
|
||||
<Route exact path={withSiteId(dashboard(), siteId)}>
|
||||
<DashboardsView siteId={siteId} history={history} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
|
||||
|
|
|
|||
|
|
@ -1,113 +1,40 @@
|
|||
//@ts-nocheck
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { SideMenuitem, SideMenuHeader, Icon, Popup, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withSiteId, dashboardSelected, metrics } from 'App/routes';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import DashbaordListModal from '../DashbaordListModal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import cn from 'classnames';
|
||||
import { SideMenuitem, SideMenuHeader } from 'UI';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { withSiteId, metrics, dashboard } from 'App/routes';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux'
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
// import stl from 'Shared/MainSearchBar/mainSearchBar.module.css';
|
||||
|
||||
const SHOW_COUNT = 8;
|
||||
|
||||
interface Props {
|
||||
interface Props extends RouteComponentProps {
|
||||
siteId: string
|
||||
history: any
|
||||
setShowAlerts: (show: boolean) => void
|
||||
}
|
||||
function DashboardSideMenu(props: RouteComponentProps<Props>) {
|
||||
function DashboardSideMenu(props: Props) {
|
||||
const { history, siteId, setShowAlerts } = props;
|
||||
const { hideModal, showModal } = useModal();
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboardId = useObserver(() => dashboardStore.selectedDashboard?.dashboardId);
|
||||
const dashboardsPicked = useObserver(() => dashboardStore.dashboards.slice(0, SHOW_COUNT));
|
||||
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
|
||||
const isMetric = history.location.pathname.includes('metrics');
|
||||
const isDashboards = history.location.pathname.includes('dashboard');
|
||||
|
||||
const redirect = (path) => {
|
||||
const redirect = (path: string) => {
|
||||
history.push(path);
|
||||
}
|
||||
|
||||
const onItemClick = (dashboard) => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId));
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
const onAddDashboardClick = (e) => {
|
||||
dashboardStore.initDashboard();
|
||||
showModal(<DashboardModal siteId={siteId} />, { right: true })
|
||||
}
|
||||
|
||||
const togglePinned = (dashboard, e) => {
|
||||
e.stopPropagation();
|
||||
dashboardStore.updatePinned(dashboard.dashboardId);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
return (
|
||||
<div>
|
||||
<SideMenuHeader
|
||||
className="mb-4 flex items-center"
|
||||
text="DASHBOARDS"
|
||||
button={
|
||||
<Button onClick={onAddDashboardClick} variant="text-primary">
|
||||
<>
|
||||
<Icon name="plus" size="16" color="main" />
|
||||
<span className="ml-1" style={{ textTransform: 'none' }}>Create</span>
|
||||
</>
|
||||
</Button>
|
||||
}
|
||||
text="Preferences"
|
||||
/>
|
||||
{dashboardsPicked.map((item: any) => (
|
||||
<SideMenuitem
|
||||
key={ item.dashboardId }
|
||||
active={item.dashboardId === dashboardId && !isMetric}
|
||||
title={ item.name }
|
||||
iconName={ item.icon }
|
||||
onClick={() => onItemClick(item)}
|
||||
className="group"
|
||||
leading = {(
|
||||
<div className="ml-2 flex items-center cursor-default">
|
||||
{item.isPublic && (
|
||||
<Popup delay={500} content="Visible to the team" hideOnClick>
|
||||
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>
|
||||
</Popup>
|
||||
)}
|
||||
{item.isPinned && <div className="p-1 pointer-events-none"><Icon name="pin-fill" size="16" /></div>}
|
||||
{!item.isPinned && (
|
||||
<Popup
|
||||
delay={500}
|
||||
content="Set as default dashboard"
|
||||
hideOnClick={true}
|
||||
>
|
||||
<div
|
||||
className={cn("p-1 invisible group-hover:visible cursor-pointer")}
|
||||
onClick={(e) => togglePinned(item, e)}
|
||||
>
|
||||
<Icon name="pin-fill" size="16" color="gray-light" />
|
||||
</div>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
{remainingDashboardsCount > 0 && (
|
||||
<div
|
||||
className="my-2 py-2 color-teal cursor-pointer"
|
||||
onClick={() => showModal(<DashbaordListModal siteId={siteId} />, {})}
|
||||
>
|
||||
{remainingDashboardsCount} More
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<SideMenuitem
|
||||
active={isDashboards}
|
||||
id="menu-manage-alerts"
|
||||
title="Dashboards"
|
||||
iconName="columns-gap"
|
||||
onClick={() => redirect(withSiteId(dashboard(), siteId))}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t w-full my-2" />
|
||||
<div className="w-full">
|
||||
<SideMenuitem
|
||||
|
|
@ -128,10 +55,10 @@ function DashboardSideMenu(props: RouteComponentProps<Props>) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(null, { setShowAlerts }),
|
||||
)(DashboardSideMenu) as React.FunctionComponent<RouteComponentProps<Props>>
|
||||
)(DashboardSideMenu)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
.tooltipContainer {
|
||||
& > tippy-popper > tippy-tooltip {
|
||||
padding: 0!important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,23 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useStore } from "App/mstore";
|
||||
import { Button, PageTitle, Loader, NoContent } from "UI";
|
||||
import { withSiteId } from "App/routes";
|
||||
import withModal from "App/components/Modal/withModal";
|
||||
import DashboardWidgetGrid from "../DashboardWidgetGrid";
|
||||
import { confirm } from "UI";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { useModal } from "App/components/Modal";
|
||||
import DashboardModal from "../DashboardModal";
|
||||
import DashboardEditModal from "../DashboardEditModal";
|
||||
import AlertFormModal from "App/components/Alerts/AlertFormModal";
|
||||
import withPageTitle from "HOCs/withPageTitle";
|
||||
import withReport from "App/components/hocs/withReport";
|
||||
import DashboardOptions from "../DashboardOptions";
|
||||
import SelectDateRange from "Shared/SelectDateRange";
|
||||
import DashboardIcon from "../../../../svg/dashboard-icn.svg";
|
||||
import { Tooltip } from "react-tippy";
|
||||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, PageTitle, Loader } from 'UI';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
import { confirm } from 'UI';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
||||
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';
|
||||
|
||||
interface IProps {
|
||||
siteId: string;
|
||||
|
|
@ -29,63 +30,53 @@ type Props = IProps & RouteComponentProps;
|
|||
function DashboardView(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { showModal } = useModal();
|
||||
|
||||
const [focusTitle, setFocusedInput] = React.useState(true);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const showAlertModal = dashboardStore.showAlertModal;
|
||||
const loading = dashboardStore.fetchingDashboard;
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const queryParams = new URLSearchParams(props.location.search);
|
||||
|
||||
const trimQuery = () => {
|
||||
if (!queryParams.has('modal')) return;
|
||||
queryParams.delete('modal');
|
||||
props.history.replace({
|
||||
search: queryParams.toString(),
|
||||
});
|
||||
};
|
||||
const pushQuery = () => {
|
||||
if (!queryParams.has('modal')) props.history.push('?modal=addMetric');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.has('modal')) {
|
||||
onAddWidgets();
|
||||
trimQuery();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isExists = dashboardStore.getDashboardById(dashboardId);
|
||||
if (!isExists) {
|
||||
props.history.push(withSiteId(`/dashboard`, siteId));
|
||||
}
|
||||
}, [dashboardId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboard || !dashboard.dashboardId) return;
|
||||
dashboardStore.fetch(dashboard.dashboardId);
|
||||
}, [dashboard]);
|
||||
|
||||
const trimQuery = () => {
|
||||
if (!queryParams.has("modal")) return;
|
||||
queryParams.delete("modal");
|
||||
props.history.replace({
|
||||
search: queryParams.toString(),
|
||||
});
|
||||
};
|
||||
const pushQuery = () => {
|
||||
if (!queryParams.has("modal")) props.history.push("?modal=addMetric");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboardId || (!dashboard && dashboardStore.dashboards.length > 0)) dashboardStore.selectDefaultDashboard();
|
||||
|
||||
if (queryParams.has("modal")) {
|
||||
onAddWidgets();
|
||||
trimQuery();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dashboardStore.selectDefaultDashboard();
|
||||
}, [siteId])
|
||||
|
||||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
showModal(
|
||||
<DashboardModal
|
||||
siteId={siteId}
|
||||
onMetricAdd={pushQuery}
|
||||
dashboardId={dashboardId}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
showModal(<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId} />, { right: true });
|
||||
};
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
showModal(<DashboardModal siteId={siteId} />, { right: true })
|
||||
}
|
||||
|
||||
const onEdit = (isTitle: boolean) => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
setFocusedInput(isTitle);
|
||||
|
|
@ -95,141 +86,99 @@ function DashboardView(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: "Confirm",
|
||||
confirmButton: "Yes, delete",
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
|
||||
})
|
||||
) {
|
||||
dashboardStore.deleteDashboard(dashboard).then(() => {
|
||||
dashboardStore.selectDefaultDashboard().then(
|
||||
({ dashboardId }) => {
|
||||
props.history.push(
|
||||
withSiteId(`/dashboard/${dashboardId}`, siteId)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
props.history.push(withSiteId("/dashboard", siteId));
|
||||
}
|
||||
);
|
||||
props.history.push(withSiteId(`/dashboard`, siteId));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!dashboard) return null;
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={
|
||||
dashboards.length === 0 ||
|
||||
!dashboard ||
|
||||
!dashboard.dashboardId
|
||||
}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<object
|
||||
style={{ width: "180px" }}
|
||||
type="image/svg+xml"
|
||||
data={DashboardIcon}
|
||||
className="no-result-icon"
|
||||
/>
|
||||
<span>
|
||||
Gather and analyze <br /> important metrics in one
|
||||
place.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
subtext={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onAddDashboardClick}
|
||||
>
|
||||
+ Create Dashboard
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{ maxWidth: "1300px", margin: "auto" }}>
|
||||
<DashboardEditModal
|
||||
show={showEditModal}
|
||||
closeHandler={() => setShowEditModal(false)}
|
||||
focusTitle={focusTitle}
|
||||
/>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-center" style={{ flex: 3 }}>
|
||||
<PageTitle
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }}>
|
||||
<DashboardEditModal show={showEditModal} closeHandler={() => setShowEditModal(false)} focusTitle={focusTitle} />
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: 'Dashboards',
|
||||
to: withSiteId('/dashboard', siteId),
|
||||
},
|
||||
{ label: (dashboard && dashboard.name) || '' },
|
||||
]}
|
||||
/>
|
||||
<div className="flex items-center mb-2 justify-between">
|
||||
<div className="flex items-center" style={{ flex: 3 }}>
|
||||
<PageTitle
|
||||
title={
|
||||
// @ts-ignore
|
||||
title={
|
||||
<Tooltip
|
||||
delay={100}
|
||||
arrow
|
||||
title="Double click to rename"
|
||||
<Tooltip delay={100} arrow 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={
|
||||
/* @ts-ignore */
|
||||
<Tooltip
|
||||
interactive
|
||||
useContext
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
hideDelay={200}
|
||||
duration={0}
|
||||
distance={20}
|
||||
html={<div style={{ padding: 0 }}><AddMetricContainer isPopup siteId={siteId} /></div>}
|
||||
>
|
||||
{dashboard?.name}
|
||||
<Button variant="primary">
|
||||
Add Metric
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
onDoubleClick={() => onEdit(true)}
|
||||
className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer"
|
||||
actionButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddWidgets}
|
||||
>
|
||||
Add Metric
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
|
||||
<div className="flex items-center flex-shrink-0 justify-end" style={{ width: '300px' }}>
|
||||
<SelectDateRange
|
||||
style={{ width: '300px' }}
|
||||
period={period}
|
||||
onChange={(period: any) => dashboardStore.setPeriod(period)}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ flex: 1, justifyContent: "end" }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 justify-end"
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<SelectDateRange
|
||||
style={{ width: "300px" }}
|
||||
period={period}
|
||||
onChange={(period: any) =>
|
||||
dashboardStore.setPeriod(period)
|
||||
}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<DashboardOptions
|
||||
editHandler={onEdit}
|
||||
deleteHandler={onDelete}
|
||||
renderReport={props.renderReport}
|
||||
isTitlePresent={!!dashboard?.description}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<DashboardOptions
|
||||
editHandler={onEdit}
|
||||
deleteHandler={onDelete}
|
||||
renderReport={props.renderReport}
|
||||
isTitlePresent={!!dashboard?.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="my-4 font-normal color-gray-dark">
|
||||
{dashboard?.description}
|
||||
</h2>
|
||||
</div>
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
dashboardId={dashboardId}
|
||||
onEditHandler={onAddWidgets}
|
||||
id="report"
|
||||
/>
|
||||
<AlertFormModal
|
||||
showModal={showAlertModal}
|
||||
onClose={() =>
|
||||
dashboardStore.updateKey("showAlertModal", false)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
<div className="pb-4">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip delay={100} arrow title="Double click to rename" className='w-fit !block'>
|
||||
<h2
|
||||
className="my-2 font-normal w-fit text-disabled-text border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
|
||||
onDoubleClick={() => onEdit(false)}
|
||||
>
|
||||
{dashboard?.description || 'Describe the purpose of this dashboard'}
|
||||
</h2>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<DashboardWidgetGrid siteId={siteId} dashboardId={dashboardId} onEditHandler={onAddWidgets} id="report" />
|
||||
<AlertFormModal showModal={showAlertModal} onClose={() => dashboardStore.updateKey('showAlertModal', false)} />
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTitle("Dashboards - OpenReplay")(
|
||||
withReport(withRouter(withModal(observer(DashboardView))))
|
||||
);
|
||||
export default withPageTitle('Dashboards - OpenReplay')(withReport(withRouter(withModal(observer(DashboardView)))));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button } from 'UI';
|
||||
import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
metrics: any[];
|
||||
siteId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function AddMetric({ metrics, history, siteId, title, description }: IProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const selectedWidgetIds = dashboardStore.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
const onSave = () => {
|
||||
if (selectedWidgetIds.length === 0) return;
|
||||
dashboardStore
|
||||
.save(dashboard)
|
||||
.then(async (syncedDashboard) => {
|
||||
if (dashboard.exists()) {
|
||||
await dashboardStore.fetch(dashboard.dashboardId);
|
||||
}
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
})
|
||||
.then(hideModal);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
const path = withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId);
|
||||
if (!queryParams.has('modal')) history.push('?modal=addMetric');
|
||||
history.push(path);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '85vw', width: 1200 }}>
|
||||
<div className="border-l shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%' }}>
|
||||
<div className="mb-6 pt-8 px-8 flex items-start justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl">{title}</h1>
|
||||
<div className="text-disabled-text">{description}</div>
|
||||
</div>
|
||||
|
||||
<Button variant="text-primary" className="font-medium ml-2" onClick={onCreateNew}>
|
||||
+ Create new
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid h-full grid-cols-4 gap-4 px-8 items-start py-1" style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', gridAutoRows: 'max-content' }}>
|
||||
{metrics ? metrics.map((metric: any) => (
|
||||
<WidgetWrapper
|
||||
key={metric.metricId}
|
||||
widget={metric}
|
||||
active={selectedWidgetIds.includes(metric.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={metric.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(metric)}
|
||||
/>
|
||||
)) : (
|
||||
<div>No custom metrics created.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="py-4 border-t px-8 bg-white w-full flex items-center justify-between">
|
||||
<div>
|
||||
{'Selected '}
|
||||
<span className="font-semibold">{selectedWidgetIds.length}</span>
|
||||
{' out of '}
|
||||
<span className="font-semibold">{metrics.length}</span>
|
||||
</div>
|
||||
<Button variant="primary" disabled={selectedWidgetIds.length === 0} onClick={onSave}>
|
||||
Add Selected
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(AddMetric));
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import AddMetric from './AddMetric';
|
||||
import AddPredefinedMetric from './AddPredefinedMetric';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface AddMetricButtonProps {
|
||||
iconName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isPremade?: boolean;
|
||||
isPopup?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function AddMetricButton({ iconName, title, description, onClick, isPremade, isPopup }: AddMetricButtonProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex items-center hover:bg-gray-lightest hover:!border-gray-light group rounded border cursor-pointer',
|
||||
isPremade ? 'bg-figmaColors-primary-outlined-hover-background' : 'bg-figmaColors-secondary-outlined-hover-background',
|
||||
isPopup ? 'p-4 z-50' : 'px-4 py-8 flex-col'
|
||||
)}
|
||||
style={{ borderColor: 'rgb(238, 238, 238)' }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'p-6 my-3 rounded-full group-hover:bg-gray-light',
|
||||
isPremade
|
||||
? 'bg-figmaColors-primary-outlined-hover-background fill-figmaColors-accent-secondary group-hover:!bg-figmaColors-accent-secondary group-hover:!fill-white'
|
||||
: 'bg-figmaColors-secondary-outlined-hover-background fill-figmaColors-secondary-outlined-resting-border group-hover:!bg-teal group-hover:!fill-white'
|
||||
)}
|
||||
>
|
||||
<Icon name={iconName} size={26} style={{ fill: 'inherit' }} />
|
||||
</div>
|
||||
<div className={isPopup ? 'flex flex-col text-left ml-4' : 'flex flex-col text-center items-center'}>
|
||||
<div className="font-bold text-base text-figmaColors-text-primary">{title}</div>
|
||||
<div className={cn('text-disabled-test text-figmaColors-text-primary text-base', isPopup ? 'w-full' : 'mt-2 w-2/3 text-center')}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddMetricContainer({ siteId, isPopup }: any) {
|
||||
const { showModal } = useModal();
|
||||
const [categories, setCategories] = React.useState<Record<string, any>[]>([]);
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
dashboardStore?.fetchTemplates(true).then((cats) => setCategories(cats));
|
||||
}, []);
|
||||
|
||||
const onAddCustomMetrics = () => {
|
||||
dashboardStore.initDashboard(dashboardStore.selectedDashboard);
|
||||
showModal(
|
||||
<AddMetric
|
||||
siteId={siteId}
|
||||
title="Custom Metrics"
|
||||
description="Metrics that are manually created by you or your team."
|
||||
metrics={categories.find((category) => category.name === 'custom')?.widgets}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
|
||||
const onAddPredefinedMetrics = () => {
|
||||
dashboardStore.initDashboard(dashboardStore.selectedDashboard);
|
||||
showModal(
|
||||
<AddPredefinedMetric
|
||||
siteId={siteId}
|
||||
title="Ready-Made Metrics"
|
||||
description="Curated metrics predfined by OpenReplay."
|
||||
categories={categories.filter((category) => category.name !== 'custom')}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
|
||||
const classes = isPopup
|
||||
? 'bg-white border rounded p-4 grid grid-rows-2 gap-4'
|
||||
: 'bg-white border border-dashed hover:!border-gray-medium rounded p-8 grid grid-cols-2 gap-8';
|
||||
return (
|
||||
<div style={{ borderColor: 'rgb(238, 238, 238)', height: isPopup ? undefined : 300 }} className={classes}>
|
||||
<AddMetricButton
|
||||
title="+ Add Custom Metric"
|
||||
description="Metrics that are manually created by you or your team"
|
||||
iconName="bar-pencil"
|
||||
onClick={onAddCustomMetrics}
|
||||
isPremade
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
<AddMetricButton
|
||||
title="+ Add Ready-Made Metric"
|
||||
description="Curated metrics predfined by OpenReplay."
|
||||
iconName="grid-check"
|
||||
onClick={onAddPredefinedMetrics}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AddMetricContainer);
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button } from 'UI';
|
||||
import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { WidgetCategoryItem } from 'App/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection';
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
categories: Record<string, any>[];
|
||||
siteId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function AddPredefinedMetric({ categories, history, siteId, title, description }: IProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
const [allCheck, setAllCheck] = React.useState(false);
|
||||
const [activeCategory, setActiveCategory] = React.useState<Record<string, any>>();
|
||||
|
||||
const scrollContainer = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const selectedWidgetIds = dashboardStore.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const totalMetricCount = categories.reduce((acc, category) => acc + category.widgets.length, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
dashboardStore?.fetchTemplates(true).then((categories) => {
|
||||
const defaultCategory = categories.filter((category: any) => category.name !== 'custom')[0];
|
||||
setActiveCategory(defaultCategory);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (scrollContainer.current) {
|
||||
scrollContainer.current.scrollTop = 0;
|
||||
}
|
||||
}, [activeCategory, scrollContainer.current]);
|
||||
|
||||
const handleWidgetCategoryClick = (category: any) => {
|
||||
setActiveCategory(category);
|
||||
setAllCheck(false);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (selectedWidgetIds.length === 0) return;
|
||||
dashboardStore
|
||||
.save(dashboard)
|
||||
.then(async (syncedDashboard) => {
|
||||
if (dashboard.exists()) {
|
||||
await dashboardStore.fetch(dashboard.dashboardId);
|
||||
}
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
})
|
||||
.then(hideModal);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
const path = withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId);
|
||||
if (!queryParams.has('modal')) history.push('?modal=addMetric');
|
||||
history.push(path);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
const toggleAllMetrics = ({ target: { checked } }: any) => {
|
||||
setAllCheck(checked);
|
||||
if (checked) {
|
||||
dashboardStore.selectWidgetsByCategory(activeCategory.name);
|
||||
} else {
|
||||
dashboardStore.removeSelectedWidgetByCategory(activeCategory);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '85vw', width: 1200 }}>
|
||||
<div className="border-l shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%' }}>
|
||||
<div className="mb-6 pt-8 px-8 flex items-start justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl">{title}</h1>
|
||||
<div className="text-disabled-text">{description}</div>
|
||||
</div>
|
||||
|
||||
<Button variant="text-primary" className="font-medium ml-2" onClick={onCreateNew}>
|
||||
+ Create Custom Metric
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex px-8 h-full" style={{ maxHeight: 'calc(100vh - 160px)' }}>
|
||||
<div style={{ flex: 3 }}>
|
||||
<div
|
||||
className="grid grid-cols-1 gap-4 py-1 pr-2"
|
||||
style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', gridAutoRows: 'max-content' }}
|
||||
>
|
||||
{activeCategory &&
|
||||
categories.map((category) => (
|
||||
<WidgetCategoryItem
|
||||
key={category.name}
|
||||
onClick={handleWidgetCategoryClick}
|
||||
category={category}
|
||||
isSelected={activeCategory.name === category.name}
|
||||
selectedWidgetIds={selectedWidgetIds}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid h-full grid-cols-4 gap-4 p-1 items-start"
|
||||
style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', flex: 9, gridAutoRows: 'max-content' }}
|
||||
>
|
||||
{activeCategory &&
|
||||
activeCategory.widgets.map((metric: any) => (
|
||||
<WidgetWrapper
|
||||
key={metric.metricId}
|
||||
widget={metric}
|
||||
active={selectedWidgetIds.includes(metric.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={metric.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(metric)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-4 border-t px-8 bg-white w-full flex items-center justify-between">
|
||||
<div>
|
||||
{'Selected '}
|
||||
<span className="font-semibold">{selectedWidgetIds.length}</span>
|
||||
{' out of '}
|
||||
<span className="font-semibold">{totalMetricCount}</span>
|
||||
</div>
|
||||
<Button variant="primary" disabled={selectedWidgetIds.length === 0} onClick={onSave}>
|
||||
Add Selected
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(AddPredefinedMetric));
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Button, Loader } from 'UI';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import AddMetricContainer from './AddMetricContainer'
|
||||
|
||||
interface Props {
|
||||
siteId: string,
|
||||
|
|
@ -18,16 +19,14 @@ function DashboardWidgetGrid(props: Props) {
|
|||
const list: any = useObserver(() => dashboard?.widgets);
|
||||
|
||||
return useObserver(() => (
|
||||
// @ts-ignore
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
icon="no-metrics-chart"
|
||||
title="No metrics added to this dashboard"
|
||||
title={<span className="text-2xl capitalize-first text-figmaColors-text-primary">Build your dashboard</span>}
|
||||
subtext={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<p>Metrics helps you visualize trends from sessions captured by OpenReplay</p>
|
||||
<Button variant="primary" onClick={props.onEditHandler}>Add Metric</Button>
|
||||
</div>
|
||||
<div className="w-4/5 m-auto"><AddMetricContainer siteId={siteId} /></div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>
|
||||
|
|
@ -42,6 +41,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
isWidget={true}
|
||||
/>
|
||||
))}
|
||||
<div className="col-span-2"><AddMetricContainer siteId={siteId} /></div>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Icon, NoContent, Label, Link, Pagination, Popup } from 'UI';
|
||||
import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date';
|
||||
import { getIcon } from 'react-toastify/dist/components';
|
||||
import { Icon, Link } from 'UI';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { Tooltip } from 'react-tippy'
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
interface Props {
|
||||
interface Props extends RouteComponentProps {
|
||||
metric: any;
|
||||
}
|
||||
|
||||
function DashboardLink({ dashboards}: any) {
|
||||
return (
|
||||
dashboards.map((dashboard: any) => (
|
||||
<React.Fragment key={dashboard.dashboardId}>
|
||||
<Link to={`/dashboard/${dashboard.dashboardId}`}>
|
||||
<div className="flex items-center mb-1 py-1">
|
||||
<div className="mr-2">
|
||||
<Icon name="circle-fill" size={4} color="gray-medium" />
|
||||
</div>
|
||||
<span className="link leading-4 capitalize-first">{dashboard.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
))
|
||||
);
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
function MetricTypeIcon({ type }: any) {
|
||||
const PopupWrapper = (props: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={<div className="capitalize">{type}</div>}
|
||||
position="top center"
|
||||
on="hover"
|
||||
hideOnScroll={true}
|
||||
>
|
||||
{props.children}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'funnel':
|
||||
|
|
@ -50,45 +23,47 @@ function MetricTypeIcon({ type }: any) {
|
|||
}
|
||||
|
||||
return (
|
||||
<PopupWrapper>
|
||||
<div className="w-8 h-8 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon()} size="14" color="tealx" />
|
||||
<Tooltip
|
||||
html={<div className="capitalize">{type}</div>}
|
||||
position="top"
|
||||
arrow
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
</PopupWrapper>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric } = props;
|
||||
|
||||
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric, history, siteId } = props;
|
||||
|
||||
const onItemClick = () => {
|
||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div className="grid grid-cols-12 p-3 border-t select-none">
|
||||
<div className="grid grid-cols-12 py-4 border-t select-none hover:bg-active-blue cursor-pointer" onClick={onItemClick}>
|
||||
<div className="col-span-3 flex items-start">
|
||||
<div className="flex items-center">
|
||||
{/* <div className="w-8 h-8 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon(metric.metricType)} size="14" color="tealx" />
|
||||
</div> */}
|
||||
<MetricTypeIcon type={metric.metricType} />
|
||||
<Link to={`/metrics/${metric.metricId}`} className="link capitalize-first">
|
||||
<div className="color-blue capitalize-first">
|
||||
{metric.name}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div><Label className="capitalize">{metric.metricType}</Label></div> */}
|
||||
<div className="col-span-2">
|
||||
<DashboardLink dashboards={metric.dashboards} />
|
||||
</div>
|
||||
<div className="col-span-3">{metric.owner}</div>
|
||||
<div>
|
||||
<div className="col-span-4">
|
||||
<div className="flex items-center">
|
||||
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
|
||||
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
<div className="col-span-2 text-right">{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricListItem;
|
||||
export default withRouter(MetricListItem);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { NoContent, Pagination } from 'UI';
|
||||
import { NoContent, Pagination, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { getRE } from 'App/utils';
|
||||
import { filterList } from 'App/utils';
|
||||
import MetricListItem from '../MetricListItem';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { IWidget } from 'App/mstore/types/widget';
|
||||
|
||||
interface Props { }
|
||||
function MetricsList(props: Props) {
|
||||
function MetricsList({ siteId }: { siteId: string }) {
|
||||
const { metricStore } = useStore();
|
||||
const metrics = useObserver(() => metricStore.metrics);
|
||||
const metricsSearch = useObserver(() => metricStore.metricsSearch);
|
||||
const filterList = (list) => {
|
||||
const filterRE = getRE(metricsSearch, 'i');
|
||||
let _list = list.filter(w => {
|
||||
const dashbaordNames = w.dashboards.map(d => d.name).join(' ');
|
||||
return filterRE.test(w.name) || filterRE.test(w.metricType) || filterRE.test(w.owner) || filterRE.test(dashbaordNames);
|
||||
});
|
||||
return _list
|
||||
|
||||
const filterByDashboard = (item: IWidget, searchRE: RegExp) => {
|
||||
const dashboardsStr = item.dashboards.map((d: any) => d.name).join(' ')
|
||||
return searchRE.test(dashboardsStr)
|
||||
}
|
||||
const list: any = metricsSearch !== '' ? filterList(metrics) : metrics;
|
||||
const list = metricsSearch !== ''
|
||||
? filterList(metrics, metricsSearch, ['name', 'metricType', 'owner'], filterByDashboard)
|
||||
: metrics;
|
||||
const lenth = list.length;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -32,29 +30,30 @@ function MetricsList(props: Props) {
|
|||
show={lenth === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
<Icon name="no-metrics" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="mt-3 text-xl">You haven't created any metrics yet</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border rounded bg-white">
|
||||
<div className="grid grid-cols-12 p-3 font-medium">
|
||||
<div className="col-span-3">Metric</div>
|
||||
{/* <div>Type</div> */}
|
||||
<div className="col-span-2">Dashboards</div>
|
||||
<div className="mt-3 border-b rounded bg-white">
|
||||
<div className="grid grid-cols-12 py-2 font-medium">
|
||||
<div className="col-span-3">Title</div>
|
||||
<div className="col-span-3">Owner</div>
|
||||
<div>Visibility</div>
|
||||
<div className="col-span-2">Last Modified</div>
|
||||
<div className="col-span-4">Visibility</div>
|
||||
<div className="col-span-2 text-right">Last Modified</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => (
|
||||
<React.Fragment key={metric.metricId}>
|
||||
<MetricListItem metric={metric} />
|
||||
<MetricListItem metric={metric} siteId={siteId} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<div className="w-full flex items-center justify-between pt-4">
|
||||
<div className="text-disabled-text">
|
||||
Showing <span className="font-semibold">{Math.min(list.length, metricStore.pageSize)}</span> out of <span className="font-semibold">{list.length}</span> metrics
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.page}
|
||||
totalPages={Math.ceil(lenth / metricStore.pageSize)}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function MetricsSearch(props) {
|
|||
debounceUpdate = debounce((key, value) => metricStore.updateKey(key, value), 500);
|
||||
}, [])
|
||||
|
||||
const write = ({ target: { name, value } }) => {
|
||||
const write = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate('metricsSearch', value);
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ function MetricsSearch(props) {
|
|||
<input
|
||||
value={query}
|
||||
name="metricsSearch"
|
||||
className="bg-white p-2 border rounded w-full pl-10"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title, type, dashboard and owner"
|
||||
onChange={write}
|
||||
/>
|
||||
|
|
@ -31,4 +31,4 @@ function MetricsSearch(props) {
|
|||
));
|
||||
}
|
||||
|
||||
export default MetricsSearch;
|
||||
export default MetricsSearch;
|
||||
|
|
|
|||
|
|
@ -9,27 +9,28 @@ import { useObserver } from 'mobx-react-lite';
|
|||
interface Props{
|
||||
siteId: number;
|
||||
}
|
||||
function MetricsView(props: Props) {
|
||||
const { siteId } = props;
|
||||
function MetricsView({ siteId }: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const metricsCount = useObserver(() => metricStore.metrics.length);
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
return useObserver(() => (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}}>
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 px-6 border">
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
<span className="text-2xl color-gray-medium ml-2">{metricsCount}</span>
|
||||
</div>
|
||||
<Link to={'/metrics/create'}><Button variant="primary">Create Metric</Button></Link>
|
||||
<div className="ml-auto w-1/3">
|
||||
<div className="ml-auto w-1/4" style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
<MetricsList />
|
||||
<div className="text-base text-disabled-text flex items-center">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Create custom Metrics to capture key interactions and track KPIs.
|
||||
</div>
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
return (
|
||||
<Loader loading={loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
|
||||
{renderChart()}
|
||||
<div style={{ minHeight: isOverviewWidget ? 100 : 240 }}>{renderChart()}</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { Button, Icon } from 'UI'
|
||||
import { Button, Icon, SegmentSelection } from 'UI'
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import { confirm, Popup } from 'UI';
|
||||
import Select from 'Shared/Select'
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
|
||||
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
|
|
@ -16,9 +15,15 @@ interface Props {
|
|||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const metricIcons = {
|
||||
timeseries: 'graph-up',
|
||||
table: 'table',
|
||||
funnel: 'funnel',
|
||||
}
|
||||
|
||||
function WidgetForm(props: Props) {
|
||||
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = useState(false);
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
|
||||
const { history, match: { params: { siteId, dashboardId } } } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const isSaving = useObserver(() => metricStore.isSaving);
|
||||
|
|
@ -65,13 +70,15 @@ function WidgetForm(props: Props) {
|
|||
metricStore.merge(obj);
|
||||
};
|
||||
|
||||
const onSelect = (_: any, option: Record<string, any>) => writeOption({ value: { value: option.value }, name: option.name})
|
||||
|
||||
const onSave = () => {
|
||||
const wasCreating = !metric.exists()
|
||||
metricStore.save(metric, dashboardId)
|
||||
.then((metric: any) => {
|
||||
if (wasCreating) {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId));
|
||||
history.replace(withSiteId(dashboardMetricDetails(dashboardId, metric.metricId), siteId));
|
||||
} else {
|
||||
history.replace(withSiteId(metricDetails(metric.metricId), siteId));
|
||||
}
|
||||
|
|
@ -94,11 +101,15 @@ function WidgetForm(props: Props) {
|
|||
<div className="form-group">
|
||||
<label className="font-medium">Metric Type</label>
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
<SegmentSelection
|
||||
icons
|
||||
outline
|
||||
name="metricType"
|
||||
options={metricTypes}
|
||||
value={metricTypes.find((i: any) => i.value === metric.metricType) || metricTypes[0]}
|
||||
onChange={ writeOption }
|
||||
className="my-3"
|
||||
onSelect={ onSelect }
|
||||
value={metricTypes.find((i) => i.value === metric.metricType) || metricTypes[0]}
|
||||
// @ts-ignore
|
||||
list={metricTypes.map((i) => ({ value: i.value, name: i.label, icon: metricIcons[i.value] }))}
|
||||
/>
|
||||
|
||||
{metric.metricType === 'timeseries' && (
|
||||
|
|
@ -201,31 +212,13 @@ function WidgetForm(props: Props) {
|
|||
</Popup>
|
||||
<div className="flex items-center">
|
||||
{metric.exists() && (
|
||||
<>
|
||||
<Button variant="text-primary" onClick={onDelete}>
|
||||
<Icon name="trash" size="14" className="mr-2" color="teal"/>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="ml-2"
|
||||
onClick={() => setShowDashboardSelectionModal(true)}
|
||||
disabled={!canAddToDashboard}
|
||||
>
|
||||
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="text-primary" onClick={onDelete}>
|
||||
<Icon name="trash" size="14" className="mr-2" color="teal"/>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{ canAddToDashboard && (
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,12 @@ function WidgetName(props: Props) {
|
|||
<Tooltip delay={100} arrow title="Double click to rename" disabled={!canEdit}>
|
||||
<div
|
||||
onDoubleClick={() => setEditing(true)}
|
||||
className={cn("text-2xl h-8 flex items-center border-transparent", canEdit && 'cursor-pointer select-none hover:border-dotted hover:border-b border-gray-medium')}
|
||||
className={
|
||||
cn(
|
||||
"text-2xl h-8 flex items-center border-transparent",
|
||||
canEdit && 'cursor-pointer select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium'
|
||||
)
|
||||
}
|
||||
>
|
||||
{ name }
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { SegmentSelection } from 'UI';
|
||||
import { SegmentSelection, Button, Icon } from 'UI';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
|
||||
// import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period';
|
||||
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
name: string;
|
||||
}
|
||||
function WidgetPreview(props: Props) {
|
||||
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = React.useState(false);
|
||||
const { className = '' } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const metric: any = useObserver(() => metricStore.instance);
|
||||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
const isTable = metric.metricType === 'table';
|
||||
|
|
@ -35,29 +38,14 @@ function WidgetPreview(props: Props) {
|
|||
// })
|
||||
// }
|
||||
|
||||
const getWidgetTitle = () => {
|
||||
if (isTimeSeries) {
|
||||
return 'Time Series';
|
||||
} else if (isTable) {
|
||||
if (metric.metricOf === FilterKey.SESSIONS) {
|
||||
// return 'Table of Sessions';
|
||||
return <div>Sessions <span className="color-gray-medium">{metric.data.total}</span></div>;
|
||||
} else if (metric.metricOf === FilterKey.ERRORS) {
|
||||
// return 'Table of Errors';
|
||||
return <div>Errors <span className="color-gray-medium">{metric.data.total}</span></div>;
|
||||
} else {
|
||||
return 'Table';
|
||||
}
|
||||
} else if (metric.metricType === 'funnel') {
|
||||
return 'Funnel';
|
||||
}
|
||||
}
|
||||
const canAddToDashboard = metric.exists() && dashboards.length > 0;
|
||||
|
||||
return useObserver(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<>
|
||||
<div className={cn(className, 'bg-white rounded')}>
|
||||
<div className="flex items-center justify-between px-4 pt-2">
|
||||
<h2 className="text-2xl">
|
||||
{getWidgetTitle()}
|
||||
{props.name}
|
||||
</h2>
|
||||
<div className="flex items-center">
|
||||
{isTimeSeries && (
|
||||
|
|
@ -99,13 +87,33 @@ function WidgetPreview(props: Props) {
|
|||
)}
|
||||
<div className="mx-4" />
|
||||
<WidgetDateRange />
|
||||
{/* add to dashboard */}
|
||||
{metric.exists() && (
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="ml-2 p-0"
|
||||
onClick={() => setShowDashboardSelectionModal(true)}
|
||||
disabled={!canAddToDashboard}
|
||||
>
|
||||
<Icon name="columns-gap-filled" size="14" className="mr-2" color="teal"/>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded p-4">
|
||||
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} />
|
||||
<div className="p-4 pt-0">
|
||||
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName />
|
||||
</div>
|
||||
</div>
|
||||
{ canAddToDashboard && (
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
export default WidgetPreview;
|
||||
export default WidgetPreview;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function WidgetSessions(props: Props) {
|
|||
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
|
||||
|
||||
return useObserver(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className={cn(className, "bg-white p-3 pb-0 rounded border")}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-2xl">Sessions</h2>
|
||||
|
|
@ -80,7 +80,7 @@ function WidgetSessions(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 bg-white p-3 rounded border">
|
||||
<div className="mt-3">
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ function WidgetView(props: Props) {
|
|||
{expanded && <WidgetForm onDelete={onBackHandler} {...props} />}
|
||||
</div>
|
||||
|
||||
<WidgetPreview className="mt-8" />
|
||||
<WidgetPreview className="mt-8" name={widget.name} />
|
||||
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
|
||||
<>
|
||||
{(widget.metricType === 'table' || widget.metricType === 'timeseries') && <WidgetSessions className="mt-8" />}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ interface Props {
|
|||
history?: any
|
||||
onClick?: () => void;
|
||||
isWidget?: boolean;
|
||||
hideName?: boolean;
|
||||
}
|
||||
function WidgetWrapper(props: Props & RouteComponentProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
|
|
@ -112,7 +113,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
<div
|
||||
className={cn("p-3 pb-4 flex items-center justify-between", { "cursor-move" : !isTemplate && isWidget })}
|
||||
>
|
||||
<div className="capitalize-first w-full font-medium">{widget.name}</div>
|
||||
{!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 && (
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default class SideSection extends React.PureComponent {
|
|||
<h3 className="text-xl mb-2">Overview</h3>
|
||||
<Trend
|
||||
chart={ data.chart24 }
|
||||
title="Last 24 hours"
|
||||
title="Past 24 hours"
|
||||
/>
|
||||
<div className="mb-6" />
|
||||
<Trend
|
||||
|
|
@ -121,5 +121,3 @@ export default class SideSection extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function FunnelWidget(props: Props) {
|
|||
}, []);
|
||||
|
||||
return useObserver(() => (
|
||||
<NoContent show={!stages || stages.length === 0}>
|
||||
<NoContent show={!stages || stages.length === 0} title="No recordings found">
|
||||
<div className="w-full">
|
||||
{ !isWidget && (
|
||||
stages.map((filter: any, index: any) => (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom';
|
|||
import cn from 'classnames';
|
||||
import {
|
||||
sessions,
|
||||
metrics,
|
||||
assist,
|
||||
client,
|
||||
dashboard,
|
||||
|
|
@ -27,6 +28,7 @@ import { useStore } from 'App/mstore';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
const DASHBOARD_PATH = dashboard();
|
||||
const METRICS_PATH = metrics();
|
||||
const SESSIONS_PATH = sessions();
|
||||
const ASSIST_PATH = assist();
|
||||
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
|
||||
|
|
@ -94,6 +96,9 @@ const Header = (props) => {
|
|||
to={ withSiteId(DASHBOARD_PATH, siteId) }
|
||||
className={ styles.nav }
|
||||
activeClassName={ styles.active }
|
||||
isActive={ (_, location) => {
|
||||
return location.pathname.includes(DASHBOARD_PATH) || location.pathname.includes(METRICS_PATH);
|
||||
}}
|
||||
>
|
||||
<span>{ 'Dashboards' }</span>
|
||||
</NavLink>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export default class SiteDropdown extends React.PureComponent {
|
|||
this.props.clearSearchLive();
|
||||
|
||||
mstore.initClient();
|
||||
};
|
||||
mstore.dashboardStore.selectDefaultDashboard();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ function Crashes({ player }) {
|
|||
<PanelLayout.Body>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ filtered.length === 0}
|
||||
>
|
||||
<Autoscroll>
|
||||
|
|
@ -48,4 +49,4 @@ function Crashes({ player }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(Crashes);
|
||||
export default observer(Crashes);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function Logs({ player }) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ filtered.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filtered.map(log =>
|
||||
|
|
@ -57,4 +58,4 @@ function Logs({ player }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(Logs);
|
||||
export default observer(Logs);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export default class Exceptions extends React.PureComponent {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ filtered.length === 0}
|
||||
title="No recordings found"
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filtered.map(e => (
|
||||
|
|
@ -118,4 +119,4 @@ export default class Exceptions extends React.PureComponent {
|
|||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export default class GraphQL extends React.PureComponent {
|
|||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ filteredList.length === 0}
|
||||
>
|
||||
<TimeTable
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export default class GraphQL extends React.PureComponent {
|
|||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ filtered.length === 0}
|
||||
>
|
||||
<TimeTable
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
OVERVIEW,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
|
|
@ -23,7 +24,7 @@ import {
|
|||
EXCEPTIONS,
|
||||
INSPECTOR,
|
||||
} from 'Duck/components/player';
|
||||
import { ReduxTime, AssistDuration } from './Time';
|
||||
import { AssistDuration } from './Time';
|
||||
import Timeline from './Timeline';
|
||||
import ControlButton from './ControlButton';
|
||||
import PlayerControls from './components/PlayerControls';
|
||||
|
|
@ -47,6 +48,16 @@ function getStorageIconName(type) {
|
|||
}
|
||||
}
|
||||
|
||||
const SKIP_INTERVALS = {
|
||||
2: 2e3,
|
||||
5: 5e3,
|
||||
10: 1e4,
|
||||
15: 15e3,
|
||||
20: 2e4,
|
||||
30: 3e4,
|
||||
60: 6e4,
|
||||
};
|
||||
|
||||
function getStorageName(type) {
|
||||
switch (type) {
|
||||
case STORAGE_TYPES.REDUX:
|
||||
|
|
@ -107,13 +118,14 @@ function getStorageName(type) {
|
|||
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
|
||||
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
|
||||
closedLive: !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
|
||||
};
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
};
|
||||
},
|
||||
{
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
}
|
||||
changeSkipInterval,}
|
||||
)
|
||||
export default class Controls extends React.Component {
|
||||
componentDidMount() {
|
||||
|
|
@ -158,8 +170,8 @@ export default class Controls extends React.Component {
|
|||
nextProps.showExceptions !== this.props.showExceptions ||
|
||||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
|
||||
nextProps.showLongtasks !== this.props.showLongtasks ||
|
||||
nextProps.liveTimeTravel !== this.props.liveTimeTravel
|
||||
)
|
||||
nextProps.liveTimeTravel !== this.props.liveTimeTravel||
|
||||
nextProps.skipInterval !== this.props.skipInterval)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -195,14 +207,14 @@ export default class Controls extends React.Component {
|
|||
};
|
||||
|
||||
forthTenSeconds = () => {
|
||||
const { time, endTime, jump } = this.props;
|
||||
jump(Math.min(endTime, time + 1e4));
|
||||
const { time, endTime, jump, skipInterval } = this.props;
|
||||
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
backTenSeconds = () => {
|
||||
//shouldComponentUpdate
|
||||
const { time, jump } = this.props;
|
||||
jump(Math.max(0, time - 1e4));
|
||||
const { time, jump, skipInterval } = this.props;
|
||||
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
goLive = () => this.props.jump(this.props.endTime);
|
||||
|
|
@ -275,7 +287,9 @@ export default class Controls extends React.Component {
|
|||
toggleSpeed,
|
||||
toggleSkip,
|
||||
liveTimeTravel,
|
||||
} = this.props;
|
||||
changeSkipInterval,
|
||||
skipInterval,
|
||||
} = this.props;
|
||||
|
||||
const toggleBottomTools = (blockName) => {
|
||||
if (blockName === INSPECTOR) {
|
||||
|
|
@ -308,7 +322,10 @@ export default class Controls extends React.Component {
|
|||
toggleSkip={toggleSkip}
|
||||
playButton={this.renderPlayBtn()}
|
||||
controlIcon={this.controlIcon}
|
||||
/>
|
||||
ref={this.speedRef}
|
||||
skipIntervals={SKIP_INTERVALS}
|
||||
setSkipInterval={changeSkipInterval}
|
||||
currentInterval={skipInterval}/>
|
||||
{/* <Button variant="text" onClick={() => toggleBottomTools(OVERVIEW)}>X-RAY</Button> */}
|
||||
<div className={cn('h-14 border-r bg-gray-light mx-6')} />
|
||||
<XRayButton isActive={bottomBlock === OVERVIEW && !inspectorMode} onClick={() => toggleBottomTools(OVERVIEW)} />
|
||||
|
|
|
|||
|
|
@ -7,66 +7,164 @@ import cn from 'classnames';
|
|||
import styles from '../controls.module.css';
|
||||
|
||||
interface Props {
|
||||
live: boolean;
|
||||
skip: boolean;
|
||||
speed: number;
|
||||
disabled: boolean;
|
||||
playButton: JSX.Element;
|
||||
backTenSeconds: () => void;
|
||||
forthTenSeconds: () => void;
|
||||
toggleSpeed: () => void;
|
||||
toggleSkip: () => void;
|
||||
controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element;
|
||||
live: boolean;
|
||||
skip: boolean;
|
||||
speed: number;
|
||||
disabled: boolean;
|
||||
playButton: JSX.Element;
|
||||
skipIntervals: Record<number, number>;
|
||||
currentInterval: number;
|
||||
setSkipInterval: (interval: number) => void;
|
||||
backTenSeconds: () => void;
|
||||
forthTenSeconds: () => void;
|
||||
toggleSpeed: () => void;
|
||||
toggleSkip: () => void;
|
||||
controlIcon: (
|
||||
icon: string,
|
||||
size: number,
|
||||
action: () => void,
|
||||
isBackwards: boolean,
|
||||
additionalClasses: string
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
function PlayerControls(props: Props) {
|
||||
const { live, skip, speed, disabled, playButton, backTenSeconds, forthTenSeconds, toggleSpeed, toggleSkip, controlIcon } = props;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{playButton}
|
||||
{!live && (
|
||||
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
|
||||
{/* @ts-ignore */}
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
<span className="px-1">/</span>
|
||||
{/* @ts-ignore */}
|
||||
<ReduxTime isCustom name="endTime" format="mm:ss" />
|
||||
</div>
|
||||
)}
|
||||
const {
|
||||
live,
|
||||
skip,
|
||||
speed,
|
||||
disabled,
|
||||
playButton,
|
||||
backTenSeconds,
|
||||
forthTenSeconds,
|
||||
toggleSpeed,
|
||||
toggleSkip,
|
||||
skipIntervals,
|
||||
setSkipInterval,
|
||||
currentInterval,
|
||||
controlIcon,
|
||||
} = props;
|
||||
const speedRef = React.useRef(null);
|
||||
const arrowBackRef = React.useRef(null);
|
||||
const arrowForwardRef = React.useRef(null);
|
||||
|
||||
<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">
|
||||
{controlIcon('skip-forward-fill', 18, backTenSeconds, true, 'hover:bg-active-blue-border color-main h-full flex items-center')}
|
||||
</Tooltip>
|
||||
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border">10s</div>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Forward 10s" delay={0} position="top">
|
||||
{controlIcon('skip-forward-fill', 18, forthTenSeconds, false, 'hover:bg-active-blue-border color-main h-full flex items-center')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
React.useEffect(() => {
|
||||
const handleKeyboard = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowRight') {
|
||||
arrowForwardRef.current.focus();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
arrowBackRef.current.focus();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
speedRef.current.focus();
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
speedRef.current.focus();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyboard);
|
||||
return () => document.removeEventListener('keydown', handleKeyboard);
|
||||
}, [speedRef, arrowBackRef, arrowForwardRef]);
|
||||
|
||||
{!live && (
|
||||
<div className="flex items-center ml-4">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Playback speed" delay={0} position="top">
|
||||
<button className={styles.speedButton} onClick={toggleSpeed} data-disabled={disabled}>
|
||||
<div>{speed + 'x'}</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
className={cn(styles.skipIntervalButton, { [styles.withCheckIcon]: skip, [styles.active]: skip }, 'ml-4')}
|
||||
onClick={toggleSkip}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
{skip && <Icon name="check" size="24" className="mr-1" />}
|
||||
{'Skip Inactivity'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{playButton}
|
||||
{!live && (
|
||||
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
|
||||
{/* @ts-ignore */}
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
<span className="px-1">/</span>
|
||||
{/* @ts-ignore */}
|
||||
<ReduxTime isCustom name="endTime" format="mm:ss" />
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
<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">
|
||||
<button ref={arrowBackRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
|
||||
{controlIcon(
|
||||
'skip-forward-fill',
|
||||
18,
|
||||
backTenSeconds,
|
||||
true,
|
||||
'hover:bg-active-blue-border color-main h-full flex items-center'
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border">
|
||||
<Tooltip
|
||||
interactive
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
hideDelay={200}
|
||||
duration={0}
|
||||
distance={20}
|
||||
html={
|
||||
<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>
|
||||
</div>
|
||||
{Object.keys(skipIntervals).map((interval) => (
|
||||
<div
|
||||
onClick={() => setSkipInterval(parseInt(interval, 10))}
|
||||
className="py-2 px-4 cursor-pointer hover:bg-active-blue border-t w-full text-left border-borderColor-gray-light-shade font-semibold"
|
||||
>
|
||||
{interval} <span className="text-disabled-text">(Secs)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{currentInterval}s
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Forward 10s" delay={0} position="top">
|
||||
<button ref={arrowForwardRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
|
||||
{controlIcon(
|
||||
'skip-forward-fill',
|
||||
18,
|
||||
forthTenSeconds,
|
||||
false,
|
||||
'hover:bg-active-blue-border color-main h-full flex items-center'
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{!live && (
|
||||
<div className="flex items-center mx-4">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Control play back speed (↑↓)" delay={0} position="top">
|
||||
<button
|
||||
ref={speedRef}
|
||||
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
|
||||
onClick={toggleSpeed}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
<div>{speed + 'x'}</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
styles.skipIntervalButton,
|
||||
{ [styles.withCheckIcon]: skip, [styles.active]: skip },
|
||||
'ml-4'
|
||||
)}
|
||||
onClick={toggleSkip}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
{skip && <Icon name="check" size="24" className="mr-1" />}
|
||||
{'Skip Inactivity'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerControls;
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent size="small" show={rows.length === 0}>
|
||||
<NoContent size="small" show={rows.length === 0} title="No recordings found">
|
||||
<div className="relative">
|
||||
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
|
||||
{timeColumns.map((_, index) => (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function Breadcrumb(props: Props) {
|
|||
);
|
||||
}
|
||||
return (
|
||||
<div key={index} className="color-gray-darkest hover:color-teal group flex items-center">
|
||||
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
|
||||
<Link to={item.to} className="flex items-center">
|
||||
{index === 0 && <Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />}
|
||||
<span className="capitalize-first">{item.label}</span>
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ function handleClickOutside(e) {
|
|||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
|
||||
export default React.memo(function OutsideClickDetectingDiv({ onClickOutside, children, ...props}) {
|
||||
function OutsideClickDetectingDiv({ onClickOutside, children, ...props}) {
|
||||
const ref = useRef(null);
|
||||
useLayoutEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
|
|
@ -44,7 +43,6 @@ export default React.memo(function OutsideClickDetectingDiv({ onClickOutside, ch
|
|||
}, [ ref ]);
|
||||
|
||||
return <div ref={ref} {...props}>{children}</div>;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default React.memo(OutsideClickDetectingDiv);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface Props {
|
|||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline'
|
||||
loading?: boolean;
|
||||
icon?: string;
|
||||
rounded?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import SVG from 'UI/SVG';
|
||||
import SVG, { IconNames } from 'UI/SVG';
|
||||
import styles from './icon.module.css';
|
||||
|
||||
interface IProps {
|
||||
name: string
|
||||
interface IProps {
|
||||
name: IconNames
|
||||
size?: number | string
|
||||
height?: number
|
||||
width?: number
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue