wip
This commit is contained in:
@@ -8,7 +8,13 @@ import { LogoSquare } from '../logo';
|
|||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
|
||||||
export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
export function ShareEnterPassword({
|
||||||
|
shareId,
|
||||||
|
shareType = 'overview',
|
||||||
|
}: {
|
||||||
|
shareId: string;
|
||||||
|
shareType?: 'overview' | 'dashboard' | 'report';
|
||||||
|
}) {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
trpc.auth.signInShare.mutationOptions({
|
trpc.auth.signInShare.mutationOptions({
|
||||||
@@ -25,6 +31,7 @@ export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
password: '',
|
password: '',
|
||||||
shareId,
|
shareId,
|
||||||
|
shareType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,6 +39,7 @@ export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
|||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
password: data.password,
|
password: data.password,
|
||||||
shareId,
|
shareId,
|
||||||
|
shareType,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,9 +48,20 @@ export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
|||||||
<div className="bg-background p-6 rounded-lg max-w-md w-full text-left">
|
<div className="bg-background p-6 rounded-lg max-w-md w-full text-left">
|
||||||
<div className="col mt-1 flex-1 gap-2">
|
<div className="col mt-1 flex-1 gap-2">
|
||||||
<LogoSquare className="size-12 mb-4" />
|
<LogoSquare className="size-12 mb-4" />
|
||||||
<div className="text-xl font-semibold">Overview is locked</div>
|
<div className="text-xl font-semibold">
|
||||||
|
{shareType === 'dashboard'
|
||||||
|
? 'Dashboard is locked'
|
||||||
|
: shareType === 'report'
|
||||||
|
? 'Report is locked'
|
||||||
|
: 'Overview is locked'}
|
||||||
|
</div>
|
||||||
<div className="text-lg text-muted-foreground leading-normal">
|
<div className="text-lg text-muted-foreground leading-normal">
|
||||||
Please enter correct password to access this overview
|
Please enter correct password to access this{' '}
|
||||||
|
{shareType === 'dashboard'
|
||||||
|
? 'dashboard'
|
||||||
|
: shareType === 'report'
|
||||||
|
? 'report'
|
||||||
|
: 'overview'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={onSubmit} className="col gap-4 mt-6">
|
<form onSubmit={onSubmit} className="col gap-4 mt-6">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -9,11 +10,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportAreaChart() {
|
export function ReportAreaChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.chart.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.chartByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.chart.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
@@ -9,11 +10,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportBarChart() {
|
export function ReportBarChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.aggregate.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.aggregateByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.aggregate.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ export type ReportChartContextType = {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}[];
|
}[];
|
||||||
}>;
|
}>;
|
||||||
report: IChartProps;
|
report: IChartProps & { id?: string };
|
||||||
isLazyLoading: boolean;
|
isLazyLoading: boolean;
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
|
shareId?: string;
|
||||||
|
shareType?: 'dashboard' | 'report';
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReportChartContextProviderProps = ReportChartContextType & {
|
type ReportChartContextProviderProps = ReportChartContextType & {
|
||||||
@@ -40,6 +42,8 @@ type ReportChartContextProviderProps = ReportChartContextType & {
|
|||||||
export type ReportChartProps = Partial<ReportChartContextType> & {
|
export type ReportChartProps = Partial<ReportChartContextType> & {
|
||||||
report: IChartInput;
|
report: IChartInput;
|
||||||
lazy?: boolean;
|
lazy?: boolean;
|
||||||
|
shareId?: string;
|
||||||
|
shareType?: 'dashboard' | 'report';
|
||||||
};
|
};
|
||||||
|
|
||||||
const context = createContext<ReportChartContextType | null>(null);
|
const context = createContext<ReportChartContextType | null>(null);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
@@ -11,11 +12,29 @@ import { Chart } from './chart';
|
|||||||
import { Summary } from './summary';
|
import { Summary } from './summary';
|
||||||
|
|
||||||
export function ReportConversionChart() {
|
export function ReportConversionChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
console.log(report.limit);
|
console.log(report.limit);
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.conversion.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.conversionByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.conversion.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useTRPC } from '@/integrations/trpc/react';
|
|||||||
import type { RouterOutputs } from '@/trpc/client';
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
@@ -14,6 +15,7 @@ import { Chart, Summary, Tables } from './chart';
|
|||||||
export function ReportFunnelChart() {
|
export function ReportFunnelChart() {
|
||||||
const {
|
const {
|
||||||
report: {
|
report: {
|
||||||
|
id,
|
||||||
series,
|
series,
|
||||||
range,
|
range,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -25,8 +27,29 @@ export function ReportFunnelChart() {
|
|||||||
breakdowns,
|
breakdowns,
|
||||||
},
|
},
|
||||||
isLazyLoading,
|
isLazyLoading,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
} = useReportChartContext();
|
} = useReportChartContext();
|
||||||
|
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||||
|
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const res = useQuery(
|
||||||
|
shareId && shareType && id
|
||||||
|
? trpc.chart.funnelByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: overviewRange ?? undefined,
|
||||||
|
startDate: overviewStartDate ?? undefined,
|
||||||
|
endDate: overviewEndDate ?? undefined,
|
||||||
|
interval: overviewInterval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !isLazyLoading && series.length > 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: (() => {
|
||||||
const input: IChartInput = {
|
const input: IChartInput = {
|
||||||
series,
|
series,
|
||||||
range,
|
range,
|
||||||
@@ -42,11 +65,10 @@ export function ReportFunnelChart() {
|
|||||||
endDate,
|
endDate,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
};
|
};
|
||||||
const trpc = useTRPC();
|
return trpc.chart.funnel.queryOptions(input, {
|
||||||
const res = useQuery(
|
|
||||||
trpc.chart.funnel.queryOptions(input, {
|
|
||||||
enabled: !isLazyLoading && input.series.length > 0,
|
enabled: !isLazyLoading && input.series.length > 0,
|
||||||
}),
|
});
|
||||||
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLazyLoading || res.isLoading) {
|
if (isLazyLoading || res.isLoading) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -9,11 +10,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportHistogramChart() {
|
export function ReportHistogramChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.chart.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.chartByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.chart.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useTRPC } from '@/integrations/trpc/react';
|
|||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -10,11 +11,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportLineChart() {
|
export function ReportLineChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.chart.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.chartByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.chart.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -9,11 +10,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportMapChart() {
|
export function ReportMapChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.chart.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.chartByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.chart.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -8,11 +9,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportMetricChart() {
|
export function ReportMetricChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.chart.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.chartByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.chart.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -9,11 +10,29 @@ import { useReportChartContext } from '../context';
|
|||||||
import { Chart } from './chart';
|
import { Chart } from './chart';
|
||||||
|
|
||||||
export function ReportPieChart() {
|
export function ReportPieChart() {
|
||||||
const { isLazyLoading, report } = useReportChartContext();
|
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.aggregate.queryOptions(report, {
|
shareId && shareType && 'id' in report && report.id
|
||||||
|
? trpc.chart.aggregateByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: report.id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: range ?? undefined,
|
||||||
|
startDate: startDate ?? undefined,
|
||||||
|
endDate: endDate ?? undefined,
|
||||||
|
interval: interval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: !isLazyLoading,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.aggregate.queryOptions(report, {
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
enabled: !isLazyLoading,
|
enabled: !isLazyLoading,
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import { TimeWindowPicker } from '@/components/time-window-picker';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { GanttChartSquareIcon } from 'lucide-react';
|
import { bind } from 'bind-event-listener';
|
||||||
|
import { GanttChartSquareIcon, ShareIcon } from 'lucide-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import type { IServiceReport } from '@openpanel/db';
|
import type { IServiceReport } from '@openpanel/db';
|
||||||
@@ -52,8 +54,19 @@ export default function ReportEditor({
|
|||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<div>
|
<div>
|
||||||
<div className="p-4">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<EditReportName />
|
<EditReportName />
|
||||||
|
{initialReport?.id && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
icon={ShareIcon}
|
||||||
|
onClick={() =>
|
||||||
|
pushModal('ShareReportModal', { reportId: initialReport.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 p-4 pt-0 md:grid-cols-6">
|
<div className="grid grid-cols-2 gap-2 p-4 pt-0 md:grid-cols-6">
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { AspectContainer } from '../aspect-container';
|
import { AspectContainer } from '../aspect-container';
|
||||||
import { ReportChartEmpty } from '../common/empty';
|
import { ReportChartEmpty } from '../common/empty';
|
||||||
import { ReportChartError } from '../common/error';
|
import { ReportChartError } from '../common/error';
|
||||||
@@ -12,6 +13,7 @@ import CohortTable from './table';
|
|||||||
export function ReportRetentionChart() {
|
export function ReportRetentionChart() {
|
||||||
const {
|
const {
|
||||||
report: {
|
report: {
|
||||||
|
id,
|
||||||
series,
|
series,
|
||||||
range,
|
range,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -21,7 +23,10 @@ export function ReportRetentionChart() {
|
|||||||
interval,
|
interval,
|
||||||
},
|
},
|
||||||
isLazyLoading,
|
isLazyLoading,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
} = useReportChartContext();
|
} = useReportChartContext();
|
||||||
|
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||||
const eventSeries = series.filter((item) => item.type === 'event');
|
const eventSeries = series.filter((item) => item.type === 'event');
|
||||||
const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(String);
|
const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(String);
|
||||||
const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(String);
|
const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(String);
|
||||||
@@ -29,7 +34,24 @@ export function ReportRetentionChart() {
|
|||||||
firstEvent.length > 0 && secondEvent.length > 0 && !isLazyLoading;
|
firstEvent.length > 0 && secondEvent.length > 0 && !isLazyLoading;
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const res = useQuery(
|
const res = useQuery(
|
||||||
trpc.chart.cohort.queryOptions(
|
shareId && shareType && id
|
||||||
|
? trpc.chart.cohortByReport.queryOptions(
|
||||||
|
{
|
||||||
|
reportId: id,
|
||||||
|
shareId,
|
||||||
|
shareType,
|
||||||
|
range: overviewRange ?? undefined,
|
||||||
|
startDate: overviewStartDate ?? undefined,
|
||||||
|
endDate: overviewEndDate ?? undefined,
|
||||||
|
interval: overviewInterval ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
staleTime: 1000 * 60 * 1,
|
||||||
|
enabled: isEnabled,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: trpc.chart.cohort.queryOptions(
|
||||||
{
|
{
|
||||||
firstEvent,
|
firstEvent,
|
||||||
secondEvent,
|
secondEvent,
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ import OverviewFilters from './overview-filters';
|
|||||||
import RequestPasswordReset from './request-reset-password';
|
import RequestPasswordReset from './request-reset-password';
|
||||||
import SaveReport from './save-report';
|
import SaveReport from './save-report';
|
||||||
import SelectBillingPlan from './select-billing-plan';
|
import SelectBillingPlan from './select-billing-plan';
|
||||||
|
import ShareDashboardModal from './share-dashboard-modal';
|
||||||
import ShareOverviewModal from './share-overview-modal';
|
import ShareOverviewModal from './share-overview-modal';
|
||||||
|
import ShareReportModal from './share-report-modal';
|
||||||
import ViewChartUsers from './view-chart-users';
|
import ViewChartUsers from './view-chart-users';
|
||||||
|
|
||||||
const modals = {
|
const modals = {
|
||||||
@@ -51,6 +53,8 @@ const modals = {
|
|||||||
EditReport: EditReport,
|
EditReport: EditReport,
|
||||||
EditReference: EditReference,
|
EditReference: EditReference,
|
||||||
ShareOverviewModal: ShareOverviewModal,
|
ShareOverviewModal: ShareOverviewModal,
|
||||||
|
ShareDashboardModal: ShareDashboardModal,
|
||||||
|
ShareReportModal: ShareReportModal,
|
||||||
AddReference: AddReference,
|
AddReference: AddReference,
|
||||||
ViewChartUsers: ViewChartUsers,
|
ViewChartUsers: ViewChartUsers,
|
||||||
Instructions: Instructions,
|
Instructions: Instructions,
|
||||||
|
|||||||
97
apps/start/src/modals/share-dashboard-modal.tsx
Normal file
97
apps/start/src/modals/share-dashboard-modal.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
|
import { handleError } from '@/integrations/trpc/react';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { zShareDashboard } from '@openpanel/validation';
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { popModal } from '.';
|
||||||
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
const validator = zShareDashboard;
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validator>;
|
||||||
|
|
||||||
|
export default function ShareDashboardModal({
|
||||||
|
dashboardId,
|
||||||
|
}: {
|
||||||
|
dashboardId: string;
|
||||||
|
}) {
|
||||||
|
const { projectId, organizationId } = useAppParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { register, handleSubmit } = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validator),
|
||||||
|
defaultValues: {
|
||||||
|
public: true,
|
||||||
|
password: '',
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
dashboardId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation(
|
||||||
|
trpc.share.createDashboard.mutationOptions({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess(res) {
|
||||||
|
queryClient.invalidateQueries(trpc.share.dashboard.pathFilter());
|
||||||
|
toast('Success', {
|
||||||
|
description: `Your dashboard is now ${
|
||||||
|
res.public ? 'public' : 'private'
|
||||||
|
}`,
|
||||||
|
action: {
|
||||||
|
label: 'View',
|
||||||
|
onClick: () =>
|
||||||
|
navigate({
|
||||||
|
to: '/share/dashboard/$shareId',
|
||||||
|
params: {
|
||||||
|
shareId: res.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
popModal();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent className="max-w-md">
|
||||||
|
<ModalHeader
|
||||||
|
title="Dashboard public availability"
|
||||||
|
text="You can choose if you want to add a password to make it a bit more private."
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((values) => {
|
||||||
|
mutation.mutate(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('password')}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button type="button" variant="outline" onClick={() => popModal()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={mutation.isPending}>
|
||||||
|
Make it public
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
91
apps/start/src/modals/share-report-modal.tsx
Normal file
91
apps/start/src/modals/share-report-modal.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
|
import { handleError } from '@/integrations/trpc/react';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { zShareReport } from '@openpanel/validation';
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { popModal } from '.';
|
||||||
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
const validator = zShareReport;
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validator>;
|
||||||
|
|
||||||
|
export default function ShareReportModal({ reportId }: { reportId: string }) {
|
||||||
|
const { projectId, organizationId } = useAppParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { register, handleSubmit } = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validator),
|
||||||
|
defaultValues: {
|
||||||
|
public: true,
|
||||||
|
password: '',
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
reportId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation(
|
||||||
|
trpc.share.createReport.mutationOptions({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess(res) {
|
||||||
|
queryClient.invalidateQueries(trpc.share.report.pathFilter());
|
||||||
|
toast('Success', {
|
||||||
|
description: `Your report is now ${res.public ? 'public' : 'private'}`,
|
||||||
|
action: {
|
||||||
|
label: 'View',
|
||||||
|
onClick: () =>
|
||||||
|
navigate({
|
||||||
|
to: '/share/report/$shareId',
|
||||||
|
params: {
|
||||||
|
shareId: res.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
popModal();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent className="max-w-md">
|
||||||
|
<ModalHeader
|
||||||
|
title="Report public availability"
|
||||||
|
text="You can choose if you want to add a password to make it a bit more private."
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((values) => {
|
||||||
|
mutation.mutate(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('password')}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button type="button" variant="outline" onClick={() => popModal()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={mutation.isPending}>
|
||||||
|
Make it public
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,9 @@ import { Route as LoginResetPasswordRouteImport } from './routes/_login.reset-pa
|
|||||||
import { Route as LoginLoginRouteImport } from './routes/_login.login'
|
import { Route as LoginLoginRouteImport } from './routes/_login.login'
|
||||||
import { Route as AppOrganizationIdRouteImport } from './routes/_app.$organizationId'
|
import { Route as AppOrganizationIdRouteImport } from './routes/_app.$organizationId'
|
||||||
import { Route as AppOrganizationIdIndexRouteImport } from './routes/_app.$organizationId.index'
|
import { Route as AppOrganizationIdIndexRouteImport } from './routes/_app.$organizationId.index'
|
||||||
|
import { Route as ShareReportShareIdRouteImport } from './routes/share.report.$shareId'
|
||||||
import { Route as ShareOverviewShareIdRouteImport } from './routes/share.overview.$shareId'
|
import { Route as ShareOverviewShareIdRouteImport } from './routes/share.overview.$shareId'
|
||||||
|
import { Route as ShareDashboardShareIdRouteImport } from './routes/share.dashboard.$shareId'
|
||||||
import { Route as StepsOnboardingProjectRouteImport } from './routes/_steps.onboarding.project'
|
import { Route as StepsOnboardingProjectRouteImport } from './routes/_steps.onboarding.project'
|
||||||
import { Route as AppOrganizationIdSettingsRouteImport } from './routes/_app.$organizationId.settings'
|
import { Route as AppOrganizationIdSettingsRouteImport } from './routes/_app.$organizationId.settings'
|
||||||
import { Route as AppOrganizationIdBillingRouteImport } from './routes/_app.$organizationId.billing'
|
import { Route as AppOrganizationIdBillingRouteImport } from './routes/_app.$organizationId.billing'
|
||||||
@@ -164,11 +166,21 @@ const AppOrganizationIdIndexRoute = AppOrganizationIdIndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => AppOrganizationIdRoute,
|
getParentRoute: () => AppOrganizationIdRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ShareReportShareIdRoute = ShareReportShareIdRouteImport.update({
|
||||||
|
id: '/share/report/$shareId',
|
||||||
|
path: '/share/report/$shareId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ShareOverviewShareIdRoute = ShareOverviewShareIdRouteImport.update({
|
const ShareOverviewShareIdRoute = ShareOverviewShareIdRouteImport.update({
|
||||||
id: '/share/overview/$shareId',
|
id: '/share/overview/$shareId',
|
||||||
path: '/share/overview/$shareId',
|
path: '/share/overview/$shareId',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ShareDashboardShareIdRoute = ShareDashboardShareIdRouteImport.update({
|
||||||
|
id: '/share/dashboard/$shareId',
|
||||||
|
path: '/share/dashboard/$shareId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const StepsOnboardingProjectRoute = StepsOnboardingProjectRouteImport.update({
|
const StepsOnboardingProjectRoute = StepsOnboardingProjectRouteImport.update({
|
||||||
id: '/onboarding/project',
|
id: '/onboarding/project',
|
||||||
path: '/onboarding/project',
|
path: '/onboarding/project',
|
||||||
@@ -498,7 +510,9 @@ export interface FileRoutesByFullPath {
|
|||||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||||
|
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||||
|
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||||
'/$organizationId/': typeof AppOrganizationIdIndexRoute
|
'/$organizationId/': typeof AppOrganizationIdIndexRoute
|
||||||
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||||
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||||
@@ -556,7 +570,9 @@ export interface FileRoutesByTo {
|
|||||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||||
|
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||||
|
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||||
'/$organizationId': typeof AppOrganizationIdIndexRoute
|
'/$organizationId': typeof AppOrganizationIdIndexRoute
|
||||||
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||||
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||||
@@ -614,7 +630,9 @@ export interface FileRoutesById {
|
|||||||
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
'/_steps/onboarding/project': typeof StepsOnboardingProjectRoute
|
'/_steps/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||||
|
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||||
|
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||||
'/_app/$organizationId/': typeof AppOrganizationIdIndexRoute
|
'/_app/$organizationId/': typeof AppOrganizationIdIndexRoute
|
||||||
'/_app/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
'/_app/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||||
'/_app/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
'/_app/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||||
@@ -683,7 +701,9 @@ export interface FileRouteTypes {
|
|||||||
| '/$organizationId/billing'
|
| '/$organizationId/billing'
|
||||||
| '/$organizationId/settings'
|
| '/$organizationId/settings'
|
||||||
| '/onboarding/project'
|
| '/onboarding/project'
|
||||||
|
| '/share/dashboard/$shareId'
|
||||||
| '/share/overview/$shareId'
|
| '/share/overview/$shareId'
|
||||||
|
| '/share/report/$shareId'
|
||||||
| '/$organizationId/'
|
| '/$organizationId/'
|
||||||
| '/$organizationId/$projectId/chat'
|
| '/$organizationId/$projectId/chat'
|
||||||
| '/$organizationId/$projectId/dashboards'
|
| '/$organizationId/$projectId/dashboards'
|
||||||
@@ -741,7 +761,9 @@ export interface FileRouteTypes {
|
|||||||
| '/$organizationId/billing'
|
| '/$organizationId/billing'
|
||||||
| '/$organizationId/settings'
|
| '/$organizationId/settings'
|
||||||
| '/onboarding/project'
|
| '/onboarding/project'
|
||||||
|
| '/share/dashboard/$shareId'
|
||||||
| '/share/overview/$shareId'
|
| '/share/overview/$shareId'
|
||||||
|
| '/share/report/$shareId'
|
||||||
| '/$organizationId'
|
| '/$organizationId'
|
||||||
| '/$organizationId/$projectId/chat'
|
| '/$organizationId/$projectId/chat'
|
||||||
| '/$organizationId/$projectId/dashboards'
|
| '/$organizationId/$projectId/dashboards'
|
||||||
@@ -798,7 +820,9 @@ export interface FileRouteTypes {
|
|||||||
| '/_app/$organizationId/billing'
|
| '/_app/$organizationId/billing'
|
||||||
| '/_app/$organizationId/settings'
|
| '/_app/$organizationId/settings'
|
||||||
| '/_steps/onboarding/project'
|
| '/_steps/onboarding/project'
|
||||||
|
| '/share/dashboard/$shareId'
|
||||||
| '/share/overview/$shareId'
|
| '/share/overview/$shareId'
|
||||||
|
| '/share/report/$shareId'
|
||||||
| '/_app/$organizationId/'
|
| '/_app/$organizationId/'
|
||||||
| '/_app/$organizationId/$projectId/chat'
|
| '/_app/$organizationId/$projectId/chat'
|
||||||
| '/_app/$organizationId/$projectId/dashboards'
|
| '/_app/$organizationId/$projectId/dashboards'
|
||||||
@@ -862,7 +886,9 @@ export interface RootRouteChildren {
|
|||||||
StepsRoute: typeof StepsRouteWithChildren
|
StepsRoute: typeof StepsRouteWithChildren
|
||||||
ApiConfigRoute: typeof ApiConfigRoute
|
ApiConfigRoute: typeof ApiConfigRoute
|
||||||
ApiHealthcheckRoute: typeof ApiHealthcheckRoute
|
ApiHealthcheckRoute: typeof ApiHealthcheckRoute
|
||||||
|
ShareDashboardShareIdRoute: typeof ShareDashboardShareIdRoute
|
||||||
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
|
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
|
||||||
|
ShareReportShareIdRoute: typeof ShareReportShareIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -965,6 +991,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppOrganizationIdIndexRouteImport
|
preLoaderRoute: typeof AppOrganizationIdIndexRouteImport
|
||||||
parentRoute: typeof AppOrganizationIdRoute
|
parentRoute: typeof AppOrganizationIdRoute
|
||||||
}
|
}
|
||||||
|
'/share/report/$shareId': {
|
||||||
|
id: '/share/report/$shareId'
|
||||||
|
path: '/share/report/$shareId'
|
||||||
|
fullPath: '/share/report/$shareId'
|
||||||
|
preLoaderRoute: typeof ShareReportShareIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/share/overview/$shareId': {
|
'/share/overview/$shareId': {
|
||||||
id: '/share/overview/$shareId'
|
id: '/share/overview/$shareId'
|
||||||
path: '/share/overview/$shareId'
|
path: '/share/overview/$shareId'
|
||||||
@@ -972,6 +1005,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ShareOverviewShareIdRouteImport
|
preLoaderRoute: typeof ShareOverviewShareIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/share/dashboard/$shareId': {
|
||||||
|
id: '/share/dashboard/$shareId'
|
||||||
|
path: '/share/dashboard/$shareId'
|
||||||
|
fullPath: '/share/dashboard/$shareId'
|
||||||
|
preLoaderRoute: typeof ShareDashboardShareIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/_steps/onboarding/project': {
|
'/_steps/onboarding/project': {
|
||||||
id: '/_steps/onboarding/project'
|
id: '/_steps/onboarding/project'
|
||||||
path: '/onboarding/project'
|
path: '/onboarding/project'
|
||||||
@@ -1751,7 +1791,9 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
StepsRoute: StepsRouteWithChildren,
|
StepsRoute: StepsRouteWithChildren,
|
||||||
ApiConfigRoute: ApiConfigRoute,
|
ApiConfigRoute: ApiConfigRoute,
|
||||||
ApiHealthcheckRoute: ApiHealthcheckRoute,
|
ApiHealthcheckRoute: ApiHealthcheckRoute,
|
||||||
|
ShareDashboardShareIdRoute: ShareDashboardShareIdRoute,
|
||||||
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
|
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
|
||||||
|
ShareReportShareIdRoute: ShareReportShareIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
ShareIcon,
|
||||||
Trash,
|
Trash,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -30,7 +31,7 @@ import { OverviewRange } from '@/components/overview/overview-range';
|
|||||||
import { PageContainer } from '@/components/page-container';
|
import { PageContainer } from '@/components/page-container';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { handleErrorToastOptions, useTRPC } from '@/integrations/trpc/react';
|
import { handleErrorToastOptions, useTRPC } from '@/integrations/trpc/react';
|
||||||
import { showConfirm } from '@/modals';
|
import { pushModal, showConfirm } from '@/modals';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@@ -484,6 +485,12 @@ function Component() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
<DropdownMenuContent align="end" className="w-[200px]">
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => pushModal('ShareDashboardModal', { dashboardId })}
|
||||||
|
>
|
||||||
|
<ShareIcon className="mr-2 size-4" />
|
||||||
|
Share dashboard
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showConfirm({
|
showConfirm({
|
||||||
|
|||||||
281
apps/start/src/routes/share.dashboard.$shareId.tsx
Normal file
281
apps/start/src/routes/share.dashboard.$shareId.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { ShareEnterPassword } from '@/components/auth/share-enter-password';
|
||||||
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
|
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||||
|
import { LoginNavbar } from '@/components/login-navbar';
|
||||||
|
import { ReportChart } from '@/components/report-chart';
|
||||||
|
import { OverviewRange } from '@/components/overview/overview-range';
|
||||||
|
import { OverviewInterval } from '@/components/overview/overview-interval';
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
|
import { PageContainer } from '@/components/page-container';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, notFound, useSearch } from '@tanstack/react-router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||||
|
import 'react-grid-layout/css/styles.css';
|
||||||
|
import 'react-resizable/css/styles.css';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
import { timeWindows } from '@openpanel/constants';
|
||||||
|
|
||||||
|
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
|
type Layout = {
|
||||||
|
i: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
minW?: number;
|
||||||
|
minH?: number;
|
||||||
|
maxW?: number;
|
||||||
|
maxH?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareSearchSchema = z.object({
|
||||||
|
header: z.optional(z.number().or(z.string().or(z.boolean()))),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/share/dashboard/$shareId')({
|
||||||
|
component: RouteComponent,
|
||||||
|
validateSearch: shareSearchSchema,
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
const share = await context.queryClient.ensureQueryData(
|
||||||
|
context.trpc.share.dashboard.queryOptions({
|
||||||
|
shareId: params.shareId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { share };
|
||||||
|
},
|
||||||
|
head: ({ loaderData }) => {
|
||||||
|
if (!loaderData || !loaderData.share) {
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: 'Share not found - OpenPanel.dev',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: `${loaderData.share.dashboard?.name} - ${loaderData.share.organization?.name} - OpenPanel.dev`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
pendingComponent: FullPageLoadingState,
|
||||||
|
errorComponent: () => (
|
||||||
|
<FullPageEmptyState
|
||||||
|
title="Share not found"
|
||||||
|
description="The dashboard you are looking for does not exist."
|
||||||
|
className="min-h-[calc(100vh-theme(spacing.16))]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Report Item Component for shared view
|
||||||
|
function ReportItem({
|
||||||
|
report,
|
||||||
|
shareId,
|
||||||
|
range,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
interval,
|
||||||
|
}: {
|
||||||
|
report: any;
|
||||||
|
shareId: string;
|
||||||
|
range: any;
|
||||||
|
startDate: any;
|
||||||
|
endDate: any;
|
||||||
|
interval: any;
|
||||||
|
}) {
|
||||||
|
const chartRange = report.range;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between border-b border-border p-4 leading-none">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{report.name}</div>
|
||||||
|
{chartRange !== null && (
|
||||||
|
<div className="mt-2 flex gap-2 ">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
(chartRange !== range && range !== null) ||
|
||||||
|
(startDate && endDate)
|
||||||
|
? 'line-through'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{timeWindows[chartRange as keyof typeof timeWindows]?.label}
|
||||||
|
</span>
|
||||||
|
{startDate && endDate ? (
|
||||||
|
<span>Custom dates</span>
|
||||||
|
) : (
|
||||||
|
range !== null &&
|
||||||
|
chartRange !== range && (
|
||||||
|
<span>
|
||||||
|
{timeWindows[range as keyof typeof timeWindows]?.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'p-4 overflow-auto flex-1',
|
||||||
|
report.chartType === 'metric' && 'p-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ReportChart
|
||||||
|
report={
|
||||||
|
{
|
||||||
|
...report,
|
||||||
|
range: range ?? report.range,
|
||||||
|
startDate: startDate ?? null,
|
||||||
|
endDate: endDate ?? null,
|
||||||
|
interval: interval ?? report.interval,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
shareId={shareId}
|
||||||
|
shareType="dashboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { shareId } = Route.useParams();
|
||||||
|
const { header } = useSearch({ from: '/share/dashboard/$shareId' });
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
|
|
||||||
|
const shareQuery = useSuspenseQuery(
|
||||||
|
trpc.share.dashboard.queryOptions({
|
||||||
|
shareId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportsQuery = useQuery(
|
||||||
|
trpc.share.dashboardReports.queryOptions({
|
||||||
|
shareId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAccess = shareQuery.data?.hasAccess;
|
||||||
|
|
||||||
|
if (!shareQuery.data) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shareQuery.data.public) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const share = shareQuery.data;
|
||||||
|
|
||||||
|
// Handle password protection
|
||||||
|
if (share.password && !hasAccess) {
|
||||||
|
return (
|
||||||
|
<ShareEnterPassword shareId={share.id} shareType="dashboard" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHeaderVisible =
|
||||||
|
header !== '0' && header !== 0 && header !== 'false' && header !== false;
|
||||||
|
|
||||||
|
const reports = reportsQuery.data ?? [];
|
||||||
|
|
||||||
|
// Convert reports to grid layout format for all breakpoints
|
||||||
|
const layouts = useMemo(() => {
|
||||||
|
const baseLayout = reports.map((report, index) => ({
|
||||||
|
i: report.id,
|
||||||
|
x: report.layout?.x ?? (index % 2) * 6,
|
||||||
|
y: report.layout?.y ?? Math.floor(index / 2) * 4,
|
||||||
|
w: report.layout?.w ?? 6,
|
||||||
|
h: report.layout?.h ?? 4,
|
||||||
|
minW: 3,
|
||||||
|
minH: 3,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create responsive layouts for different breakpoints
|
||||||
|
return {
|
||||||
|
lg: baseLayout,
|
||||||
|
md: baseLayout,
|
||||||
|
sm: baseLayout.map((item) => ({ ...item, w: Math.min(item.w, 6) })),
|
||||||
|
xs: baseLayout.map((item) => ({ ...item, w: 4, x: 0 })),
|
||||||
|
xxs: baseLayout.map((item) => ({ ...item, w: 2, x: 0 })),
|
||||||
|
};
|
||||||
|
}, [reports]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isHeaderVisible && (
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<LoginNavbar className="relative p-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PageContainer>
|
||||||
|
<div className="sticky-header [animation-range:50px_100px]!">
|
||||||
|
<div className="p-4 col gap-2 mx-auto max-w-7xl">
|
||||||
|
<div className="row justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<OverviewRange />
|
||||||
|
<OverviewInterval />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{reports.length === 0 ? (
|
||||||
|
<FullPageEmptyState title="No reports" />
|
||||||
|
) : (
|
||||||
|
<div className="w-full overflow-hidden -mx-4">
|
||||||
|
<style>{`
|
||||||
|
.react-grid-item {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
.react-grid-item.react-grid-placeholder {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<ResponsiveGridLayout
|
||||||
|
className="layout"
|
||||||
|
layouts={layouts}
|
||||||
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
|
cols={{ lg: 12, md: 12, sm: 6, xs: 4, xxs: 2 }}
|
||||||
|
rowHeight={100}
|
||||||
|
draggableHandle=".drag-handle"
|
||||||
|
compactType="vertical"
|
||||||
|
preventCollision={false}
|
||||||
|
isDraggable={false}
|
||||||
|
isResizable={false}
|
||||||
|
margin={[16, 16]}
|
||||||
|
transformScale={1}
|
||||||
|
useCSSTransforms={true}
|
||||||
|
>
|
||||||
|
{reports.map((report) => (
|
||||||
|
<div key={report.id}>
|
||||||
|
<ReportItem
|
||||||
|
report={report}
|
||||||
|
shareId={shareId}
|
||||||
|
range={range}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
interval={interval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ResponsiveGridLayout>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
142
apps/start/src/routes/share.report.$shareId.tsx
Normal file
142
apps/start/src/routes/share.report.$shareId.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { ShareEnterPassword } from '@/components/auth/share-enter-password';
|
||||||
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
|
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||||
|
import { LoginNavbar } from '@/components/login-navbar';
|
||||||
|
import { ReportChart } from '@/components/report-chart';
|
||||||
|
import { OverviewRange } from '@/components/overview/overview-range';
|
||||||
|
import { OverviewInterval } from '@/components/overview/overview-interval';
|
||||||
|
import { PageContainer } from '@/components/page-container';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, notFound, useSearch } from '@tanstack/react-router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const shareSearchSchema = z.object({
|
||||||
|
header: z.optional(z.number().or(z.string().or(z.boolean()))),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/share/report/$shareId')({
|
||||||
|
component: RouteComponent,
|
||||||
|
validateSearch: shareSearchSchema,
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
const share = await context.queryClient.ensureQueryData(
|
||||||
|
context.trpc.share.report.queryOptions({
|
||||||
|
shareId: params.shareId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
return { share: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = await context.queryClient.ensureQueryData(
|
||||||
|
context.trpc.report.get.queryOptions({
|
||||||
|
reportId: share.reportId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { share, report };
|
||||||
|
},
|
||||||
|
head: ({ loaderData }) => {
|
||||||
|
if (!loaderData || !loaderData.share) {
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: 'Share not found - OpenPanel.dev',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: `${loaderData.report?.name || 'Report'} - ${loaderData.share.organization?.name} - OpenPanel.dev`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
pendingComponent: FullPageLoadingState,
|
||||||
|
errorComponent: () => (
|
||||||
|
<FullPageEmptyState
|
||||||
|
title="Share not found"
|
||||||
|
description="The report you are looking for does not exist."
|
||||||
|
className="min-h-[calc(100vh-theme(spacing.16))]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { shareId } = Route.useParams();
|
||||||
|
const { header } = useSearch({ from: '/share/report/$shareId' });
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const shareQuery = useSuspenseQuery(
|
||||||
|
trpc.share.report.queryOptions({
|
||||||
|
shareId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportQuery = useSuspenseQuery(
|
||||||
|
trpc.report.get.queryOptions({
|
||||||
|
reportId: shareQuery.data!.reportId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAccess = shareQuery.data?.hasAccess;
|
||||||
|
|
||||||
|
if (!shareQuery.data) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shareQuery.data.public) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const share = shareQuery.data;
|
||||||
|
const report = reportQuery.data;
|
||||||
|
|
||||||
|
// Handle password protection
|
||||||
|
if (share.password && !hasAccess) {
|
||||||
|
return <ShareEnterPassword shareId={share.id} shareType="report" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHeaderVisible =
|
||||||
|
header !== '0' && header !== 0 && header !== 'false' && header !== false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isHeaderVisible && (
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<LoginNavbar className="relative p-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PageContainer>
|
||||||
|
<div className="sticky-header [animation-range:50px_100px]!">
|
||||||
|
<div className="p-4 col gap-2 mx-auto max-w-7xl">
|
||||||
|
<div className="row justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<OverviewRange />
|
||||||
|
<OverviewInterval />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-4 border-b">
|
||||||
|
<div className="font-medium text-xl">{report.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<ReportChart
|
||||||
|
report={report}
|
||||||
|
shareId={shareId}
|
||||||
|
shareType="report"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."share_dashboards" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"dashboardId" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"password" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."share_reports" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"reportId" UUID NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"password" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_dashboards_id_key" ON "public"."share_dashboards"("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_dashboards_dashboardId_key" ON "public"."share_dashboards"("dashboardId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_reports_id_key" ON "public"."share_reports"("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_reports_reportId_key" ON "public"."share_reports"("reportId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_dashboards" ADD CONSTRAINT "share_dashboards_dashboardId_fkey" FOREIGN KEY ("dashboardId") REFERENCES "public"."dashboards"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_dashboards" ADD CONSTRAINT "share_dashboards_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_dashboards" ADD CONSTRAINT "share_dashboards_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_reports" ADD CONSTRAINT "share_reports_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "public"."reports"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_reports" ADD CONSTRAINT "share_reports_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."share_reports" ADD CONSTRAINT "share_reports_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -56,6 +56,8 @@ model Organization {
|
|||||||
Client Client[]
|
Client Client[]
|
||||||
Dashboard Dashboard[]
|
Dashboard Dashboard[]
|
||||||
ShareOverview ShareOverview[]
|
ShareOverview ShareOverview[]
|
||||||
|
ShareDashboard ShareDashboard[]
|
||||||
|
ShareReport ShareReport[]
|
||||||
integrations Integration[]
|
integrations Integration[]
|
||||||
invites Invite[]
|
invites Invite[]
|
||||||
timezone String?
|
timezone String?
|
||||||
@@ -189,6 +191,8 @@ model Project {
|
|||||||
reports Report[]
|
reports Report[]
|
||||||
dashboards Dashboard[]
|
dashboards Dashboard[]
|
||||||
share ShareOverview?
|
share ShareOverview?
|
||||||
|
shareDashboards ShareDashboard[]
|
||||||
|
shareReports ShareReport[]
|
||||||
meta EventMeta[]
|
meta EventMeta[]
|
||||||
references Reference[]
|
references Reference[]
|
||||||
access ProjectAccess[]
|
access ProjectAccess[]
|
||||||
@@ -290,6 +294,7 @@ model Dashboard {
|
|||||||
projectId String
|
projectId String
|
||||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
share ShareDashboard?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
@@ -328,6 +333,7 @@ model Report {
|
|||||||
dashboardId String
|
dashboardId String
|
||||||
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
|
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
|
||||||
layout ReportLayout?
|
layout ReportLayout?
|
||||||
|
share ShareReport?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
@@ -372,6 +378,38 @@ model ShareOverview {
|
|||||||
@@map("shares")
|
@@map("shares")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ShareDashboard {
|
||||||
|
id String @unique
|
||||||
|
dashboardId String @unique
|
||||||
|
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
|
||||||
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
organizationId String
|
||||||
|
projectId String
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
public Boolean @default(false)
|
||||||
|
password String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@map("share_dashboards")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ShareReport {
|
||||||
|
id String @unique
|
||||||
|
reportId String @unique @db.Uuid
|
||||||
|
report Report @relation(fields: [reportId], references: [id], onDelete: Cascade)
|
||||||
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
organizationId String
|
||||||
|
projectId String
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
public Boolean @default(false)
|
||||||
|
password String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@map("share_reports")
|
||||||
|
}
|
||||||
|
|
||||||
model EventMeta {
|
model EventMeta {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
name String
|
name String
|
||||||
|
|||||||
@@ -18,3 +18,100 @@ export function getShareByProjectId(projectId: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dashboard sharing functions
|
||||||
|
export function getShareDashboardById(id: string) {
|
||||||
|
return db.shareDashboard.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
dashboard: {
|
||||||
|
include: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShareDashboardByDashboardId(dashboardId: string) {
|
||||||
|
return db.shareDashboard.findUnique({
|
||||||
|
where: {
|
||||||
|
dashboardId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report sharing functions
|
||||||
|
export function getShareReportById(id: string) {
|
||||||
|
return db.shareReport.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
report: {
|
||||||
|
include: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShareReportByReportId(reportId: string) {
|
||||||
|
return db.shareReport.findUnique({
|
||||||
|
where: {
|
||||||
|
reportId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation for secure endpoints
|
||||||
|
export async function validateReportAccess(
|
||||||
|
reportId: string,
|
||||||
|
shareId: string,
|
||||||
|
shareType: 'dashboard' | 'report',
|
||||||
|
) {
|
||||||
|
if (shareType === 'dashboard') {
|
||||||
|
const share = await db.shareDashboard.findUnique({
|
||||||
|
where: { id: shareId },
|
||||||
|
include: {
|
||||||
|
dashboard: {
|
||||||
|
include: {
|
||||||
|
reports: {
|
||||||
|
where: { id: reportId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!share || !share.public) {
|
||||||
|
throw new Error('Share not found or not public');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!share.dashboard.reports.some((r) => r.id === reportId)) {
|
||||||
|
throw new Error('Report does not belong to this dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return share;
|
||||||
|
} else {
|
||||||
|
const share = await db.shareReport.findUnique({
|
||||||
|
where: { id: shareId },
|
||||||
|
include: {
|
||||||
|
report: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!share || !share.public) {
|
||||||
|
throw new Error('Share not found or not public');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (share.reportId !== reportId) {
|
||||||
|
throw new Error('Report ID mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
return share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -352,8 +352,23 @@ export const authRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.input(zSignInShare)
|
.input(zSignInShare)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { password, shareId } = input;
|
const { password, shareId, shareType = 'overview' } = input;
|
||||||
const share = await getShareOverviewById(input.shareId);
|
|
||||||
|
let share: { password: string | null; public: boolean } | null = null;
|
||||||
|
let cookieName = '';
|
||||||
|
|
||||||
|
if (shareType === 'overview') {
|
||||||
|
share = await getShareOverviewById(shareId);
|
||||||
|
cookieName = `shared-overview-${shareId}`;
|
||||||
|
} else if (shareType === 'dashboard') {
|
||||||
|
const { getShareDashboardById } = await import('@openpanel/db');
|
||||||
|
share = await getShareDashboardById(shareId);
|
||||||
|
cookieName = `shared-dashboard-${shareId}`;
|
||||||
|
} else if (shareType === 'report') {
|
||||||
|
const { getShareReportById } = await import('@openpanel/db');
|
||||||
|
share = await getShareReportById(shareId);
|
||||||
|
cookieName = `shared-report-${shareId}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!share) {
|
if (!share) {
|
||||||
throw TRPCNotFoundError('Share not found');
|
throw TRPCNotFoundError('Share not found');
|
||||||
@@ -373,7 +388,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw TRPCAccessError('Incorrect password');
|
throw TRPCAccessError('Incorrect password');
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.setCookie(`shared-overview-${shareId}`, '1', {
|
ctx.setCookie(cookieName, '1', {
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
...COOKIE_OPTIONS,
|
...COOKIE_OPTIONS,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ import {
|
|||||||
getEventFiltersWhereClause,
|
getEventFiltersWhereClause,
|
||||||
getEventMetasCached,
|
getEventMetasCached,
|
||||||
getProfilesCached,
|
getProfilesCached,
|
||||||
|
getReportById,
|
||||||
getSelectPropertyKey,
|
getSelectPropertyKey,
|
||||||
getSettingsForProject,
|
getSettingsForProject,
|
||||||
onlyReportEvents,
|
onlyReportEvents,
|
||||||
sankeyService,
|
sankeyService,
|
||||||
|
validateReportAccess,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import {
|
import {
|
||||||
type IChartEvent,
|
type IChartEvent,
|
||||||
@@ -815,6 +817,397 @@ export const chartRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return profiles;
|
return profiles;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
chartByReport: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
reportId: z.string(),
|
||||||
|
shareId: z.string(),
|
||||||
|
shareType: z.enum(['dashboard', 'report']),
|
||||||
|
range: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
interval: zTimeInterval.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// Validate access
|
||||||
|
await validateReportAccess(
|
||||||
|
input.reportId,
|
||||||
|
input.shareId,
|
||||||
|
input.shareType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load report from DB
|
||||||
|
const report = await getReportById(input.reportId);
|
||||||
|
if (!report) {
|
||||||
|
throw TRPCAccessError('Report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build chart input from report, merging date overrides
|
||||||
|
const chartInput: z.infer<typeof zChartInput> = {
|
||||||
|
projectId: report.projectId,
|
||||||
|
chartType: report.chartType,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
interval: input.interval ?? report.interval,
|
||||||
|
range: input.range ?? report.range,
|
||||||
|
startDate: input.startDate ?? null,
|
||||||
|
endDate: input.endDate ?? null,
|
||||||
|
previous: report.previous,
|
||||||
|
formula: report.formula,
|
||||||
|
metric: report.metric,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ChartEngine.execute(chartInput);
|
||||||
|
}),
|
||||||
|
|
||||||
|
aggregateByReport: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
reportId: z.string(),
|
||||||
|
shareId: z.string(),
|
||||||
|
shareType: z.enum(['dashboard', 'report']),
|
||||||
|
range: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
interval: zTimeInterval.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// Validate access
|
||||||
|
await validateReportAccess(
|
||||||
|
input.reportId,
|
||||||
|
input.shareId,
|
||||||
|
input.shareType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load report from DB
|
||||||
|
const report = await getReportById(input.reportId);
|
||||||
|
if (!report) {
|
||||||
|
throw TRPCAccessError('Report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build chart input from report, merging date overrides
|
||||||
|
const chartInput: z.infer<typeof zChartInput> = {
|
||||||
|
projectId: report.projectId,
|
||||||
|
chartType: report.chartType,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
interval: input.interval ?? report.interval,
|
||||||
|
range: input.range ?? report.range,
|
||||||
|
startDate: input.startDate ?? null,
|
||||||
|
endDate: input.endDate ?? null,
|
||||||
|
previous: report.previous,
|
||||||
|
formula: report.formula,
|
||||||
|
metric: report.metric,
|
||||||
|
};
|
||||||
|
|
||||||
|
return AggregateChartEngine.execute(chartInput);
|
||||||
|
}),
|
||||||
|
|
||||||
|
funnelByReport: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
reportId: z.string(),
|
||||||
|
shareId: z.string(),
|
||||||
|
shareType: z.enum(['dashboard', 'report']),
|
||||||
|
range: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
interval: zTimeInterval.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// Validate access
|
||||||
|
await validateReportAccess(
|
||||||
|
input.reportId,
|
||||||
|
input.shareId,
|
||||||
|
input.shareType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load report from DB
|
||||||
|
const report = await getReportById(input.reportId);
|
||||||
|
if (!report) {
|
||||||
|
throw TRPCAccessError('Report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timezone } = await getSettingsForProject(report.projectId);
|
||||||
|
const currentPeriod = getChartStartEndDate(
|
||||||
|
{
|
||||||
|
range: input.range ?? report.range,
|
||||||
|
startDate: input.startDate ?? null,
|
||||||
|
endDate: input.endDate ?? null,
|
||||||
|
interval: input.interval ?? report.interval,
|
||||||
|
},
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
const previousPeriod = getChartPrevStartEndDate(currentPeriod);
|
||||||
|
|
||||||
|
const [current, previous] = await Promise.all([
|
||||||
|
funnelService.getFunnel({
|
||||||
|
projectId: report.projectId,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
...currentPeriod,
|
||||||
|
timezone,
|
||||||
|
funnelGroup: report.funnelGroup,
|
||||||
|
funnelWindow: report.funnelWindow,
|
||||||
|
}),
|
||||||
|
report.previous
|
||||||
|
? funnelService.getFunnel({
|
||||||
|
projectId: report.projectId,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
...previousPeriod,
|
||||||
|
timezone,
|
||||||
|
funnelGroup: report.funnelGroup,
|
||||||
|
funnelWindow: report.funnelWindow,
|
||||||
|
})
|
||||||
|
: Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
current,
|
||||||
|
previous,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
cohortByReport: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
reportId: z.string(),
|
||||||
|
shareId: z.string(),
|
||||||
|
shareType: z.enum(['dashboard', 'report']),
|
||||||
|
range: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
interval: zTimeInterval.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// Validate access
|
||||||
|
await validateReportAccess(
|
||||||
|
input.reportId,
|
||||||
|
input.shareId,
|
||||||
|
input.shareType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load report from DB
|
||||||
|
const report = await getReportById(input.reportId);
|
||||||
|
if (!report) {
|
||||||
|
throw TRPCAccessError('Report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timezone } = await getSettingsForProject(report.projectId);
|
||||||
|
const eventSeries = onlyReportEvents(report.series);
|
||||||
|
const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (firstEvent.length === 0 || secondEvent.length === 0) {
|
||||||
|
throw new Error('Report must have at least 2 event series');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = getChartStartEndDate(
|
||||||
|
{
|
||||||
|
range: input.range ?? report.range,
|
||||||
|
startDate: input.startDate ?? null,
|
||||||
|
endDate: input.endDate ?? null,
|
||||||
|
interval: input.interval ?? report.interval,
|
||||||
|
},
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
const interval = (input.interval ?? report.interval) as
|
||||||
|
| 'minute'
|
||||||
|
| 'hour'
|
||||||
|
| 'day'
|
||||||
|
| 'week'
|
||||||
|
| 'month';
|
||||||
|
const diffInterval = {
|
||||||
|
minute: () => differenceInDays(dates.endDate, dates.startDate),
|
||||||
|
hour: () => differenceInDays(dates.endDate, dates.startDate),
|
||||||
|
day: () => differenceInDays(dates.endDate, dates.startDate),
|
||||||
|
week: () => differenceInWeeks(dates.endDate, dates.startDate),
|
||||||
|
month: () => differenceInMonths(dates.endDate, dates.startDate),
|
||||||
|
}[interval]();
|
||||||
|
const sqlInterval = {
|
||||||
|
minute: 'DAY',
|
||||||
|
hour: 'DAY',
|
||||||
|
day: 'DAY',
|
||||||
|
week: 'WEEK',
|
||||||
|
month: 'MONTH',
|
||||||
|
}[interval];
|
||||||
|
|
||||||
|
const sqlToStartOf = {
|
||||||
|
minute: 'toDate',
|
||||||
|
hour: 'toDate',
|
||||||
|
day: 'toDate',
|
||||||
|
week: 'toStartOfWeek',
|
||||||
|
month: 'toStartOfMonth',
|
||||||
|
}[interval];
|
||||||
|
|
||||||
|
const countCriteria =
|
||||||
|
(report.criteria ?? 'on_or_after') === 'on_or_after' ? '>=' : '=';
|
||||||
|
|
||||||
|
const usersSelect = range(0, diffInterval + 1)
|
||||||
|
.map(
|
||||||
|
(index) =>
|
||||||
|
`groupUniqArrayIf(profile_id, x_after_cohort ${countCriteria} ${index}) AS interval_${index}_users`,
|
||||||
|
)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const countsSelect = range(0, diffInterval + 1)
|
||||||
|
.map(
|
||||||
|
(index) =>
|
||||||
|
`length(interval_${index}_users) AS interval_${index}_user_count`,
|
||||||
|
)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const whereEventNameIs = (event: string[]) => {
|
||||||
|
if (event.length === 1) {
|
||||||
|
return `name = ${sqlstring.escape(event[0])}`;
|
||||||
|
}
|
||||||
|
return `name IN (${event.map((e) => sqlstring.escape(e)).join(',')})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cohortQuery = `
|
||||||
|
WITH
|
||||||
|
cohort_users AS (
|
||||||
|
SELECT
|
||||||
|
profile_id AS userID,
|
||||||
|
project_id,
|
||||||
|
${sqlToStartOf}(created_at) AS cohort_interval
|
||||||
|
FROM ${TABLE_NAMES.cohort_events_mv}
|
||||||
|
WHERE ${whereEventNameIs(firstEvent)}
|
||||||
|
AND project_id = ${sqlstring.escape(report.projectId)}
|
||||||
|
AND created_at BETWEEN toDate('${utc(dates.startDate)}') AND toDate('${utc(dates.endDate)}')
|
||||||
|
),
|
||||||
|
last_event AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
profile_id,
|
||||||
|
project_id,
|
||||||
|
toDate(created_at) AS event_date
|
||||||
|
FROM cohort_events_mv
|
||||||
|
WHERE ${whereEventNameIs(secondEvent)}
|
||||||
|
AND project_id = ${sqlstring.escape(report.projectId)}
|
||||||
|
AND created_at BETWEEN toDate('${utc(dates.startDate)}') AND toDate('${utc(dates.endDate)}') + INTERVAL ${diffInterval} ${sqlInterval}
|
||||||
|
),
|
||||||
|
retention_matrix AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
f.cohort_interval,
|
||||||
|
l.profile_id,
|
||||||
|
dateDiff('${sqlInterval}', f.cohort_interval, ${sqlToStartOf}(l.event_date)) AS x_after_cohort
|
||||||
|
FROM cohort_users AS f
|
||||||
|
INNER JOIN last_event AS l ON f.userID = l.profile_id
|
||||||
|
WHERE (l.event_date >= f.cohort_interval)
|
||||||
|
AND (l.event_date <= (f.cohort_interval + INTERVAL ${diffInterval} ${sqlInterval}))
|
||||||
|
),
|
||||||
|
interval_users AS (
|
||||||
|
SELECT
|
||||||
|
cohort_interval,
|
||||||
|
${usersSelect}
|
||||||
|
FROM retention_matrix
|
||||||
|
GROUP BY cohort_interval
|
||||||
|
),
|
||||||
|
cohort_sizes AS (
|
||||||
|
SELECT
|
||||||
|
cohort_interval,
|
||||||
|
COUNT(DISTINCT userID) AS total_first_event_count
|
||||||
|
FROM cohort_users
|
||||||
|
GROUP BY cohort_interval
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cohort_interval,
|
||||||
|
cohort_sizes.total_first_event_count,
|
||||||
|
${countsSelect}
|
||||||
|
FROM interval_users
|
||||||
|
LEFT JOIN cohort_sizes AS cs ON cohort_interval = cs.cohort_interval
|
||||||
|
ORDER BY cohort_interval ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const cohortData = await chQuery<{
|
||||||
|
cohort_interval: string;
|
||||||
|
total_first_event_count: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}>(cohortQuery);
|
||||||
|
|
||||||
|
return processCohortData(cohortData, diffInterval);
|
||||||
|
}),
|
||||||
|
|
||||||
|
conversionByReport: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
reportId: z.string(),
|
||||||
|
shareId: z.string(),
|
||||||
|
shareType: z.enum(['dashboard', 'report']),
|
||||||
|
range: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
interval: zTimeInterval.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// Validate access
|
||||||
|
await validateReportAccess(
|
||||||
|
input.reportId,
|
||||||
|
input.shareId,
|
||||||
|
input.shareType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load report from DB
|
||||||
|
const report = await getReportById(input.reportId);
|
||||||
|
if (!report) {
|
||||||
|
throw TRPCAccessError('Report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timezone } = await getSettingsForProject(report.projectId);
|
||||||
|
const currentPeriod = getChartStartEndDate(
|
||||||
|
{
|
||||||
|
range: input.range ?? report.range,
|
||||||
|
startDate: input.startDate ?? null,
|
||||||
|
endDate: input.endDate ?? null,
|
||||||
|
interval: input.interval ?? report.interval,
|
||||||
|
},
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
const previousPeriod = getChartPrevStartEndDate(currentPeriod);
|
||||||
|
|
||||||
|
const [current, previous] = await Promise.all([
|
||||||
|
conversionService.getConversion({
|
||||||
|
projectId: report.projectId,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
...currentPeriod,
|
||||||
|
timezone,
|
||||||
|
}),
|
||||||
|
report.previous
|
||||||
|
? conversionService.getConversion({
|
||||||
|
projectId: report.projectId,
|
||||||
|
series: report.series,
|
||||||
|
breakdowns: report.breakdowns,
|
||||||
|
...previousPeriod,
|
||||||
|
timezone,
|
||||||
|
})
|
||||||
|
: Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: current.map((serie, sIndex) => ({
|
||||||
|
...serie,
|
||||||
|
data: serie.data.map((d, dIndex) => ({
|
||||||
|
...d,
|
||||||
|
previousRate: previous?.[sIndex]?.data?.[dIndex]?.rate,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
previous,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function processCohortData(
|
function processCohortData(
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import ShortUniqueId from 'short-unique-id';
|
import ShortUniqueId from 'short-unique-id';
|
||||||
|
|
||||||
import { db } from '@openpanel/db';
|
import {
|
||||||
import { zShareOverview } from '@openpanel/validation';
|
db,
|
||||||
|
getReportsByDashboardId,
|
||||||
|
getReportById,
|
||||||
|
getShareDashboardById,
|
||||||
|
getShareReportById,
|
||||||
|
} from '@openpanel/db';
|
||||||
|
import { zShareDashboard, zShareOverview, zShareReport } from '@openpanel/validation';
|
||||||
|
|
||||||
import { hashPassword } from '@openpanel/auth';
|
import { hashPassword } from '@openpanel/auth';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { TRPCNotFoundError } from '../errors';
|
import { getProjectAccess } from '../access';
|
||||||
|
import { TRPCAccessError, TRPCNotFoundError } from '../errors';
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 6 });
|
const uid = new ShortUniqueId({ length: 6 });
|
||||||
@@ -85,4 +92,206 @@ 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: {
|
||||||
|
select: {
|
||||||
|
name: 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}`],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -246,6 +246,22 @@ export const zShareOverview = z.object({
|
|||||||
public: z.boolean(),
|
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({
|
export const zCreateReference = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
@@ -485,6 +501,7 @@ export type IRequestResetPassword = z.infer<typeof zRequestResetPassword>;
|
|||||||
export const zSignInShare = z.object({
|
export const zSignInShare = z.object({
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
shareId: 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>;
|
export type ISignInShare = z.infer<typeof zSignInShare>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user