ui: some changes for card creation flow, add series table to CustomMetricLineChart.tsx
This commit is contained in:
parent
e5267497a6
commit
170e85b505
20 changed files with 501 additions and 272 deletions
|
|
@ -1,74 +1,224 @@
|
|||
import React from 'react'
|
||||
import {Styles} from '../../common';
|
||||
import {ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts';
|
||||
import {LineChart, Line, Legend} from 'recharts';
|
||||
import React, { useState } from 'react';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { Button, Table } from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
LineChart,
|
||||
Line,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
params: any;
|
||||
// seriesMap: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
data: any;
|
||||
params: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
}
|
||||
|
||||
const initTableProps = [{
|
||||
title: 'Series',
|
||||
dataIndex: 'seriesName',
|
||||
key: 'seriesName',
|
||||
},
|
||||
{
|
||||
title: 'Avg.',
|
||||
dataIndex: 'average',
|
||||
key: 'average',
|
||||
}
|
||||
]
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
const {
|
||||
data = {chart: [], namesMap: []},
|
||||
params,
|
||||
colors,
|
||||
onClick = () => null,
|
||||
yaxis = {...Styles.yaxis},
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
} = props;
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
params,
|
||||
colors,
|
||||
onClick = () => null,
|
||||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
} = props;
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const hasMultipleSeries = data.namesMap.length > 1;
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const [tableProps, setTableProps] = useState<TableProps['columns']>(initTableProps);
|
||||
// console.log(params.density / 7, data.chart)
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<LineChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||
onClick={onClick}
|
||||
// isAnimationActive={ false }
|
||||
const columnNames = new Set();
|
||||
/**
|
||||
* basically we have an array of
|
||||
* { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
|
||||
* which we turn into a table where each series of filters = row;
|
||||
* and each unique time = column
|
||||
* + average for each row
|
||||
* [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
|
||||
* */
|
||||
React.useEffect(()=> {
|
||||
setTableProps(initTableProps)
|
||||
const series = Object.keys(data.chart[0])
|
||||
.filter((key) => key !== 'time' && key !== 'timestamp')
|
||||
columnNames.clear()
|
||||
data.chart.forEach((p: any) => {
|
||||
columnNames.add(p.time)
|
||||
}) // for example: mon, tue, wed, thu, fri, sat, sun
|
||||
const avg: any = {} // { seriesName: {itemsCount: 0, total: 0} }
|
||||
const items: Record<string, any>[] = []; // as many items (rows) as we have series in filter
|
||||
series.forEach(s => {
|
||||
items.push({ seriesName: s, average: 0 })
|
||||
avg[s] = { itemsCount: 0, total: 0 }
|
||||
})
|
||||
const tableCols: { title: string, dataIndex: string, key: string }[] = [];
|
||||
Array.from(columnNames).forEach((name: string) => {
|
||||
tableCols.push({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
key: name,
|
||||
})
|
||||
const values = data.chart.filter((p) => p.time === name)
|
||||
series.forEach((s) => {
|
||||
const toDateAvg = values.reduce((acc, curr) => acc + curr[s], 0) / values.length;
|
||||
avg[s].itemsCount += 1
|
||||
avg[s].total += toDateAvg
|
||||
const ind = items.findIndex((item) => item.seriesName === s)
|
||||
if (ind === -1) return
|
||||
items[ind][name] = (values.reduce((acc, curr) => acc + curr[s], 0) / values.length)
|
||||
.toFixed(2)
|
||||
})
|
||||
})
|
||||
Object.keys(avg).forEach((key) => {
|
||||
const ind = items.findIndex((item) => item.seriesName === key)
|
||||
if (ind === -1) return
|
||||
items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2)
|
||||
})
|
||||
|
||||
setTableProps((prev) => [...prev, ...tableCols])
|
||||
setTableData(items)
|
||||
}, [data.chart.length])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<LineChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density / 7}
|
||||
/>
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || 'Number of Sessions',
|
||||
}}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
|
||||
{Array.isArray(data.namesMap) &&
|
||||
data.namesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={key === 'Total' ? 0 : 0.6}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
activeDot={{
|
||||
fill: key === 'Total' ? 'transparent' : colors[index],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{hasMultipleSeries ? (
|
||||
<div className={'relative -mx-4 px-4'}>
|
||||
<div
|
||||
className={
|
||||
'absolute left-0 right-0 top-0 border-t border-t-gray-lighter'
|
||||
}
|
||||
/>
|
||||
<div className={'absolute top-0 left-1/2 z-10'} style={{ transform: 'translate(-50%, -50%)' }}>
|
||||
<Button
|
||||
icon={showTable ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
size={'small'}
|
||||
type={'default'}
|
||||
onClick={() => setShowTable(!showTable)}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density / 7}
|
||||
/>
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || "Number of Sessions"
|
||||
}}
|
||||
/>
|
||||
{!hideLegend && <Legend />}
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={key === 'Total' ? 0 : 0.6}
|
||||
// fill="url(#colorCount)"
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
{showTable ? 'Hide Table' : 'Show Table'}
|
||||
</Button>
|
||||
</div>
|
||||
{showTable ? (
|
||||
<Table
|
||||
columns={tableProps}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size={'small'}
|
||||
className={'py-6'}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricLineChart
|
||||
function CustomTooltip({ active, payload, label }) {
|
||||
if (!active) return;
|
||||
|
||||
const shownPayloads = payload.filter((p) => !p.hide);
|
||||
return (
|
||||
<div className={'flex flex-col gap-1 bg-white shadow border rounded p-2'}>
|
||||
{shownPayloads.map((p, index) => (
|
||||
<>
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<div
|
||||
style={{ borderRadius: 99, background: p.color }}
|
||||
className={'h-5 w-5 flex items-center justify-center'}
|
||||
>
|
||||
<div className={'invert text-sm'}>{index + 1}</div>
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.name}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ borderLeft: `2px solid ${p.color}` }}
|
||||
className={'flex flex-col py-2 px-2 ml-2'}
|
||||
>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
{label}, {formatTimeOrDate(p.payload.timestamp)}
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.value}</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricLineChart;
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ export default {
|
|||
axisLine: {stroke: '#CCCCCC'},
|
||||
interval: 0,
|
||||
dataKey: "time",
|
||||
tick: {fill: '#999999', fontSize: 9},
|
||||
tick: {fill: '#000000', fontSize: 9},
|
||||
tickLine: {stroke: '#CCCCCC'},
|
||||
strokeWidth: 0.5
|
||||
},
|
||||
yaxis: {
|
||||
axisLine: {stroke: '#CCCCCC'},
|
||||
tick: {fill: '#999999', fontSize: 9},
|
||||
tick: {fill: '#000000', fontSize: 9},
|
||||
tickLine: {stroke: '#CCCCCC'},
|
||||
},
|
||||
axisLabelLeft: {
|
||||
|
|
@ -50,9 +50,6 @@ export default {
|
|||
tickFormatterBytes: val => Math.round(val / 1024 / 1024),
|
||||
chartMargins: {left: 0, right: 20, top: 10, bottom: 5},
|
||||
tooltip: {
|
||||
cursor: {
|
||||
fill: '#f6f6f6'
|
||||
},
|
||||
contentStyle: {
|
||||
padding: '5px',
|
||||
background: 'white',
|
||||
|
|
|
|||
|
|
@ -11,41 +11,59 @@ import {
|
|||
AppWindow,
|
||||
Combine,
|
||||
Users,
|
||||
ArrowDown10,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { Icon } from 'UI';
|
||||
import FilterSeries from "App/mstore/types/filterSeries";
|
||||
import { CARD_LIST, CardType } from "../DashboardList/NewDashModal/ExampleCards";
|
||||
import { useStore } from 'App/mstore';
|
||||
import {
|
||||
HEATMAP,
|
||||
FUNNEL,
|
||||
TABLE,
|
||||
TIMESERIES,
|
||||
USER_PATH,
|
||||
} from 'App/constants/card';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { dashboardMetricCreate, withSiteId, metricCreate } from 'App/routes'
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
|
||||
interface TabItem {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
const tabItems: Record<string, TabItem[]> = {
|
||||
product_analytics: [
|
||||
{
|
||||
icon: <LineChart width={16} />,
|
||||
title: 'Trends',
|
||||
type: TIMESERIES,
|
||||
description: 'Track session trends over time.',
|
||||
},
|
||||
{
|
||||
icon: <AlignStartVertical width={16} />,
|
||||
title: 'Funnel',
|
||||
title: 'Funnels',
|
||||
type: FUNNEL,
|
||||
description: 'Visualize user progression through critical steps.',
|
||||
},
|
||||
{
|
||||
icon: <Icon name={'dashboards/user-journey'} color={'inherit'} size={16} />,
|
||||
title: 'Journeys',
|
||||
type: USER_PATH,
|
||||
description: 'Understand the paths users take through your product.',
|
||||
},
|
||||
{
|
||||
icon: <Icon name={'dashboards/cohort-chart'} color={'inherit'} size={16} />,
|
||||
title: 'Retention',
|
||||
description: 'Analyze user retention over specific time periods.',
|
||||
},
|
||||
// { TODO: 1.23+
|
||||
// icon: <Icon name={'dashboards/cohort-chart'} color={'inherit'} size={16} />,
|
||||
// title: 'Retention',
|
||||
// type: RETENTION,
|
||||
// description: 'Analyze user retention over specific time periods.',
|
||||
// },
|
||||
{
|
||||
icon: <Icon name={'dashboards/heatmap-2'} color={'inherit'} size={16} />,
|
||||
title: 'Heatmaps',
|
||||
type: HEATMAP,
|
||||
description: 'Generate a report using by asking AI.',
|
||||
},
|
||||
],
|
||||
|
|
@ -53,21 +71,25 @@ const tabItems: Record<string, TabItem[]> = {
|
|||
{
|
||||
icon: <Icon name={'dashboards/circle-alert'} color={'inherit'} size={16} />,
|
||||
title: 'JS Errors',
|
||||
type: FilterKey.ERRORS,
|
||||
description: 'Monitor JS errors affecting user experience.',
|
||||
},
|
||||
{
|
||||
icon: <ArrowUpDown width={16} />,
|
||||
title: 'Top Network Requests',
|
||||
type: FilterKey.FETCH,
|
||||
description: 'Identify the most frequent network requests.',
|
||||
},
|
||||
{
|
||||
icon: <WifiOff width={16} />,
|
||||
title: '4xx/5xx Requests',
|
||||
type: TIMESERIES + '_4xx_requests',
|
||||
description: 'Track client and server errors for performance issues.',
|
||||
},
|
||||
{
|
||||
icon: <Turtle width={16} />,
|
||||
title: 'Slow Network Requests',
|
||||
type: TIMESERIES + '_slow_network_requests',
|
||||
description: 'Pinpoint the slowest network requests causing delays.',
|
||||
},
|
||||
],
|
||||
|
|
@ -75,37 +97,83 @@ const tabItems: Record<string, TabItem[]> = {
|
|||
{
|
||||
icon: <FileStack width={16} />,
|
||||
title: 'Top Pages',
|
||||
type: FilterKey.LOCATION,
|
||||
description: 'Discover the most visited pages on your site.',
|
||||
},
|
||||
{
|
||||
icon: <AppWindow width={16} />,
|
||||
title: 'Top Browsers',
|
||||
type: FilterKey.USER_BROWSER,
|
||||
description: 'Analyze the browsers your visitors are using the most.',
|
||||
},
|
||||
{
|
||||
icon: <Combine width={16} />,
|
||||
title: 'Top Referrer',
|
||||
type: FilterKey.REFERRER,
|
||||
description: 'See where your traffic is coming from.',
|
||||
},
|
||||
{
|
||||
icon: <Users width={16} />,
|
||||
title: 'Top Users',
|
||||
type: FilterKey.USERID,
|
||||
description: 'Identify the users with the most interactions.',
|
||||
},
|
||||
{
|
||||
icon: <ArrowDown10 width={16} />,
|
||||
title: 'Speed Index by Country',
|
||||
description: 'Measure performance across different regions.',
|
||||
},
|
||||
// { TODO: 1.23+ maybe
|
||||
// icon: <ArrowDown10 width={16} />,
|
||||
// title: 'Speed Index by Country',
|
||||
// type: TABLE,
|
||||
// description: 'Measure performance across different regions.',
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
function CategoryTab({ tab }: { tab: string }) {
|
||||
function CategoryTab({ tab, inCards }: { tab: string, inCards?: boolean }) {
|
||||
const items = tabItems[tab];
|
||||
const { metricStore, projectsStore, dashboardStore } = useStore();
|
||||
const history = useHistory();
|
||||
|
||||
const handleCardSelection = (card: string) => {
|
||||
metricStore.init();
|
||||
const selectedCard = CARD_LIST.find((c) => c.key === card) as CardType;
|
||||
|
||||
const cardData: any = {
|
||||
metricType: selectedCard.cardType,
|
||||
name: selectedCard.title,
|
||||
metricOf: selectedCard.metricOf
|
||||
};
|
||||
|
||||
if (selectedCard.filters) {
|
||||
cardData.series = [
|
||||
new FilterSeries().fromJson({
|
||||
name: "Series 1",
|
||||
filter: {
|
||||
filters: selectedCard.filters,
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
if (selectedCard.cardType === FUNNEL) {
|
||||
cardData.series = [];
|
||||
cardData.series.filter = [];
|
||||
}
|
||||
|
||||
metricStore.merge(cardData);
|
||||
metricStore.instance.resetDefaults();
|
||||
|
||||
if (projectsStore.activeSiteId) {
|
||||
if (inCards) {
|
||||
history.push(withSiteId(metricCreate(), projectsStore.activeSiteId));
|
||||
} else if (dashboardStore.selectedDashboard) {
|
||||
history.push(withSiteId(dashboardMetricCreate(dashboardStore.selectedDashboard.dashboardId), projectsStore.activeSiteId));
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
onClick={() => handleCardSelection(item.type)}
|
||||
key={index}
|
||||
className={
|
||||
'flex items-start gap-2 p-2 hover:bg-active-blue rounded-xl hover:text-blue group cursor-pointer'
|
||||
|
|
@ -124,7 +192,7 @@ function CategoryTab({ tab }: { tab: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AddCardSection() {
|
||||
function AddCardSection({ inCards }: { inCards?: boolean }) {
|
||||
const [tab, setTab] = React.useState('product_analytics');
|
||||
const options = [
|
||||
{ label: 'Product Analytics', value: 'product_analytics' },
|
||||
|
|
@ -137,9 +205,9 @@ function AddCardSection() {
|
|||
return (
|
||||
<div
|
||||
className={
|
||||
'm-10 py-8 px-8 rounded-xl bg-white border border-gray-lighter flex flex-col gap-4'
|
||||
'py-8 px-8 rounded-xl bg-white border border-gray-lighter flex flex-col gap-4'
|
||||
}
|
||||
style={{ width: 620, height: 430 }}
|
||||
style={{ width: 520, height: 400 }}
|
||||
>
|
||||
<div
|
||||
className={'flex justify-between border-b border-b-gray-lighter p-2'}
|
||||
|
|
@ -161,7 +229,7 @@ function AddCardSection() {
|
|||
onChange={(value) => setTab(value)}
|
||||
/>
|
||||
</div>
|
||||
<CategoryTab tab={tab} />
|
||||
<CategoryTab tab={tab} inCards={inCards} />
|
||||
<div
|
||||
className={
|
||||
'w-full flex items-center justify-center border-t mt-auto border-t-gray-lighter gap-2 pt-2'
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import {
|
|||
TABLE,
|
||||
TIMESERIES,
|
||||
USER_PATH,
|
||||
PERFORMANCE,
|
||||
} from 'App/constants/card';
|
||||
PERFORMANCE, RETENTION
|
||||
} from "App/constants/card";
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { BarChart, TrendingUp, SearchSlash } from 'lucide-react';
|
||||
import ByIssues from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues';
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
<Loader loading={loading}>
|
||||
{
|
||||
list?.length === 0 ? (
|
||||
<div className={'flex-1 flex justify-center items-center'} style={{ minHeight: 620 }}>
|
||||
<div className={'flex-1 flex justify-center items-center pt-10'} style={{ minHeight: 620 }}>
|
||||
<AddCardSection />
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ function FilterSeries(props: Props) {
|
|||
canExclude = false,
|
||||
expandable = false,
|
||||
} = props;
|
||||
const [expanded, setExpanded] = useState(!expandable);
|
||||
const [expanded, setExpanded] = useState(hideHeader || !expandable);
|
||||
const { series, seriesIndex } = props;
|
||||
const [prevLength, setPrevLength] = useState(0);
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ function FilterSeries(props: Props) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{expandable && (
|
||||
{!hideHeader && expandable && (
|
||||
<Space
|
||||
className="justify-between w-full py-2 cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
|
|
@ -212,7 +212,7 @@ function FilterSeries(props: Props) {
|
|||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
mergeUp
|
||||
mergeUp={!hideHeader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,74 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { PageTitle, Toggler, Icon } from "UI";
|
||||
import { PageTitle, Icon } from 'UI';
|
||||
import { Segmented, Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import AddCardSection from '../AddCardSection/AddCardSection';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import Select from 'Shared/Select';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { DROPDOWN_OPTIONS } from 'App/constants/card';
|
||||
import AddCardModal from 'Components/Dashboard/components/AddCardModal';
|
||||
import { useModal } from 'Components/Modal';
|
||||
import AddCardSelectionModal from "Components/Dashboard/components/AddCardSelectionModal";
|
||||
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
|
||||
import { INDEXES } from "App/constants/zindex";
|
||||
|
||||
function MetricViewHeader({ siteId }: { siteId: string }) {
|
||||
const { metricStore } = useStore();
|
||||
const filter = metricStore.filter;
|
||||
const { showModal } = useModal();
|
||||
const [showAddCardModal, setShowAddCardModal] = React.useState(false);
|
||||
function MetricViewHeader() {
|
||||
const { metricStore } = useStore();
|
||||
const filter = metricStore.filter;
|
||||
const [showAddCardModal, setShowAddCardModal] = React.useState(false);
|
||||
const modalBgRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Set the default sort order to 'desc'
|
||||
useEffect(() => {
|
||||
metricStore.updateKey('sort', { by: 'desc' });
|
||||
}, [metricStore]);
|
||||
// Set the default sort order to 'desc'
|
||||
useEffect(() => {
|
||||
metricStore.updateKey('sort', { by: 'desc' });
|
||||
}, [metricStore]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center justify-between px-6'>
|
||||
<div className='flex items-baseline mr-3'>
|
||||
<PageTitle title='Cards' className='' />
|
||||
</div>
|
||||
<div className='ml-auto flex items-center'>
|
||||
<Button type='primary'
|
||||
onClick={() => setShowAddCardModal(true)}
|
||||
icon={<PlusOutlined />}
|
||||
>Create Card</Button>
|
||||
<div className='ml-4 w-1/4' style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Cards" className="" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setShowAddCardModal(true)}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Create Card
|
||||
</Button>
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-y px-6 py-1 mt-2 flex items-center w-full justify-between'>
|
||||
<div className='items-center flex gap-4'>
|
||||
<Select
|
||||
options={[{ label: 'All Types', value: 'all' }, ...DROPDOWN_OPTIONS]}
|
||||
name='type'
|
||||
defaultValue={filter.type}
|
||||
onChange={({ value }) =>
|
||||
metricStore.updateKey('filter', { ...filter, type: value.value })
|
||||
}
|
||||
plain={true}
|
||||
isSearchable={true}
|
||||
/>
|
||||
<div className="border-y px-6 py-1 mt-2 flex items-center w-full justify-between">
|
||||
<div className="items-center flex gap-4">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'All Types', value: 'all' },
|
||||
...DROPDOWN_OPTIONS,
|
||||
]}
|
||||
name="type"
|
||||
defaultValue={filter.type}
|
||||
onChange={({ value }) =>
|
||||
metricStore.updateKey('filter', { ...filter, type: value.value })
|
||||
}
|
||||
plain={true}
|
||||
isSearchable={true}
|
||||
/>
|
||||
|
||||
<DashboardDropdown
|
||||
plain={false}
|
||||
onChange={(value: any) =>
|
||||
metricStore.updateKey('filter', { ...filter, dashboard: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<DashboardDropdown
|
||||
plain={false}
|
||||
onChange={(value: any) =>
|
||||
metricStore.updateKey('filter', { ...filter, dashboard: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-6'>
|
||||
<ListViewToggler />
|
||||
<div className="flex items-center gap-6">
|
||||
<ListViewToggler />
|
||||
|
||||
{/* <Toggler
|
||||
{/* <Toggler
|
||||
label='My Cards'
|
||||
checked={filter.showMine}
|
||||
name='test'
|
||||
|
|
@ -73,70 +77,91 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
|
|||
metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine })
|
||||
}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<NewDashboardModal
|
||||
onClose={() => setShowAddCardModal(false)}
|
||||
open={showAddCardModal}
|
||||
isCreatingNewCard={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{showAddCardModal ? (
|
||||
<div
|
||||
ref={modalBgRef}
|
||||
onClick={(e) => {
|
||||
if (modalBgRef.current === e.target) {
|
||||
setShowAddCardModal(false);
|
||||
}
|
||||
}}
|
||||
className={
|
||||
'fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-gray-lightest'
|
||||
}
|
||||
style={{ background: 'rgba(0,0,0,0.5)', zIndex: INDEXES.POPUP_GUIDE_BG }}
|
||||
>
|
||||
<AddCardSection inCards />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(MetricViewHeader);
|
||||
|
||||
function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) {
|
||||
const { dashboardStore, metricStore } = useStore();
|
||||
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
|
||||
key: i.id,
|
||||
label: i.name,
|
||||
value: i.dashboardId
|
||||
}));
|
||||
function DashboardDropdown({
|
||||
onChange,
|
||||
plain = false,
|
||||
}: {
|
||||
plain?: boolean;
|
||||
onChange: any;
|
||||
}) {
|
||||
const { dashboardStore, metricStore } = useStore();
|
||||
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
|
||||
key: i.id,
|
||||
label: i.name,
|
||||
value: i.dashboardId,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
isSearchable={true}
|
||||
placeholder='Filter by Dashboard'
|
||||
plain={plain}
|
||||
options={dashboardOptions}
|
||||
value={metricStore.filter.dashboard}
|
||||
onChange={({ value }: any) => onChange(value)}
|
||||
isMulti={true}
|
||||
color='black'
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
isSearchable={true}
|
||||
placeholder="Filter by Dashboard"
|
||||
plain={plain}
|
||||
options={dashboardOptions}
|
||||
value={metricStore.filter.dashboard}
|
||||
onChange={({ value }: any) => onChange(value)}
|
||||
isMulti={true}
|
||||
color="black"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ListViewToggler() {
|
||||
const { metricStore } = useStore();
|
||||
const listView = useObserver(() => metricStore.listView);
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<Segmented
|
||||
size='small'
|
||||
options={[
|
||||
{
|
||||
label: <div className={'flex items-center gap-2'}>
|
||||
<Icon name={'list-alt'} color={'inherit'} />
|
||||
<div>List</div>
|
||||
</div>,
|
||||
value: 'list'
|
||||
},
|
||||
{
|
||||
label: <div className={'flex items-center gap-2'}>
|
||||
<Icon name={'grid'} color={'inherit'} />
|
||||
<div>Grid</div>
|
||||
</div>,
|
||||
value: 'grid'
|
||||
}
|
||||
]}
|
||||
onChange={(val) => {
|
||||
metricStore.updateKey('listView', val === 'list')
|
||||
}}
|
||||
value={listView ? 'list' : 'grid'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const { metricStore } = useStore();
|
||||
const listView = useObserver(() => metricStore.listView);
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Segmented
|
||||
size="small"
|
||||
options={[
|
||||
{
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Icon name={'list-alt'} color={'inherit'} />
|
||||
<div>List</div>
|
||||
</div>
|
||||
),
|
||||
value: 'list',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Icon name={'grid'} color={'inherit'} />
|
||||
<div>Grid</div>
|
||||
</div>
|
||||
),
|
||||
value: 'grid',
|
||||
},
|
||||
]}
|
||||
onChange={(val) => {
|
||||
metricStore.updateKey('listView', val === 'list');
|
||||
}}
|
||||
value={listView ? 'list' : 'grid'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ function WidgetChart(props: Props) {
|
|||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
|
||||
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
|
|||
metric.updateKey('hasChanged', true)
|
||||
}
|
||||
|
||||
console.log(metric.series, isTable, isClickMap, isInsights, isPathAnalysis, isFunnel)
|
||||
return (
|
||||
<>
|
||||
{metric.series.length > 0 &&
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ function WidgetView(props: Props) {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
metricStore.init();
|
||||
if (!metricStore.instance) {
|
||||
metricStore.init();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import NoSessionsMessage from 'Shared/NoSessionsMessage';
|
||||
import MainSearchBar from 'Shared/MainSearchBar';
|
||||
|
|
@ -12,6 +12,7 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
|||
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from '@/mstore';
|
||||
import NotesRoute from "../shared/SessionsTabOverview/components/Notes/NotesRoute";
|
||||
|
||||
// @ts-ignore
|
||||
interface IProps extends RouteComponentProps {
|
||||
|
|
@ -34,8 +35,11 @@ function Overview({ match: { params } }: IProps) {
|
|||
}, [tab]);
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact strict
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(bookmarks(), siteId)]}
|
||||
>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NoSessionsMessage siteId={siteId} />
|
||||
<SearchActions />
|
||||
|
|
@ -44,6 +48,9 @@ function Overview({ match: { params } }: IProps) {
|
|||
<SessionsTabOverview />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(notes(), siteId)}>
|
||||
<NotesRoute />
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(fflags(), siteId)}>
|
||||
<FFlagsList siteId={siteId} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const FilterList = observer((props: Props) => {
|
|||
borderTopRightRadius: props.mergeUp ? 0 : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={'flex items-center gap-2 mb-2'}>
|
||||
<div className={'flex items-center mb-2'} style={{ gap: '0.65rem' }}>
|
||||
<div className="font-semibold">Filters</div>
|
||||
<FilterSelection mode={'filters'} filter={undefined} onFilterClick={onAddFilter}>
|
||||
<Button icon={<Filter size={16} strokeWidth={1} />} type="default" size={'small'}>
|
||||
|
|
|
|||
|
|
@ -145,6 +145,17 @@ interface Props {
|
|||
mode: 'filters' | 'events';
|
||||
}
|
||||
|
||||
export const getNewIcon = (filter: Record<string, any>) => {
|
||||
if (filter.icon?.includes('metadata')) {
|
||||
return IconMap[FilterKey.METADATA];
|
||||
}
|
||||
// @ts-ignore
|
||||
if (IconMap[filter.key]) {
|
||||
// @ts-ignore
|
||||
return IconMap[filter.key];
|
||||
} else return <Icon name={filter.icon} size={16} />;
|
||||
};
|
||||
|
||||
function FilterModal(props: Props) {
|
||||
const {
|
||||
isLive,
|
||||
|
|
@ -195,17 +206,6 @@ function FilterModal(props: Props) {
|
|||
matchingCategories.length === 0 &&
|
||||
Object.keys(matchingFilters).length === 0;
|
||||
|
||||
const getNewIcon = (filter: Record<string, any>) => {
|
||||
if (filter.icon?.includes('metadata')) {
|
||||
return IconMap[FilterKey.METADATA];
|
||||
}
|
||||
// @ts-ignore
|
||||
if (IconMap[filter.key]) {
|
||||
// @ts-ignore
|
||||
return IconMap[filter.key];
|
||||
} else return <Icon name={filter.icon} size={16} />;
|
||||
};
|
||||
|
||||
const displayedFilters =
|
||||
category === 'ALL'
|
||||
? Object.entries(matchingFilters).flatMap(([category, filters]) =>
|
||||
|
|
@ -262,39 +262,6 @@ function FilterModal(props: Props) {
|
|||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{/*<div*/}
|
||||
{/* className={searchQuery && !isResultEmpty ? "mb-6" : ""}*/}
|
||||
{/* style={{ columns: matchingCategories.length > 1 ? "auto 200px" : 1 }}*/}
|
||||
{/*>*/}
|
||||
{/* {matchingCategories.map((key) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <div*/}
|
||||
{/* className="mb-6 flex flex-col gap-2 break-inside-avoid"*/}
|
||||
{/* key={key}*/}
|
||||
{/* >*/}
|
||||
{/* <div className="uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm">*/}
|
||||
{/* {key}*/}
|
||||
{/* </div>*/}
|
||||
{/* <div>*/}
|
||||
{/* {matchingFilters[key] &&*/}
|
||||
{/* matchingFilters[key].map((filter: Record<string, any>) => (*/}
|
||||
{/* <div*/}
|
||||
{/* key={filter.label}*/}
|
||||
{/* className={cn(*/}
|
||||
{/* stl.optionItem,*/}
|
||||
{/* 'flex items-center py-2 cursor-pointer -mx-2 px-2 gap-2 rounded-lg hover:shadow-sm'*/}
|
||||
{/* )}*/}
|
||||
{/* onClick={() => onFilterClick({ ...filter, value: [''] })}*/}
|
||||
{/* >*/}
|
||||
{/* {getNewIcon(filter)}*/}
|
||||
{/* <span>{filter.label}</span>*/}
|
||||
{/* </div>*/}
|
||||
{/* ))}*/}
|
||||
{/* </div>*/}
|
||||
{/* </div>*/}
|
||||
{/* );*/}
|
||||
{/* })}*/}
|
||||
{/*</div>*/}
|
||||
{showSearchList && (
|
||||
<Loader loading={fetchingFilterSearchList}>
|
||||
<div className="-mx-6 px-6">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
|||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { getNewIcon } from "../FilterModal/FilterModal";
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ function FilterSelection(props: Props) {
|
|||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg py-1 px-2 flex items-center cursor-pointer bg-white border border-gray-light text-ellipsis',
|
||||
'rounded-lg py-1 px-2 flex items-center gap-1 cursor-pointer bg-white border border-gray-light text-ellipsis',
|
||||
{ 'opacity-50 pointer-events-none': disabled }
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -67,6 +68,9 @@ function FilterSelection(props: Props) {
|
|||
}}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<div>
|
||||
{getNewIcon(filter)}
|
||||
</div>
|
||||
<div
|
||||
className="overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate"
|
||||
style={{ textOverflow: 'ellipsis' }}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||
import { useStore } from 'App/mstore';
|
||||
|
||||
import LatestSessionsMessage from './components/LatestSessionsMessage';
|
||||
import NotesList from './components/Notes/NoteList';
|
||||
import SessionHeader from './components/SessionHeader';
|
||||
import SessionList from './components/SessionList';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -12,7 +11,6 @@ function SessionsTabOverview() {
|
|||
const [query, setQuery] = React.useState('');
|
||||
const { aiFiltersStore, searchStore } = useStore();
|
||||
const appliedFilter = searchStore.instance;
|
||||
const isNotesRoute = searchStore.activeTab.type === 'notes';
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
|
@ -38,11 +36,7 @@ function SessionsTabOverview() {
|
|||
<SessionHeader />
|
||||
<div className="border-b" />
|
||||
<LatestSessionsMessage />
|
||||
{!isNotesRoute ? (
|
||||
<SessionList />
|
||||
) : (
|
||||
<NotesList />
|
||||
)}
|
||||
<SessionList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import NotesList from "./NoteList";
|
||||
import NoteTags from "./NoteTags";
|
||||
|
||||
function NotesRoute() {
|
||||
return (
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className={"widget-wrapper"}>
|
||||
<div className="flex items-center px-4 py-2 justify-between w-full border-b">
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<h2 className="text-2xl capitalize mr-4">Notes</h2>
|
||||
<NoteTags />
|
||||
</div>
|
||||
</div>
|
||||
<NotesList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotesRoute
|
||||
|
|
@ -17,9 +17,6 @@ function SessionHeader() {
|
|||
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (activeTab.type === 'notes') {
|
||||
return 'Notes';
|
||||
}
|
||||
if (activeTab.type === 'bookmarks') {
|
||||
return isEnterprise ? 'Vault' : 'Bookmarks';
|
||||
}
|
||||
|
|
@ -35,7 +32,6 @@ function SessionHeader() {
|
|||
return (
|
||||
<div className="flex items-center px-4 py-1 justify-between w-full">
|
||||
<h2 className="text-2xl capitalize mr-4">{title}</h2>
|
||||
{activeTab.type !== 'notes' ? (
|
||||
<div className="flex items-center w-full justify-end">
|
||||
{activeTab.type !== 'bookmarks' && (
|
||||
<>
|
||||
|
|
@ -48,13 +44,6 @@ function SessionHeader() {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeTab.type === 'notes' && (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<NoteTags />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export default class MetricStore {
|
|||
}
|
||||
}
|
||||
|
||||
console.log('ch', obj)
|
||||
Object.assign(this.instance, obj);
|
||||
this.instance.updateKey('hasChanged', updateChangeFlag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ export default class ProjectsStore {
|
|||
return this.active ? ['ios', 'android'].includes(this.active.platform) : false;
|
||||
}
|
||||
|
||||
get activeSiteId() {
|
||||
return this.active?.id || this.siteId;
|
||||
}
|
||||
|
||||
getSiteId = () => {
|
||||
return {
|
||||
siteId: this.siteId,
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export default class Widget {
|
|||
return unique;
|
||||
}, []);
|
||||
} else {
|
||||
const updatedData: any = this.calculateTotalSeries(data);
|
||||
const updatedData: any = data; // we don't use total anymore this.calculateTotalSeries(data);
|
||||
_data['chart'] = getChartFormatter(period)(updatedData);
|
||||
_data['namesMap'] = Array.isArray(updatedData)
|
||||
? updatedData
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue