mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add support for weekly data on template insights (#9997)
Close https://github.com/coder/coder/issues/9495
This commit is contained in:
+11
-8
@@ -1474,28 +1474,31 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
|
||||
};
|
||||
};
|
||||
|
||||
type InsightsFilter = {
|
||||
export type InsightsParams = {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
template_ids: string;
|
||||
};
|
||||
|
||||
export const getInsightsUserLatency = async (
|
||||
filters: InsightsFilter,
|
||||
filters: InsightsParams,
|
||||
): Promise<TypesGen.UserLatencyInsightsResponse> => {
|
||||
const params = new URLSearchParams(filters);
|
||||
const response = await axios.get(`/api/v2/insights/user-latency?${params}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export type InsightsTemplateParams = InsightsParams & {
|
||||
interval: "day" | "week";
|
||||
};
|
||||
|
||||
export const getInsightsTemplate = async (
|
||||
filters: InsightsFilter,
|
||||
params: InsightsTemplateParams,
|
||||
): Promise<TypesGen.TemplateInsightsResponse> => {
|
||||
const params = new URLSearchParams({
|
||||
...filters,
|
||||
interval: "day",
|
||||
});
|
||||
const response = await axios.get(`/api/v2/insights/templates?${params}`);
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const response = await axios.get(
|
||||
`/api/v2/insights/templates?${searchParams}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as API from "api/api";
|
||||
|
||||
export const insightsTemplate = (params: API.InsightsTemplateParams) => {
|
||||
return {
|
||||
queryKey: ["insights", "templates", params.template_ids, params],
|
||||
queryFn: () => API.getInsightsTemplate(params),
|
||||
};
|
||||
};
|
||||
|
||||
export const insightsUserLatency = (params: API.InsightsParams) => {
|
||||
return {
|
||||
queryKey: ["insights", "userLatency", params.template_ids, params],
|
||||
queryFn: () => API.getInsightsUserLatency(params),
|
||||
};
|
||||
};
|
||||
+16
-20
@@ -1,7 +1,6 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import useTheme from "@mui/styles/useTheme";
|
||||
import * as TypesGen from "api/typesGenerated";
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
@@ -38,20 +37,19 @@ ChartJS.register(
|
||||
Legend,
|
||||
);
|
||||
|
||||
export interface DAUChartProps {
|
||||
daus: TypesGen.DAUsResponse;
|
||||
export interface ActiveUserChartProps {
|
||||
data: { date: string; amount: number }[];
|
||||
interval: "day" | "week";
|
||||
}
|
||||
|
||||
export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
export const ActiveUserChart: FC<ActiveUserChartProps> = ({
|
||||
data,
|
||||
interval,
|
||||
}) => {
|
||||
const theme: Theme = useTheme();
|
||||
|
||||
const labels = daus.entries.map((val) => {
|
||||
return dayjs(val.date).format("YYYY-MM-DD");
|
||||
});
|
||||
|
||||
const data = daus.entries.map((val) => {
|
||||
return val.amount;
|
||||
});
|
||||
const labels = data.map((val) => dayjs(val.date).format("YYYY-MM-DD"));
|
||||
const chartData = data.map((val) => val.amount);
|
||||
|
||||
defaults.font.family = theme.typography.fontFamily as string;
|
||||
defaults.color = theme.palette.text.secondary;
|
||||
@@ -82,11 +80,11 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
|
||||
x: {
|
||||
ticks: {
|
||||
stepSize: daus.entries.length > 10 ? 2 : undefined,
|
||||
stepSize: data.length > 10 ? 2 : undefined,
|
||||
},
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "day",
|
||||
unit: interval,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -101,7 +99,7 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
datasets: [
|
||||
{
|
||||
label: "Daily Active Users",
|
||||
data: data,
|
||||
data: chartData,
|
||||
pointBackgroundColor: theme.palette.info.light,
|
||||
pointBorderColor: theme.palette.info.light,
|
||||
borderColor: theme.palette.info.light,
|
||||
@@ -115,17 +113,15 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DAUTitle = () => {
|
||||
export const ActiveUsersTitle = () => {
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
Daily Active Users
|
||||
Active Users
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>
|
||||
How do we calculate daily active users?
|
||||
</HelpTooltipTitle>
|
||||
<HelpTooltipTitle>How do we calculate active users?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
When a connection is initiated to a user's workspace they are
|
||||
considered a daily active user. e.g. apps, web terminal, SSH
|
||||
considered an active user. e.g. apps, web terminal, SSH
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
@@ -1,7 +1,10 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { ClibaseOption, DAUsResponse } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart";
|
||||
import {
|
||||
ActiveUserChart,
|
||||
ActiveUsersTitle,
|
||||
} from "components/ActiveUserChart/ActiveUserChart";
|
||||
import { Header } from "components/DeploySettingsLayout/Header";
|
||||
import OptionsTable from "components/DeploySettingsLayout/OptionsTable";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
@@ -32,8 +35,8 @@ export const GeneralSettingsPageView = ({
|
||||
)}
|
||||
{deploymentDAUs && (
|
||||
<Box height={200} sx={{ mb: 3 }}>
|
||||
<ChartSection title={<DAUTitle />}>
|
||||
<DAUChart daus={deploymentDAUs} />
|
||||
<ChartSection title={<ActiveUsersTitle />}>
|
||||
<ActiveUserChart data={deploymentDAUs.entries} interval="day" />
|
||||
</ChartSection>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import CheckOutlined from "@mui/icons-material/CheckOutlined";
|
||||
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
export const insightsIntervals = {
|
||||
day: {
|
||||
label: "Daily",
|
||||
},
|
||||
week: {
|
||||
label: "Weekly",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type InsightsInterval = keyof typeof insightsIntervals;
|
||||
|
||||
export const IntervalMenu = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: InsightsInterval;
|
||||
onChange: (value: InsightsInterval) => void;
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
ref={anchorRef}
|
||||
id="interval-button"
|
||||
aria-controls={open ? "interval-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={() => setOpen(true)}
|
||||
endIcon={<ExpandMoreOutlined />}
|
||||
>
|
||||
{insightsIntervals[value].label}
|
||||
</Button>
|
||||
<Menu
|
||||
id="interval-menu"
|
||||
anchorEl={anchorRef.current}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "interval-button",
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{Object.entries(insightsIntervals).map(([interval, { label }]) => {
|
||||
return (
|
||||
<MenuItem
|
||||
css={{ fontSize: 14, justifyContent: "space-between" }}
|
||||
key={interval}
|
||||
onClick={() => {
|
||||
onChange(interval as InsightsInterval);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<Box css={{ width: 16, height: 16 }}>
|
||||
{value === interval && (
|
||||
<CheckOutlined css={{ width: 16, height: 16 }} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,8 +3,10 @@ import Box from "@mui/material/Box";
|
||||
import { styled, useTheme } from "@mui/material/styles";
|
||||
import { BoxProps } from "@mui/system";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getInsightsTemplate, getInsightsUserLatency } from "api/api";
|
||||
import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart";
|
||||
import {
|
||||
ActiveUsersTitle,
|
||||
ActiveUserChart,
|
||||
} from "components/ActiveUserChart/ActiveUserChart";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import {
|
||||
HelpTooltip,
|
||||
@@ -19,49 +21,53 @@ import { Helmet } from "react-helmet-async";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
DAUsResponse,
|
||||
Template,
|
||||
TemplateAppUsage,
|
||||
TemplateInsightsResponse,
|
||||
TemplateParameterUsage,
|
||||
TemplateParameterValue,
|
||||
UserLatencyInsightsResponse,
|
||||
} from "api/typesGenerated";
|
||||
import { ComponentProps, ReactNode, useState } from "react";
|
||||
import { subDays, isToday } from "date-fns";
|
||||
import { ComponentProps, ReactNode } from "react";
|
||||
import { subDays, addWeeks } from "date-fns";
|
||||
import "react-date-range/dist/styles.css";
|
||||
import "react-date-range/dist/theme/default.css";
|
||||
import { DateRange, DateRangeValue } from "./DateRange";
|
||||
import { DateRange as DailyPicker, DateRangeValue } from "./DateRange";
|
||||
import Link from "@mui/material/Link";
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
|
||||
import CancelOutlined from "@mui/icons-material/CancelOutlined";
|
||||
import { getDateRangeFilter } from "./utils";
|
||||
import { getDateRangeFilter, lastWeeks } from "./utils";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import LinkOutlined from "@mui/icons-material/LinkOutlined";
|
||||
import { InsightsInterval, IntervalMenu } from "./IntervalMenu";
|
||||
import { WeekPicker, numberOfWeeksOptions } from "./WeekPicker";
|
||||
import { insightsTemplate, insightsUserLatency } from "api/queries/insights";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const DEFAULT_NUMBER_OF_WEEKS = numberOfWeeksOptions[0];
|
||||
|
||||
export default function TemplateInsightsPage() {
|
||||
const now = new Date();
|
||||
const [dateRangeValue, setDateRangeValue] = useState<DateRangeValue>({
|
||||
startDate: subDays(now, 6),
|
||||
endDate: now,
|
||||
});
|
||||
const { template } = useTemplateLayoutContext();
|
||||
const insightsFilter = {
|
||||
template_ids: template.id,
|
||||
...getDateRangeFilter({
|
||||
startDate: dateRangeValue.startDate,
|
||||
endDate: dateRangeValue.endDate,
|
||||
now,
|
||||
isToday,
|
||||
}),
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const defaultInterval = getDefaultInterval(template);
|
||||
const interval =
|
||||
(searchParams.get("interval") as InsightsInterval) || defaultInterval;
|
||||
|
||||
const dateRange = getDateRange(searchParams, interval);
|
||||
const setDateRange = (newDateRange: DateRangeValue) => {
|
||||
searchParams.set("startDate", newDateRange.startDate.toISOString());
|
||||
searchParams.set("endDate", newDateRange.endDate.toISOString());
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
const { data: templateInsights } = useQuery({
|
||||
queryKey: ["templates", template.id, "usage", insightsFilter],
|
||||
queryFn: () => getInsightsTemplate(insightsFilter),
|
||||
});
|
||||
const { data: userLatency } = useQuery({
|
||||
queryKey: ["templates", template.id, "user-latency", insightsFilter],
|
||||
queryFn: () => getInsightsUserLatency(insightsFilter),
|
||||
});
|
||||
|
||||
const commonFilters = {
|
||||
template_ids: template.id,
|
||||
...getDateRangeFilter(dateRange),
|
||||
};
|
||||
const insightsFilter = { ...commonFilters, interval };
|
||||
const { data: templateInsights } = useQuery(insightsTemplate(insightsFilter));
|
||||
const { data: userLatency } = useQuery(insightsUserLatency(commonFilters));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,28 +75,88 @@ export default function TemplateInsightsPage() {
|
||||
<title>{getTemplatePageTitle("Insights", template)}</title>
|
||||
</Helmet>
|
||||
<TemplateInsightsPageView
|
||||
dateRange={
|
||||
<DateRange value={dateRangeValue} onChange={setDateRangeValue} />
|
||||
controls={
|
||||
<>
|
||||
<IntervalMenu
|
||||
value={interval}
|
||||
onChange={(interval) => {
|
||||
// When going from daily to week we need to set a safe week range
|
||||
if (interval === "week") {
|
||||
setDateRange(lastWeeks(DEFAULT_NUMBER_OF_WEEKS));
|
||||
}
|
||||
searchParams.set("interval", interval);
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
/>
|
||||
{interval === "day" ? (
|
||||
<DailyPicker value={dateRange} onChange={setDateRange} />
|
||||
) : (
|
||||
<WeekPicker value={dateRange} onChange={setDateRange} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
templateInsights={templateInsights}
|
||||
userLatency={userLatency}
|
||||
interval={interval}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getDefaultInterval = (template: Template) => {
|
||||
const now = new Date();
|
||||
const templateCreateDate = new Date(template.created_at);
|
||||
const hasFiveWeeksOrMore = addWeeks(templateCreateDate, 5) < now;
|
||||
return hasFiveWeeksOrMore ? "week" : "day";
|
||||
};
|
||||
|
||||
const getDateRange = (
|
||||
searchParams: URLSearchParams,
|
||||
interval: InsightsInterval,
|
||||
) => {
|
||||
const startDate = searchParams.get("startDate");
|
||||
const endDate = searchParams.get("endDate");
|
||||
|
||||
if (startDate && endDate) {
|
||||
return {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
};
|
||||
}
|
||||
|
||||
if (interval === "day") {
|
||||
return {
|
||||
startDate: subDays(new Date(), 6),
|
||||
endDate: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
return lastWeeks(DEFAULT_NUMBER_OF_WEEKS);
|
||||
};
|
||||
|
||||
export const TemplateInsightsPageView = ({
|
||||
templateInsights,
|
||||
userLatency,
|
||||
dateRange,
|
||||
controls,
|
||||
interval,
|
||||
}: {
|
||||
templateInsights: TemplateInsightsResponse | undefined;
|
||||
userLatency: UserLatencyInsightsResponse | undefined;
|
||||
dateRange: ReactNode;
|
||||
controls: ReactNode;
|
||||
interval: InsightsInterval;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ mb: 4 }}>{dateRange}</Box>
|
||||
<Box
|
||||
css={(theme) => ({
|
||||
marginBottom: theme.spacing(4),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
})}
|
||||
>
|
||||
{controls}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
@@ -99,8 +165,9 @@ export const TemplateInsightsPageView = ({
|
||||
gap: (theme) => theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<DailyUsersPanel
|
||||
<ActiveUsersPanel
|
||||
sx={{ gridColumn: "span 2" }}
|
||||
interval={interval}
|
||||
data={templateInsights?.interval_reports}
|
||||
/>
|
||||
<UserLatencyPanel data={userLatency} />
|
||||
@@ -117,23 +184,33 @@ export const TemplateInsightsPageView = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DailyUsersPanel = ({
|
||||
const ActiveUsersPanel = ({
|
||||
data,
|
||||
interval,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["interval_reports"] | undefined;
|
||||
interval: InsightsInterval;
|
||||
}) => {
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
<PanelHeader>
|
||||
<PanelTitle>
|
||||
<DAUTitle />
|
||||
<ActiveUsersTitle />
|
||||
</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{data && data.length === 0 && <NoDataAvailable />}
|
||||
{data && data.length > 0 && <DAUChart daus={mapToDAUsResponse(data)} />}
|
||||
{data && data.length > 0 && (
|
||||
<ActiveUserChart
|
||||
interval={interval}
|
||||
data={data.map((d) => ({
|
||||
amount: d.active_users,
|
||||
date: d.start_time,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
);
|
||||
@@ -584,22 +661,6 @@ const TextValue = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function mapToDAUsResponse(
|
||||
data: TemplateInsightsResponse["interval_reports"],
|
||||
): DAUsResponse {
|
||||
return {
|
||||
tz_hour_offset: 0,
|
||||
entries: data
|
||||
? data.map((d) => {
|
||||
return {
|
||||
amount: d.active_users,
|
||||
date: d.start_time,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return seconds + " seconds";
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import CheckOutlined from "@mui/icons-material/CheckOutlined";
|
||||
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useState, useRef } from "react";
|
||||
import { DateRangeValue } from "./DateRange";
|
||||
import { differenceInWeeks } from "date-fns";
|
||||
import { lastWeeks } from "./utils";
|
||||
|
||||
export const numberOfWeeksOptions = [4, 12, 24, 48] as const;
|
||||
|
||||
export const WeekPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
// Why +1? If you get the week 1 and week 2 the diff is 1, but there are 2 weeks
|
||||
const numberOfWeeks = differenceInWeeks(value.endDate, value.startDate) + 1;
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
ref={anchorRef}
|
||||
id="interval-button"
|
||||
aria-controls={open ? "interval-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={() => setOpen(true)}
|
||||
endIcon={<ExpandMoreOutlined />}
|
||||
>
|
||||
Last {numberOfWeeks} weeks
|
||||
</Button>
|
||||
<Menu
|
||||
id="interval-menu"
|
||||
anchorEl={anchorRef.current}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "interval-button",
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{numberOfWeeksOptions.map((option) => {
|
||||
const optionRange = lastWeeks(option);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
css={{ fontSize: 14, justifyContent: "space-between" }}
|
||||
key={option}
|
||||
onClick={() => {
|
||||
onChange(optionRange);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
Last {option} weeks
|
||||
<Box css={{ width: 16, height: 16 }}>
|
||||
{numberOfWeeks === option && (
|
||||
<CheckOutlined css={{ width: 16, height: 16 }} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,33 @@
|
||||
import { addDays, addHours, format, startOfDay, startOfHour } from "date-fns";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
format,
|
||||
startOfDay,
|
||||
startOfHour,
|
||||
isToday as isTodayDefault,
|
||||
startOfWeek,
|
||||
endOfDay,
|
||||
endOfWeek,
|
||||
isSunday,
|
||||
subWeeks,
|
||||
} from "date-fns";
|
||||
|
||||
export function getDateRangeFilter({
|
||||
startDate,
|
||||
endDate,
|
||||
now,
|
||||
isToday,
|
||||
}: {
|
||||
type GetDateRangeFilterOptions = {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
now: Date;
|
||||
isToday: (date: Date) => boolean;
|
||||
}) {
|
||||
// Testing purposes
|
||||
now?: Date;
|
||||
isToday?: (date: Date) => boolean;
|
||||
};
|
||||
|
||||
export function getDateRangeFilter(props: GetDateRangeFilterOptions) {
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
now = new Date(),
|
||||
isToday = isTodayDefault,
|
||||
} = props;
|
||||
|
||||
return {
|
||||
start_time: toISOLocal(startOfDay(startDate)),
|
||||
end_time: toISOLocal(
|
||||
@@ -24,3 +41,10 @@ export function getDateRangeFilter({
|
||||
function toISOLocal(d: Date) {
|
||||
return format(d, "yyyy-MM-dd'T'HH:mm:ssxxx");
|
||||
}
|
||||
|
||||
export const lastWeeks = (numberOfWeeks: number) => {
|
||||
const now = new Date();
|
||||
const startDate = startOfWeek(subWeeks(now, numberOfWeeks));
|
||||
const endDate = isSunday(now) ? endOfDay(now) : endOfWeek(subWeeks(now, 1));
|
||||
return { startDate, endDate };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user