Skip to content

Commit

Permalink
Dashboards: allow changing date filters & refresh (#3363)
Browse files Browse the repository at this point in the history
* DashboardHeader to ts

* Convert files to typescript

* Use BindLogic instead of passing logics as arguments

* Improve typing for dashboards

* Fix DashboardHeader types

* Improve typing: DashboardItem

* Fix InsightHistoryPanel, SharedDashboard types

* Add WIP code for date ranges on dashboards

* DateFilter/dateFilterLogic to typescript

* Show 'Custom' daterange by default on a dashboard

* Border for dates in Dashboard

* Add dashboard.filters field

* Populate dashboard.filters, use it in code

* Fix some type errors in dashboardItemsModel

* Fetch dashboard items again on date change

* Make dashboard items refresh as dashboard gets updated

* Add refresh button to DashboardHeader

* Make saving dashboard items work again

* Update dashboard type

* Don't update URL in DateFilter

* Update selected time range on load

* Avoid flickering in DateFilter

* Only show 'Custom' in dashboards

* Show calendar icon next to date filter in dashboard

* Avoid double-loading results due to date change

* Kill some duplicated code

* Avoid cache when filters change

(datefilters on dashboard)

* Test individual update_cache method

* Add test for import_from and attributes being updated

* Fix typing errors

* Update api/dashboard tests

* Avoid redirects when changing date range for funnels or retention

Previously funnelLogic might be mounted in the background, causing a
reload

* Add a missing breakpoint

* Reformat with black
  • Loading branch information
macobo committed Feb 23, 2021
1 parent 817e117 commit f403900
Show file tree
Hide file tree
Showing 33 changed files with 491 additions and 205 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,50 @@ import moment from 'moment'
import { dateFilterLogic } from './dateFilterLogic'
import { dateMapping, isDate, dateFilterToText } from 'lib/utils'

export function DateFilter({ style, disabled }) {
interface Props {
defaultValue: string
showCustom?: boolean
bordered?: boolean
makeLabel?: (key: string) => React.ReactNode
style?: React.CSSProperties
onChange?: () => void
disabled?: boolean
}

export function DateFilter({
bordered,
defaultValue,
showCustom,
style,
disabled,
makeLabel,
onChange,
}: Props): JSX.Element {
const {
dates: { dateFrom, dateTo },
} = useValues(dateFilterLogic)

const { setDates } = useActions(dateFilterLogic)
const [rangeDateFrom, setRangeDateFrom] = useState(isDate.test(dateFrom) && moment(dateFrom).toDate())
const [rangeDateTo, setRangeDateTo] = useState(isDate.test(dateTo) && moment(dateTo).toDate())
const [rangeDateFrom, setRangeDateFrom] = useState(
dateFrom && isDate.test(dateFrom as string) ? moment(dateFrom) : undefined
)
const [rangeDateTo, setRangeDateTo] = useState(dateTo && isDate.test(dateTo as string) ? moment(dateTo) : undefined)
const [dateRangeOpen, setDateRangeOpen] = useState(false)
const [open, setOpen] = useState(false)

function onClickOutside() {
function onClickOutside(): void {
setOpen(false)
setDateRangeOpen(false)
}

function setDate(fromDate, toDate) {
function setDate(fromDate: string, toDate: string): void {
setDates(fromDate, toDate)
if (onChange) {
onChange()
}
}

function _onChange(v) {
function _onChange(v: string): void {
if (v === 'Date range') {
if (open) {
setOpen(false)
Expand All @@ -35,38 +59,38 @@ export function DateFilter({ style, disabled }) {
}
}

function onBlur() {
function onBlur(): void {
if (dateRangeOpen) {
return
}
onClickOutside()
}

function onClick() {
function onClick(): void {
if (dateRangeOpen) {
return
}
setOpen(!open)
}

function dropdownOnClick(e) {
function dropdownOnClick(e: React.MouseEvent): void {
e.preventDefault()
setOpen(true)
setDateRangeOpen(false)
document.getElementById('daterange_selector').focus()
document.getElementById('daterange_selector')?.focus()
}

function onApplyClick() {
function onApplyClick(): void {
onClickOutside()
setDate(moment(rangeDateFrom).format('YYYY-MM-DD'), moment(rangeDateTo).format('YYYY-MM-DD'))
}

return (
<Select
data-attr="date-filter"
bordered={false}
bordered={bordered}
id="daterange_selector"
value={dateFilterToText(dateFrom, dateTo)}
value={dateFilterToText(dateFrom, dateTo, defaultValue)}
onChange={_onChange}
style={{
marginRight: 4,
Expand All @@ -78,7 +102,8 @@ export function DateFilter({ style, disabled }) {
listHeight={440}
dropdownMatchSelectWidth={false}
disabled={disabled}
dropdownRender={(menu) => {
optionLabelProp={makeLabel ? 'label' : undefined}
dropdownRender={(menu: React.ReactElement) => {
if (dateRangeOpen) {
return (
<DatePickerDropdown
Expand All @@ -91,15 +116,18 @@ export function DateFilter({ style, disabled }) {
rangeDateTo={rangeDateTo}
/>
)
} else if (open) {
} else {
return menu
}
}}
>
{[
...Object.entries(dateMapping).map(([key]) => {
if (key === 'Custom' && !showCustom) {
return null
}
return (
<Select.Option key={key} value={key}>
<Select.Option key={key} value={key} label={makeLabel ? makeLabel(key) : undefined}>
{key}
</Select.Option>
)
Expand All @@ -113,12 +141,20 @@ export function DateFilter({ style, disabled }) {
)
}

function DatePickerDropdown(props) {
const dropdownRef = useRef()
let [calendarOpen, setCalendarOpen] = useState(false)
function DatePickerDropdown(props: {
onClickOutside: () => void
onClick: (e: React.MouseEvent) => void
onDateFromChange: (date: moment.Moment | undefined) => void
onDateToChange: (date: moment.Moment | undefined) => void
onApplyClick: () => void
rangeDateFrom: string | moment.Moment | undefined
rangeDateTo: string | moment.Moment | undefined
}): JSX.Element {
const dropdownRef = useRef<HTMLDivElement | null>(null)
const [calendarOpen, setCalendarOpen] = useState(false)

let onClickOutside = (event) => {
if (!dropdownRef.current.contains(event.target) && !calendarOpen) {
const onClickOutside = (event: MouseEvent): void => {
if ((!event.target || !dropdownRef.current?.contains(event.target as any)) && !calendarOpen) {
props.onClickOutside()
}
}
Expand Down Expand Up @@ -164,9 +200,9 @@ function DatePickerDropdown(props) {
setCalendarOpen(open)
}}
onChange={(dates) => {
if (dates.length === 2) {
props.onDateFromChange(dates[0])
props.onDateToChange(dates[1])
if (dates && dates.length === 2) {
props.onDateFromChange(dates[0] || undefined)
props.onDateToChange(dates[1] || undefined)
}
}}
popupStyle={{ zIndex: 999999 }}
Expand Down
35 changes: 0 additions & 35 deletions frontend/src/lib/components/DateFilter/dateFilterLogic.js

This file was deleted.

51 changes: 51 additions & 0 deletions frontend/src/lib/components/DateFilter/dateFilterLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { kea } from 'kea'
import { router } from 'kea-router'
import { Moment } from 'moment'
import { dateFilterLogicType } from 'lib/components/DateFilter/dateFilterLogicType'
import { objectsEqual } from 'lib/utils'

interface UrlParams {
date_from?: string
date_to?: string
}

export const dateFilterLogic = kea<dateFilterLogicType<UrlParams, Moment>>({
actions: () => ({
setDates: (dateFrom: string | Moment | undefined, dateTo: string | Moment | undefined) => ({
dateFrom,
dateTo,
}),
}),
reducers: () => ({
dates: [
{
dateFrom: undefined as string | Moment | undefined,
dateTo: undefined as string | Moment | undefined,
},
{
setDates: (_, dates) => dates,
},
],
}),
listeners: ({ values }) => ({
setDates: () => {
const { date_from, date_to, ...searchParams } = router.values.searchParams // eslint-disable-line
const { pathname } = router.values.location

searchParams.date_from = values.dates.dateFrom
searchParams.date_to = values.dates.dateTo

if (
(pathname === '/insights' && !objectsEqual(date_from, values.dates.dateFrom)) ||
!objectsEqual(date_to, values.dates.dateTo)
) {
router.actions.push(pathname, searchParams)
}
},
}),
urlToAction: ({ actions }) => ({
'/insights': (_: any, { date_from, date_to }: UrlParams) => {
actions.setDates(date_from, date_to)
},
}),
})
11 changes: 8 additions & 3 deletions frontend/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ export function determineDifferenceType(
}

export const dateMapping: Record<string, string[]> = {
Custom: [],
Today: ['dStart'],
Yesterday: ['-1d', 'dStart'],
'Last 24 hours': ['-24h'],
Expand All @@ -471,7 +472,11 @@ export const dateMapping: Record<string, string[]> = {

export const isDate = /([0-9]{4}-[0-9]{2}-[0-9]{2})/

export function dateFilterToText(dateFrom: string | moment.Moment, dateTo: string | moment.Moment): string {
export function dateFilterToText(
dateFrom: string | moment.Moment | undefined,
dateTo: string | moment.Moment | undefined,
defaultValue: string
): string {
if (moment.isMoment(dateFrom) && moment.isMoment(dateTo)) {
return `${dateFrom.format('YYYY-MM-DD')} - ${dateTo.format('YYYY-MM-DD')}`
}
Expand All @@ -483,9 +488,9 @@ export function dateFilterToText(dateFrom: string | moment.Moment, dateTo: strin
if (dateFrom === 'dStart') {
return 'Today'
} // Changed to "last 24 hours" but this is backwards compatibility
let name = 'Last 7 days'
let name = defaultValue
Object.entries(dateMapping).map(([key, value]) => {
if (value[0] === dateFrom && value[1] === dateTo) {
if (value[0] === dateFrom && value[1] === dateTo && key !== 'Custom') {
name = key
}
})[0]
Expand Down
29 changes: 14 additions & 15 deletions frontend/src/models/dashboardItemsModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import { toast } from 'react-toastify'
import { DashboardItemType } from '~/types'
import { dashboardsModel } from './dashboardsModel'
import { Link } from 'lib/components/Link'
import { dashboardItemsModelType } from '~/models/dashboardItemsModelType'

export const dashboardItemsModel = kea({
export const dashboardItemsModel = kea<dashboardItemsModelType<DashboardItemType>>({
actions: () => ({
renameDashboardItem: (item) => ({ item }),
renameDashboardItemSuccess: (item) => ({ item }),
duplicateDashboardItem: (item, dashboardId, move = false) => ({ item, dashboardId, move }),
duplicateDashboardItemSuccess: (item) => ({ item }),
renameDashboardItem: (item: DashboardItemType) => ({ item }),
renameDashboardItemSuccess: (item: DashboardItemType) => ({ item }),
duplicateDashboardItem: (item: DashboardItemType, dashboardId?: number, move: boolean = false) => ({
item,
dashboardId,
move,
}),
duplicateDashboardItemSuccess: (item: DashboardItemType) => ({ item }),
refreshAllDashboardItems: (filters: Record<string, any>) => filters,
}),
listeners: ({ actions }) => ({
renameDashboardItem: async ({ item }) => {
Expand All @@ -28,20 +34,12 @@ export const dashboardItemsModel = kea({
},
})
},
duplicateDashboardItem: async ({
item,
dashboardId,
move,
}: {
item: DashboardItemType
dashboardId: number
move: boolean
}) => {
duplicateDashboardItem: async ({ item, dashboardId, move }) => {
if (!item) {
return
}

const layouts = {}
const layouts: Record<string, any> = {}
Object.entries(item.layouts || {}).forEach(([size, { w, h }]) => {
layouts[size] = { w, h }
})
Expand All @@ -66,6 +64,7 @@ export const dashboardItemsModel = kea({
</Link>
.&nbsp;
<Link
to="#"
onClick={async () => {
toast.dismiss(toastId)
const [restoredItem, deletedItem] = await Promise.all([
Expand Down
Loading

0 comments on commit f403900

Please sign in to comment.