feat: share dashboard & reports, sankey report, new widgets

* fix: prompt card shadows on light mode

* fix: handle past_due and unpaid from polar

* wip

* wip

* wip 1

* fix: improve types for chart/reports

* wip share
This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-14 09:21:18 +01:00
committed by GitHub
parent 39251c8598
commit ed1c57dbb8
105 changed files with 6633 additions and 1273 deletions

View File

@@ -86,41 +86,12 @@ export const zChartBreakdown = z.object({
name: z.string(),
});
// Support both old format (array of events without type) and new format (array of event/formula items)
// Preprocess to normalize: if item has 'type' field, use discriminated union; otherwise, add type: 'event'
export const zChartSeries = z.preprocess((val) => {
if (!val) return val;
let processedVal = val;
// If the input is an object with numeric keys, convert it to an array
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
const keys = Object.keys(val).sort(
(a, b) => Number.parseInt(a) - Number.parseInt(b),
);
processedVal = keys.map((key) => (val as any)[key]);
}
if (!Array.isArray(processedVal)) return processedVal;
return processedVal.map((item: any) => {
// If item already has type field, return as-is
if (item && typeof item === 'object' && 'type' in item) {
return item;
}
// Otherwise, add type: 'event' for backward compatibility
if (item && typeof item === 'object' && 'name' in item) {
return { ...item, type: 'event' };
}
return item;
});
}, z
export const zChartSeries = z
.array(zChartEventItem)
.describe(
'Array of series (events or formulas) to be tracked and displayed in the chart',
));
);
// Keep zChartEvents as an alias for backward compatibility during migration
export const zChartEvents = zChartSeries;
export const zChartBreakdowns = z.array(zChartBreakdown);
export const zChartType = z.enum(objectToZodEnums(chartTypes));
@@ -135,7 +106,61 @@ export const zRange = z.enum(objectToZodEnums(timeWindows));
export const zCriteria = z.enum(['on_or_after', 'on']);
export const zChartInputBase = z.object({
// Report Options - Discriminated union based on chart type
export const zFunnelOptions = z.object({
type: z.literal('funnel'),
funnelGroup: z.string().optional(),
funnelWindow: z.number().optional(),
});
export const zRetentionOptions = z.object({
type: z.literal('retention'),
criteria: zCriteria.optional(),
});
export const zSankeyOptions = z.object({
type: z.literal('sankey'),
mode: z.enum(['between', 'after', 'before']),
steps: z.number().min(2).max(10).default(5),
exclude: z.array(z.string()).default([]),
include: z.array(z.string()).optional(),
});
export const zReportOptions = z.discriminatedUnion('type', [
zFunnelOptions,
zRetentionOptions,
zSankeyOptions,
]);
export type IReportOptions = z.infer<typeof zReportOptions>;
export type ISankeyOptions = z.infer<typeof zSankeyOptions>;
export const zWidgetType = z.enum(['realtime', 'counter']);
export type IWidgetType = z.infer<typeof zWidgetType>;
export const zRealtimeWidgetOptions = z.object({
type: z.literal('realtime'),
referrers: z.boolean().default(true),
countries: z.boolean().default(true),
paths: z.boolean().default(false),
});
export const zCounterWidgetOptions = z.object({
type: z.literal('counter'),
});
export const zWidgetOptions = z.discriminatedUnion('type', [
zRealtimeWidgetOptions,
zCounterWidgetOptions,
]);
export type IWidgetOptions = z.infer<typeof zWidgetOptions>;
export type ICounterWidgetOptions = z.infer<typeof zCounterWidgetOptions>;
export type IRealtimeWidgetOptions = z.infer<typeof zRealtimeWidgetOptions>;
// Base input schema - for API calls, engine, chart queries
export const zReportInput = z.object({
projectId: z.string().describe('The ID of the project this chart belongs to'),
chartType: zChartType
.default('linear')
.describe('What type of chart should be displayed'),
@@ -153,6 +178,18 @@ export const zChartInputBase = z.object({
range: zRange
.default('30d')
.describe('The time range for which data should be displayed'),
startDate: z
.string()
.nullish()
.describe(
'Custom start date for the data range (overrides range if provided)',
),
endDate: z
.string()
.nullish()
.describe(
'Custom end date for the data range (overrides range if provided)',
),
previous: z
.boolean()
.default(false)
@@ -166,19 +203,6 @@ export const zChartInputBase = z.object({
.describe(
'The aggregation method for the metric (e.g., sum, count, average)',
),
projectId: z.string().describe('The ID of the project this chart belongs to'),
startDate: z
.string()
.nullish()
.describe(
'Custom start date for the data range (overrides range if provided)',
),
endDate: z
.string()
.nullish()
.describe(
'Custom end date for the data range (overrides range if provided)',
),
limit: z
.number()
.optional()
@@ -187,32 +211,14 @@ export const zChartInputBase = z.object({
.number()
.optional()
.describe('Skip how many series should be returned'),
criteria: zCriteria
options: zReportOptions
.optional()
.describe('Filtering criteria for retention chart (e.g., on_or_after, on)'),
funnelGroup: z
.string()
.describe('Chart-specific options (funnel, retention, sankey)'),
// Optional display fields
name: z.string().optional().describe('The user-defined name for the report'),
lineType: zLineType
.optional()
.describe(
'Group identifier for funnel analysis, e.g. "profile_id" or "session_id"',
),
funnelWindow: z
.number()
.optional()
.describe('Time window in hours for funnel analysis'),
});
export const zChartInput = z.preprocess((val) => {
if (val && typeof val === 'object' && 'events' in val && !('series' in val)) {
// Migrate old 'events' field to 'series'
return { ...val, series: val.events };
}
return val;
}, zChartInputBase);
export const zReportInput = zChartInputBase.extend({
name: z.string().describe('The user-defined name for the report'),
lineType: zLineType.describe('The visual style of the line in the chart'),
.describe('The visual style of the line in the chart'),
unit: z
.string()
.optional()
@@ -221,17 +227,19 @@ export const zReportInput = zChartInputBase.extend({
),
});
export const zChartInputAI = zReportInput
.omit({
startDate: true,
endDate: true,
lineType: true,
unit: true,
})
.extend({
startDate: z.string().describe('The start date for the report'),
endDate: z.string().describe('The end date for the report'),
});
// Complete report schema - for saved reports
export const zReport = zReportInput.extend({
name: z
.string()
.default('Untitled')
.describe('The user-defined name for the report'),
lineType: zLineType
.default('monotone')
.describe('The visual style of the line in the chart'),
});
// Alias for backward compatibility
export const zChartInput = zReportInput;
export const zInviteUser = z.object({
email: z.string().email(),
@@ -247,6 +255,22 @@ export const zShareOverview = z.object({
public: z.boolean(),
});
export const zShareDashboard = z.object({
organizationId: z.string(),
projectId: z.string(),
dashboardId: z.string(),
password: z.string().nullable(),
public: z.boolean(),
});
export const zShareReport = z.object({
organizationId: z.string(),
projectId: z.string(),
reportId: z.string(),
password: z.string().nullable(),
public: z.boolean(),
});
export const zCreateReference = z.object({
title: z.string(),
description: z.string().nullish(),
@@ -486,6 +510,10 @@ export type IRequestResetPassword = z.infer<typeof zRequestResetPassword>;
export const zSignInShare = z.object({
password: z.string().min(1),
shareId: z.string().min(1),
shareType: z
.enum(['overview', 'dashboard', 'report'])
.optional()
.default('overview'),
});
export type ISignInShare = z.infer<typeof zSignInShare>;

View File

@@ -1,28 +0,0 @@
import { zChartEvents } from '.';
const events = [
{
id: 'sAmT',
type: 'event',
name: 'session_end',
segment: 'event',
filters: [],
},
{
id: '5K2v',
type: 'event',
name: 'session_start',
segment: 'event',
filters: [],
},
{
id: 'lQiQ',
type: 'formula',
formula: 'A/B',
displayName: '',
},
];
const res = zChartEvents.safeParse(events);
console.log(res);

View File

@@ -10,26 +10,28 @@ import type {
zChartEventItem,
zChartEventSegment,
zChartFormula,
zChartInput,
zChartInputAI,
zChartSeries,
zChartType,
zCriteria,
zLineType,
zMetric,
zRange,
zReport,
zReportInput,
zTimeInterval,
} from './index';
export type IChartInput = z.infer<typeof zChartInput>;
export type IChartInputAi = z.infer<typeof zChartInputAI>;
export type IChartProps = z.infer<typeof zReportInput> & {
name: string;
lineType: IChartLineType;
unit?: string;
previousIndicatorInverted?: boolean;
};
// For saved reports - complete report with required display fields
export type IReport = z.infer<typeof zReport>;
// For API/engine use - flexible input
export type IReportInput = z.infer<typeof zReportInput>;
// With resolved dates (engine internal)
export interface IReportInputWithDates extends IReportInput {
startDate: string;
endDate: string;
}
export type IChartEvent = z.infer<typeof zChartEvent>;
export type IChartFormula = z.infer<typeof zChartFormula>;
export type IChartEventItem = z.infer<typeof zChartEventItem>;
@@ -48,16 +50,12 @@ export type IChartType = z.infer<typeof zChartType>;
export type IChartMetric = z.infer<typeof zMetric>;
export type IChartLineType = z.infer<typeof zLineType>;
export type IChartRange = z.infer<typeof zRange>;
export interface IChartInputWithDates extends IChartInput {
startDate: string;
endDate: string;
}
export type IGetChartDataInput = {
event: IChartEvent;
projectId: string;
startDate: string;
endDate: string;
} & Omit<IChartInput, 'series' | 'name' | 'startDate' | 'endDate' | 'range'>;
} & Omit<IReportInput, 'series' | 'startDate' | 'endDate' | 'range'>;
export type ICriteria = z.infer<typeof zCriteria>;
export type PreviousValue =