fix timepicker
This commit is contained in:
@@ -1,24 +1,27 @@
|
|||||||
import { pushModal } from '@/modals';
|
|
||||||
import type {
|
import type {
|
||||||
IReport,
|
|
||||||
IChartRange,
|
IChartRange,
|
||||||
IChartType,
|
IChartType,
|
||||||
IInterval,
|
IInterval,
|
||||||
|
IReport,
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
import { SaveIcon } from 'lucide-react';
|
import { SaveIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ReportChart } from '../report-chart';
|
|
||||||
import { ReportChartType } from '../report/ReportChartType';
|
import { ReportChartType } from '../report/ReportChartType';
|
||||||
import { ReportInterval } from '../report/ReportInterval';
|
import { ReportInterval } from '../report/ReportInterval';
|
||||||
|
import { ReportChart } from '../report-chart';
|
||||||
import { TimeWindowPicker } from '../time-window-picker';
|
import { TimeWindowPicker } from '../time-window-picker';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
|
|
||||||
export function ChatReport({
|
export function ChatReport({
|
||||||
lazy,
|
lazy,
|
||||||
...props
|
...props
|
||||||
}: { report: IReport & { startDate: string; endDate: string }; lazy: boolean }) {
|
}: {
|
||||||
|
report: IReport & { startDate: string; endDate: string };
|
||||||
|
lazy: boolean;
|
||||||
|
}) {
|
||||||
const [chartType, setChartType] = useState<IChartType>(
|
const [chartType, setChartType] = useState<IChartType>(
|
||||||
props.report.chartType,
|
props.report.chartType
|
||||||
);
|
);
|
||||||
const [startDate, setStartDate] = useState<string>(props.report.startDate);
|
const [startDate, setStartDate] = useState<string>(props.report.startDate);
|
||||||
const [endDate, setEndDate] = useState<string>(props.report.endDate);
|
const [endDate, setEndDate] = useState<string>(props.report.endDate);
|
||||||
@@ -35,47 +38,48 @@ export function ChatReport({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="text-center text-sm font-mono font-medium pt-4">
|
<div className="pt-4 text-center font-medium font-mono text-sm">
|
||||||
{props.report.name}
|
{props.report.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<ReportChart lazy={lazy} report={report} />
|
<ReportChart lazy={lazy} report={report} />
|
||||||
</div>
|
</div>
|
||||||
<div className="row justify-between gap-1 border-t border-border p-2">
|
<div className="row justify-between gap-1 border-border border-t p-2">
|
||||||
<div className="col md:row gap-1">
|
<div className="col md:row gap-1">
|
||||||
<TimeWindowPicker
|
<TimeWindowPicker
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
onChange={setRange}
|
|
||||||
value={report.range}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
endDate={report.endDate}
|
endDate={report.endDate}
|
||||||
|
onChange={setRange}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
onIntervalChange={setInterval}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
startDate={report.startDate}
|
startDate={report.startDate}
|
||||||
|
value={report.range}
|
||||||
/>
|
/>
|
||||||
<ReportInterval
|
<ReportInterval
|
||||||
|
chartType={chartType}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
interval={interval}
|
interval={interval}
|
||||||
range={range}
|
|
||||||
chartType={chartType}
|
|
||||||
onChange={setInterval}
|
onChange={setInterval}
|
||||||
|
range={range}
|
||||||
/>
|
/>
|
||||||
<ReportChartType
|
<ReportChartType
|
||||||
value={chartType}
|
|
||||||
onChange={(type) => {
|
onChange={(type) => {
|
||||||
setChartType(type);
|
setChartType(type);
|
||||||
}}
|
}}
|
||||||
|
value={chartType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon={SaveIcon}
|
icon={SaveIcon}
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pushModal('SaveReport', {
|
pushModal('SaveReport', {
|
||||||
report,
|
report,
|
||||||
disableRedirect: true,
|
disableRedirect: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
Save report
|
Save report
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,17 +2,25 @@ import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
|||||||
import { TimeWindowPicker } from '@/components/time-window-picker';
|
import { TimeWindowPicker } from '@/components/time-window-picker';
|
||||||
|
|
||||||
export function OverviewRange() {
|
export function OverviewRange() {
|
||||||
const { range, setRange, setStartDate, setEndDate, endDate, startDate } =
|
const {
|
||||||
useOverviewOptions();
|
range,
|
||||||
|
setRange,
|
||||||
|
setStartDate,
|
||||||
|
setEndDate,
|
||||||
|
endDate,
|
||||||
|
startDate,
|
||||||
|
setInterval,
|
||||||
|
} = useOverviewOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeWindowPicker
|
<TimeWindowPicker
|
||||||
onChange={setRange}
|
|
||||||
value={range}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
onChange={setRange}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
onIntervalChange={setInterval}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
|
value={range}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { ReportChart } from '@/components/report-chart';
|
import type { IServiceReport } from '@openpanel/db';
|
||||||
|
import { GanttChartSquareIcon, ShareIcon } from 'lucide-react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import EditReportName from '../report/edit-report-name';
|
||||||
import { ReportChartType } from '@/components/report/ReportChartType';
|
import { ReportChartType } from '@/components/report/ReportChartType';
|
||||||
import { ReportInterval } from '@/components/report/ReportInterval';
|
import { ReportInterval } from '@/components/report/ReportInterval';
|
||||||
import { ReportLineType } from '@/components/report/ReportLineType';
|
import { ReportLineType } from '@/components/report/ReportLineType';
|
||||||
@@ -14,18 +17,13 @@ import {
|
|||||||
setReport,
|
setReport,
|
||||||
} from '@/components/report/reportSlice';
|
} from '@/components/report/reportSlice';
|
||||||
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
||||||
|
import { ReportChart } from '@/components/report-chart';
|
||||||
import { TimeWindowPicker } from '@/components/time-window-picker';
|
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 { pushModal } from '@/modals';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { bind } from 'bind-event-listener';
|
|
||||||
import { GanttChartSquareIcon, ShareIcon } from 'lucide-react';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import type { IServiceReport } from '@openpanel/db';
|
|
||||||
import EditReportName from '../report/edit-report-name';
|
|
||||||
|
|
||||||
interface ReportEditorProps {
|
interface ReportEditorProps {
|
||||||
report: IServiceReport | null;
|
report: IServiceReport | null;
|
||||||
@@ -54,15 +52,15 @@ export default function ReportEditor({
|
|||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<div>
|
<div>
|
||||||
<div className="p-4 flex items-center justify-between">
|
<div className="flex items-center justify-between p-4">
|
||||||
<EditReportName />
|
<EditReportName />
|
||||||
{initialReport?.id && (
|
{initialReport?.id && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
icon={ShareIcon}
|
icon={ShareIcon}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pushModal('ShareReportModal', { reportId: initialReport.id })
|
pushModal('ShareReportModal', { reportId: initialReport.id })
|
||||||
}
|
}
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
@@ -71,9 +69,9 @@ export default function ReportEditor({
|
|||||||
<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>
|
||||||
<Button
|
<Button
|
||||||
|
className="self-start"
|
||||||
icon={GanttChartSquareIcon}
|
icon={GanttChartSquareIcon}
|
||||||
variant="cta"
|
variant="cta"
|
||||||
className="self-start"
|
|
||||||
>
|
>
|
||||||
Pick events
|
Pick events
|
||||||
</Button>
|
</Button>
|
||||||
@@ -88,23 +86,26 @@ export default function ReportEditor({
|
|||||||
/>
|
/>
|
||||||
<TimeWindowPicker
|
<TimeWindowPicker
|
||||||
className="min-w-0 flex-1"
|
className="min-w-0 flex-1"
|
||||||
|
endDate={report.endDate}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
dispatch(changeDateRanges(value));
|
dispatch(changeDateRanges(value));
|
||||||
}}
|
}}
|
||||||
value={report.range}
|
|
||||||
onStartDateChange={(date) => dispatch(changeStartDate(date))}
|
|
||||||
onEndDateChange={(date) => dispatch(changeEndDate(date))}
|
onEndDateChange={(date) => dispatch(changeEndDate(date))}
|
||||||
endDate={report.endDate}
|
onIntervalChange={(interval) =>
|
||||||
|
dispatch(changeInterval(interval))
|
||||||
|
}
|
||||||
|
onStartDateChange={(date) => dispatch(changeStartDate(date))}
|
||||||
startDate={report.startDate}
|
startDate={report.startDate}
|
||||||
|
value={report.range}
|
||||||
/>
|
/>
|
||||||
<ReportInterval
|
<ReportInterval
|
||||||
|
chartType={report.chartType}
|
||||||
className="min-w-0 flex-1"
|
className="min-w-0 flex-1"
|
||||||
|
endDate={report.endDate}
|
||||||
interval={report.interval}
|
interval={report.interval}
|
||||||
onChange={(newInterval) => dispatch(changeInterval(newInterval))}
|
onChange={(newInterval) => dispatch(changeInterval(newInterval))}
|
||||||
range={report.range}
|
range={report.range}
|
||||||
chartType={report.chartType}
|
|
||||||
startDate={report.startDate}
|
startDate={report.startDate}
|
||||||
endDate={report.endDate}
|
|
||||||
/>
|
/>
|
||||||
<ReportLineType className="min-w-0 flex-1" />
|
<ReportLineType className="min-w-0 flex-1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +115,7 @@ export default function ReportEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 p-4" id="report-editor">
|
<div className="flex flex-col gap-4 p-4" id="report-editor">
|
||||||
{report.ready && (
|
{report.ready && (
|
||||||
<ReportChart report={{ ...report, projectId }} isEditMode />
|
<ReportChart isEditMode report={{ ...report, projectId }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { timeWindows } from '@openpanel/constants';
|
||||||
|
import type { IChartRange, IInterval } from '@openpanel/validation';
|
||||||
|
import { bind } from 'bind-event-listener';
|
||||||
|
import { endOfDay, format, startOfDay } from 'date-fns';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -11,24 +17,18 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { pushModal, useOnPushModal } from '@/modals';
|
import { pushModal, useOnPushModal } from '@/modals';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { bind } from 'bind-event-listener';
|
|
||||||
import { CalendarIcon } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress';
|
import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress';
|
||||||
import { timeWindows } from '@openpanel/constants';
|
|
||||||
import type { IChartRange } from '@openpanel/validation';
|
|
||||||
import { endOfDay, format, startOfDay } from 'date-fns';
|
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
value: IChartRange;
|
value: IChartRange;
|
||||||
onChange: (value: IChartRange) => void;
|
onChange: (value: IChartRange) => void;
|
||||||
onStartDateChange: (date: string) => void;
|
onStartDateChange: (date: string) => void;
|
||||||
onEndDateChange: (date: string) => void;
|
onEndDateChange: (date: string) => void;
|
||||||
|
onIntervalChange: (interval: IInterval) => void;
|
||||||
endDate: string | null;
|
endDate: string | null;
|
||||||
startDate: string | null;
|
startDate: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
}
|
||||||
export function TimeWindowPicker({
|
export function TimeWindowPicker({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -36,6 +36,7 @@ export function TimeWindowPicker({
|
|||||||
onStartDateChange,
|
onStartDateChange,
|
||||||
endDate,
|
endDate,
|
||||||
onEndDateChange,
|
onEndDateChange,
|
||||||
|
onIntervalChange,
|
||||||
className,
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const isDateRangerPickerOpen = useRef(false);
|
const isDateRangerPickerOpen = useRef(false);
|
||||||
@@ -46,10 +47,11 @@ export function TimeWindowPicker({
|
|||||||
|
|
||||||
const handleCustom = useCallback(() => {
|
const handleCustom = useCallback(() => {
|
||||||
pushModal('DateRangerPicker', {
|
pushModal('DateRangerPicker', {
|
||||||
onChange: ({ startDate, endDate }) => {
|
onChange: ({ startDate, endDate, interval }) => {
|
||||||
onStartDateChange(format(startOfDay(startDate), 'yyyy-MM-dd HH:mm:ss'));
|
onStartDateChange(format(startOfDay(startDate), 'yyyy-MM-dd HH:mm:ss'));
|
||||||
onEndDateChange(format(endOfDay(endDate), 'yyyy-MM-dd HH:mm:ss'));
|
onEndDateChange(format(endOfDay(endDate), 'yyyy-MM-dd HH:mm:ss'));
|
||||||
onChange('custom');
|
onChange('custom');
|
||||||
|
onIntervalChange(interval);
|
||||||
},
|
},
|
||||||
startDate: startDate ? new Date(startDate) : undefined,
|
startDate: startDate ? new Date(startDate) : undefined,
|
||||||
endDate: endDate ? new Date(endDate) : undefined,
|
endDate: endDate ? new Date(endDate) : undefined,
|
||||||
@@ -69,7 +71,7 @@ export function TimeWindowPicker({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const match = Object.values(timeWindows).find(
|
const match = Object.values(timeWindows).find(
|
||||||
(tw) => event.key === tw.shortcut.toLowerCase(),
|
(tw) => event.key === tw.shortcut.toLowerCase()
|
||||||
);
|
);
|
||||||
if (match?.key === 'custom') {
|
if (match?.key === 'custom') {
|
||||||
handleCustom();
|
handleCustom();
|
||||||
@@ -84,9 +86,9 @@ export function TimeWindowPicker({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
icon={CalendarIcon}
|
|
||||||
className={cn('justify-start', className)}
|
className={cn('justify-start', className)}
|
||||||
|
icon={CalendarIcon}
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
{timeWindow?.label}
|
{timeWindow?.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
DayPicker,
|
DayPicker,
|
||||||
getDefaultClassNames,
|
getDefaultClassNames,
|
||||||
} from 'react-day-picker';
|
} from 'react-day-picker';
|
||||||
|
|
||||||
import { Button, buttonVariants } from '@/components/ui/button';
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -29,99 +28,93 @@ function Calendar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
captionLayout={captionLayout}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
'group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
||||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
captionLayout={captionLayout}
|
|
||||||
formatters={{
|
|
||||||
formatMonthDropdown: (date) =>
|
|
||||||
date.toLocaleString('default', { month: 'short' }),
|
|
||||||
...formatters,
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
root: cn('w-fit', defaultClassNames.root),
|
root: cn('w-fit', defaultClassNames.root),
|
||||||
months: cn(
|
months: cn(
|
||||||
'flex gap-4 flex-col sm:flex-row relative',
|
'relative flex flex-col gap-4 sm:flex-row',
|
||||||
defaultClassNames.months,
|
defaultClassNames.months
|
||||||
),
|
),
|
||||||
month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
|
month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
|
||||||
nav: cn(
|
nav: cn(
|
||||||
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
|
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
|
||||||
defaultClassNames.nav,
|
defaultClassNames.nav
|
||||||
),
|
),
|
||||||
button_previous: cn(
|
button_previous: cn(
|
||||||
buttonVariants({ variant: buttonVariant }),
|
buttonVariants({ variant: buttonVariant }),
|
||||||
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
|
||||||
defaultClassNames.button_previous,
|
defaultClassNames.button_previous
|
||||||
),
|
),
|
||||||
button_next: cn(
|
button_next: cn(
|
||||||
buttonVariants({ variant: buttonVariant }),
|
buttonVariants({ variant: buttonVariant }),
|
||||||
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
|
||||||
defaultClassNames.button_next,
|
defaultClassNames.button_next
|
||||||
),
|
),
|
||||||
month_caption: cn(
|
month_caption: cn(
|
||||||
'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
|
'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
|
||||||
defaultClassNames.month_caption,
|
defaultClassNames.month_caption
|
||||||
),
|
),
|
||||||
dropdowns: cn(
|
dropdowns: cn(
|
||||||
'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
|
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
|
||||||
defaultClassNames.dropdowns,
|
defaultClassNames.dropdowns
|
||||||
),
|
),
|
||||||
dropdown_root: cn(
|
dropdown_root: cn(
|
||||||
'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
|
'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
|
||||||
defaultClassNames.dropdown_root,
|
defaultClassNames.dropdown_root
|
||||||
),
|
),
|
||||||
dropdown: cn(
|
dropdown: cn(
|
||||||
'absolute bg-popover inset-0 opacity-0',
|
'absolute inset-0 bg-popover opacity-0',
|
||||||
defaultClassNames.dropdown,
|
defaultClassNames.dropdown
|
||||||
),
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
'select-none font-medium',
|
'select-none font-medium',
|
||||||
captionLayout === 'label'
|
captionLayout === 'label'
|
||||||
? 'text-sm'
|
? 'text-sm'
|
||||||
: 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
|
: 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
|
||||||
defaultClassNames.caption_label,
|
defaultClassNames.caption_label
|
||||||
),
|
),
|
||||||
table: 'w-full border-collapse',
|
table: 'w-full border-collapse',
|
||||||
weekdays: cn('flex', defaultClassNames.weekdays),
|
weekdays: cn('flex', defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
|
'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',
|
||||||
defaultClassNames.weekday,
|
defaultClassNames.weekday
|
||||||
),
|
),
|
||||||
week: cn('flex w-full mt-2', defaultClassNames.week),
|
week: cn('mt-2 flex w-full', defaultClassNames.week),
|
||||||
week_number_header: cn(
|
week_number_header: cn(
|
||||||
'select-none w-(--cell-size)',
|
'w-(--cell-size) select-none',
|
||||||
defaultClassNames.week_number_header,
|
defaultClassNames.week_number_header
|
||||||
),
|
),
|
||||||
week_number: cn(
|
week_number: cn(
|
||||||
'text-[0.8rem] select-none text-muted-foreground',
|
'select-none text-[0.8rem] text-muted-foreground',
|
||||||
defaultClassNames.week_number,
|
defaultClassNames.week_number
|
||||||
),
|
),
|
||||||
day: cn(
|
day: cn(
|
||||||
'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
|
'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md',
|
||||||
defaultClassNames.day,
|
defaultClassNames.day
|
||||||
),
|
),
|
||||||
range_start: cn(
|
range_start: cn(
|
||||||
'rounded-l-md bg-accent',
|
'rounded-l-md bg-accent',
|
||||||
defaultClassNames.range_start,
|
defaultClassNames.range_start
|
||||||
),
|
),
|
||||||
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
||||||
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
|
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
|
||||||
today: cn(
|
today: cn(
|
||||||
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
|
'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
|
||||||
defaultClassNames.today,
|
defaultClassNames.today
|
||||||
),
|
),
|
||||||
outside: cn(
|
outside: cn(
|
||||||
'text-muted-foreground aria-selected:text-muted-foreground',
|
'text-muted-foreground aria-selected:text-muted-foreground',
|
||||||
defaultClassNames.outside,
|
defaultClassNames.outside
|
||||||
),
|
),
|
||||||
disabled: cn(
|
disabled: cn(
|
||||||
'text-muted-foreground opacity-50',
|
'text-muted-foreground opacity-50',
|
||||||
defaultClassNames.disabled,
|
defaultClassNames.disabled
|
||||||
),
|
),
|
||||||
hidden: cn('invisible', defaultClassNames.hidden),
|
hidden: cn('invisible', defaultClassNames.hidden),
|
||||||
...classNames,
|
...classNames,
|
||||||
@@ -130,9 +123,9 @@ function Calendar({
|
|||||||
Root: ({ className, rootRef, ...props }) => {
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className={cn(className)}
|
||||||
data-slot="calendar"
|
data-slot="calendar"
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -169,6 +162,12 @@ function Calendar({
|
|||||||
},
|
},
|
||||||
...components,
|
...components,
|
||||||
}}
|
}}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString('default', { month: 'short' }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -184,29 +183,31 @@ function CalendarDayButton({
|
|||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null);
|
const ref = React.useRef<HTMLButtonElement>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (modifiers.focused) ref.current?.focus();
|
if (modifiers.focused) {
|
||||||
|
ref.current?.focus();
|
||||||
|
}
|
||||||
}, [modifiers.focused]);
|
}, [modifiers.focused]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
className={cn(
|
||||||
variant="ghost"
|
'flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-start=true]:rounded-l-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70',
|
||||||
size="icon"
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
data-day={day.date.toLocaleDateString()}
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
data-selected-single={
|
data-selected-single={
|
||||||
modifiers.selected &&
|
modifiers.selected &&
|
||||||
!modifiers.range_start &&
|
!modifiers.range_start &&
|
||||||
!modifiers.range_end &&
|
!modifiers.range_end &&
|
||||||
!modifiers.range_middle
|
!modifiers.range_middle
|
||||||
}
|
}
|
||||||
data-range-start={modifiers.range_start}
|
ref={ref}
|
||||||
data-range-end={modifiers.range_end}
|
size="icon"
|
||||||
data-range-middle={modifiers.range_middle}
|
variant="ghost"
|
||||||
className={cn(
|
|
||||||
'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70',
|
|
||||||
defaultClassNames.day,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
|
import { getDefaultIntervalByDates } from '@openpanel/constants';
|
||||||
|
import type { IInterval } from '@openpanel/validation';
|
||||||
|
import { endOfDay, subMonths } from 'date-fns';
|
||||||
|
import { CheckIcon, XIcon } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { popModal } from '.';
|
||||||
|
import { ModalContent } from './Modal/Container';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import { subMonths } from 'date-fns';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import { CheckIcon, XIcon } from 'lucide-react';
|
|
||||||
import { popModal } from '.';
|
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
onChange: (payload: { startDate: Date; endDate: Date }) => void;
|
onChange: (payload: {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
interval: IInterval;
|
||||||
|
}) => void;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
};
|
}
|
||||||
export default function DateRangerPicker({
|
export default function DateRangerPicker({
|
||||||
onChange,
|
onChange,
|
||||||
startDate: initialStartDate,
|
startDate: initialStartDate,
|
||||||
@@ -25,20 +29,20 @@ export default function DateRangerPicker({
|
|||||||
const [endDate, setEndDate] = useState(initialEndDate);
|
const [endDate, setEndDate] = useState(initialEndDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent className="p-4 md:p-8 min-w-fit">
|
<ModalContent className="min-w-fit p-4 md:p-8">
|
||||||
<Calendar
|
<Calendar
|
||||||
captionLayout="dropdown"
|
captionLayout="dropdown"
|
||||||
initialFocus
|
className="mx-auto min-h-[310px] p-0 [&_table]:mx-auto [&_table]:w-auto"
|
||||||
mode="range"
|
|
||||||
defaultMonth={subMonths(
|
defaultMonth={subMonths(
|
||||||
startDate ? new Date(startDate) : new Date(),
|
startDate ? new Date(startDate) : new Date(),
|
||||||
isBelowSm ? 0 : 1,
|
isBelowSm ? 0 : 1
|
||||||
)}
|
)}
|
||||||
selected={{
|
hidden={{
|
||||||
from: startDate,
|
after: endOfDay(new Date()),
|
||||||
to: endDate,
|
|
||||||
}}
|
}}
|
||||||
toDate={new Date()}
|
initialFocus
|
||||||
|
mode="range"
|
||||||
|
numberOfMonths={isBelowSm ? 1 : 2}
|
||||||
onSelect={(range) => {
|
onSelect={(range) => {
|
||||||
if (range?.from) {
|
if (range?.from) {
|
||||||
setStartDate(range.from);
|
setStartDate(range.from);
|
||||||
@@ -47,33 +51,39 @@ export default function DateRangerPicker({
|
|||||||
setEndDate(range.to);
|
setEndDate(range.to);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
numberOfMonths={isBelowSm ? 1 : 2}
|
selected={{
|
||||||
className="mx-auto min-h-[310px] [&_table]:mx-auto [&_table]:w-auto p-0"
|
from: startDate,
|
||||||
|
to: endDate,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="col flex-col-reverse md:row gap-2">
|
<div className="col md:row flex-col-reverse gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
icon={XIcon}
|
||||||
|
onClick={() => popModal()}
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => popModal()}
|
|
||||||
icon={XIcon}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{startDate && endDate && (
|
{startDate && endDate && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
className="md:ml-auto"
|
className="md:ml-auto"
|
||||||
|
icon={startDate && endDate ? CheckIcon : XIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
popModal();
|
popModal();
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
onChange({
|
onChange({
|
||||||
startDate: startDate,
|
startDate,
|
||||||
endDate: endDate,
|
endDate,
|
||||||
|
interval: getDefaultIntervalByDates(
|
||||||
|
startDate.toISOString(),
|
||||||
|
endDate.toISOString()
|
||||||
|
)!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
icon={startDate && endDate ? CheckIcon : XIcon}
|
type="button"
|
||||||
>
|
>
|
||||||
{startDate && endDate
|
{startDate && endDate
|
||||||
? `Select ${formatDate(startDate)} - ${formatDate(endDate)}`
|
? `Select ${formatDate(startDate)} - ${formatDate(endDate)}`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type GscChartData = { date: string; clicks: number; impressions: number };
|
|||||||
|
|
||||||
const { TooltipProvider, Tooltip: GscTooltip } = createChartTooltip<
|
const { TooltipProvider, Tooltip: GscTooltip } = createChartTooltip<
|
||||||
GscChartData,
|
GscChartData,
|
||||||
Record<string, never>
|
Record<string, unknown>
|
||||||
>(({ data }) => {
|
>(({ data }) => {
|
||||||
const item = data[0];
|
const item = data[0];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@@ -267,7 +267,7 @@ function GscViewsChart({
|
|||||||
const yAxisProps = useYAxisProps();
|
const yAxisProps = useYAxisProps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider data={[]}>
|
<TooltipProvider>
|
||||||
<ResponsiveContainer height={160} width="100%">
|
<ResponsiveContainer height={160} width="100%">
|
||||||
<ComposedChart data={data}>
|
<ComposedChart data={data}>
|
||||||
<defs>
|
<defs>
|
||||||
@@ -328,7 +328,7 @@ function GscTimeseriesChart({
|
|||||||
const yAxisProps = useYAxisProps();
|
const yAxisProps = useYAxisProps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider data={data}>
|
<TooltipProvider>
|
||||||
<ResponsiveContainer height={160} width="100%">
|
<ResponsiveContainer height={160} width="100%">
|
||||||
<ComposedChart data={data}>
|
<ComposedChart data={data}>
|
||||||
<defs>
|
<defs>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {
|
|
||||||
getDefaultIntervalByRange,
|
|
||||||
intervals,
|
|
||||||
timeWindows,
|
|
||||||
} from '@openpanel/constants';
|
|
||||||
import type { IChartRange, IInterval } from '@openpanel/validation';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { SearchIcon } from 'lucide-react';
|
import { SearchIcon } from 'lucide-react';
|
||||||
import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs';
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
@@ -23,8 +16,11 @@ import {
|
|||||||
createChartTooltip,
|
createChartTooltip,
|
||||||
} from '@/components/charts/chart-tooltip';
|
} from '@/components/charts/chart-tooltip';
|
||||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
|
import { OverviewInterval } from '@/components/overview/overview-interval';
|
||||||
import { OverviewMetricCard } from '@/components/overview/overview-metric-card';
|
import { OverviewMetricCard } from '@/components/overview/overview-metric-card';
|
||||||
|
import { OverviewRange } from '@/components/overview/overview-range';
|
||||||
import { OverviewWidgetTable } from '@/components/overview/overview-widget-table';
|
import { OverviewWidgetTable } from '@/components/overview/overview-widget-table';
|
||||||
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { GscCannibalization } from '@/components/page/gsc-cannibalization';
|
import { GscCannibalization } from '@/components/page/gsc-cannibalization';
|
||||||
import { GscCtrBenchmark } from '@/components/page/gsc-ctr-benchmark';
|
import { GscCtrBenchmark } from '@/components/page/gsc-ctr-benchmark';
|
||||||
import { GscPositionChart } from '@/components/page/gsc-position-chart';
|
import { GscPositionChart } from '@/components/page/gsc-position-chart';
|
||||||
@@ -32,14 +28,12 @@ import { PagesInsights } from '@/components/page/pages-insights';
|
|||||||
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 { Pagination } from '@/components/pagination';
|
import { Pagination } from '@/components/pagination';
|
||||||
import { ReportInterval } from '@/components/report/ReportInterval';
|
|
||||||
import {
|
import {
|
||||||
useYAxisProps,
|
useYAxisProps,
|
||||||
X_AXIS_STYLE_PROPS,
|
X_AXIS_STYLE_PROPS,
|
||||||
} from '@/components/report-chart/common/axis';
|
} from '@/components/report-chart/common/axis';
|
||||||
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
||||||
import { Skeleton } from '@/components/skeleton';
|
import { Skeleton } from '@/components/skeleton';
|
||||||
import { TimeWindowPicker } from '@/components/time-window-picker';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
@@ -94,27 +88,13 @@ function SeoPage() {
|
|||||||
const { projectId, organizationId } = useAppParams();
|
const { projectId, organizationId } = useAppParams();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { range, startDate, endDate, interval } = useOverviewOptions();
|
||||||
const [range, setRange] = useQueryState(
|
|
||||||
'range',
|
|
||||||
parseAsStringEnum(Object.keys(timeWindows) as IChartRange[]).withDefault(
|
|
||||||
'30d' as IChartRange
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const [startDate, setStartDate] = useQueryState('start', parseAsString);
|
|
||||||
const [endDate, setEndDate] = useQueryState('end', parseAsString);
|
|
||||||
const [interval, setInterval] = useQueryState(
|
|
||||||
'interval',
|
|
||||||
parseAsStringEnum(Object.keys(intervals) as IInterval[]).withDefault(
|
|
||||||
(getDefaultIntervalByRange(range) ?? 'day') as IInterval
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const dateInput = {
|
const dateInput = {
|
||||||
range,
|
range,
|
||||||
interval,
|
interval,
|
||||||
startDate: startDate ?? undefined,
|
startDate,
|
||||||
endDate: endDate ?? undefined,
|
endDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectionQuery = useQuery(
|
const connectionQuery = useQuery(
|
||||||
@@ -265,31 +245,8 @@ function SeoPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ReportInterval
|
<OverviewRange />
|
||||||
chartType="linear"
|
<OverviewInterval />
|
||||||
endDate={endDate}
|
|
||||||
interval={interval ?? 'day'}
|
|
||||||
onChange={(v) => setInterval(v)}
|
|
||||||
range={range}
|
|
||||||
startDate={startDate}
|
|
||||||
/>
|
|
||||||
<TimeWindowPicker
|
|
||||||
endDate={endDate}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (v !== 'custom') {
|
|
||||||
setStartDate(null);
|
|
||||||
setEndDate(null);
|
|
||||||
}
|
|
||||||
setInterval(
|
|
||||||
(getDefaultIntervalByRange(v) ?? 'day') as IInterval
|
|
||||||
);
|
|
||||||
setRange(v);
|
|
||||||
}}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
startDate={startDate}
|
|
||||||
value={range}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description={`Search performance for ${connection.siteUrl}`}
|
description={`Search performance for ${connection.siteUrl}`}
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ const zGscDateInput = z.object({
|
|||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
range: zRange,
|
range: zRange,
|
||||||
interval: zTimeInterval.optional().default('day'),
|
interval: zTimeInterval.optional().default('day'),
|
||||||
|
startDate: z.string().nullish(),
|
||||||
|
endDate: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function resolveDates(
|
async function resolveDates(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
input: { range: string; startDate?: string; endDate?: string }
|
input: { range: string; startDate?: string | null; endDate?: string | null }
|
||||||
) {
|
) {
|
||||||
const { timezone } = await getSettingsForProject(projectId);
|
const { timezone } = await getSettingsForProject(projectId);
|
||||||
const { startDate, endDate } = getChartStartEndDate(
|
const { startDate, endDate } = getChartStartEndDate(
|
||||||
|
|||||||
Reference in New Issue
Block a user