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,11 +1,23 @@
import ShortUniqueId from 'short-unique-id';
import { db } from '@openpanel/db';
import { zShareOverview } from '@openpanel/validation';
import {
db,
getReportById,
getReportsByDashboardId,
getShareDashboardById,
getShareReportById,
transformReport,
} from '@openpanel/db';
import {
zShareDashboard,
zShareOverview,
zShareReport,
} from '@openpanel/validation';
import { hashPassword } from '@openpanel/auth';
import { z } from 'zod';
import { TRPCNotFoundError } from '../errors';
import { getProjectAccess } from '../access';
import { TRPCAccessError, TRPCNotFoundError } from '../errors';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
const uid = new ShortUniqueId({ length: 6 });
@@ -85,4 +97,203 @@ export const shareRouter = createTRPCRouter({
},
});
}),
// Dashboard sharing
dashboard: publicProcedure
.input(
z
.object({
dashboardId: z.string(),
})
.or(
z.object({
shareId: z.string(),
}),
),
)
.query(async ({ input, ctx }) => {
const share = await db.shareDashboard.findUnique({
include: {
organization: {
select: {
name: true,
},
},
project: {
select: {
name: true,
},
},
dashboard: {
select: {
name: true,
},
},
},
where:
'dashboardId' in input
? {
dashboardId: input.dashboardId,
}
: {
id: input.shareId,
},
});
if (!share) {
if ('shareId' in input) {
throw TRPCNotFoundError('Dashboard share not found');
}
return null;
}
return {
...share,
hasAccess: !!ctx.cookies[`shared-dashboard-${share?.id}`],
};
}),
createDashboard: protectedProcedure
.input(zShareDashboard)
.mutation(async ({ input, ctx }) => {
const access = await getProjectAccess({
projectId: input.projectId,
userId: ctx.session.userId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const passwordHash = input.password
? await hashPassword(input.password)
: null;
return db.shareDashboard.upsert({
where: {
dashboardId: input.dashboardId,
},
create: {
id: uid.rnd(),
organizationId: input.organizationId,
projectId: input.projectId,
dashboardId: input.dashboardId,
public: input.public,
password: passwordHash,
},
update: {
public: input.public,
password: passwordHash,
},
});
}),
dashboardReports: publicProcedure
.input(
z.object({
shareId: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const share = await getShareDashboardById(input.shareId);
if (!share || !share.public) {
throw TRPCNotFoundError('Dashboard share not found');
}
// Check password access
const hasAccess = !!ctx.cookies[`shared-dashboard-${share.id}`];
if (share.password && !hasAccess) {
throw TRPCAccessError('Password required');
}
return getReportsByDashboardId(share.dashboardId);
}),
// Report sharing
report: publicProcedure
.input(
z
.object({
reportId: z.string(),
})
.or(
z.object({
shareId: z.string(),
}),
),
)
.query(async ({ input, ctx }) => {
const share = await db.shareReport.findUnique({
include: {
organization: {
select: {
name: true,
},
},
project: {
select: {
name: true,
},
},
report: true,
},
where:
'reportId' in input
? {
reportId: input.reportId,
}
: {
id: input.shareId,
},
});
if (!share) {
if ('shareId' in input) {
throw TRPCNotFoundError('Report share not found');
}
return null;
}
return {
...share,
hasAccess: !!ctx.cookies[`shared-report-${share?.id}`],
report: transformReport(share.report),
};
}),
createReport: protectedProcedure
.input(zShareReport)
.mutation(async ({ input, ctx }) => {
const access = await getProjectAccess({
projectId: input.projectId,
userId: ctx.session.userId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const passwordHash = input.password
? await hashPassword(input.password)
: null;
return db.shareReport.upsert({
where: {
reportId: input.reportId,
},
create: {
id: uid.rnd(),
organizationId: input.organizationId,
projectId: input.projectId,
reportId: input.reportId,
public: input.public,
password: passwordHash,
},
update: {
public: input.public,
password: passwordHash,
},
});
}),
});