wip
This commit is contained in:
@@ -8,7 +8,13 @@ import { LogoSquare } from '../logo';
|
||||
import { Button } from '../ui/button';
|
||||
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 mutation = useMutation(
|
||||
trpc.auth.signInShare.mutationOptions({
|
||||
@@ -25,6 +31,7 @@ export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
||||
defaultValues: {
|
||||
password: '',
|
||||
shareId,
|
||||
shareType,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,6 +39,7 @@ export function ShareEnterPassword({ shareId }: { shareId: string }) {
|
||||
mutation.mutate({
|
||||
password: data.password,
|
||||
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="col mt-1 flex-1 gap-2">
|
||||
<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">
|
||||
Please enter correct password to access this overview
|
||||
Please enter correct password to access this{' '}
|
||||
{shareType === 'dashboard'
|
||||
? 'dashboard'
|
||||
: shareType === 'report'
|
||||
? 'report'
|
||||
: 'overview'}
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={onSubmit} className="col gap-4 mt-6">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -9,15 +10,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportAreaChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
@@ -9,15 +10,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportBarChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.aggregate.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -28,9 +28,11 @@ export type ReportChartContextType = {
|
||||
onClick: () => void;
|
||||
}[];
|
||||
}>;
|
||||
report: IChartProps;
|
||||
report: IChartProps & { id?: string };
|
||||
isLazyLoading: boolean;
|
||||
isEditMode: boolean;
|
||||
shareId?: string;
|
||||
shareType?: 'dashboard' | 'report';
|
||||
};
|
||||
|
||||
type ReportChartContextProviderProps = ReportChartContextType & {
|
||||
@@ -40,6 +42,8 @@ type ReportChartContextProviderProps = ReportChartContextType & {
|
||||
export type ReportChartProps = Partial<ReportChartContextType> & {
|
||||
report: IChartInput;
|
||||
lazy?: boolean;
|
||||
shareId?: string;
|
||||
shareType?: 'dashboard' | 'report';
|
||||
};
|
||||
|
||||
const context = createContext<ReportChartContextType | null>(null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
@@ -11,15 +12,33 @@ import { Chart } from './chart';
|
||||
import { Summary } from './summary';
|
||||
|
||||
export function ReportConversionChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
console.log(report.limit);
|
||||
const res = useQuery(
|
||||
trpc.chart.conversion.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTRPC } from '@/integrations/trpc/react';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
@@ -14,6 +15,7 @@ import { Chart, Summary, Tables } from './chart';
|
||||
export function ReportFunnelChart() {
|
||||
const {
|
||||
report: {
|
||||
id,
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
@@ -25,28 +27,48 @@ export function ReportFunnelChart() {
|
||||
breakdowns,
|
||||
},
|
||||
isLazyLoading,
|
||||
shareId,
|
||||
shareType,
|
||||
} = useReportChartContext();
|
||||
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||
|
||||
const input: IChartInput = {
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
interval: 'day',
|
||||
chartType: 'funnel',
|
||||
breakdowns,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
startDate,
|
||||
endDate,
|
||||
limit: 20,
|
||||
};
|
||||
const trpc = useTRPC();
|
||||
const res = useQuery(
|
||||
trpc.chart.funnel.queryOptions(input, {
|
||||
enabled: !isLazyLoading && input.series.length > 0,
|
||||
}),
|
||||
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 = {
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
interval: 'day',
|
||||
chartType: 'funnel',
|
||||
breakdowns,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
startDate,
|
||||
endDate,
|
||||
limit: 20,
|
||||
};
|
||||
return trpc.chart.funnel.queryOptions(input, {
|
||||
enabled: !isLazyLoading && input.series.length > 0,
|
||||
});
|
||||
})(),
|
||||
);
|
||||
|
||||
if (isLazyLoading || res.isLoading) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -9,15 +10,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportHistogramChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -10,15 +11,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportLineChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -9,15 +10,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportMapChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -8,15 +9,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportMetricChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.chart.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -9,15 +10,33 @@ import { useReportChartContext } from '../context';
|
||||
import { Chart } from './chart';
|
||||
|
||||
export function ReportPieChart() {
|
||||
const { isLazyLoading, report } = useReportChartContext();
|
||||
const { isLazyLoading, report, shareId, shareType } = useReportChartContext();
|
||||
const trpc = useTRPC();
|
||||
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||
|
||||
const res = useQuery(
|
||||
trpc.chart.aggregate.queryOptions(report, {
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
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,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: !isLazyLoading,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -18,8 +18,10 @@ import { TimeWindowPicker } from '@/components/time-window-picker';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { pushModal } from '@/modals';
|
||||
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 type { IServiceReport } from '@openpanel/db';
|
||||
@@ -52,8 +54,19 @@ export default function ReportEditor({
|
||||
return (
|
||||
<Sheet>
|
||||
<div>
|
||||
<div className="p-4">
|
||||
<div className="p-4 flex items-center justify-between">
|
||||
<EditReportName />
|
||||
{initialReport?.id && (
|
||||
<Button
|
||||
variant="outline"
|
||||
icon={ShareIcon}
|
||||
onClick={() =>
|
||||
pushModal('ShareReportModal', { reportId: initialReport.id })
|
||||
}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 p-4 pt-0 md:grid-cols-6">
|
||||
<SheetTrigger asChild>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { AspectContainer } from '../aspect-container';
|
||||
import { ReportChartEmpty } from '../common/empty';
|
||||
import { ReportChartError } from '../common/error';
|
||||
@@ -12,6 +13,7 @@ import CohortTable from './table';
|
||||
export function ReportRetentionChart() {
|
||||
const {
|
||||
report: {
|
||||
id,
|
||||
series,
|
||||
range,
|
||||
projectId,
|
||||
@@ -21,7 +23,10 @@ export function ReportRetentionChart() {
|
||||
interval,
|
||||
},
|
||||
isLazyLoading,
|
||||
shareId,
|
||||
shareType,
|
||||
} = useReportChartContext();
|
||||
const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions();
|
||||
const eventSeries = series.filter((item) => item.type === 'event');
|
||||
const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(String);
|
||||
const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(String);
|
||||
@@ -29,23 +34,40 @@ export function ReportRetentionChart() {
|
||||
firstEvent.length > 0 && secondEvent.length > 0 && !isLazyLoading;
|
||||
const trpc = useTRPC();
|
||||
const res = useQuery(
|
||||
trpc.chart.cohort.queryOptions(
|
||||
{
|
||||
firstEvent,
|
||||
secondEvent,
|
||||
projectId,
|
||||
range,
|
||||
startDate,
|
||||
endDate,
|
||||
criteria,
|
||||
interval,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: isEnabled,
|
||||
},
|
||||
),
|
||||
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,
|
||||
secondEvent,
|
||||
projectId,
|
||||
range,
|
||||
startDate,
|
||||
endDate,
|
||||
criteria,
|
||||
interval,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
enabled: isEnabled,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (!isEnabled) {
|
||||
|
||||
@@ -30,7 +30,9 @@ import OverviewFilters from './overview-filters';
|
||||
import RequestPasswordReset from './request-reset-password';
|
||||
import SaveReport from './save-report';
|
||||
import SelectBillingPlan from './select-billing-plan';
|
||||
import ShareDashboardModal from './share-dashboard-modal';
|
||||
import ShareOverviewModal from './share-overview-modal';
|
||||
import ShareReportModal from './share-report-modal';
|
||||
import ViewChartUsers from './view-chart-users';
|
||||
|
||||
const modals = {
|
||||
@@ -51,6 +53,8 @@ const modals = {
|
||||
EditReport: EditReport,
|
||||
EditReference: EditReference,
|
||||
ShareOverviewModal: ShareOverviewModal,
|
||||
ShareDashboardModal: ShareDashboardModal,
|
||||
ShareReportModal: ShareReportModal,
|
||||
AddReference: AddReference,
|
||||
ViewChartUsers: ViewChartUsers,
|
||||
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 AppOrganizationIdRouteImport } from './routes/_app.$organizationId'
|
||||
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 ShareDashboardShareIdRouteImport } from './routes/share.dashboard.$shareId'
|
||||
import { Route as StepsOnboardingProjectRouteImport } from './routes/_steps.onboarding.project'
|
||||
import { Route as AppOrganizationIdSettingsRouteImport } from './routes/_app.$organizationId.settings'
|
||||
import { Route as AppOrganizationIdBillingRouteImport } from './routes/_app.$organizationId.billing'
|
||||
@@ -164,11 +166,21 @@ const AppOrganizationIdIndexRoute = AppOrganizationIdIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AppOrganizationIdRoute,
|
||||
} as any)
|
||||
const ShareReportShareIdRoute = ShareReportShareIdRouteImport.update({
|
||||
id: '/share/report/$shareId',
|
||||
path: '/share/report/$shareId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ShareOverviewShareIdRoute = ShareOverviewShareIdRouteImport.update({
|
||||
id: '/share/overview/$shareId',
|
||||
path: '/share/overview/$shareId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ShareDashboardShareIdRoute = ShareDashboardShareIdRouteImport.update({
|
||||
id: '/share/dashboard/$shareId',
|
||||
path: '/share/dashboard/$shareId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const StepsOnboardingProjectRoute = StepsOnboardingProjectRouteImport.update({
|
||||
id: '/onboarding/project',
|
||||
path: '/onboarding/project',
|
||||
@@ -498,7 +510,9 @@ export interface FileRoutesByFullPath {
|
||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||
'/$organizationId/': typeof AppOrganizationIdIndexRoute
|
||||
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||
@@ -556,7 +570,9 @@ export interface FileRoutesByTo {
|
||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||
'/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||
'/$organizationId': typeof AppOrganizationIdIndexRoute
|
||||
'/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||
'/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||
@@ -614,7 +630,9 @@ export interface FileRoutesById {
|
||||
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||
'/_steps/onboarding/project': typeof StepsOnboardingProjectRoute
|
||||
'/share/dashboard/$shareId': typeof ShareDashboardShareIdRoute
|
||||
'/share/overview/$shareId': typeof ShareOverviewShareIdRoute
|
||||
'/share/report/$shareId': typeof ShareReportShareIdRoute
|
||||
'/_app/$organizationId/': typeof AppOrganizationIdIndexRoute
|
||||
'/_app/$organizationId/$projectId/chat': typeof AppOrganizationIdProjectIdChatRoute
|
||||
'/_app/$organizationId/$projectId/dashboards': typeof AppOrganizationIdProjectIdDashboardsRoute
|
||||
@@ -683,7 +701,9 @@ export interface FileRouteTypes {
|
||||
| '/$organizationId/billing'
|
||||
| '/$organizationId/settings'
|
||||
| '/onboarding/project'
|
||||
| '/share/dashboard/$shareId'
|
||||
| '/share/overview/$shareId'
|
||||
| '/share/report/$shareId'
|
||||
| '/$organizationId/'
|
||||
| '/$organizationId/$projectId/chat'
|
||||
| '/$organizationId/$projectId/dashboards'
|
||||
@@ -741,7 +761,9 @@ export interface FileRouteTypes {
|
||||
| '/$organizationId/billing'
|
||||
| '/$organizationId/settings'
|
||||
| '/onboarding/project'
|
||||
| '/share/dashboard/$shareId'
|
||||
| '/share/overview/$shareId'
|
||||
| '/share/report/$shareId'
|
||||
| '/$organizationId'
|
||||
| '/$organizationId/$projectId/chat'
|
||||
| '/$organizationId/$projectId/dashboards'
|
||||
@@ -798,7 +820,9 @@ export interface FileRouteTypes {
|
||||
| '/_app/$organizationId/billing'
|
||||
| '/_app/$organizationId/settings'
|
||||
| '/_steps/onboarding/project'
|
||||
| '/share/dashboard/$shareId'
|
||||
| '/share/overview/$shareId'
|
||||
| '/share/report/$shareId'
|
||||
| '/_app/$organizationId/'
|
||||
| '/_app/$organizationId/$projectId/chat'
|
||||
| '/_app/$organizationId/$projectId/dashboards'
|
||||
@@ -862,7 +886,9 @@ export interface RootRouteChildren {
|
||||
StepsRoute: typeof StepsRouteWithChildren
|
||||
ApiConfigRoute: typeof ApiConfigRoute
|
||||
ApiHealthcheckRoute: typeof ApiHealthcheckRoute
|
||||
ShareDashboardShareIdRoute: typeof ShareDashboardShareIdRoute
|
||||
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
|
||||
ShareReportShareIdRoute: typeof ShareReportShareIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -965,6 +991,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppOrganizationIdIndexRouteImport
|
||||
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': {
|
||||
id: '/share/overview/$shareId'
|
||||
path: '/share/overview/$shareId'
|
||||
@@ -972,6 +1005,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ShareOverviewShareIdRouteImport
|
||||
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': {
|
||||
id: '/_steps/onboarding/project'
|
||||
path: '/onboarding/project'
|
||||
@@ -1751,7 +1791,9 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
StepsRoute: StepsRouteWithChildren,
|
||||
ApiConfigRoute: ApiConfigRoute,
|
||||
ApiHealthcheckRoute: ApiHealthcheckRoute,
|
||||
ShareDashboardShareIdRoute: ShareDashboardShareIdRoute,
|
||||
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
|
||||
ShareReportShareIdRoute: ShareReportShareIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MoreHorizontal,
|
||||
PlusIcon,
|
||||
RotateCcw,
|
||||
ShareIcon,
|
||||
Trash,
|
||||
TrashIcon,
|
||||
} from 'lucide-react';
|
||||
@@ -30,7 +31,7 @@ import { OverviewRange } from '@/components/overview/overview-range';
|
||||
import { PageContainer } from '@/components/page-container';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { handleErrorToastOptions, useTRPC } from '@/integrations/trpc/react';
|
||||
import { showConfirm } from '@/modals';
|
||||
import { pushModal, showConfirm } from '@/modals';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -484,6 +485,12 @@ function Component() {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() => pushModal('ShareDashboardModal', { dashboardId })}
|
||||
>
|
||||
<ShareIcon className="mr-2 size-4" />
|
||||
Share dashboard
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user