ui: some changes for card creation flow, add series table to CustomMetricLineChart.tsx

This commit is contained in:
nick-delirium 2024-11-25 17:44:49 +01:00
parent e5267497a6
commit 170e85b505
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
20 changed files with 501 additions and 272 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,7 +58,9 @@ function WidgetView(props: Props) {
}
});
} else {
metricStore.init();
if (!metricStore.instance) {
metricStore.init();
}
}
}, []);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -125,6 +125,7 @@ export default class MetricStore {
}
}
console.log('ch', obj)
Object.assign(this.instance, obj);
this.instance.updateKey('hasChanged', updateChangeFlag);
}

View file

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

View file

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