Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions src/api/query-hooks/useConfigChangesHooks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { configChangesDefaultDateFilter } from "@flanksource-ui/components/Configs/Changes/ConfigChangesFilters/ConfigChangesDateRangeFIlter";
import {
configChangesDefaultDateFilter,
configChangesGraphDefaultDateFilterParams
} from "@flanksource-ui/components/Configs/Changes/ConfigChangesFilters/ConfigChangesDateRangeFIlter";
import { useShowDeletedConfigs } from "@flanksource-ui/store/preference.state";
import { useConfigChangesArbitraryFilters } from "@flanksource-ui/hooks/useConfigChangesArbitraryFilters";
import useReactTablePaginationState from "@flanksource-ui/ui/DataTable/Hooks/useReactTablePaginationState";
import useReactTableSortState from "@flanksource-ui/ui/DataTable/Hooks/useReactTableSortState";
import useTimeRangeParams from "@flanksource-ui/ui/Dates/TimeRangePicker/useTimeRangeParams";
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
import {
UseQueryOptions,
useInfiniteQuery,
useQuery
} from "@tanstack/react-query";
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { usePrefixedSearchParams } from "@flanksource-ui/hooks/usePrefixedSearchParams";
Expand Down Expand Up @@ -167,3 +174,66 @@ export function useGetConfigChangesByIDQuery(
...queryOptions
});
}

export function useGetAllConfigsChangesInfiniteQuery({
pageSize = 1000,
maxChanges,
paramPrefix,
enabled = true
}: {
pageSize?: number;
maxChanges?: number;
paramPrefix?: string;
enabled?: boolean;
} = {}) {
const showChangesFromDeletedConfigs = useShowDeletedConfigs();
const { timeRangeValue } = useTimeRangeParams(
configChangesGraphDefaultDateFilterParams,
paramPrefix
);
const [params] = usePrefixedSearchParams(paramPrefix, false, {
sortBy: "created_at",
sortDirection: "desc"
});
const changeType = params.get("changeType") ?? undefined;
const severity = params.get("severity") ?? undefined;
const configType = params.get("configType") ?? undefined;
const from = timeRangeValue?.from ?? undefined;
const to = timeRangeValue?.to ?? undefined;
const [sortBy] = useReactTableSortState({ paramPrefix });
const configTypes = params.get("configTypes") ?? "all";
const tags = useConfigChangesTagsFilter(paramPrefix);
const arbitraryFilter = useConfigChangesArbitraryFilters(paramPrefix);

const filterProps = {
include_deleted_configs: showChangesFromDeletedConfigs,
changeType,
severity,
from,
to,
configTypes,
configType,
sortBy: sortBy[0]?.id,
sortOrder: (sortBy[0]?.desc ? "desc" : "asc") as "asc" | "desc",
pageSize,
arbitraryFilter,
tags
};

return useInfiniteQuery({
queryKey: ["configs", "changes", "infinite", filterProps, maxChanges],
queryFn: ({ pageParam = 0 }) =>
getConfigsChanges({ ...filterProps, pageIndex: pageParam }),
getNextPageParam: (_lastPage, allPages) => {
const total = allPages[0]?.total ?? 0;
const limit = maxChanges ? Math.min(total, maxChanges) : total;
const loaded = allPages.reduce(
(count, page) => count + (page.changes?.length ?? 0),
0
);
return loaded < limit ? allPages.length : undefined;
},
keepPreviousData: true,
enabled
});
}
1 change: 1 addition & 0 deletions src/api/types/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ConfigChange extends CreatedAt {
name?: string;
created_by?: string;
tags?: Record<string, any>;
path?: string;
first_observed?: string;
count?: number;
inserted_at?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { useConfigChangesViewToggleState } from "@flanksource-ui/components/Configs/Changes/ConfigChangesViewToggle";
import { TimeRangePicker } from "@flanksource-ui/ui/Dates/TimeRangePicker";
import { parseDateMath } from "@flanksource-ui/ui/Dates/TimeRangePicker/parseDateMath";
import useTimeRangeParams from "@flanksource-ui/ui/Dates/TimeRangePicker/useTimeRangeParams";
import dayjs from "dayjs";
import { useCallback, useEffect, useMemo } from "react";
import { URLSearchParamsInit } from "react-router-dom";
import {
RangeOptionsCategory,
TimeRangeOption,
timeRangeOptionsToAbsolute
} from "@flanksource-ui/ui/Dates/TimeRangePicker/rangeOptions";

type Props = {
paramsToReset?: string[];
Expand All @@ -13,21 +22,137 @@ export const configChangesDefaultDateFilter: URLSearchParamsInit = {
range: "now-2d"
};

export const configChangesGraphDefaultDateFilter = {
type: "relative",
display: "2 hours",
range: "now-2h"
} satisfies TimeRangeOption;

export const configChangesGraphDefaultDateFilterParams: URLSearchParamsInit = {
rangeType: "relative",
display: configChangesGraphDefaultDateFilter.display,
range: configChangesGraphDefaultDateFilter.range
};

const graphRangeOptionsCategories: RangeOptionsCategory[] = [
{
name: "Relative time ranges",
type: "past",
options: [
{ type: "relative", display: "5 minutes", range: "now-5m" },
{ type: "relative", display: "15 minutes", range: "now-15m" },
{ type: "relative", display: "30 minutes", range: "now-30m" },
{ type: "relative", display: "1 hour", range: "now-1h" },
{ type: "relative", display: "2 hours", range: "now-2h" },
{ type: "relative", display: "3 hours", range: "now-3h" },
{ type: "relative", display: "6 hours", range: "now-6h" },
{ type: "relative", display: "12 hours", range: "now-12h" },
{ type: "relative", display: "24 hours", range: "now-24h" },
{ type: "relative", display: "2 days", range: "now-2d" },
{ type: "relative", display: "7 days", range: "now-7d" }
]
}
];

const MAX_GRAPH_RANGE_DAYS = 7;
const MAX_GRAPH_RANGE_MS = MAX_GRAPH_RANGE_DAYS * 24 * 60 * 60 * 1000;

const mappedRangesOverGraphLimit = new Set([
"Previous month",
"Previous year",
"This month",
"This month so far",
"This year",
"This year so far"
]);
Comment on lines +60 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hard-code This month so far / This year so far as over-limit.

These labels are not always longer than 7 days. On May 6, 2026, This month so far spans May 1, 2026 → May 6, 2026, so Line 79 rejects a valid graph range before the absolute-duration check runs. This year so far has the same problem during January 1–7. Please rely on the resolved from/to bounds for these dynamic mapped ranges instead of the display text.

💡 Minimal fix
 const mappedRangesOverGraphLimit = new Set([
   "Previous month",
   "Previous year",
   "This month",
-  "This month so far",
   "This year",
-  "This year so far"
 ]);

Also applies to: 77-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/Configs/Changes/ConfigChangesFilters/ConfigChangesDateRangeFIlter.tsx`
around lines 54 - 61, The code currently treats labels in
mappedRangesOverGraphLimit (notably "This month so far" and "This year so far")
as always over-limit which incorrectly rejects dynamic ranges; update the logic
that rejects ranges (the check referencing mappedRangesOverGraphLimit in
ConfigChangesDateRangeFIlter.tsx) to not special-case these "so far" labels but
instead compute the actual duration from the resolved from/to bounds (the
mapped/display range → resolved from/to used elsewhere in this component) and
use that duration for the graph-limit decision; remove "This month so far" and
"This year so far" from mappedRangesOverGraphLimit and ensure the rejection uses
the computed difference between resolved from and to for all mapped ranges.


function resolveDate(value: string, roundUp = false) {
if (value === "now") {
return dayjs();
}
if (value.startsWith("now")) {
return dayjs(parseDateMath(value, roundUp));
}
return dayjs(value);
}

function isRangeOverGraphLimit(range?: TimeRangeOption) {
if (!range) {
return false;
}
if (
range.type === "mapped" &&
mappedRangesOverGraphLimit.has(range.display)
) {
return true;
}

const { from, to } = timeRangeOptionsToAbsolute(range);
const fromDate = resolveDate(from);
const toDate = resolveDate(to, true);

if (!fromDate.isValid() || !toDate.isValid()) {
return false;
}

return toDate.diff(fromDate) > MAX_GRAPH_RANGE_MS;
}

export default function ConfigChangesDateRangeFilter({
paramsToReset = [],
paramPrefix
}: Props) {
const view = useConfigChangesViewToggleState();
const isGraphView = view === "Graph";
const defaultDateFilter = useMemo(
() =>
isGraphView
? configChangesGraphDefaultDateFilterParams
: configChangesDefaultDateFilter,
[isGraphView]
);
const { setTimeRangeParams, getTimeRangeFromUrl } = useTimeRangeParams(
configChangesDefaultDateFilter,
defaultDateFilter,
paramPrefix
);

const timeRangeValue = getTimeRangeFromUrl();

const validateGraphTimeRange = useCallback(
(timeRange: TimeRangeOption) => {
if (isGraphView && isRangeOverGraphLimit(timeRange)) {
return `Graph mode supports a maximum time range of ${MAX_GRAPH_RANGE_DAYS} days.`;
}
},
[isGraphView]
);

const setValidTimeRangeParams = useCallback(
(timeRange: TimeRangeOption) => {
setTimeRangeParams(
validateGraphTimeRange(timeRange)
? configChangesGraphDefaultDateFilter
: timeRange,
paramsToReset
);
},
[paramsToReset, setTimeRangeParams, validateGraphTimeRange]
);

useEffect(() => {
if (isGraphView && isRangeOverGraphLimit(timeRangeValue)) {
setTimeRangeParams(configChangesGraphDefaultDateFilter, paramsToReset);
}
}, [isGraphView, paramsToReset, setTimeRangeParams, timeRangeValue]);
Comment on lines +142 to +146

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid overwriting the shared time-range params on a view toggle.

This effect turns a non-graph range into now-2h as soon as the user enters Graph view. Because the time range lives in the URL, switching back to Table loses the user's original selection as well. A view toggle should not destructively rewrite shared filters; this needs a graph-only fallback or separate range state per view.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/Configs/Changes/ConfigChangesFilters/ConfigChangesDateRangeFIlter.tsx`
around lines 140 - 144, The effect in useEffect currently overwrites the shared
URL time-range params when toggling to Graph view by calling
setTimeRangeParams(configChangesGraphDefaultDateFilter, paramsToReset) whenever
isGraphView && isRangeOverGraphLimit(timeRangeValue); instead, avoid mutating
the shared filter: implement a graph-only fallback or separate per-view
state—e.g., introduce a graphTimeRange (or graph-specific param key) and only
apply configChangesGraphDefaultDateFilter to that graph-only state when
isGraphView is true, leaving the shared timeRangeValue and URL params untouched;
update the useEffect to conditionally set only the graph-specific state/params
(using isRangeOverGraphLimit(timeRangeValue) to detect overflow) rather than
calling setTimeRangeParams on the shared paramsToReset.


return (
<TimeRangePicker
onChange={(timeRange) => setTimeRangeParams(timeRange, paramsToReset)}
onChange={setValidTimeRangeParams}
value={timeRangeValue}
rangeOptionsCategories={
isGraphView ? graphRangeOptionsCategories : undefined
}
validateRange={validateGraphTimeRange}
/>
);
}
Loading
Loading