diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx index 05a6acd4..916c1af6 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx @@ -187,9 +187,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) { setMetric(index); }} > - }> - - +
{selectedMetric.events[0]?.displayName}
- }> - - + diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx index 762d0c81..a35b92ce 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx @@ -2,6 +2,7 @@ import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import ServerLiveCounter from '@/components/overview/live-counter'; import { OverviewFilters } from '@/components/overview/overview-filters'; import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons'; +import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram'; import { OverviewShare } from '@/components/overview/overview-share'; import OverviewTopDevices from '@/components/overview/overview-top-devices'; import OverviewTopEvents from '@/components/overview/overview-top-events'; @@ -42,19 +43,22 @@ export default async function Page({ return ( - -
- - -
-
- - + +
+
+ + +
+
+ + +
+
-
- +
+
diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/reports/report-editor.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/reports/report-editor.tsx index 8e600c7f..0304b616 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/reports/report-editor.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/reports/report-editor.tsx @@ -1,9 +1,8 @@ 'use client'; -import { Suspense, useEffect } from 'react'; +import { useEffect } from 'react'; import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; import { ReportChartType } from '@/components/report/ReportChartType'; import { ReportInterval } from '@/components/report/ReportInterval'; import { ReportLineType } from '@/components/report/ReportLineType'; @@ -18,6 +17,7 @@ import { import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; +import { useAppParams } from '@/hooks/useAppParams'; import { useDispatch, useSelector } from '@/redux'; import type { IServiceReport } from '@/server/services/reports.service'; import { GanttChartSquareIcon } from 'lucide-react'; @@ -29,6 +29,7 @@ interface ReportEditorProps { export default function ReportEditor({ report: initialReport, }: ReportEditorProps) { + const { projectId } = useAppParams(); const dispatch = useDispatch(); const report = useSelector((state) => state.report); @@ -72,11 +73,7 @@ export default function ReportEditor({
- {report.ready && ( - }> - - - )} + {report.ready && }
diff --git a/apps/web/src/app/(public)/share/overview/[id]/page.tsx b/apps/web/src/app/(public)/share/overview/[id]/page.tsx index 4b335a57..d25c841c 100644 --- a/apps/web/src/app/(public)/share/overview/[id]/page.tsx +++ b/apps/web/src/app/(public)/share/overview/[id]/page.tsx @@ -8,6 +8,7 @@ import { Logo } from '@/components/Logo'; import ServerLiveCounter from '@/components/overview/live-counter'; import { OverviewFilters } from '@/components/overview/overview-filters'; import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons'; +import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram'; import OverviewTopDevices from '@/components/overview/overview-top-devices'; import OverviewTopEvents from '@/components/overview/overview-top-events'; import OverviewTopGeo from '@/components/overview/overview-top-geo'; @@ -49,18 +50,21 @@ export default async function Page({ params: { id } }: PageProps) {
- -
- - -
-
- + +
+
+ + +
+
+ +
+
-
- +
+
diff --git a/apps/web/src/components/overview/live-counter/live-counter.tsx b/apps/web/src/components/overview/live-counter/live-counter.tsx index e99abe0a..2b5c1cf8 100644 --- a/apps/web/src/components/overview/live-counter/live-counter.tsx +++ b/apps/web/src/components/overview/live-counter/live-counter.tsx @@ -12,6 +12,8 @@ import dynamic from 'next/dynamic'; import useWebSocket from 'react-use-websocket'; import { toast } from 'sonner'; +import { useOverviewOptions } from '../useOverviewOptions'; + export interface LiveCounterProps { data: number; projectId: string; @@ -25,6 +27,7 @@ const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), { const FIFTEEN_SECONDS = 1000 * 15; export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) { + const { setLiveHistogram } = useOverviewOptions(); const ws = String(process.env.NEXT_PUBLIC_API_URL) .replace(/^https/, 'wss') .replace(/^http/, 'ws'); @@ -52,8 +55,11 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) { return ( - -
+ + - {counter} unique visitors last 5 minutes +

{counter} unique visitors last 5 minutes

+

Click to see activity for the last 30 minutes

); diff --git a/apps/web/src/components/overview/overview-filters-buttons.tsx b/apps/web/src/components/overview/overview-filters-buttons.tsx index c21b1664..d089258b 100644 --- a/apps/web/src/components/overview/overview-filters-buttons.tsx +++ b/apps/web/src/components/overview/overview-filters-buttons.tsx @@ -1,5 +1,6 @@ 'use client'; +import { cn } from '@/utils/cn'; import { X } from 'lucide-react'; import { Button } from '../ui/button'; @@ -7,8 +8,10 @@ import { useOverviewOptions } from './useOverviewOptions'; export function OverviewFiltersButtons() { const options = useOverviewOptions(); + const activeFilter = options.filters.length > 0; + return ( - <> +
{options.referrer && ( )} - +
); } diff --git a/apps/web/src/components/overview/overview-live-histogram.tsx b/apps/web/src/components/overview/overview-live-histogram.tsx new file mode 100644 index 00000000..d47f18a7 --- /dev/null +++ b/apps/web/src/components/overview/overview-live-histogram.tsx @@ -0,0 +1,68 @@ +'use client'; + +import type { IChartInput } from '@/types'; +import { cn } from '@/utils/cn'; +import { ChevronsUpDownIcon } from 'lucide-react'; +import AnimateHeight from 'react-animate-height'; + +import { Chart } from '../report/chart'; +import { Widget, WidgetBody, WidgetHead } from '../Widget'; +import { useOverviewOptions } from './useOverviewOptions'; + +interface OverviewLiveHistogramProps { + projectId: string; +} +export function OverviewLiveHistogram({ + projectId, +}: OverviewLiveHistogramProps) { + const { liveHistogram, setLiveHistogram } = useOverviewOptions(); + const report: IChartInput = { + projectId, + events: [ + { + segment: 'user', + filters: [ + { + id: '1', + name: 'name', + operator: 'is', + value: ['screen_view', 'session_start'], + }, + ], + id: 'A', + name: '*', + displayName: 'Active users', + }, + ], + chartType: 'histogram', + interval: 'minute', + range: '30min', + name: '', + metric: 'sum', + breakdowns: [], + lineType: 'monotone', + previous: true, + }; + + return ( + + + + + + + + + + ); +} diff --git a/apps/web/src/components/overview/overview-top-devices.tsx b/apps/web/src/components/overview/overview-top-devices.tsx index 1d4a1987..525cee7f 100644 --- a/apps/web/src/components/overview/overview-top-devices.tsx +++ b/apps/web/src/components/overview/overview-top-devices.tsx @@ -1,8 +1,6 @@ 'use client'; -import { Suspense } from 'react'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -25,6 +23,7 @@ export default function OverviewTopDevices({ setBrowserVersion, setOS, setOSVersion, + setDevice, } = useOverviewOptions(); const [widget, setWidget, widgets] = useOverviewWidget('tech', { devices: { @@ -187,31 +186,32 @@ export default function OverviewTopDevices({ - }> - { - switch (widget.key) { - case 'browser': - setWidget('browser_version'); - setBrowser(item.name); - break; - case 'browser_version': - setBrowserVersion(item.name); - break; - case 'os': - setWidget('os_version'); - setOS(item.name); - break; - case 'os_version': - setOSVersion(item.name); - break; - } - }} - /> - + { + switch (widget.key) { + case 'devices': + setDevice(item.name); + break; + case 'browser': + setWidget('browser_version'); + setBrowser(item.name); + break; + case 'browser_version': + setBrowserVersion(item.name); + break; + case 'os': + setWidget('os_version'); + setOS(item.name); + break; + case 'os_version': + setOSVersion(item.name); + break; + } + }} + /> diff --git a/apps/web/src/components/overview/overview-top-events.tsx b/apps/web/src/components/overview/overview-top-events.tsx index 07cb4431..b9ef4e5a 100644 --- a/apps/web/src/components/overview/overview-top-events.tsx +++ b/apps/web/src/components/overview/overview-top-events.tsx @@ -74,9 +74,7 @@ export default function OverviewTopEvents({ - }> - - + diff --git a/apps/web/src/components/overview/overview-top-geo.tsx b/apps/web/src/components/overview/overview-top-geo.tsx index 59e70084..b6bdd992 100644 --- a/apps/web/src/components/overview/overview-top-geo.tsx +++ b/apps/web/src/components/overview/overview-top-geo.tsx @@ -149,28 +149,26 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) { - }> - { - switch (widget.key) { - case 'countries': - setWidget('regions'); - setCountry(item.name); - break; - case 'regions': - setWidget('cities'); - setRegion(item.name); - break; - case 'cities': - setCity(item.name); - break; - } - }} - /> - + { + switch (widget.key) { + case 'countries': + setWidget('regions'); + setCountry(item.name); + break; + case 'regions': + setWidget('cities'); + setRegion(item.name); + break; + case 'cities': + setCity(item.name); + break; + } + }} + /> diff --git a/apps/web/src/components/overview/overview-top-pages.tsx b/apps/web/src/components/overview/overview-top-pages.tsx index d5539c52..59b79c72 100644 --- a/apps/web/src/components/overview/overview-top-pages.tsx +++ b/apps/web/src/components/overview/overview-top-pages.tsx @@ -120,16 +120,14 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { - }> - { - setPage(item.name); - }} - /> - + { + setPage(item.name); + }} + /> diff --git a/apps/web/src/components/overview/overview-top-sources.tsx b/apps/web/src/components/overview/overview-top-sources.tsx index 905142f7..e798dd39 100644 --- a/apps/web/src/components/overview/overview-top-sources.tsx +++ b/apps/web/src/components/overview/overview-top-sources.tsx @@ -275,43 +275,41 @@ export default function OverviewTopSources({ - }> - { - switch (widget.key) { - case 'all': - setReferrerName(item.name); - setWidget('domain'); - break; - case 'domain': - setReferrer(item.name); - break; - case 'type': - setReferrerType(item.name); - setWidget('domain'); - break; - case 'utm_source': - setUtmSource(item.name); - break; - case 'utm_medium': - setUtmMedium(item.name); - break; - case 'utm_campaign': - setUtmCampaign(item.name); - break; - case 'utm_term': - setUtmTerm(item.name); - break; - case 'utm_content': - setUtmContent(item.name); - break; - } - }} - /> - + { + switch (widget.key) { + case 'all': + setReferrerName(item.name); + setWidget('domain'); + break; + case 'domain': + setReferrer(item.name); + break; + case 'type': + setReferrerType(item.name); + setWidget('domain'); + break; + case 'utm_source': + setUtmSource(item.name); + break; + case 'utm_medium': + setUtmMedium(item.name); + break; + case 'utm_campaign': + setUtmCampaign(item.name); + break; + case 'utm_term': + setUtmTerm(item.name); + break; + case 'utm_content': + setUtmContent(item.name); + break; + } + }} + /> diff --git a/apps/web/src/components/overview/overview-widget.tsx b/apps/web/src/components/overview/overview-widget.tsx index 3be1dc73..92a8ba6a 100644 --- a/apps/web/src/components/overview/overview-widget.tsx +++ b/apps/web/src/components/overview/overview-widget.tsx @@ -1,9 +1,8 @@ 'use client'; -import { Children, useCallback, useEffect, useRef, useState } from 'react'; +import { Children, useEffect, useRef, useState } from 'react'; import { useThrottle } from '@/hooks/useThrottle'; import { cn } from '@/utils/cn'; -import throttle from 'lodash.throttle'; import { ChevronsUpDownIcon } from 'lucide-react'; import { last } from 'ramda'; diff --git a/apps/web/src/components/overview/useOverviewOptions.ts b/apps/web/src/components/overview/useOverviewOptions.ts index c1400630..a5e73df7 100644 --- a/apps/web/src/components/overview/useOverviewOptions.ts +++ b/apps/web/src/components/overview/useOverviewOptions.ts @@ -107,6 +107,12 @@ export function useOverviewOptions() { parseAsString.withOptions(nuqsOptions) ); + // Toggles + const [liveHistogram, setLiveHistogram] = useQueryState( + 'live', + parseAsBoolean.withDefault(false).withOptions(nuqsOptions) + ); + const filters = useMemo(() => { const filters: IChartInput['events'][number]['filters'] = []; @@ -337,5 +343,9 @@ export function useOverviewOptions() { setOS, osVersion, setOSVersion, + + // Toggles + liveHistogram, + setLiveHistogram, }; } diff --git a/apps/web/src/components/report/chart/ChartProvider.tsx b/apps/web/src/components/report/chart/ChartProvider.tsx index 5f26c859..e852f44a 100644 --- a/apps/web/src/components/report/chart/ChartProvider.tsx +++ b/apps/web/src/components/report/chart/ChartProvider.tsx @@ -3,6 +3,7 @@ import { createContext, memo, + Suspense, useContext, useEffect, useMemo, @@ -12,6 +13,7 @@ import type { IChartSerie } from '@/server/api/routers/chart'; import type { IChartInput } from '@/types'; import { ChartLoading } from './ChartLoading'; +import { MetricCardLoading } from './MetricCard'; export interface ChartContextType extends IChartInput { editMode?: boolean; @@ -47,10 +49,10 @@ export function ChartProvider({ ({ + ...props, editMode: editMode ?? false, previous: previous ?? false, hideID: hideID ?? false, - ...props, }), [editMode, previous, hideID, props] )} @@ -64,20 +66,34 @@ export function withChartProivder( WrappedComponent: React.FC ) { const WithChartProvider = (props: ComponentProps & ChartContextType) => { - const [mounted, setMounted] = useState(props.chartType === 'metric'); + const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) { - return ; + return props.chartType === 'metric' ? ( + + ) : ( + + ); } return ( - - - + + ) : ( + + ) + } + > + + + + ); }; diff --git a/apps/web/src/components/report/chart/LazyChart.tsx b/apps/web/src/components/report/chart/LazyChart.tsx index 5aa2672c..2fd3cb89 100644 --- a/apps/web/src/components/report/chart/LazyChart.tsx +++ b/apps/web/src/components/report/chart/LazyChart.tsx @@ -23,13 +23,11 @@ export function LazyChart(props: ReportChartProps & ChartContextType) { return (
- }> - {once.current || inViewport ? ( - - ) : ( - - )} - + {once.current || inViewport ? ( + + ) : ( + + )}
); } diff --git a/apps/web/src/components/report/chart/ReportAreaChart.tsx b/apps/web/src/components/report/chart/ReportAreaChart.tsx index 09162bc9..2f5ca221 100644 --- a/apps/web/src/components/report/chart/ReportAreaChart.tsx +++ b/apps/web/src/components/report/chart/ReportAreaChart.tsx @@ -20,6 +20,7 @@ import { getYAxisWidth } from './chart-utils'; import { useChartContext } from './ChartProvider'; import { ReportChartTooltip } from './ReportChartTooltip'; import { ReportTable } from './ReportTable'; +import { ResponsiveContainer } from './ResponsiveContainer'; interface ReportAreaChartProps { data: IChartData; @@ -39,83 +40,72 @@ export function ReportAreaChart({ return ( <> -
- - {({ width }) => ( - - } /> - formatDate(m)} - tickLine={false} - allowDuplicatedCategory={false} - /> - + + {({ width, height }) => ( + + } /> + formatDate(m)} + tickLine={false} + allowDuplicatedCategory={false} + /> + - {series.map((serie) => { - const color = getChartColor(serie.index); - return ( - - - - - - - - - - ); - })} - - - )} - -
+ {series.map((serie) => { + const color = getChartColor(serie.index); + return ( + + + + + + + + + + ); + })} + + + )} + {editMode && ( state.report.interval); const formatDate = useFormatDateInterval(interval); const number = useNumber(); if (!active || !payload) { diff --git a/apps/web/src/components/report/chart/ReportHistogramChart.tsx b/apps/web/src/components/report/chart/ReportHistogramChart.tsx index 080bb19a..50a63125 100644 --- a/apps/web/src/components/report/chart/ReportHistogramChart.tsx +++ b/apps/web/src/components/report/chart/ReportHistogramChart.tsx @@ -13,6 +13,7 @@ import { getYAxisWidth } from './chart-utils'; import { useChartContext } from './ChartProvider'; import { ReportChartTooltip } from './ReportChartTooltip'; import { ReportTable } from './ReportTable'; +import { ResponsiveContainer } from './ResponsiveContainer'; interface ReportHistogramChartProps { data: IChartData; @@ -43,61 +44,52 @@ export function ReportHistogramChart({ return ( <> -
- - {({ width }) => ( - - - } cursor={} /> - - - {series.map((serie) => { - return ( - - {previous && ( - - )} + + {({ width, height }) => ( + + + } cursor={} /> + + + {series.map((serie) => { + return ( + + {previous && ( - - ); - })} - - )} - -
+ )} + + + ); + })} + + )} + {editMode && ( -
- - {({ width }) => ( - - - - } /> - formatDate(m)} - tickLine={false} - allowDuplicatedCategory={false} - /> - {series.map((serie) => { - return ( - + + {({ width, height }) => ( + + + + } /> + formatDate(m)} + tickLine={false} + allowDuplicatedCategory={false} + /> + {series.map((serie) => { + return ( + + + {previous && ( - {previous && ( - - )} - - ); - })} - - )} - -
+ )} + + ); + })} + + )} + {editMode && ( React.ReactNode; +} + +export function ResponsiveContainer({ children }: ResponsiveContainerProps) { + const { editMode } = useChartContext(); + const maxHeight = 300; + const minHeight = 200; + return ( +
+ + {({ width }) => + children({ + width, + height: Math.min( + Math.max(width * 0.5625, minHeight), + // we add p-4 (16px) padding in edit mode + editMode ? maxHeight - 16 : maxHeight + ), + }) + } + +
+ ); +} diff --git a/apps/web/src/components/report/chart/SerieIcon.tsx b/apps/web/src/components/report/chart/SerieIcon.tsx index 3ccead30..62e811b1 100644 --- a/apps/web/src/components/report/chart/SerieIcon.tsx +++ b/apps/web/src/components/report/chart/SerieIcon.tsx @@ -60,9 +60,9 @@ export function SerieIcon({ name, ...props }: SerieIconProps) { )) as LucideIcon; } - return ( + return Icon ? (
- {Icon ? : null} +
- ); + ) : null; } diff --git a/apps/web/src/components/report/chart/index.tsx b/apps/web/src/components/report/chart/index.tsx index 41206b7b..a8334c68 100644 --- a/apps/web/src/components/report/chart/index.tsx +++ b/apps/web/src/components/report/chart/index.tsx @@ -20,80 +20,78 @@ export type ReportChartProps = IChartInput & { initialData?: RouterOutputs['chart']['chart']; }; -export const Chart = memo( - withChartProivder(function Chart({ - interval, - events, - breakdowns, - chartType, - name, - range, - lineType, - previous, - formula, - unit, - metric, - projectId, - }: ReportChartProps) { - const [data] = api.chart.chart.useSuspenseQuery( - { - // dont send lineType since it does not need to be sent - lineType: 'monotone', - interval, - chartType, - events, - breakdowns, - name, - range, - startDate: null, - endDate: null, - projectId, - previous, - formula, - unit, - metric, - }, - { - keepPreviousData: true, - } +export const Chart = withChartProivder(function Chart({ + interval, + events, + breakdowns, + chartType, + name, + range, + lineType, + previous, + formula, + unit, + metric, + projectId, +}: ReportChartProps) { + const [data] = api.chart.chart.useSuspenseQuery( + { + // dont send lineType since it does not need to be sent + lineType: 'monotone', + interval, + chartType, + events, + breakdowns, + name, + range, + startDate: null, + endDate: null, + projectId, + previous, + formula, + unit, + metric, + }, + { + keepPreviousData: true, + } + ); + + if (data.series.length === 0) { + return ; + } + + if (chartType === 'map') { + return ; + } + + if (chartType === 'histogram') { + return ; + } + + if (chartType === 'bar') { + return ; + } + + if (chartType === 'metric') { + return ; + } + + if (chartType === 'pie') { + return ; + } + + if (chartType === 'linear') { + return ( + ); + } - if (data.series.length === 0) { - return ; - } + if (chartType === 'area') { + return ( + + ); + } - if (chartType === 'map') { - return ; - } - - if (chartType === 'histogram') { - return ; - } - - if (chartType === 'bar') { - return ; - } - - if (chartType === 'metric') { - return ; - } - - if (chartType === 'pie') { - return ; - } - - if (chartType === 'linear') { - return ( - - ); - } - - if (chartType === 'area') { - return ( - - ); - } - - return

Unknown chart type

; - }) -); + return

Unknown chart type

; +}); diff --git a/apps/web/src/components/report/sidebar/EventPropertiesCombobox.tsx b/apps/web/src/components/report/sidebar/EventPropertiesCombobox.tsx index 9f2e7a5a..ed21b2e6 100644 --- a/apps/web/src/components/report/sidebar/EventPropertiesCombobox.tsx +++ b/apps/web/src/components/report/sidebar/EventPropertiesCombobox.tsx @@ -1,11 +1,11 @@ import { api } from '@/app/_trpc/client'; import { Combobox } from '@/components/ui/combobox'; +import { useAppParams } from '@/hooks/useAppParams'; import { useDispatch } from '@/redux'; import type { IChartEvent } from '@/types'; import { cn } from '@/utils/cn'; import { DatabaseIcon } from 'lucide-react'; -import { useChartContext } from '../chart/ChartProvider'; import { changeEvent } from '../reportSlice'; interface EventPropertiesComboboxProps { @@ -16,7 +16,7 @@ export function EventPropertiesCombobox({ event, }: EventPropertiesComboboxProps) { const dispatch = useDispatch(); - const { projectId } = useChartContext(); + const { projectId } = useAppParams(); const query = api.chart.properties.useQuery( { diff --git a/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx b/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx index fb8e9efc..dcbb63fc 100644 --- a/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx +++ b/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx @@ -3,17 +3,17 @@ import { api } from '@/app/_trpc/client'; import { ColorSquare } from '@/components/ColorSquare'; import { Combobox } from '@/components/ui/combobox'; +import { useAppParams } from '@/hooks/useAppParams'; import { useDispatch, useSelector } from '@/redux'; import type { IChartBreakdown } from '@/types'; import { SplitIcon } from 'lucide-react'; -import { useChartContext } from '../chart/ChartProvider'; import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice'; import { ReportBreakdownMore } from './ReportBreakdownMore'; import type { ReportEventMoreProps } from './ReportEventMore'; export function ReportBreakdowns() { - const { projectId } = useChartContext(); + const { projectId } = useAppParams(); const selectedBreakdowns = useSelector((state) => state.report.breakdowns); const dispatch = useDispatch(); const propertiesQuery = api.chart.properties.useQuery({ diff --git a/apps/web/src/components/report/sidebar/ReportEvents.tsx b/apps/web/src/components/report/sidebar/ReportEvents.tsx index 3a460d49..ea258b92 100644 --- a/apps/web/src/components/report/sidebar/ReportEvents.tsx +++ b/apps/web/src/components/report/sidebar/ReportEvents.tsx @@ -6,12 +6,12 @@ import { Dropdown } from '@/components/Dropdown'; import { Checkbox } from '@/components/ui/checkbox'; import { Combobox } from '@/components/ui/combobox'; import { Input } from '@/components/ui/input'; +import { useAppParams } from '@/hooks/useAppParams'; import { useDebounceFn } from '@/hooks/useDebounceFn'; import { useDispatch, useSelector } from '@/redux'; import type { IChartEvent } from '@/types'; import { GanttChart, GanttChartIcon, Users } from 'lucide-react'; -import { useChartContext } from '../chart/ChartProvider'; import { addEvent, changeEvent, @@ -28,7 +28,8 @@ export function ReportEvents() { const previous = useSelector((state) => state.report.previous); const selectedEvents = useSelector((state) => state.report.events); const dispatch = useDispatch(); - const { projectId } = useChartContext(); + const { projectId } = useAppParams(); + const eventsQuery = api.chart.events.useQuery({ projectId, }); diff --git a/apps/web/src/components/report/sidebar/filters/FilterItem.tsx b/apps/web/src/components/report/sidebar/filters/FilterItem.tsx index 011b0b77..8e457836 100644 --- a/apps/web/src/components/report/sidebar/filters/FilterItem.tsx +++ b/apps/web/src/components/report/sidebar/filters/FilterItem.tsx @@ -4,6 +4,7 @@ import { Dropdown } from '@/components/Dropdown'; import { Button } from '@/components/ui/button'; import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; import { RenderDots } from '@/components/ui/RenderDots'; +import { useAppParams } from '@/hooks/useAppParams'; import { useMappings } from '@/hooks/useMappings'; import { useDispatch } from '@/redux'; import type { @@ -14,7 +15,6 @@ import type { import { operators } from '@/utils/constants'; import { SlidersHorizontal, Trash } from 'lucide-react'; -import { useChartContext } from '../../chart/ChartProvider'; import { changeEvent } from '../../reportSlice'; interface FilterProps { @@ -23,7 +23,7 @@ interface FilterProps { } export function FilterItem({ filter, event }: FilterProps) { - const { projectId } = useChartContext(); + const { projectId } = useAppParams(); const getLabel = useMappings(); const dispatch = useDispatch(); const potentialValues = api.chart.values.useQuery({ diff --git a/apps/web/src/components/report/sidebar/filters/FiltersCombobox.tsx b/apps/web/src/components/report/sidebar/filters/FiltersCombobox.tsx index 0423ec8e..e639b61c 100644 --- a/apps/web/src/components/report/sidebar/filters/FiltersCombobox.tsx +++ b/apps/web/src/components/report/sidebar/filters/FiltersCombobox.tsx @@ -1,10 +1,10 @@ import { api } from '@/app/_trpc/client'; import { Combobox } from '@/components/ui/combobox'; +import { useAppParams } from '@/hooks/useAppParams'; import { useDispatch } from '@/redux'; import type { IChartEvent } from '@/types'; import { FilterIcon } from 'lucide-react'; -import { useChartContext } from '../../chart/ChartProvider'; import { changeEvent } from '../../reportSlice'; interface FiltersComboboxProps { @@ -13,7 +13,7 @@ interface FiltersComboboxProps { export function FiltersCombobox({ event }: FiltersComboboxProps) { const dispatch = useDispatch(); - const { projectId } = useChartContext(); + const { projectId } = useAppParams(); const query = api.chart.properties.useQuery( { diff --git a/apps/web/src/hooks/useVisibleSeries.ts b/apps/web/src/hooks/useVisibleSeries.ts index 49ef5938..0fb0b209 100644 --- a/apps/web/src/hooks/useVisibleSeries.ts +++ b/apps/web/src/hooks/useVisibleSeries.ts @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { IChartData } from '@/app/_trpc/client'; export type IVisibleSeries = ReturnType['series']; @@ -10,6 +10,12 @@ export function useVisibleSeries(data: IChartData, limit?: number | undefined) { data?.series?.slice(0, max).map((serie) => serie.name) ?? [] ); + useEffect(() => { + setVisibleSeries( + data?.series?.slice(0, max).map((serie) => serie.name) ?? [] + ); + }, [data, max]); + return useMemo(() => { return { series: data.series diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 8a42ef2c..68bdefb5 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -4,13 +4,9 @@ import { authMiddleware } from '@clerk/nextjs'; // Please edit this to allow other routes to be public as needed. // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware export default authMiddleware({ - publicRoutes: [ - '/share/overview/:id', - '/api/trpc/chart.chart', - '/api/trpc/chart.values', - ], + publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'], }); export const config = { - matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], + matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api)(.*)'], }; diff --git a/apps/web/src/server/api/routers/chart.helpers.ts b/apps/web/src/server/api/routers/chart.helpers.ts index f054716f..170b58da 100644 --- a/apps/web/src/server/api/routers/chart.helpers.ts +++ b/apps/web/src/server/api/routers/chart.helpers.ts @@ -10,7 +10,7 @@ import { round } from '@/utils/math'; import * as mathjs from 'mathjs'; import { sort } from 'ramda'; -import { chQuery } from '@mixan/db'; +import { chQuery, convertClickhouseDateToJs } from '@mixan/db'; export type GetChartDataResult = Awaited>; export interface ResultItem { @@ -73,7 +73,7 @@ function fillEmptySpotsInTimeline( const getMinute = (date: Date) => date.getUTCMinutes(); const item = items.find((item) => { - const date = new Date(item.date); + const date = convertClickhouseDateToJs(item.date); if (interval === 'month') { return ( diff --git a/apps/web/src/server/api/routers/chart.ts b/apps/web/src/server/api/routers/chart.ts index 2982d734..1f2f524f 100644 --- a/apps/web/src/server/api/routers/chart.ts +++ b/apps/web/src/server/api/routers/chart.ts @@ -10,7 +10,7 @@ import { zChartInput } from '@/utils/validation'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; import { z } from 'zod'; -import { chQuery } from '@mixan/db'; +import { chQuery, createSqlBuilder } from '@mixan/db'; import { getChartData, withFormula } from './chart.helpers'; @@ -117,16 +117,20 @@ export const chartRouter = createTRPCRouter({ }) ) .query(async ({ input: { event, property, projectId } }) => { - const sql = property.startsWith('properties.') - ? `SELECT distinct mapValues(mapExtractKeyLike(properties, '${property - .replace(/^properties\./, '') - .replace( - '.*.', - '.%.' - )}')) as values from events where name = '${event}' AND project_id = '${projectId}';` - : `SELECT ${property} as values from events where name = '${event}' AND project_id = '${projectId}';`; + const { sb, getSql } = createSqlBuilder(); + sb.where.project_id = `project_id = '${projectId}'`; + if (event !== '*') { + sb.where.event = `name = '${event}'`; + } + if (property.startsWith('properties.')) { + sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, '${property + .replace(/^properties\./, '') + .replace('.*.', '.%.')}')) as values`; + } else { + sb.select.values = `${property} as values`; + } - const events = await chQuery<{ values: string[] }>(sql); + const events = await chQuery<{ values: string[] }>(getSql()); const values = pipe( (data: typeof events) => map(prop('values'), data), diff --git a/packages/db/src/clickhouse-client.ts b/packages/db/src/clickhouse-client.ts index 051bbb29..65946db0 100644 --- a/packages/db/src/clickhouse-client.ts +++ b/packages/db/src/clickhouse-client.ts @@ -49,3 +49,7 @@ export function formatClickhouseDate(_date: Date | string) { const date = typeof _date === 'string' ? new Date(_date) : _date; return date.toISOString().replace('T', ' ').replace(/Z+$/, ''); } + +export function convertClickhouseDateToJs(date: string) { + return new Date(date.replace(' ', 'T') + 'Z'); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 394f5efd..60149b1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4036,7 +4036,7 @@ packages: resolution: {integrity: sha512-bOhuFnlRaS7CU33+rFFIWdcET/Vkyn1vsN8BYFwCDEF5P1fVVvYN7bFOsQLTMD3nvi35C1AGmtqUr/Wfv8Xaow==} engines: {node: '>=12'} dependencies: - '@expo/spawn-async': 1.5.0 + '@expo/spawn-async': 1.7.2 exec-async: 2.2.0 dev: false @@ -4044,7 +4044,7 @@ packages: resolution: {integrity: sha512-LKdo/6y4W7llZ6ghsg1kdx2CeH/qR/c6QI/JI8oPUvppsZoeIYjSkdflce978fAMfR8IXoi0wt0jA2w0kWpwbg==} dependencies: '@expo/json-file': 8.3.0 - '@expo/spawn-async': 1.5.0 + '@expo/spawn-async': 1.7.2 ansi-regex: 5.0.1 chalk: 4.1.2 find-up: 5.0.0 @@ -8908,7 +8908,7 @@ packages: engines: {node: '>=12.13.0'} hasBin: true dependencies: - '@types/node': 20.11.17 + '@types/node': 18.19.15 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -8919,7 +8919,7 @@ packages: /chromium-edge-launcher@1.0.0: resolution: {integrity: sha512-pgtgjNKZ7i5U++1g1PWv75umkHvhVTDOQIZ+sjeUX9483S7Y6MUvO0lrd7ShGlQlFHMN4SwKTCq/X8hWrbv2KA==} dependencies: - '@types/node': 20.11.17 + '@types/node': 18.19.15 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2