diff --git a/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx b/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx index 522ba901a977e..d0764df2ad599 100644 --- a/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx +++ b/frontend/src/lib/components/IntervalFilter/IntervalFilter.tsx @@ -1,4 +1,5 @@ -import { LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' +import { IconPin } from '@posthog/icons' +import { LemonButton, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' @@ -12,27 +13,43 @@ interface IntervalFilterProps { export function IntervalFilter({ disabled }: IntervalFilterProps): JSX.Element { const { insightProps } = useValues(insightLogic) - const { interval, enabledIntervals } = useValues(insightVizDataLogic(insightProps)) - const { updateQuerySource } = useActions(insightVizDataLogic(insightProps)) + const { interval, enabledIntervals, isIntervalManuallySet } = useValues(insightVizDataLogic(insightProps)) + const { updateQuerySource, setIsIntervalManuallySet } = useActions(insightVizDataLogic(insightProps)) return ( <> grouped by - { - updateQuerySource({ interval: value } as Partial) - }} - options={Object.entries(enabledIntervals).map(([value, { label, disabledReason, hidden }]) => ({ - value: value as IntervalType, - label, - hidden, - disabledReason, - }))} - /> + {isIntervalManuallySet ? ( + { + setIsIntervalManuallySet(false) + }} + tooltip="Unpin interval" + className="flex-1" + center + size="small" + icon={} + > + {interval || 'day'} + + ) : ( + { + updateQuerySource({ interval: value } as Partial) + }} + options={Object.entries(enabledIntervals).map(([value, { label, disabledReason, hidden }]) => ({ + value: value as IntervalType, + label, + hidden, + disabledReason, + }))} + /> + )} ) } diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index aec4a1eb32ed8..14b0b4cbd393d 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -100,6 +100,7 @@ export const insightVizDataLogic = kea([ updateDisplay: (display: ChartDisplayType | undefined) => ({ display }), updateHiddenLegendIndexes: (hiddenLegendIndexes: number[] | undefined) => ({ hiddenLegendIndexes }), setTimedOutQueryId: (id: string | null) => ({ id }), + setIsIntervalManuallySet: (isIntervalManuallySet: boolean) => ({ isIntervalManuallySet }), }), reducers({ @@ -109,6 +110,18 @@ export const insightVizDataLogic = kea([ setTimedOutQueryId: (_, { id }) => id, }, ], + + // Whether the interval has been manually set by the user. If true, prevents auto-adjusting the interval when date range changes. Reference: https://github.com/PostHog/posthog/issues/22785 + isIntervalManuallySet: [ + false, + { + updateQuerySource: (state, { querySource }) => { + // If interval is explicitly included in the update, mark it as manually set + return 'interval' in querySource ? true : state + }, + setIsIntervalManuallySet: (_, { isIntervalManuallySet }) => isIntervalManuallySet, + }, + ], }), selectors({ @@ -332,7 +345,7 @@ export const insightVizDataLogic = kea([ // We use 512 for query timeouts // Async queries put the error message on data.error_message, while synchronous ones use detail return insightDataError?.status === 400 || insightDataError?.status === 512 - ? (insightDataError.detail || insightDataError.data?.error_message)?.replace('Try ', 'Try ') // Add unbreakable space for better line breaking + ? (insightDataError.detail || insightDataError.data?.error_message)?.replace('Try ', 'Try ') // Add unbreakable space for better line breaking : null }, ], @@ -401,7 +414,11 @@ export const insightVizDataLogic = kea([ ...values.query, source: { ...values.querySource, - ...handleQuerySourceUpdateSideEffects(querySource, values.querySource as InsightQueryNode), + ...handleQuerySourceUpdateSideEffects( + querySource, + values.querySource as InsightQueryNode, + values.isIntervalManuallySet + ), }, } as Node) }, @@ -487,7 +504,8 @@ const getActiveUsersMath = ( const handleQuerySourceUpdateSideEffects = ( update: QuerySourceUpdate, - currentState: InsightQueryNode + currentState: InsightQueryNode, + isIntervalManuallySet: boolean ): QuerySourceUpdate => { const mergedUpdate = { ...update } as InsightQueryNode @@ -536,7 +554,8 @@ const handleQuerySourceUpdateSideEffects = ( update.dateRange && update.dateRange.date_from && (update.dateRange.date_from !== currentState.dateRange?.date_from || - update.dateRange.date_to !== currentState.dateRange?.date_to) + update.dateRange.date_to !== currentState.dateRange?.date_to) && + !isIntervalManuallySet // Only auto-adjust interval if not manually set ) { const { date_from, date_to } = { ...currentState.dateRange, ...update.dateRange } diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index 3d59bb5f18d71..0114e54cf72c2 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -207,6 +207,7 @@ Only 400 (`normal`), 500 (`var(--font-medium)`), 600 (`var(--font-semibold)`), o --content-link: var(--brand-500); --content-link-hover: var(--brand-400); --content-link-pressed: var(--brand-600); + --content-warning: var(--orange-400); --content-warning-bold: var(--orange-700); --content-danger: var(--red-500); --content-danger-bold: var(--red-600); @@ -577,6 +578,7 @@ body { &[theme='dark'] { // Semantic colors (Dark mode) WIP --content-primary: var(--neutral-cool-100); + --content-warning: var(--orange-300); --content-warning-bold: var(--orange-100); --content-danger-bold: var(--red-100); --content-success-bold: var(--green-100);