feat(ui) - funnels more steps
This commit is contained in:
parent
fb164af465
commit
1bcb0dfc01
14 changed files with 212 additions and 111 deletions
|
|
@ -18,10 +18,10 @@ function ErrorListItem(props: Props) {
|
|||
const { error, className = '' } = props;
|
||||
return (
|
||||
<div
|
||||
className={ cn("border p-3 flex justify-between cursor-pointer py-4 hover:bg-active-blue mb-3", className) }
|
||||
className={ cn("border p-3 grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue mb-3", className) }
|
||||
id="error-item"
|
||||
>
|
||||
<div className={ cn("flex-1 leading-tight") } >
|
||||
<div className={ cn("col-span-6 leading-tight") } >
|
||||
<div>
|
||||
<ErrorName
|
||||
icon={error.status === IGNORED ? 'ban' : null }
|
||||
|
|
@ -35,26 +35,31 @@ function ErrorListItem(props: Props) {
|
|||
</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>
|
||||
<div className="col-span-2">
|
||||
<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>
|
||||
</div>
|
||||
<ErrorLabel
|
||||
// className={stl.sessions}
|
||||
topValue={ error.sessions }
|
||||
bottomValue="Sessions"
|
||||
className="col-span-1"
|
||||
/>
|
||||
<ErrorLabel
|
||||
// className={stl.users}
|
||||
topValue={ error.users }
|
||||
bottomValue="Users"
|
||||
className="col-span-1"
|
||||
/>
|
||||
<ErrorLabel
|
||||
// className={stl.occurrence}
|
||||
topValue={ `${error.lastOccurrence && diffFromNowString(error.lastOccurrence)} ago` }
|
||||
bottomValue="Last Seen"
|
||||
className="col-span-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { Loader } from 'UI';
|
||||
import FunnelIssuesDropdown from '../FunnelIssuesDropdown';
|
||||
import FunnelIssuesSort from '../FunnelIssuesSort';
|
||||
import FunnelIssuesList from '../FunnelIssuesList';
|
||||
import { DateTime } from 'luxon';
|
||||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
function FunnelIssues() {
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const [data, setData] = useState<any>({ issues: [] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isMounted = useIsMounted()
|
||||
// const funnel = useObserver(() => funnelStore.instance);
|
||||
// const funnel = useObserver(() => metricStore.instance);
|
||||
// const issues = useObserver(() => funnelStore.issues);
|
||||
// const loading = useObserver(() => funnelStore.isLoadingIssues);
|
||||
|
||||
const fetchIssues = (filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
|
|
@ -35,7 +30,6 @@ function FunnelIssues() {
|
|||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
|
||||
|
||||
|
||||
const depsString = JSON.stringify(widget.series);
|
||||
useEffect(() => {
|
||||
|
|
@ -54,17 +48,7 @@ function FunnelIssues() {
|
|||
</div>
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!loading && data.issues.length === 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 issues found</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FunnelIssuesList issues={data.issues} />
|
||||
</NoContent>
|
||||
<FunnelIssuesList issues={data.issues} />
|
||||
</Loader>
|
||||
</div>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { Component, ReactNode, FunctionComponent, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import Select from 'Shared/Select'
|
||||
import { components } from 'react-select';
|
||||
import { Icon } from 'UI';
|
||||
import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters';
|
||||
import { useStore } from 'App/mstore';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
|
||||
const options = [
|
||||
{ value: "click_rage", label: "Click Rage" },
|
||||
|
|
@ -20,7 +21,7 @@ const options = [
|
|||
{ value: "js_error", label: "Error" }
|
||||
]
|
||||
|
||||
function FunnelIssuesDropdown(props) {
|
||||
function FunnelIssuesDropdown() {
|
||||
const { funnelStore } = useStore();
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [selectedValues, setSelectedValues] = React.useState<any>([]);
|
||||
|
|
@ -48,12 +49,20 @@ function FunnelIssuesDropdown(props) {
|
|||
}
|
||||
}
|
||||
|
||||
const onClickOutside = () => {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
<Select
|
||||
menuIsOpen={isOpen}
|
||||
onMenuOpen={() => setIsOpen(true)}
|
||||
onMenuClose={() => setIsOpen(false)}
|
||||
// onMenuOpen={() => setIsOpen(true)}
|
||||
// onMenuClose={() => setIsOpen(false)}
|
||||
options={filteredOptions}
|
||||
onChange={handleChange}
|
||||
styles={{
|
||||
|
|
@ -77,11 +86,19 @@ function FunnelIssuesDropdown(props) {
|
|||
IndicatorsContainer: (): any => null,
|
||||
Control: ({ children, ...props }: any) => (
|
||||
<components.Control {...props}>
|
||||
{ children }
|
||||
<button className="px-2 py-1 bg-white rounded-2xl border border-teal border-dashed color-teal flex items-center hover:bg-active-blue" onClick={() => setIsOpen(!isOpen)}>
|
||||
<Icon name="funnel" size={16} color="teal" />
|
||||
<span className="ml-2">Issues</span>
|
||||
</button>
|
||||
<OutsideClickDetectingDiv
|
||||
// className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
|
||||
onClickOutside={onClickOutside}
|
||||
>
|
||||
{ children }
|
||||
<button
|
||||
className="px-2 py-1 bg-white rounded-2xl border border-teal border-dashed color-teal flex items-center hover:bg-active-blue"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<Icon name="funnel" size={16} color="teal" />
|
||||
<span className="ml-2">Issues</span>
|
||||
</button>
|
||||
</OutsideClickDetectingDiv>
|
||||
</components.Control>
|
||||
),
|
||||
Placeholder: (): any => null,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import { useStore } from 'App/mstore';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import FunnelIssuesListItem from '../FunnelIssuesListItem';
|
||||
import { NoContent } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
issues: any;
|
||||
}
|
||||
function FunnelIssuesList(props: Props) {
|
||||
const { issues } = props;
|
||||
const { issues, loading } = props;
|
||||
const { funnelStore } = useStore();
|
||||
const issuesSort = useObserver(() => funnelStore.issuesSort);
|
||||
const issuesFilter = useObserver(() => funnelStore.issuesFilter.map((issue: any) => issue.value));
|
||||
|
|
@ -17,14 +20,22 @@ function FunnelIssuesList(props: Props) {
|
|||
filteredIssues = useObserver(() => issuesSort.order === 'desc' ? filteredIssues.reverse() : filteredIssues);
|
||||
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<NoContent
|
||||
show={!loading && filteredIssues.length === 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 issues found</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{filteredIssues.map((issue: any, index: React.Key) => (
|
||||
<div key={index} className="mb-4">
|
||||
<FunnelIssuesListItem issue={issue} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
</NoContent>
|
||||
))
|
||||
}
|
||||
|
||||
export default FunnelIssuesList;
|
||||
|
|
@ -41,7 +41,6 @@ function WidgetChart(props: Props) {
|
|||
|
||||
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
const isFunnel = metric.metricType === 'funnel';
|
||||
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
|
|
@ -66,7 +65,6 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
|
||||
const depsString = JSON.stringify(_metric.series);
|
||||
|
||||
const fetchMetricChartData = (metric: any, payload: any, isWidget: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true)
|
||||
|
|
@ -88,6 +86,7 @@ function WidgetChart(props: Props) {
|
|||
debounceRequest(metric, payload, isWidget);
|
||||
}, [period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]);
|
||||
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType, metricOf } = metric;
|
||||
|
||||
|
|
@ -100,7 +99,7 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
|
||||
if (metricType === 'funnel') {
|
||||
return <FunnelWidget metric={metric} />
|
||||
return <FunnelWidget metric={metric} isWidget={isWidget} />
|
||||
}
|
||||
|
||||
if (metricType === 'predefined') {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ function WidgetForm(props: Props) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
{metric.metricType === 'table' && !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
|
||||
<>
|
||||
<span className="mx-3">showing</span>
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SegmentSelection } from 'UI';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
// import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -13,16 +14,21 @@ interface Props {
|
|||
function WidgetPreview(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
const metric: any = useObserver(() => metricStore.instance);
|
||||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
const isTable = metric.metricType === 'table';
|
||||
// const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
const disableVisualization = useObserver(() => metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS);
|
||||
const period = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
|
||||
const chagneViewType = (e, { name, value }: any) => {
|
||||
metric.update({ [ name ]: value });
|
||||
}
|
||||
|
||||
const onChangePeriod = (period: any) => {
|
||||
dashboardStore.setDrillDownPeriod(period);
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -39,8 +45,8 @@ function WidgetPreview(props: Props) {
|
|||
onSelect={ chagneViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={ [
|
||||
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
|
||||
{ value: 'progress', name: 'Progress', icon: 'hash' },
|
||||
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
|
||||
{ value: 'progress', name: 'Progress', icon: 'hash' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -69,7 +75,8 @@ function WidgetPreview(props: Props) {
|
|||
<span className="mr-1 color-gray-medium">Time Range</span>
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
onChange={(period: any) => dashboardStore.setPeriod(period)}
|
||||
// onChange={(period: any) => metric.setPeriod(period)}
|
||||
onChange={onChangePeriod}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { NoContent, Dropdown, Icon, Loader, Pagination } from 'UI';
|
||||
import { NoContent, Loader, Pagination } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -15,19 +15,29 @@ interface Props {
|
|||
}
|
||||
function WidgetSessions(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const [activeSeries, setActiveSeries] = useState('all');
|
||||
const [data, setData] = useState<any>([]);
|
||||
const isMounted = useIsMounted()
|
||||
const isMounted = useIsMounted();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const filteredSessions = getListSessionsBySeries(data, activeSeries);
|
||||
const { dashboardStore, metricStore } = useStore();
|
||||
const filter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
const widget: any = useObserver(() => metricStore.instance);
|
||||
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const [timestamps, setTimestamps] = useState<any>({
|
||||
startTimestamp: 0,
|
||||
endTimestamp: 0,
|
||||
});
|
||||
const [seriesOptions, setSeriesOptions] = useState([
|
||||
{ label: 'All', value: 'all' },
|
||||
]);
|
||||
|
||||
const [activeSeries, setActiveSeries] = useState('all');
|
||||
|
||||
const writeOption = (e, { name, value }) => setActiveSeries(value.value);
|
||||
const writeOption = ({ value }: any) => setActiveSeries(value.value);
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const seriesOptions = data.map(item => ({
|
||||
const seriesOptions = data.map((item: any) => ({
|
||||
label: item.seriesName,
|
||||
value: item.seriesId,
|
||||
}));
|
||||
|
|
@ -37,22 +47,15 @@ function WidgetSessions(props: Props) {
|
|||
]);
|
||||
}, [data]);
|
||||
|
||||
const fetchSessions = (metricId, filter) => {
|
||||
const fetchSessions = (metricId: any, filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true)
|
||||
widget.fetchSessions(metricId, filter).then(res => {
|
||||
widget.fetchSessions(metricId, filter).then((res: any) => {
|
||||
setData(res)
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
const filteredSessions = getListSessionsBySeries(data, activeSeries);
|
||||
const { dashboardStore, metricStore } = useStore();
|
||||
const filter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
const widget: any = useObserver(() => metricStore.instance);
|
||||
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []);
|
||||
|
||||
const depsString = JSON.stringify(widget.series);
|
||||
|
|
@ -60,6 +63,12 @@ function WidgetSessions(props: Props) {
|
|||
debounceRequest(widget.metricId, { ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
|
||||
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const timestamps = drillDownPeriod.toTimestamps();
|
||||
console.log('timestamps', timestamps);
|
||||
debounceRequest(widget.metricId, { startTime: timestamps.startTimestamp, endTime: timestamps.endTimestamp, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
|
||||
}, [drillDownPeriod]);
|
||||
|
||||
return useObserver(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -71,18 +80,6 @@ function WidgetSessions(props: Props) {
|
|||
{ widget.metricType !== 'table' && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Series</span>
|
||||
{/* <Dropdown
|
||||
// className={stl.dropdown}
|
||||
className="font-medium flex items-center hover:bg-gray-light rounded px-2 py-1"
|
||||
direction="left"
|
||||
options={ seriesOptions }
|
||||
name="change"
|
||||
value={ activeSeries }
|
||||
onChange={ writeOption }
|
||||
id="change-dropdown"
|
||||
// icon={null}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className="ml-2" /> }
|
||||
/> */}
|
||||
<Select
|
||||
options={ seriesOptions }
|
||||
onChange={ writeOption }
|
||||
|
|
@ -102,7 +99,6 @@ function WidgetSessions(props: Props) {
|
|||
</div>
|
||||
}
|
||||
show={filteredSessions.sessions.length === 0}
|
||||
// animatedIcon="no-results"
|
||||
>
|
||||
{filteredSessions.sessions.map((session: any) => (
|
||||
<SessionItem key={ session.sessionId } session={ session } />
|
||||
|
|
@ -112,7 +108,7 @@ function WidgetSessions(props: Props) {
|
|||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
totalPages={Math.ceil(filteredSessions.total / metricStore.sessionsPageSize)}
|
||||
onPageChange={(page) => metricStore.updateKey('sessionsPage', page)}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
|
|
@ -124,12 +120,12 @@ function WidgetSessions(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
const getListSessionsBySeries = (data, seriesId) => {
|
||||
const getListSessionsBySeries = (data: any, seriesId: any) => {
|
||||
const arr: any = { sessions: [], total: 0 };
|
||||
data.forEach(element => {
|
||||
data.forEach((element: any) => {
|
||||
if (seriesId === 'all') {
|
||||
const sessionIds = arr.sessions.map(i => i.sessionId);
|
||||
arr.sessions.push(...element.sessions.filter(i => !sessionIds.includes(i.sessionId)));
|
||||
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
|
||||
arr.sessions.push(...element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId)));
|
||||
arr.total = element.total
|
||||
} else {
|
||||
if (element.seriesId === seriesId) {
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ interface Props {
|
|||
}
|
||||
function WidgetView(props: Props) {
|
||||
const { match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { metricStore } = useStore();
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const widget = useObserver(() => metricStore.instance);
|
||||
const loading = useObserver(() => metricStore.isLoading);
|
||||
const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (metricId && metricId !== 'create') {
|
||||
metricStore.fetch(metricId);
|
||||
metricStore.fetch(metricId, dashboardStore.period);
|
||||
} else if (metricId === 'create') {
|
||||
metricStore.init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,27 +8,41 @@ import { useObserver } from 'mobx-react-lite';
|
|||
|
||||
interface Props {
|
||||
metric: Widget;
|
||||
isWidget?: boolean
|
||||
}
|
||||
function FunnelWidget(props: Props) {
|
||||
const { metric } = props;
|
||||
const { metric, isWidget = false } = props;
|
||||
const funnel = metric.data.funnel || { stages: [] };
|
||||
// pic firt and last from array
|
||||
const totalSteps = funnel.stages.length;
|
||||
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
|
||||
const hasMoreSteps = funnel.stages.length > 2;
|
||||
const lastStage = funnel.stages[funnel.stages.length - 1];
|
||||
|
||||
return useObserver(() => (
|
||||
<>
|
||||
<div className="w-full">
|
||||
{funnel.stages.map((filter: any, index: any) => (
|
||||
<div key={index} className={cn("flex items-start mb-4", stl.step, { [stl['step-disabled']] : !filter.isActive })}>
|
||||
<div className="z-10 w-6 h-6 border mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
|
||||
{index + 1}
|
||||
</div>
|
||||
<Funnelbar key={index} filter={filter} />
|
||||
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
|
||||
<button onClick={() => filter.updateKey('isActive', !filter.isActive)}>
|
||||
<Icon name="eye-slash-fill" color={filter.isActive ? "gray-light" : "gray-darkest"} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ !isWidget && (
|
||||
stages.map((filter: any, index: any) => (
|
||||
<Stage key={index} index={index + 1} isWidget={isWidget} stage={filter} />
|
||||
))
|
||||
)}
|
||||
|
||||
{ isWidget && (
|
||||
<>
|
||||
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
|
||||
|
||||
{ hasMoreSteps && (
|
||||
<>
|
||||
<EmptyStage total={totalSteps} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{funnel.stages.length > 1 && (
|
||||
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center pb-4">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -59,4 +73,45 @@ function FunnelWidget(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
function EmptyStage({ total }: any) {
|
||||
return useObserver( () => (
|
||||
<div className={cn("flex items-center mb-4 pb-3", stl.step)}>
|
||||
<IndexNumber index={0} />
|
||||
<div className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal" style={{ width: '100px'}}>+{total} steps</div>
|
||||
<div className="border-b w-full border-dotted"></div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
function Stage({ stage, index, isWidget }: any) {
|
||||
return useObserver( () => (
|
||||
<div className={cn("flex items-start", stl.step, { [stl['step-disabled']] : !stage.isActive })}>
|
||||
<IndexNumber index={index } />
|
||||
<Funnelbar filter={stage} />
|
||||
{!isWidget && (
|
||||
<BarActions bar={stage} />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
function IndexNumber({ index }: any) {
|
||||
return (
|
||||
<div className="z-10 w-6 h-6 border mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
|
||||
{index}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function BarActions({ bar }: any) {
|
||||
return useObserver(() => (
|
||||
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
|
||||
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
|
||||
<Icon name="eye-slash-fill" color={bar.isActive ? "gray-light" : "gray-darkest"} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
export default FunnelWidget;
|
||||
|
|
@ -20,6 +20,7 @@ export interface IDashboardSotre {
|
|||
endTimestamp: number
|
||||
period: Period
|
||||
drillDownFilter: IFilter
|
||||
drillDownPeriod: Period
|
||||
|
||||
siteId: any
|
||||
currentWidget: Widget
|
||||
|
|
@ -84,6 +85,7 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
widgets: Widget[] = []
|
||||
period: Period = Period({ rangeName: LAST_30_DAYS })
|
||||
drillDownFilter: Filter = new Filter()
|
||||
drillDownPeriod: Period = Period({ rangeName: LAST_30_DAYS });
|
||||
startTimestamp: number = 0
|
||||
endTimestamp: number = 0
|
||||
|
||||
|
|
@ -107,6 +109,7 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
drillDownFilter: observable.ref,
|
||||
widgetCategories: observable.ref,
|
||||
selectedDashboard: observable.ref,
|
||||
drillDownPeriod: observable,
|
||||
resetCurrentWidget: action,
|
||||
addDashboard: action,
|
||||
removeDashboard: action,
|
||||
|
|
@ -130,13 +133,15 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
fetchTemplates: action,
|
||||
updatePinned: action,
|
||||
setPeriod: action,
|
||||
setDrillDownPeriod: action,
|
||||
|
||||
fetchMetricChartData: action
|
||||
})
|
||||
|
||||
const drillDownPeriod = Period({ rangeName: LAST_30_DAYS }).toTimestamps();
|
||||
this.drillDownFilter.updateKey('startTimestamp', drillDownPeriod.startTimestamp)
|
||||
this.drillDownFilter.updateKey('endTimestamp', drillDownPeriod.endTimestamp)
|
||||
this.drillDownPeriod = Period({ rangeName: LAST_24_HOURS });
|
||||
const timeStamps = this.drillDownPeriod.toTimestamps();
|
||||
this.drillDownFilter.updateKey('startTimestamp', timeStamps.startTimestamp)
|
||||
this.drillDownFilter.updateKey('endTimestamp', timeStamps.endTimestamp)
|
||||
}
|
||||
|
||||
toggleAllSelectedWidgets(isSelected: boolean) {
|
||||
|
|
@ -434,6 +439,10 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
this.period = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
|
||||
}
|
||||
|
||||
setDrillDownPeriod(period: any) {
|
||||
this.drillDownPeriod = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
|
||||
}
|
||||
|
||||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
const period = this.period.toTimestamps()
|
||||
const params = { ...period, ...data, key: metric.predefinedKey }
|
||||
|
|
|
|||
|
|
@ -78,14 +78,6 @@ export default class MetricStore implements IMetricStore {
|
|||
|
||||
paginatedList: computed,
|
||||
})
|
||||
|
||||
// reaction(
|
||||
// () => this.metricsSearch,
|
||||
// (metricsSearch) => { // TODO filter the list for View
|
||||
// this.page = 1
|
||||
// this.paginatedList
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
||||
// State Actions
|
||||
|
|
@ -172,11 +164,14 @@ export default class MetricStore implements IMetricStore {
|
|||
})
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
fetch(id: string, period?: any) {
|
||||
this.isLoading = true
|
||||
return metricService.getMetric(id)
|
||||
.then((metric: any) => {
|
||||
return this.instance = new Widget().fromJson(metric)
|
||||
// if (period) {
|
||||
// metric.period = period
|
||||
// }
|
||||
return this.instance = new Widget().fromJson(metric, period)
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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';
|
||||
import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period';
|
||||
|
||||
export interface IWidget {
|
||||
metricId: any
|
||||
|
|
@ -40,6 +41,8 @@ export interface IWidget {
|
|||
|
||||
params: any
|
||||
|
||||
period: any
|
||||
|
||||
updateKey(key: string, value: any): void
|
||||
removeSeries(index: number): void
|
||||
addSeries(): void
|
||||
|
|
@ -52,6 +55,7 @@ export interface IWidget {
|
|||
toWidget(): any
|
||||
setData(data: any): void
|
||||
fetchSessions(metricId: any, filter: any): Promise<any>
|
||||
setPeriod(period: Period): void
|
||||
}
|
||||
export default class Widget implements IWidget {
|
||||
public static get ID_KEY():string { return "metricId" }
|
||||
|
|
@ -75,6 +79,8 @@ export default class Widget implements IWidget {
|
|||
page: number = 1
|
||||
limit: number = 5
|
||||
params: any = { density: 70 }
|
||||
|
||||
period: any = Period({ rangeName: LAST_24_HOURS }) // temp value in detail view
|
||||
|
||||
sessionsLoading: boolean = false
|
||||
|
||||
|
|
@ -117,6 +123,7 @@ export default class Widget implements IWidget {
|
|||
validate: action,
|
||||
update: action,
|
||||
updateKey: action,
|
||||
setPeriod: action,
|
||||
})
|
||||
|
||||
const filterSeries = new FilterSeries()
|
||||
|
|
@ -137,7 +144,7 @@ export default class Widget implements IWidget {
|
|||
this.series.push(series)
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
fromJson(json: any, period?: any) {
|
||||
json.config = json.config || {}
|
||||
runInAction(() => {
|
||||
this.metricId = json.metricId
|
||||
|
|
@ -155,10 +162,18 @@ export default class Widget implements IWidget {
|
|||
this.config = json.config
|
||||
this.position = json.config.position
|
||||
this.predefinedKey = json.predefinedKey
|
||||
|
||||
if (period) {
|
||||
this.period = period
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
setPeriod(period: any) {
|
||||
this.period = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
|
||||
}
|
||||
|
||||
toWidget(): any {
|
||||
return {
|
||||
config: {
|
||||
|
|
@ -186,7 +201,7 @@ export default class Widget implements IWidget {
|
|||
series: this.series.map((series: any) => series.toJson()),
|
||||
config: {
|
||||
...this.config,
|
||||
col: this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS ? 4 : this.config.col
|
||||
col: this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS || this.metricOf === FilterKey.SESSIONS ? 4 : this.config.col
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,14 @@ export default Record({
|
|||
end: range.end.unix() * 1000,
|
||||
}
|
||||
},
|
||||
// fromFilter: filter => {
|
||||
// const range = getRange(filter.rangeName);
|
||||
// return {
|
||||
// start: range.start.unix() * 1000,
|
||||
// end: range.end.unix() * 1000,
|
||||
// rangeName: filter.rangeName,
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
toJSON() {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue