feat(site): add date range picker for the template insights (#8976)

This commit is contained in:
Bruno Quaresma
2023-08-08 13:43:21 -03:00
committed by GitHub
parent f4122fa9f5
commit 76b1594670
6 changed files with 392 additions and 38 deletions
@@ -0,0 +1,234 @@
import Box from "@mui/material/Box"
import { styled } from "@mui/material/styles"
import { ComponentProps, useRef, useState } from "react"
import "react-date-range/dist/styles.css"
import "react-date-range/dist/theme/default.css"
import Button from "@mui/material/Button"
import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"
import Popover from "@mui/material/Popover"
import { DateRangePicker, createStaticRanges } from "react-date-range"
import { format, subDays } from "date-fns"
// The type definition from @types is wrong
declare module "react-date-range" {
export function createStaticRanges(
ranges: Omit<StaticRange, "isSelected">[],
): StaticRange[]
}
export type DateRangeValue = {
startDate: Date
endDate: Date
}
type RangesState = NonNullable<ComponentProps<typeof DateRangePicker>["ranges"]>
export const DateRange = ({
value,
onChange,
}: {
value: DateRangeValue
onChange: (value: DateRangeValue) => void
}) => {
const selectionStatusRef = useRef<"idle" | "selecting">("idle")
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [ranges, setRanges] = useState<RangesState>([
{
...value,
key: "selection",
},
])
const currentRange = {
startDate: ranges[0].startDate as Date,
endDate: ranges[0].endDate as Date,
}
const handleClose = () => {
onChange({
startDate: currentRange.startDate,
endDate: currentRange.endDate,
})
setIsOpen(false)
}
return (
<>
<Button ref={anchorRef} onClick={() => setIsOpen(true)}>
<span>{format(currentRange.startDate, "MMM d, Y")}</span>
<ArrowRightAltOutlined sx={{ width: 16, height: 16, mx: 1 }} />
<span>{format(currentRange.endDate, "MMM d, Y")}</span>
</Button>
<Popover
anchorEl={anchorRef.current}
open={isOpen}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
sx={{
"& .MuiPaper-root": {
marginTop: 1,
},
}}
>
<DateRangePickerWrapper
component={DateRangePicker}
onChange={(item) => {
const range = item.selection
setRanges([range])
// When it is the first selection, we don't want to close the popover
// We have to do that ourselves because the library doesn't provide a way to do it
if (selectionStatusRef.current === "idle") {
selectionStatusRef.current = "selecting"
return
}
selectionStatusRef.current = "idle"
const startDate = range.startDate as Date
const endDate = range.endDate as Date
onChange({
startDate,
endDate,
})
setIsOpen(false)
}}
moveRangeOnFirstSelection={false}
months={2}
ranges={ranges}
maxDate={new Date()}
direction="horizontal"
staticRanges={createStaticRanges([
{
label: "Today",
range: () => ({
startDate: new Date(),
endDate: new Date(),
}),
},
{
label: "Yesterday",
range: () => ({
startDate: subDays(new Date(), 1),
endDate: subDays(new Date(), 1),
}),
},
{
label: "Last 7 days",
range: () => ({
startDate: subDays(new Date(), 6),
endDate: new Date(),
}),
},
{
label: "Last 14 days",
range: () => ({
startDate: subDays(new Date(), 13),
endDate: new Date(),
}),
},
{
label: "Last 30 days",
range: () => ({
startDate: subDays(new Date(), 29),
endDate: new Date(),
}),
},
])}
/>
</Popover>
</>
)
}
const DateRangePickerWrapper: typeof Box = styled(Box)(({ theme }) => ({
"& .rdrDefinedRangesWrapper": {
background: theme.palette.background.paper,
borderColor: theme.palette.divider,
},
"& .rdrStaticRange": {
background: theme.palette.background.paper,
border: 0,
fontSize: 14,
color: theme.palette.text.secondary,
"&:hover .rdrStaticRangeLabel": {
background: theme.palette.background.paperLight,
color: theme.palette.text.primary,
},
"&.rdrStaticRangeSelected": {
color: `${theme.palette.text.primary} !important`,
},
},
"& .rdrInputRanges": {
display: "none",
},
"& .rdrDateDisplayWrapper": {
backgroundColor: theme.palette.background.paper,
},
"& .rdrCalendarWrapper": {
backgroundColor: theme.palette.background.paperLight,
},
"& .rdrDateDisplayItem": {
background: "transparent",
borderColor: theme.palette.divider,
"& input": {
color: theme.palette.text.secondary,
},
"&.rdrDateDisplayItemActive": {
borderColor: theme.palette.text.primary,
backgroundColor: theme.palette.background.paperLight,
"& input": {
color: theme.palette.text.primary,
},
},
},
"& .rdrMonthPicker select, & .rdrYearPicker select": {
color: theme.palette.text.primary,
appearance: "auto",
background: "transparent",
},
"& .rdrMonthName, & .rdrWeekDay": {
color: theme.palette.text.secondary,
},
"& .rdrDayPassive .rdrDayNumber span": {
color: theme.palette.text.disabled,
},
"& .rdrDayNumber span": {
color: theme.palette.text.primary,
},
"& .rdrDayToday .rdrDayNumber span": {
fontWeight: 900,
"&:after": {
display: "none",
},
},
"& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
color: theme.palette.primary.main,
},
"& .rdrDayDisabled": {
backgroundColor: "transparent",
"& .rdrDayNumber span": {
color: theme.palette.text.disabled,
},
},
}))
@@ -25,28 +25,40 @@ import {
TemplateParameterValue,
UserLatencyInsightsResponse,
} from "api/typesGenerated"
import { ComponentProps } from "react"
import { subDays, addHours, startOfHour } from "date-fns"
import { ComponentProps, ReactNode, useState } from "react"
import { subDays, isToday } from "date-fns"
import "react-date-range/dist/styles.css"
import "react-date-range/dist/theme/default.css"
import { DateRange, DateRangeValue } from "./DateRange"
import { useDashboard } from "components/Dashboard/DashboardProvider"
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"
import Link from "@mui/material/Link"
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"
import CancelOutlined from "@mui/icons-material/CancelOutlined"
import { getDateRangeFilter } from "./utils"
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,
start_time: toStartTimeFilter(subDays(now, 7)),
end_time: startOfHour(addHours(now, 1)).toISOString(),
...getDateRangeFilter({
startDate: dateRangeValue.startDate,
endDate: dateRangeValue.endDate,
now,
isToday,
}),
}
const { data: templateInsights } = useQuery({
queryKey: ["templates", template.id, "usage"],
queryKey: ["templates", template.id, "usage", insightsFilter],
queryFn: () => getInsightsTemplate(insightsFilter),
})
const { data: userLatency } = useQuery({
queryKey: ["templates", template.id, "user-latency"],
queryKey: ["templates", template.id, "user-latency", insightsFilter],
queryFn: () => getInsightsUserLatency(insightsFilter),
})
const dashboard = useDashboard()
@@ -60,6 +72,9 @@ export default function TemplateInsightsPage() {
<title>{getTemplatePageTitle("Insights", template)}</title>
</Helmet>
<TemplateInsightsPageView
dateRange={
<DateRange value={dateRangeValue} onChange={setDateRangeValue} />
}
templateInsights={templateInsights}
userLatency={userLatency}
shouldDisplayParameters={shouldDisplayParameters}
@@ -72,36 +87,41 @@ export const TemplateInsightsPageView = ({
templateInsights,
userLatency,
shouldDisplayParameters,
dateRange,
}: {
templateInsights: TemplateInsightsResponse | undefined
userLatency: UserLatencyInsightsResponse | undefined
shouldDisplayParameters: boolean
dateRange: ReactNode
}) => {
return (
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gridTemplateRows: "440px auto",
gap: (theme) => theme.spacing(3),
}}
>
<DailyUsersPanel
sx={{ gridColumn: "span 2" }}
data={templateInsights?.interval_reports}
/>
<UserLatencyPanel data={userLatency} />
<TemplateUsagePanel
sx={{ gridColumn: "span 3" }}
data={templateInsights?.report.apps_usage}
/>
{shouldDisplayParameters && (
<TemplateParametersUsagePanel
sx={{ gridColumn: "span 3" }}
data={templateInsights?.report.parameters_usage}
<>
<Box sx={{ mb: 4 }}>{dateRange}</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gridTemplateRows: "440px auto",
gap: (theme) => theme.spacing(3),
}}
>
<DailyUsersPanel
sx={{ gridColumn: "span 2" }}
data={templateInsights?.interval_reports}
/>
)}
</Box>
<UserLatencyPanel data={userLatency} />
<TemplateUsagePanel
sx={{ gridColumn: "span 3" }}
data={templateInsights?.report.apps_usage}
/>
{shouldDisplayParameters && (
<TemplateParametersUsagePanel
sx={{ gridColumn: "span 3" }}
data={templateInsights?.report.parameters_usage}
/>
)}
</Box>
</>
)
}
@@ -551,20 +571,12 @@ function mapToDAUsResponse(
entries: data.map((d) => {
return {
amount: d.active_users,
date: d.end_time,
date: d.start_time,
}
}),
}
}
function toStartTimeFilter(date: Date) {
date.setHours(0, 0, 0, 0)
const year = date.getUTCFullYear()
const month = String(date.getUTCMonth() + 1).padStart(2, "0")
const day = String(date.getUTCDate()).padStart(2, "0")
return `${year}-${month}-${day}T00:00:00Z`
}
function formatTime(seconds: number): string {
if (seconds < 60) {
return seconds + " seconds"
@@ -0,0 +1,36 @@
import { getDateRangeFilter } from "./utils"
describe("getDateRangeFilter", () => {
it("returns the start time at the start of the day", () => {
const date = new Date("2020-01-01T12:00:00.000Z")
const { start_time } = getDateRangeFilter({
startDate: date,
endDate: date,
now: date,
isToday: () => false,
})
expect(start_time).toEqual("2020-01-01T00:00:00+00:00")
})
it("returns the end time at the start of the next day", () => {
const date = new Date("2020-01-01T12:00:00.000Z")
const { end_time } = getDateRangeFilter({
startDate: date,
endDate: date,
now: date,
isToday: () => false,
})
expect(end_time).toEqual("2020-01-02T00:00:00+00:00")
})
it("returns the end time at the start of the next hour if the end date is today", () => {
const date = new Date("2020-01-01T12:00:00.000Z")
const { end_time } = getDateRangeFilter({
startDate: date,
endDate: date,
now: date,
isToday: () => true,
})
expect(end_time).toEqual("2020-01-01T13:00:00+00:00")
})
})
@@ -0,0 +1,26 @@
import { addDays, addHours, format, startOfDay, startOfHour } from "date-fns"
export function getDateRangeFilter({
startDate,
endDate,
now,
isToday,
}: {
startDate: Date
endDate: Date
now: Date
isToday: (date: Date) => boolean
}) {
return {
start_time: toISOLocal(startOfDay(startDate)),
end_time: toISOLocal(
isToday(endDate)
? startOfHour(addHours(now, 1))
: startOfDay(addDays(endDate, 1)),
),
}
}
function toISOLocal(d: Date) {
return format(d, "yyyy-MM-dd'T'HH:mm:ssxxx")
}