fix: improve types for chart/reports
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Markdown } from '@/components/markdown';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { zChartInputAI } from '@openpanel/validation';
|
||||
import { zReport } from '@openpanel/validation';
|
||||
import { z } from 'zod';
|
||||
import type { UIMessage } from 'ai';
|
||||
import { Loader2Icon, UserIcon } from 'lucide-react';
|
||||
import { Fragment, memo } from 'react';
|
||||
@@ -77,7 +78,10 @@ export const ChatMessage = memo(
|
||||
const { result } = p.toolInvocation;
|
||||
|
||||
if (result.type === 'report') {
|
||||
const report = zChartInputAI.safeParse(result.report);
|
||||
const report = zReport.extend({
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
}).safeParse(result.report);
|
||||
if (report.success) {
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pushModal } from '@/modals';
|
||||
import type {
|
||||
IChartInputAi,
|
||||
IReport,
|
||||
IChartRange,
|
||||
IChartType,
|
||||
IInterval,
|
||||
@@ -16,7 +16,7 @@ import { Button } from '../ui/button';
|
||||
export function ChatReport({
|
||||
lazy,
|
||||
...props
|
||||
}: { report: IChartInputAi; lazy: boolean }) {
|
||||
}: { report: IReport & { startDate: string; endDate: string }; lazy: boolean }) {
|
||||
const [chartType, setChartType] = useState<IChartType>(
|
||||
props.report.chartType,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IReportInput } from '@openpanel/validation';
|
||||
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -74,7 +74,7 @@ export default function OverviewTopEvents({
|
||||
},
|
||||
});
|
||||
|
||||
const report: IChartInput = useMemo(
|
||||
const report: IReportInput = useMemo(
|
||||
() => ({
|
||||
limit: 1000,
|
||||
projectId,
|
||||
@@ -96,9 +96,7 @@ export default function OverviewTopEvents({
|
||||
},
|
||||
],
|
||||
chartType: 'bar' as const,
|
||||
lineType: 'monotone' as const,
|
||||
interval,
|
||||
name: widget.title,
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum' as const,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
import { ReportChart } from '../report-chart';
|
||||
import { SerieIcon } from '../report-chart/common/serie-icon';
|
||||
import { ReportChartShortcut } from '../report-chart/shortcut';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OVERVIEW_COLUMNS_NAME } from './overview-constants';
|
||||
import OverviewDetailsButton from './overview-details-button';
|
||||
@@ -210,8 +211,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<div className="title">Map</div>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<ReportChart
|
||||
report={{
|
||||
<ReportChartShortcut
|
||||
{...{
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -234,7 +235,6 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
interval: interval,
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ReportChart } from '@/components/report-chart';
|
||||
import { Widget, WidgetBody } from '@/components/widget';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
import type { IReport } from '@openpanel/validation';
|
||||
import { WidgetHead } from '../overview/overview-widget';
|
||||
|
||||
type Props = {
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
export const ProfileCharts = memo(
|
||||
({ profileId, projectId }: Props) => {
|
||||
const pageViewsChart: IChartProps = {
|
||||
const pageViewsChart: IReport = {
|
||||
projectId,
|
||||
chartType: 'linear',
|
||||
series: [
|
||||
@@ -46,7 +46,7 @@ export const ProfileCharts = memo(
|
||||
metric: 'sum',
|
||||
};
|
||||
|
||||
const eventsChart: IChartProps = {
|
||||
const eventsChart: IReport = {
|
||||
projectId,
|
||||
chartType: 'linear',
|
||||
series: [
|
||||
|
||||
@@ -2,11 +2,7 @@ import isEqual from 'lodash.isequal';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
IChartInput,
|
||||
IChartProps,
|
||||
IChartSerie,
|
||||
} from '@openpanel/validation';
|
||||
import type { IChartSerie, IReportInput } from '@openpanel/validation';
|
||||
|
||||
export type ReportChartContextType = {
|
||||
options: Partial<{
|
||||
@@ -27,7 +23,7 @@ export type ReportChartContextType = {
|
||||
onClick: () => void;
|
||||
}[];
|
||||
}>;
|
||||
report: IChartInput & { id?: string };
|
||||
report: IReportInput & { id?: string };
|
||||
isLazyLoading: boolean;
|
||||
isEditMode: boolean;
|
||||
shareId?: string;
|
||||
@@ -39,7 +35,7 @@ type ReportChartContextProviderProps = ReportChartContextType & {
|
||||
};
|
||||
|
||||
export type ReportChartProps = Partial<ReportChartContextType> & {
|
||||
report: IChartInput;
|
||||
report: IReportInput & { id?: string };
|
||||
lazy?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -131,34 +131,36 @@ export function Tables({
|
||||
series: reportSeries,
|
||||
breakdowns: reportBreakdowns,
|
||||
previous,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
options,
|
||||
},
|
||||
} = useReportChartContext();
|
||||
|
||||
const funnelOptions = options?.type === 'funnel' ? options : undefined;
|
||||
const funnelWindow = funnelOptions?.funnelWindow;
|
||||
const funnelGroup = funnelOptions?.funnelGroup;
|
||||
|
||||
const handleInspectStep = (step: (typeof steps)[0], stepIndex: number) => {
|
||||
if (!projectId || !step.event.id) return;
|
||||
|
||||
// For funnels, we need to pass the step index so the modal can query
|
||||
// users who completed at least that step in the funnel sequence
|
||||
pushModal('ViewChartUsers', {
|
||||
type: 'funnel',
|
||||
report: {
|
||||
projectId,
|
||||
series: reportSeries,
|
||||
breakdowns: reportBreakdowns || [],
|
||||
interval: interval || 'day',
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
previous,
|
||||
chartType: 'funnel',
|
||||
metric: 'sum',
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
},
|
||||
stepIndex, // Pass the step index for funnel queries
|
||||
});
|
||||
pushModal('ViewChartUsers', {
|
||||
type: 'funnel',
|
||||
report: {
|
||||
projectId,
|
||||
series: reportSeries,
|
||||
breakdowns: reportBreakdowns || [],
|
||||
interval: interval || 'day',
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
previous,
|
||||
chartType: 'funnel',
|
||||
metric: 'sum',
|
||||
options: funnelOptions,
|
||||
},
|
||||
stepIndex, // Pass the step index for funnel queries
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className={cn('col @container divide-y divide-border card')}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RouterOutputs } from '@/trpc/client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IReportInput } from '@openpanel/validation';
|
||||
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
@@ -19,8 +19,7 @@ export function ReportFunnelChart() {
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
options,
|
||||
startDate,
|
||||
endDate,
|
||||
previous,
|
||||
@@ -32,23 +31,22 @@ export function ReportFunnelChart() {
|
||||
} = useReportChartContext();
|
||||
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||
|
||||
const funnelOptions = options?.type === 'funnel' ? options : undefined;
|
||||
|
||||
const trpc = useTRPC();
|
||||
const input: IChartInput = {
|
||||
const input: IReportInput = {
|
||||
series,
|
||||
range: overviewRange ?? range,
|
||||
projectId,
|
||||
interval: overviewInterval ?? interval ?? 'day',
|
||||
chartType: 'funnel',
|
||||
breakdowns,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
startDate: overviewStartDate ?? startDate,
|
||||
endDate: overviewEndDate ?? endDate,
|
||||
limit: 20,
|
||||
shareId,
|
||||
reportId: id,
|
||||
options: funnelOptions,
|
||||
};
|
||||
const res = useQuery(
|
||||
trpc.chart.funnel.queryOptions(input, {
|
||||
|
||||
@@ -17,20 +17,29 @@ export function ReportRetentionChart() {
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
options,
|
||||
startDate,
|
||||
endDate,
|
||||
criteria,
|
||||
interval,
|
||||
},
|
||||
isLazyLoading,
|
||||
shareId,
|
||||
} = useReportChartContext();
|
||||
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||
const {
|
||||
range: overviewRange,
|
||||
startDate: overviewStartDate,
|
||||
endDate: overviewEndDate,
|
||||
interval: overviewInterval,
|
||||
} = useOverviewOptions();
|
||||
const eventSeries = series.filter((item) => item.type === 'event');
|
||||
const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(String);
|
||||
const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(String);
|
||||
const isEnabled =
|
||||
firstEvent.length > 0 && secondEvent.length > 0 && !isLazyLoading;
|
||||
|
||||
const retentionOptions = options?.type === 'retention' ? options : undefined;
|
||||
const criteria = retentionOptions?.criteria ?? 'on_or_after';
|
||||
|
||||
const trpc = useTRPC();
|
||||
const res = useQuery(
|
||||
trpc.chart.cohort.queryOptions(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IReportInput } from '@openpanel/validation';
|
||||
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
@@ -28,7 +28,7 @@ export function ReportSankeyChart() {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const input: IChartInput = {
|
||||
const input: IReportInput = {
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
|
||||
@@ -26,7 +26,6 @@ export const ReportChartShortcut = ({
|
||||
return (
|
||||
<ReportChart
|
||||
report={{
|
||||
name: 'Shortcut',
|
||||
projectId,
|
||||
range,
|
||||
breakdowns: breakdowns ?? [],
|
||||
|
||||
@@ -172,15 +172,13 @@ export function ReportItem({
|
||||
)}
|
||||
>
|
||||
<ReportChart
|
||||
report={
|
||||
{
|
||||
...report,
|
||||
range: range ?? report.range,
|
||||
startDate: startDate ?? null,
|
||||
endDate: endDate ?? null,
|
||||
interval: interval ?? report.interval,
|
||||
} as any
|
||||
}
|
||||
report={{
|
||||
...report,
|
||||
range: range ?? report.range,
|
||||
startDate: startDate ?? null,
|
||||
endDate: endDate ?? null,
|
||||
interval: interval ?? report.interval,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +240,6 @@ export function ReportItemReadOnly({
|
||||
)}
|
||||
>
|
||||
<ReportChart
|
||||
type="inputs"
|
||||
report={{
|
||||
...report,
|
||||
range: range ?? report.range,
|
||||
|
||||
@@ -12,29 +12,28 @@ import type {
|
||||
IChartBreakdown,
|
||||
IChartEventItem,
|
||||
IChartLineType,
|
||||
IChartProps,
|
||||
IChartRange,
|
||||
IChartType,
|
||||
IInterval,
|
||||
IReport,
|
||||
IReportOptions,
|
||||
UnionOmit,
|
||||
zCriteria,
|
||||
} from '@openpanel/validation';
|
||||
import type { z } from 'zod';
|
||||
|
||||
type InitialState = IChartProps & {
|
||||
type InitialState = IReport & {
|
||||
id?: string;
|
||||
dirty: boolean;
|
||||
ready: boolean;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
options?: IReportOptions;
|
||||
};
|
||||
|
||||
// First approach: define the initial state using that type
|
||||
const initialState: InitialState = {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
// TODO: remove this
|
||||
projectId: '',
|
||||
name: '',
|
||||
chartType: 'linear',
|
||||
@@ -50,9 +49,6 @@ const initialState: InitialState = {
|
||||
unit: undefined,
|
||||
metric: 'sum',
|
||||
limit: 500,
|
||||
criteria: 'on_or_after',
|
||||
funnelGroup: undefined,
|
||||
funnelWindow: undefined,
|
||||
options: undefined,
|
||||
};
|
||||
|
||||
@@ -75,7 +71,7 @@ export const reportSlice = createSlice({
|
||||
ready: true,
|
||||
};
|
||||
},
|
||||
setReport(state, action: PayloadAction<IChartProps>) {
|
||||
setReport(state, action: PayloadAction<IReport>) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
@@ -265,7 +261,14 @@ export const reportSlice = createSlice({
|
||||
|
||||
changeCriteria(state, action: PayloadAction<z.infer<typeof zCriteria>>) {
|
||||
state.dirty = true;
|
||||
state.criteria = action.payload;
|
||||
if (!state.options || state.options.type !== 'retention') {
|
||||
state.options = {
|
||||
type: 'retention',
|
||||
criteria: action.payload,
|
||||
};
|
||||
} else {
|
||||
state.options.criteria = action.payload;
|
||||
}
|
||||
},
|
||||
|
||||
changeUnit(state, action: PayloadAction<string | undefined>) {
|
||||
@@ -275,12 +278,28 @@ export const reportSlice = createSlice({
|
||||
|
||||
changeFunnelGroup(state, action: PayloadAction<string | undefined>) {
|
||||
state.dirty = true;
|
||||
state.funnelGroup = action.payload || undefined;
|
||||
if (!state.options || state.options.type !== 'funnel') {
|
||||
state.options = {
|
||||
type: 'funnel',
|
||||
funnelGroup: action.payload,
|
||||
funnelWindow: undefined,
|
||||
};
|
||||
} else {
|
||||
state.options.funnelGroup = action.payload;
|
||||
}
|
||||
},
|
||||
|
||||
changeFunnelWindow(state, action: PayloadAction<number | undefined>) {
|
||||
state.dirty = true;
|
||||
state.funnelWindow = action.payload || undefined;
|
||||
if (!state.options || state.options.type !== 'funnel') {
|
||||
state.options = {
|
||||
type: 'funnel',
|
||||
funnelGroup: undefined,
|
||||
funnelWindow: action.payload,
|
||||
};
|
||||
} else {
|
||||
state.options.funnelWindow = action.payload;
|
||||
}
|
||||
},
|
||||
changeOptions(state, action: PayloadAction<IReportOptions | undefined>) {
|
||||
state.dirty = true;
|
||||
|
||||
@@ -332,14 +332,13 @@ export function ReportSeries() {
|
||||
}}
|
||||
placeholder="Select event"
|
||||
items={eventNames}
|
||||
className="flex-1"
|
||||
/>
|
||||
{showFormula && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
icon={PiIcon}
|
||||
className="flex-1 justify-start text-left"
|
||||
className="flex-1 justify-start text-left px-4"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addSerie({
|
||||
@@ -349,7 +348,6 @@ export function ReportSeries() {
|
||||
}),
|
||||
);
|
||||
}}
|
||||
className="px-4"
|
||||
>
|
||||
Add Formula
|
||||
<PlusIcon className="size-4 ml-auto text-muted-foreground" />
|
||||
|
||||
@@ -23,11 +23,15 @@ import {
|
||||
export function ReportSettings() {
|
||||
const chartType = useSelector((state) => state.report.chartType);
|
||||
const previous = useSelector((state) => state.report.previous);
|
||||
const criteria = useSelector((state) => state.report.criteria);
|
||||
const unit = useSelector((state) => state.report.unit);
|
||||
const funnelGroup = useSelector((state) => state.report.funnelGroup);
|
||||
const funnelWindow = useSelector((state) => state.report.funnelWindow);
|
||||
const options = useSelector((state) => state.report.options);
|
||||
|
||||
const retentionOptions = options?.type === 'retention' ? options : undefined;
|
||||
const criteria = retentionOptions?.criteria ?? 'on_or_after';
|
||||
|
||||
const funnelOptions = options?.type === 'funnel' ? options : undefined;
|
||||
const funnelGroup = funnelOptions?.funnelGroup;
|
||||
const funnelWindow = funnelOptions?.funnelWindow;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { projectId } = useAppParams();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
import type { IReport } from '@openpanel/validation';
|
||||
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type Props = {
|
||||
chart: IChartProps;
|
||||
chart: IReport;
|
||||
};
|
||||
|
||||
const OverviewChartDetails = (props: Props) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
import type { IReport } from '@openpanel/validation';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
@@ -21,7 +21,7 @@ import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type SaveReportProps = {
|
||||
report: IChartProps;
|
||||
report: IReport;
|
||||
disableRedirect?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTRPC } from '@/integrations/trpc/react';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IReportInput } from '@openpanel/validation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -152,7 +152,7 @@ function ProfileList({ profiles }: { profiles: any[] }) {
|
||||
// Chart-specific props and component
|
||||
interface ChartUsersViewProps {
|
||||
chartData: IChartData;
|
||||
report: IChartInput;
|
||||
report: IReportInput;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ function ChartUsersView({ chartData, report, date }: ChartUsersViewProps) {
|
||||
|
||||
// Funnel-specific props and component
|
||||
interface FunnelUsersViewProps {
|
||||
report: IChartInput;
|
||||
report: IReportInput;
|
||||
stepIndex: number;
|
||||
}
|
||||
|
||||
@@ -297,8 +297,14 @@ function FunnelUsersView({ report, stepIndex }: FunnelUsersViewProps) {
|
||||
series: report.series,
|
||||
stepIndex: stepIndex,
|
||||
showDropoffs: showDropoffs,
|
||||
funnelWindow: report.funnelWindow,
|
||||
funnelGroup: report.funnelGroup,
|
||||
funnelWindow:
|
||||
report.options?.type === 'funnel'
|
||||
? report.options.funnelWindow
|
||||
: undefined,
|
||||
funnelGroup:
|
||||
report.options?.type === 'funnel'
|
||||
? report.options.funnelGroup
|
||||
: undefined,
|
||||
breakdowns: report.breakdowns,
|
||||
},
|
||||
{
|
||||
@@ -371,12 +377,12 @@ type ViewChartUsersProps =
|
||||
| {
|
||||
type: 'chart';
|
||||
chartData: IChartData;
|
||||
report: IChartInput;
|
||||
report: IReportInput;
|
||||
date: string;
|
||||
}
|
||||
| {
|
||||
type: 'funnel';
|
||||
report: IChartInput;
|
||||
report: IReportInput;
|
||||
stepIndex: number;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user