feat(ui) - sessions - widget - pagination
This commit is contained in:
parent
10c064c99c
commit
93455cd746
17 changed files with 207 additions and 55 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorLabel'
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorName';
|
||||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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 || {});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue