remote pull and resolved conflicts

This commit is contained in:
Shekar Siri 2022-08-16 15:05:59 +02:00
commit b763a1ebab
125 changed files with 1339 additions and 592 deletions

View file

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

View file

@ -91,7 +91,6 @@ function AssistActions({
React.useEffect(() => {
if (!onCall && isCallActive && agentIds) {
logger.log('joinig the party', agentIds)
setPrestart(true);
call(agentIds)
}

View file

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

View file

@ -30,6 +30,7 @@ export default class BreakdownOfLoadedResources extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

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

View file

@ -37,6 +37,7 @@ export default class CallsErrors4xx extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -37,6 +37,7 @@ export default class CallsErrors5xx extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -27,6 +27,7 @@ export default class CpuLoad extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<div className="flex items-center justify-end mb-3">

View file

@ -30,6 +30,7 @@ export default class Crashes extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -34,7 +34,7 @@ function CustomMetricPieChart(props: Props) {
}
}
return (
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
<NoContent size="small" title="No recordings found" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
<ResponsiveContainer height={ 220 } width="100%">
<PieChart>
<Pie

View file

@ -2,7 +2,7 @@ import React from 'react'
import { Table } from '../../common';
import { List } from 'immutable';
import { filtersMap } from 'Types/filter/newFilter';
import { NoContent } from 'UI';
import { NoContent, Icon } from 'UI';
import { tableColumnName } from 'App/constants/filterOptions';
import { numberWithCommas } from 'App/utils';
@ -49,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) }

View file

@ -1,11 +1,10 @@
import React, { useEffect } from "react";
import { Pagination, NoContent } from "UI";
import { Pagination, NoContent, Icon } from "UI";
import ErrorListItem from "App/components/Dashboard/components/Errors/ErrorListItem";
import { withRouter, RouteComponentProps } from "react-router-dom";
import { useModal } from "App/components/Modal";
import ErrorDetailsModal from "App/components/Dashboard/components/Errors/ErrorDetailsModal";
import { useStore } from "App/mstore";
import { overPastString } from "App/dateRange";
interface Props {
metric: any;
data: any;
@ -18,7 +17,6 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
const errorId = new URLSearchParams(props.location.search).get("errorId");
const { showModal, hideModal } = useModal();
const { dashboardStore } = useStore();
const period = dashboardStore.period;
const onErrorClick = (e: any, error: any) => {
e.stopPropagation();
@ -46,9 +44,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 &&

View file

@ -3,7 +3,7 @@ import React from "react";
import SessionItem from "Shared/SessionItem";
import { Pagination, NoContent } from "UI";
import { useStore } from "App/mstore";
import { overPastString } from "App/dateRange";
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
interface Props {
metric: any;
@ -26,7 +26,13 @@ function CustomMetricTableSessions(props: Props) {
data.sessions.length === 0
}
size="small"
title={`No sessions found ${overPastString(period)}`}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={170} />
<div className="mt-2" />
<div className="text-center text-gray-600">No relevant sessions found for the selected time period.</div>
</div>
}
>
<div className="pb-4">
{data.sessions &&

View file

@ -104,6 +104,7 @@ function CustomMetricWidget(props: Props) {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -44,6 +44,7 @@ export default class DomBuildingTime extends React.PureComponent {
return (
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<React.Fragment>
@ -60,6 +61,7 @@ export default class DomBuildingTime extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.size === 0 }
>
<ResponsiveContainer height={ 200 } width="100%">

View file

@ -29,6 +29,7 @@ export default class ErrorsByOrigin extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -31,6 +31,7 @@ export default class ErrorsByType extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">

View file

@ -15,6 +15,7 @@ export default class ErrorsPerDomain extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.size === 0 }
>
<div className="w-full pt-3" style={{ height: '240px' }}>

View file

@ -26,6 +26,7 @@ export default class FPS extends React.PureComponent {
return (
<Loader loading={ loading } size="small">
<NoContent
title="No recordings found"
size="small"
show={ data.chart.length === 0 }
>

View file

@ -12,6 +12,7 @@ export default class LastFeedbacks extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ sessions.size === 0 }
>
{ sessions.map(({

View file

@ -26,6 +26,7 @@ export default class MemoryConsumption extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ data.chart.length === 0 }
>
<div className="flex items-center justify-end mb-3">

View file

@ -48,6 +48,7 @@ export default class MostImpactfulErrors extends React.PureComponent {
<Loader loading={ loading } size="small">
<NoContent
size="small"
title="No recordings found"
show={ errors.size === 0 }
>
<Table

View file

@ -19,6 +19,7 @@ function BreakdownOfLoadedResources(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
@ -46,4 +47,4 @@ function BreakdownOfLoadedResources(props: Props) {
);
}
export default BreakdownOfLoadedResources;
export default BreakdownOfLoadedResources;

View file

@ -19,6 +19,7 @@ function CPULoad(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -54,4 +55,4 @@ function CPULoad(props: Props) {
);
}
export default CPULoad;
export default CPULoad;

View file

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

View file

@ -16,6 +16,7 @@ function CallsErrors4xx(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -46,4 +47,4 @@ function CallsErrors4xx(props: Props) {
);
}
export default CallsErrors4xx;
export default CallsErrors4xx;

View file

@ -16,6 +16,7 @@ function CallsErrors5xx(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -46,4 +47,4 @@ function CallsErrors5xx(props: Props) {
);
}
export default CallsErrors5xx;
export default CallsErrors5xx;

View file

@ -18,6 +18,7 @@ function Crashes(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -52,4 +53,4 @@ function Crashes(props: Props) {
);
}
export default Crashes;
export default Crashes;

View file

@ -33,6 +33,7 @@ function DomBuildingTime(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
>
<>
@ -87,4 +88,4 @@ export default withRequest({
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(DomBuildingTime)
})(DomBuildingTime)

View file

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

View file

@ -16,6 +16,7 @@ function ErrorsByType(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -48,4 +49,4 @@ function ErrorsByType(props: Props) {
);
}
export default ErrorsByType;
export default ErrorsByType;

View file

@ -17,6 +17,7 @@ function ErrorsPerDomain(props: Props) {
size="small"
show={ metric.data.chart.length === 0 }
style={{ height: '240px'}}
title="No recordings found"
>
<div className="w-full" style={{ height: '240px' }}>
{metric.data.chart.map((item, i) =>
@ -34,4 +35,4 @@ function ErrorsPerDomain(props: Props) {
);
}
export default ErrorsPerDomain;
export default ErrorsPerDomain;

View file

@ -19,6 +19,7 @@ function FPS(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
>
<>
@ -57,4 +58,4 @@ function FPS(props: Props) {
);
}
export default FPS;
export default FPS;

View file

@ -22,6 +22,7 @@ function MemoryConsumption(props: Props) {
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
title="No recordings found"
>
<>
<div className="flex items-center justify-end mb-3">
@ -60,4 +61,4 @@ function MemoryConsumption(props: Props) {
);
}
export default MemoryConsumption;
export default MemoryConsumption;

View file

@ -17,6 +17,7 @@ function ResourceLoadedVsResponseEnd(props: Props) {
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
title="No recordings found"
>
<ResponsiveContainer height={ 246 } width="100%">
<ComposedChart

View file

@ -18,6 +18,7 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
title="No recordings found"
>
<ResponsiveContainer height={ 240 } width="100%">
<ComposedChart
@ -69,4 +70,4 @@ function ResourceLoadedVsVisuallyComplete(props: Props) {
);
}
export default ResourceLoadedVsVisuallyComplete;
export default ResourceLoadedVsVisuallyComplete;

View file

@ -53,6 +53,7 @@ function ResourceLoadingTime(props: Props) {
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
title="No recordings found"
>
<>
<div className="flex items-center mb-3">
@ -119,4 +120,4 @@ export default withRequest({
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(ResourceLoadingTime)
})(ResourceLoadingTime)

View file

@ -34,6 +34,7 @@ function ResponseTime(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
>
<>
@ -88,4 +89,4 @@ export default withRequest({
requestName: "fetchOptions",
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
method: 'GET'
})(ResponseTime)
})(ResponseTime)

View file

@ -49,6 +49,7 @@ function ResponseTimeDistribution(props: Props) {
return (
<NoContent
size="small"
title="No recordings found"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
>
@ -125,4 +126,4 @@ function ResponseTimeDistribution(props: Props) {
);
}
export default ResponseTimeDistribution;
export default ResponseTimeDistribution;

View file

@ -15,6 +15,7 @@ function SessionsAffectedByJSErrors(props: Props) {
const { data, metric } = props;
return (
<NoContent
title="No recordings found"
size="small"
show={ metric.data.chart.length === 0 }
style={ { height: '240px' } }
@ -44,4 +45,4 @@ function SessionsAffectedByJSErrors(props: Props) {
);
}
export default SessionsAffectedByJSErrors;
export default SessionsAffectedByJSErrors;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
.tooltipContainer {
& > tippy-popper > tippy-tooltip {
padding: 0!important;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,7 +54,8 @@ export default class SiteDropdown extends React.PureComponent {
this.props.clearSearchLive();
mstore.initClient();
};
mstore.dashboardStore.selectDefaultDashboard();
}
render() {
const {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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