feat(ui) - sessions - widget - pagination

This commit is contained in:
Shekar Siri 2022-06-17 15:38:16 +02:00 committed by rjshrjndrn
parent 506fefb6e1
commit f53ad1bbc4
17 changed files with 207 additions and 55 deletions

View file

@ -1,15 +1,49 @@
import React from 'react';
import { Pagination } from 'UI';
import ErrorListItem from '../../../components/Errors/ErrorListItem';
const PER_PAGE = 5;
interface Props {
metric: any;
isTemplate?: boolean;
isEdit?: boolean;
}
function CustomMetricTableErrors(props) {
function CustomMetricTableErrors(props: Props) {
const { metric, isEdit = false } = props;
return (
<div>
{metric.data.errors && metric.data.errors.map((error: any, index: any) => (
<ErrorListItem error={error} />
))}
{isEdit && (
<div className="my-6 flex items-center justify-center">
<Pagination
page={metric.page}
totalPages={Math.ceil(metric.data.total / metric.limit)}
onPageChange={(page: any) => metric.updateKey('page', page)}
limit={metric.limit}
debounceRequest={500}
/>
</div>
)}
{!isEdit && (
<ViewMore total={metric.data.total} limit={metric.limit} />
)}
</div>
);
}
export default CustomMetricTableErrors;
export default CustomMetricTableErrors;
const ViewMore = ({ total, limit }: any) => total > limit && (
<div className="my-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
<div className="text-center">
<div className="color-teal text-lg">
All <span className="font-medium">{total}</span> errors
</div>
</div>
</div>
);

View file

@ -1,51 +1,45 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import SessionItem from 'Shared/SessionItem';
import { Pagination } from 'UI';
import { useStore } from 'App/mstore';
import { Pagination, NoContent } from 'UI';
const PER_PAGE = 10;
interface Props {
data: any
metric?: any
metric: any;
isTemplate?: boolean;
isEdit?: boolean;
}
function CustomMetricTableSessions(props: Props) {
const { data = { sessions: [], total: 0 }, isEdit = false } = props;
const currentPage = 1;
const { metricStore } = useStore();
const metric: any = useObserver(() => metricStore.instance);
const { isEdit = false, metric } = props;
return (
<div>
{data.sessions && data.sessions.map((session: any, index: any) => (
<SessionItem session={session} />
return useObserver(() => (
<NoContent show={!metric || !metric.data || !metric.data.sessions || metric.data.sessions.length === 0} size="small">
{metric.data.sessions && metric.data.sessions.map((session: any, index: any) => (
<SessionItem session={session} key={session.sessionId} />
))}
{isEdit && (
<div className="my-6 flex items-center justify-center">
<Pagination
page={currentPage}
totalPages={Math.ceil(data.total / PER_PAGE)}
page={metric.page}
totalPages={Math.ceil(metric.data.total / metric.limit)}
onPageChange={(page: any) => metric.updateKey('page', page)}
limit={PER_PAGE}
limit={metric.data.total}
debounceRequest={500}
/>
</div>
)}
{!isEdit && (
<ViewMore total={data.total} />
<ViewMore total={metric.data.total} limit={metric.limit} />
)}
</div>
);
</NoContent>
));
}
export default CustomMetricTableSessions;
const ViewMore = ({ total }: any) => total > PER_PAGE && (
const ViewMore = ({ total, limit }: any) => total > limit && (
<div className="my-4 flex items-center justify-center cursor-pointer w-fit mx-auto">
<div className="text-center">
<div className="color-teal text-lg">

View file

@ -10,7 +10,7 @@ interface Props {
onEditHandler: () => void;
id?: string;
}
function DashboardWidgetGrid(props) {
function DashboardWidgetGrid(props: Props) {
const { dashboardId, siteId } = props;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
@ -31,12 +31,12 @@ function DashboardWidgetGrid(props) {
}
>
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>
{list && list.map((item, index) => (
{list && list.map((item: any, index: any) => (
<WidgetWrapper
index={index}
widget={item}
key={item.widgetId}
moveListItem={(dragIndex, hoverIndex) => dashboard.swapWidgetPosition(dragIndex, hoverIndex)}
moveListItem={(dragIndex: any, hoverIndex: any) => dashboard.swapWidgetPosition(dragIndex, hoverIndex)}
dashboardId={dashboardId}
siteId={siteId}
isWidget={true}

View file

@ -0,0 +1,23 @@
import React from 'react';
import cn from "classnames";
interface Props {
className?: string;
topValue: string;
topValueSize?: string;
bottomValue: string;
topMuted?: boolean;
bottomMuted?: boolean;
}
function ErrorLabel({ className, topValue, topValueSize = 'text-base', bottomValue, topMuted = false, bottomMuted = false }: Props) {
return (
<div className={ cn(className, "flex flex-col items-center px-4") } >
<div className={ cn(topValueSize, { "color-gray-medium": topMuted }) } >{ topValue }</div>
<div className={ cn("font-light text-sm", { "color-gray-medium": bottomMuted }) }>{ bottomValue }</div>
</div>
)
}
ErrorLabel.displayName = "ErrorLabel";
export default ErrorLabel;

View file

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

View file

@ -1,14 +1,77 @@
import React from 'react';
import cn from 'classnames';
import moment from 'moment';
import { error as errorRoute } from 'App/routes';
import { IGNORED, RESOLVED } from 'Types/errorInfo';
import { Link, Label } from 'UI';
import ErrorName from '../ErrorName';
import ErrorLabel from '../ErrorLabel';
import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts';
import { Styles } from '../../../Widgets/common';
import { diffFromNowString } from 'App/date';
interface Props {
error: any;
className?: string;
}
function ErrorListItem(props: Props) {
const { error, className = '' } = props;
return (
<div>
Errors List Item
</div>
<div
className={ cn("border p-3 flex justify-between cursor-pointer py-4 hover:bg-active-blue mb-3", className) }
id="error-item"
>
<div className={ cn("flex-1 leading-tight") } >
<div>
<ErrorName
icon={error.status === IGNORED ? 'ban' : null }
lineThrough={error.status === RESOLVED}
name={ error.name }
message={ error.stack0InfoString }
bold={ !error.viewed }
/>
<div className={ cn("truncate color-gray-medium", { "line-through" : error.status === RESOLVED}) }>
{ error.message }
</div>
</div>
</div>
<BarChart width={ 150 } height={ 40 } data={ error.chart }>
<XAxis hide dataKey="timestamp" />
<YAxis hide domain={[0, 'dataMax + 8']} />
<Tooltip {...Styles.tooltip} label="Sessions" content={<CustomTooltip />} />
<Bar name="Sessions" minPointSize={1} dataKey="count" fill="#A8E0DA" />
</BarChart>
<ErrorLabel
// className={stl.sessions}
topValue={ error.sessions }
bottomValue="Sessions"
/>
<ErrorLabel
// className={stl.users}
topValue={ error.users }
bottomValue="Users"
/>
<ErrorLabel
// className={stl.occurrence}
topValue={ `${error.lastOccurrence && diffFromNowString(error.lastOccurrence)} ago` }
bottomValue="Last Seen"
/>
</div>
);
}
export default ErrorListItem;
export default ErrorListItem;
const CustomTooltip = ({ active, payload, label }: any) => {
if (active) {
const p = payload[0].payload;
return (
<div className="rounded border bg-white p-2">
<p className="label text-sm color-gray-medium">{`${p.timestamp ? moment(p.timestamp).format('l') : ''}`}</p>
<p className="text-sm">Sessions: {p.count}</p>
</div>
);
}
return null;
};

View file

@ -0,0 +1,15 @@
import React from 'react';
import cn from "classnames";
function ErrorText({ className, icon, name, message, bold, lineThrough = false }: any) {
return (
<div className={ cn("mb-1 truncate", { "font-weight-bold": bold }) }>
<span className={cn("code-font color-red", className, { 'line-through' : lineThrough })}>{ name }</span>
<span className={cn('color-gray-darkest ml-2', { 'line-through' : lineThrough })}>{ message }</span>
</div>
);
}
ErrorText.displayName = "ErrorText";
export default ErrorText;

View file

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

View file

@ -12,11 +12,13 @@ import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMe
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted'
import { FilterKey } from 'Types/filter/filterType';
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
import ErrorsWidget from '../Errors/ErrorsWidget';
import SessionWidget from '../Sessions/SessionWidget';
import CustomMetricTableSessions from '../../Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors';
interface Props {
metric: any;
isWidget?: boolean;
@ -130,12 +132,19 @@ function WidgetChart(props: Props) {
}
if (metricType === 'table') {
if (metricOf === 'SESSIONS') {
if (metricOf === FilterKey.SESSIONS) {
return (
<CustomMetricTableSessions
metric={_metric}
data={data}
// onClick={onChartClick}
metric={metric}
isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
)
}
if (metricOf === FilterKey.ERRORS) {
return (
<CustomMetricTableErrors
metric={metric}
isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
@ -165,7 +174,7 @@ function WidgetChart(props: Props) {
return <div>Unknown</div>;
}
return useObserver(() => (
<Loader loading={!isFunnel && loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
<Loader loading={loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
{renderChart()}
</Loader>
));

View file

@ -54,11 +54,11 @@ function WidgetView(props: Props) {
className={cn(
"px-6 py-4 flex justify-between items-center",
{
'cursor-pointer hover:bg-active-blue hover:shadow-border-blue': !expanded,
'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded,
}
)}
onClick={openEdit}
>
>
<h1 className="mb-0 text-2xl">
<WidgetName
name={widget.name}

View file

@ -12,10 +12,9 @@ import Label from 'Components/Errors/ui/Label';
import stl from './listItem.module.css';
import { Styles } from '../../../Dashboard/Widgets/common';
const CustomTooltip = ({ active, payload, label }) => {
if (active) {
const p = payload[0].payload;
const p = payload[0].payload;
return (
<div className="rounded border bg-white p-2">
<p className="label text-sm color-gray-medium">{`${moment(p.timestamp).format('l')}`}</p>

View file

@ -7,7 +7,7 @@ interface Props {
}
function FunnelBar(props: Props) {
const { filter } = props;
const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues);
// const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues);
return (
<div className="w-full mb-4">

View file

@ -8,6 +8,8 @@ import { getChartFormatter } from 'Types/dashboard/helper';
import Filter, { IFilter } from "./types/filter";
import Funnel from "./types/funnel";
import Session from "./types/session";
import Error from "./types/error";
import { FilterKey } from 'Types/filter/filterType';
export interface IDashboardSotre {
dashboards: IDashboard[]
@ -132,7 +134,7 @@ export default class DashboardStore implements IDashboardSotre {
fetchMetricChartData: action
})
const drillDownPeriod = Period({ rangeName: LAST_24_HOURS }).toTimestamps();
const drillDownPeriod = Period({ rangeName: LAST_30_DAYS }).toTimestamps();
this.drillDownFilter.updateKey('startTimestamp', drillDownPeriod.startTimestamp)
this.drillDownFilter.updateKey('endTimestamp', drillDownPeriod.endTimestamp)
}
@ -459,8 +461,10 @@ export default class DashboardStore implements IDashboardSotre {
}
// TODO refactor to widget class
if (metric.metricOf === 'SESSIONS') {
if (metric.metricOf === FilterKey.SESSIONS) {
_data['sessions'] = data.sessions.map((s: any) => new Session().fromJson(s))
} else if (metric.metricOf === FilterKey.ERRORS) {
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s))
} else {
if (data.hasOwnProperty('chart')) {
_data['chart'] = getChartFormatter(this.period)(data.chart)

View file

@ -2,7 +2,6 @@
export default class Error {
sessionId: string = ''
messageId: string = ''
timestamp: string = ''
errorId: string = ''
projectId: string = ''
source: string = ''
@ -11,6 +10,12 @@ export default class Error {
time: string = ''
function: string = '?'
stack0InfoString: string = ''
chart: any = []
sessions: number = 0
users: number = 0
lastOccurrence: string = ''
timestamp: string = ''
constructor() {
}
@ -26,7 +31,10 @@ export default class Error {
this.message = json.message
this.time = json.time
this.function = json.function
this.chart = json.chart
this.stack0InfoString = getStck0InfoString(json.stack || [])
this.sessions = json.sessions
this.users = json.users
return this
}
}

View file

@ -31,9 +31,9 @@ export default class Funnel implements IFunnel {
const firstStage = json.stages[0]
const lastStage = json.stages[json.stages.length - 1]
this.lostConversions = firstStage.sessionsCount - lastStage.sessionsCount
this.lostConversionsPercentage = this.lostConversions / firstStage.sessionsCount * 100
this.lostConversionsPercentage = Math.round(this.lostConversions / firstStage.sessionsCount * 100)
this.totalConversions = lastStage.sessionsCount
this.totalConversionsPercentage = this.totalConversions / firstStage.sessionsCount * 100
this.totalConversionsPercentage = Math.round(this.totalConversions / firstStage.sessionsCount * 100)
this.conversionImpact = this.lostConversions ? Math.round((this.lostConversions / firstStage.sessionsCount) * 100) : 0;
this.stages = json.stages ? json.stages.map((stage: any, index: number) => new FunnelStage().fromJSON(stage, firstStage.sessionsCount, index > 0 ? json.stages[index - 1].sessionsCount : stage.sessionsCount)) : []
this.affectedUsers = firstStage.usersCount ? firstStage.usersCount - lastStage.usersCount : 0;

View file

@ -5,6 +5,7 @@ import { metricService } from "App/services";
import Session from "App/mstore/types/session";
import Funnelissue from 'App/mstore/types/funnelIssue';
import { issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
export interface IWidget {
metricId: any
@ -58,7 +59,7 @@ export default class Widget implements IWidget {
widgetId: any = undefined
name: string = "New Metric"
// metricType: string = "timeseries"
metricType: string = "table"
metricType: string = "timeseries"
metricOf: string = "sessionCount"
metricValue: string = ""
viewType: string = "lineChart"
@ -79,6 +80,8 @@ export default class Widget implements IWidget {
position: number = 0
data: any = {
sessions: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
@ -93,7 +96,7 @@ export default class Widget implements IWidget {
constructor() {
makeAutoObservable(this, {
sessionsLoading: observable,
data: observable.ref,
data: observable,
metricId: observable,
widgetId: observable,
name: observable,
@ -183,7 +186,7 @@ export default class Widget implements IWidget {
series: this.series.map((series: any) => series.toJson()),
config: {
...this.config,
col: this.metricType === 'funnel' ? 4 : this.config.col
col: this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS ? 4 : this.config.col
},
}
}
@ -204,7 +207,7 @@ export default class Widget implements IWidget {
setData(data: any) {
runInAction(() => {
Object.assign(this.data, data)
this.data = data;
})
}
@ -236,8 +239,6 @@ export default class Widget implements IWidget {
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService.fetchIssue(funnelId, issueId, params).then((response: any) => {
response = response[0]
console.log('response', response)
resolve({
issue: new Funnelissue().fromJSON(response.issue),
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)),

View file

@ -89,7 +89,7 @@ export default class MetricService implements IMetricService {
getMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
const path = isWidget ? `/metrics/${metric.metricId}/chart` : `/metrics/try`;
return this.client.post(path, data)
.then((response: { json: () => any; }) => response.json())
.then(fetchErrorCheck)
.then((response: { data: any; }) => response.data || {});
}