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

@@ -1,6 +1,5 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { endOfDay, format, isSameDay, isSameMonth, startOfDay } from 'date-fns';
import { shortId } from '@openpanel/common';
import {
@@ -12,18 +11,19 @@ import {
import type {
IChartBreakdown,
IChartEventItem,
IChartFormula,
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;
@@ -34,7 +34,6 @@ type InitialState = IChartProps & {
const initialState: InitialState = {
ready: false,
dirty: false,
// TODO: remove this
projectId: '',
name: '',
chartType: 'linear',
@@ -50,9 +49,7 @@ const initialState: InitialState = {
unit: undefined,
metric: 'sum',
limit: 500,
criteria: 'on_or_after',
funnelGroup: undefined,
funnelWindow: undefined,
options: undefined,
};
export const reportSlice = createSlice({
@@ -74,7 +71,7 @@ export const reportSlice = createSlice({
ready: true,
};
},
setReport(state, action: PayloadAction<IChartProps>) {
setReport(state, action: PayloadAction<IReport>) {
return {
...state,
...action.payload,
@@ -187,6 +184,16 @@ export const reportSlice = createSlice({
state.dirty = true;
state.chartType = action.payload;
// Initialize sankey options if switching to sankey
if (action.payload === 'sankey' && !state.options) {
state.options = {
type: 'sankey',
mode: 'after',
steps: 5,
exclude: [],
};
}
if (
!isMinuteIntervalEnabledByRange(state.range) &&
state.interval === 'minute'
@@ -254,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>) {
@@ -264,12 +278,88 @@ 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;
state.options = action.payload || undefined;
},
changeSankeyMode(
state,
action: PayloadAction<'between' | 'after' | 'before'>,
) {
state.dirty = true;
if (!state.options) {
state.options = {
type: 'sankey',
mode: action.payload,
steps: 5,
exclude: [],
};
} else if (state.options.type === 'sankey') {
state.options.mode = action.payload;
}
},
changeSankeySteps(state, action: PayloadAction<number>) {
state.dirty = true;
if (!state.options) {
state.options = {
type: 'sankey',
mode: 'after',
steps: action.payload,
exclude: [],
};
} else if (state.options.type === 'sankey') {
state.options.steps = action.payload;
}
},
changeSankeyExclude(state, action: PayloadAction<string[]>) {
state.dirty = true;
if (!state.options) {
state.options = {
type: 'sankey',
mode: 'after',
steps: 5,
exclude: action.payload,
};
} else if (state.options.type === 'sankey') {
state.options.exclude = action.payload;
}
},
changeSankeyInclude(state, action: PayloadAction<string[] | undefined>) {
state.dirty = true;
if (!state.options) {
state.options = {
type: 'sankey',
mode: 'after',
steps: 5,
exclude: [],
include: action.payload,
};
} else if (state.options.type === 'sankey') {
state.options.include = action.payload;
}
},
reorderEvents(
state,
@@ -311,6 +401,11 @@ export const {
changeUnit,
changeFunnelGroup,
changeFunnelWindow,
changeOptions,
changeSankeyMode,
changeSankeySteps,
changeSankeyExclude,
changeSankeyInclude,
reorderEvents,
} = reportSlice.actions;