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
17 changes: 12 additions & 5 deletions src/api/query-hooks/useConfigChangesHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ function useConfigChangesTagsFilter(paramPrefix?: string) {
export function useGetAllConfigsChangesQuery(
{
paramPrefix,
cursor,
...queryOptions
}: UseQueryOptions<CatalogChangesSearchResponse> & {
paramPrefix?: string;
cursor?: string;
} = {
enabled: true,
keepPreviousData: true
Expand Down Expand Up @@ -73,8 +75,8 @@ export function useGetAllConfigsChangesQuery(
include_deleted_configs: showChangesFromDeletedConfigs,
changeType,
severity,
from,
to,
from: cursor ?? from,
to: cursor ? undefined : to,
configTypes,
configType,
sortBy: sortBy[0]?.id,
Expand All @@ -93,7 +95,12 @@ export function useGetAllConfigsChangesQuery(
}

export function useGetConfigChangesByIDQuery(
queryOptions: UseQueryOptions<CatalogChangesSearchResponse> = {
{
cursor,
...queryOptions
}: UseQueryOptions<CatalogChangesSearchResponse> & {
cursor?: string;
} = {
enabled: true,
keepPreviousData: true
}
Expand Down Expand Up @@ -141,8 +148,8 @@ export function useGetConfigChangesByIDQuery(
include_deleted_configs: showChangesFromDeletedConfigs,
changeType: change_type,
severity,
from,
to,
from: cursor ?? from,
to: cursor ? undefined : to,
configTypes,
sortBy: sortBy[0]?.id,
sortOrder: sortBy[0]?.desc ? "desc" : "asc",
Expand Down
1 change: 1 addition & 0 deletions src/api/types/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface ConfigChange extends CreatedAt {
tags?: Record<string, any>;
first_observed?: string;
count?: number;
inserted_at?: string;
}

export interface Change {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export function FilterBadge({ filters, paramKey }: FilterBadgeProps) {

type ConfigChangeFiltersProps = React.HTMLProps<HTMLDivElement> & {
paramsToReset?: string[];
extra?: React.ReactNode;
};

export function ConfigChangeFilters({
className,
paramsToReset = [],
extra,
...props
}: ConfigChangeFiltersProps) {
const [params] = useSearchParams();
Expand Down Expand Up @@ -100,6 +102,7 @@ export function ConfigChangeFilters({
<ConfigTagsDropdown />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
<ShowDeletedConfigs />
{extra}
</div>
</FormikFilterForm>
<div className="flex flex-wrap gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import ConfigTypesTristateDropdown from "../../ConfigChangesFilters/ConfigTypesT
type ConfigChangeFiltersProps = {
className?: string;
paramsToReset?: string[];
extra?: React.ReactNode;
};

export function ConfigRelatedChangesFilters({
className,
paramsToReset = []
paramsToReset = [],
extra
}: ConfigChangeFiltersProps) {
const arbitraryFilters = useConfigChangesArbitraryFilters();

Expand All @@ -35,6 +37,7 @@ export function ConfigRelatedChangesFilters({
<ConfigRelatedChangesToggles />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
<ShowDeletedConfigs />
{extra}
</div>
</FormikFilterForm>
<div className="flex flex-wrap gap-2">
Expand Down
100 changes: 92 additions & 8 deletions src/pages/config/ConfigChangesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useGetAllConfigsChangesQuery } from "@flanksource-ui/api/query-hooks/useConfigChangesHooks";
import { ConfigChange } from "@flanksource-ui/api/types/configs";
import { ConfigChangeTable } from "@flanksource-ui/components/Configs/Changes/ConfigChangeTable";
import { ConfigChangeFilters } from "@flanksource-ui/components/Configs/Changes/ConfigChangesFilters/ConfigChangesFilters";
import ConfigPageTabs from "@flanksource-ui/components/Configs/ConfigPageTabs";
Expand All @@ -12,9 +13,22 @@ import {
import { Head } from "@flanksource-ui/ui/Head";
import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout";
import { refreshButtonClickedTrigger } from "@flanksource-ui/ui/SlidingSideBar/SlidingSideBar";
import { Toggle } from "@flanksource-ui/ui/FormControls/Toggle";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";

function getNewestCreatedAt(changes: ConfigChange[]): string | undefined {
let latest: string | undefined;
for (const c of changes) {
const ts = typeof c.created_at === "string" ? c.created_at : undefined;
if (ts && (!latest || ts > latest)) {
latest = ts;
}
}
return latest;
}
Comment on lines +21 to +30
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

Use inserted_at for the tail cursor.

The live stream is advancing on created_at, but this feature is supposed to fetch deltas by inserted_at. With the current cursor, a later-inserted row whose created_at is older than the cursor is silently missed.

Suggested fix
-function getNewestCreatedAt(changes: ConfigChange[]): string | undefined {
+function getNewestInsertedAt(changes: ConfigChange[]): string | undefined {
   return changes.reduce(
     (latest, c) =>
-      c.created_at && (!latest || c.created_at > latest)
-        ? c.created_at
+      c.inserted_at && (!latest || c.inserted_at > latest)
+        ? c.inserted_at
         : latest,
     undefined as string | undefined
   );
 }
@@
-      setTailCursor(getNewestCreatedAt(data.changes));
+      setTailCursor(getNewestInsertedAt(data.changes));
@@
-    const newest = getNewestCreatedAt(incoming);
+    const newest = getNewestInsertedAt(incoming);

Also applies to: 58-60, 83-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/ConfigChangesPage.tsx` around lines 21 - 29, The tail cursor
is being computed from created_at but must use inserted_at; update
getNewestCreatedAt to read and compare c.inserted_at (returning inserted_at as
string | undefined) instead of c.created_at, and replace any other occurrences
that derive the tail cursor from created_at (the similar logic at the other two
locations referenced) so all cursor computation and comparisons consistently use
inserted_at.


export function ConfigChangesPage() {
const [, setRefreshButtonClickedTrigger] = useAtom(
refreshButtonClickedTrigger
Expand All @@ -31,22 +45,83 @@ export function ConfigChangesPage() {

const pageSize = params.get("pageSize") ?? "200";

const [liveTail, setLiveTail] = useState(false);
const [tailCursor, setTailCursor] = useState<string | undefined>(undefined);
const [tailedChanges, setTailedChanges] = useState<ConfigChange[]>([]);

const { data, isLoading, error, isRefetching, refetch } =
useGetAllConfigsChangesQuery({
keepPreviousData: true
});

const changes = (data?.changes ?? []).map((changes) => ({
...changes,
// Initialize cursor from base data when live tail is turned on
useEffect(() => {
if (liveTail && data?.changes?.length && !tailCursor) {
setTailCursor(getNewestCreatedAt(data.changes));
}
Comment on lines +57 to +61
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

Live mode is incorrect on page 2+.

tailCursor is seeded from the newest row in the currently loaded page, not from the actual head of the filtered result set. If someone enables Live while viewing an older page, the first poll will replay rows from earlier pages as "new" and distort both the table and the derived totals.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/ConfigChangesPage.tsx` around lines 56 - 60, The bug seeds
tailCursor from the currently loaded page (getNewestCreatedAt(data.changes)),
causing “new” rows when Live is enabled on page >1; change the logic in the
useEffect that watches liveTail to instead determine the true head of the
filtered result set (not the current page) — e.g., when liveTail becomes true
call the existing data-fetching logic or a lightweight API endpoint to fetch the
newest change for the current filters (page=1 sorted by created_at desc or a
dedicated newestChangedAt query), then setTailCursor to that returned
created_at; update the useEffect around
liveTail/tailCursor/setTailCursor/getNewestCreatedAt to use that server-derived
newest timestamp so enabling Live never replays older pages as new.

}, [liveTail, data, tailCursor]);

// Reset when live tail is turned off
useEffect(() => {
if (!liveTail) {
setTailedChanges([]);
setTailCursor(undefined);
}
}, [liveTail]);
Comment on lines +64 to +70
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

Reset tail state when the query scope changes.

The tail state is only cleared when liveTail turns off. If filters or pagination change while it stays on, tailedChanges and tailCursor carry over from the previous scope, which can mix results from different queries and skip valid rows in the new scope.

Also applies to: 71-76


const { data: pollData } = useGetAllConfigsChangesQuery({
cursor: tailCursor,
keepPreviousData: false,
enabled: liveTail && !!tailCursor,
refetchInterval: liveTail ? 5000 : false
});

// Accumulate new items from poll and advance cursor
useEffect(() => {
if (!pollData?.changes?.length) return;

const incoming = pollData.changes;
const newest = getNewestCreatedAt(incoming);
if (newest) {
setTailCursor((prev) => (!prev || newest > prev ? newest : prev));
}

setTailedChanges((prev) => {
const incomingIds = new Set(incoming.map((c) => c.id));
const filtered = prev.filter((c) => !incomingIds.has(c.id));
return [...incoming, ...filtered];
});
}, [pollData]);

const baseChanges = (data?.changes ?? []).map((change) => ({
...change,
config: {
id: change.config_id!,
type: change.type!,
name: change.name!,
deleted_at: change.deleted_at
}
}));

const tailedWithConfig = tailedChanges.map((change) => ({
...change,
config: {
id: changes.config_id!,
type: changes.type!,
name: changes.name!,
deleted_at: changes.deleted_at
id: change.config_id!,
type: change.type!,
name: change.name!,
deleted_at: change.deleted_at
}
}));

const totalChanges = data?.total ?? 0;
const tailedIds = new Set(tailedWithConfig.map((c) => c.id));
const baseWithoutTailed = baseChanges.filter((c) => !tailedIds.has(c.id));
const baseIds = new Set(baseChanges.map((c) => c.id));
const newTailedCount = tailedWithConfig.filter(
(c) => !baseIds.has(c.id)
).length;
const changes = [...tailedWithConfig, ...baseWithoutTailed];

const totalChanges = (data?.total ?? 0) + newTailedCount;
const totalChangesPages = Math.ceil(totalChanges / parseInt(pageSize));

const errorMessage =
Expand Down Expand Up @@ -99,7 +174,16 @@ export function ConfigChangesPage() {
<InfoMessage message={errorMessage} />
) : (
<>
<ConfigChangeFilters paramsToReset={["page"]} />
<ConfigChangeFilters
paramsToReset={["page"]}
extra={
<Toggle
label="Live"
value={liveTail}
onChange={setLiveTail}
/>
}
/>
<ConfigChangeTable
data={changes}
isLoading={isLoading}
Expand Down
93 changes: 86 additions & 7 deletions src/pages/config/details/ConfigDetailsChangesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { useGetConfigChangesByIDQuery } from "@flanksource-ui/api/query-hooks/useConfigChangesHooks";
import { ConfigChange } from "@flanksource-ui/api/types/configs";
import { ConfigChangeTable } from "@flanksource-ui/components/Configs/Changes/ConfigChangeTable";
import { ConfigRelatedChangesFilters } from "@flanksource-ui/components/Configs/Changes/ConfigsRelatedChanges/FilterBar/ConfigRelatedChangesFilters";
import { ConfigDetailsTabs } from "@flanksource-ui/components/Configs/ConfigDetailsTabs";
import { InfoMessage } from "@flanksource-ui/components/InfoMessage";
import { Toggle } from "@flanksource-ui/ui/FormControls/Toggle";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";

function getNewestCreatedAt(changes: ConfigChange[]): string | undefined {
let latest: string | undefined;
for (const c of changes) {
const ts = typeof c.created_at === "string" ? c.created_at : undefined;
if (ts && (!latest || ts > latest)) {
latest = ts;
}
}
return latest;
}
Comment on lines +11 to +20
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

Use inserted_at instead of created_at for the tail cursor.

Per the PR objectives, the delta fetch should use inserted_at to identify new entries. Using created_at can miss rows that were inserted after the cursor timestamp but have an older created_at value.

Suggested fix
-function getNewestCreatedAt(changes: ConfigChange[]): string | undefined {
+function getNewestInsertedAt(changes: ConfigChange[]): string | undefined {
   return changes.reduce<string | undefined>(
     (latest, c) =>
-      c.created_at && (!latest || c.created_at > latest)
-        ? c.created_at
+      c.inserted_at && (!latest || c.inserted_at > latest)
+        ? c.inserted_at
         : latest,
     undefined
   );
 }

Update the call sites at lines 43 and 67 accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/details/ConfigDetailsChangesPage.tsx` around lines 11 - 19,
The helper getNewestCreatedAt currently computes the tail cursor using
created_at; change it to use the inserted_at field instead (e.g., rename to
getNewestInsertedAt or update its logic) so the reduce compares c.inserted_at
and returns the newest inserted_at string (handle missing values the same way as
before). Then update all call sites that use getNewestCreatedAt (the two places
where the tail cursor is built) to call the new/updated helper so the delta
fetch uses inserted_at rather than created_at.


export function ConfigDetailsChangesPage() {
const { id } = useParams();

Expand All @@ -15,21 +29,81 @@ export function ConfigDetailsChangesPage() {

const pageSize = params.get("pageSize") ?? "200";

const [liveTail, setLiveTail] = useState(false);
const [tailCursor, setTailCursor] = useState<string | undefined>(undefined);
const [tailedChanges, setTailedChanges] = useState<ConfigChange[]>([]);

const { data, isLoading, error, refetch } = useGetConfigChangesByIDQuery({
keepPreviousData: true,
enabled: !!id
});

const changes = (data?.changes ?? []).map((changes) => ({
...changes,
// Initialize cursor from base data when live tail is turned on
useEffect(() => {
if (liveTail && data?.changes?.length && !tailCursor) {
setTailCursor(getNewestCreatedAt(data.changes));
}
Comment on lines +41 to +45
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

Bootstrapping from data.changes breaks Live mode.

This only seeds from the currently loaded slice, and the poll query will not run until tailCursor is set. That means enabling Live on an empty result never starts polling, and enabling it on an older slice can replay already-existing rows as “new”. Seed from a server-derived head for the current filters, or at least from a server-side “now” when there are no loaded rows.

Also applies to: 56-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/details/ConfigDetailsChangesPage.tsx` around lines 41 - 45,
The current useEffect seeds tailCursor from the loaded slice (data.changes)
which prevents live polling when the slice is empty or causes replay of old
rows; change the logic in the useEffect that watches liveTail/data/changing
tailCursor (and the similar block at lines 56-60) to request a server-derived
head/timestamp for the current filters when enabling Live instead of (or if)
data.changes is empty: call a backend helper (e.g., fetchHeadCursorForFilters or
getServerNow) and setTailCursor with that server head when data.changes.length
=== 0, otherwise keep the existing getNewestCreatedAt(data.changes) behavior;
ensure the network call runs only when liveTail becomes true and tailCursor is
unset so polling starts correctly.

}, [liveTail, data, tailCursor]);

// Reset when live tail is turned off
useEffect(() => {
if (!liveTail) {
setTailedChanges([]);
setTailCursor(undefined);
}
}, [liveTail]);
Comment on lines +48 to +54
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

Reset live-tail state when the query context changes.

While liveTail is on, the base query is disabled but the filter bar stays active. Changing filters/sort/time range/config id updates the hook inputs, yet tailedChanges and tailCursor are only cleared when liveTail flips to false. That can mix rows from the previous query into the new one and poll from the wrong cursor.

Please clear/reseed the tail state whenever the query-shaping params change, or disable those controls while live mode is active.

Also applies to: 123-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/details/ConfigDetailsChangesPage.tsx` around lines 47 - 53,
When liveTail is active the tailed state is not reset when query-shaping
parameters change, causing mixing of rows and wrong cursors; update the
useEffect that currently depends only on liveTail to also depend on the
query-shaping params (e.g., filters, sort, time range, config id or the hook
inputs that derive the base query) and call setTailedChanges([]) and
setTailCursor(undefined) whenever any of those params change, and apply the same
change to the other similar effect block referenced in the comment so the tail
state is reseeded whenever the query context updates.


const { data: pollData } = useGetConfigChangesByIDQuery({
cursor: tailCursor,
keepPreviousData: false,
enabled: liveTail && !!id && !!tailCursor,
refetchInterval: liveTail ? 5000 : false
});

// Accumulate new items from poll and advance cursor
useEffect(() => {
if (!pollData?.changes?.length) return;

const incoming = pollData.changes;
const newest = getNewestCreatedAt(incoming);
if (newest) {
setTailCursor((prev) => (!prev || newest > prev ? newest : prev));
}
Comment on lines +56 to +71
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 | 🔴 Critical

Live polling can permanently skip changes.

useGetConfigChangesByIDQuery still applies the table's pageIndex/pageSize when from_inserted_at is set (src/api/query-hooks/useConfigChangesHooks.ts:152-165). In live mode that means this fetch only returns one page of the delta. If the user is not on page 0, or more than one page of changes arrives between polls, Line 69 advances tailCursor past unseen rows and they are never fetched.

Please make the tail query ignore normal pagination, or drain all delta pages before moving the cursor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/details/ConfigDetailsChangesPage.tsx` around lines 55 - 70,
The live-polling query useGetConfigChangesByIDQuery currently respects table
pagination so setTailCursor can skip unseen rows; modify the tail fetch to
ignore normal pagination or fully drain all delta pages before advancing the
cursor: either (A) update the call-site to pass explicit pagination override
flags (e.g., pageIndex: 0 and pageSize: a large value or a new ignorePagination:
true option) to useGetConfigChangesByIDQuery so the poll returns the entire
delta, or (B) implement a drain loop after receiving pollData in
ConfigDetailsChangesPage that repeatedly requests subsequent pages from
useGetConfigChangesByIDQuery (or the underlying fetch function from
useConfigChangesHooks) until no more changes remain, then compute newest via
getNewestInsertedAt and call setTailCursor with that newest value; ensure this
change references useGetConfigChangesByIDQuery, tailCursor, liveTail,
getNewestInsertedAt, and setTailCursor so the tail cursor only advances after
all delta pages are consumed.


setTailedChanges((prev) => {
const incomingIds = new Set(incoming.map((c) => c.id));
const filtered = prev.filter((c) => !incomingIds.has(c.id));
return [...incoming, ...filtered];
});
}, [pollData]);

const baseChanges = (data?.changes ?? []).map((change) => ({
...change,
config: {
id: changes.config_id!,
type: changes.type!,
name: changes.name!
id: change.config_id!,
type: change.type!,
name: change.name!
}
}));

const totalChanges = data?.total ?? 0;
const tailedWithConfig = tailedChanges.map((change) => ({
...change,
config: {
id: change.config_id!,
type: change.type!,
name: change.name!
}
}));
Comment on lines +80 to +96
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 | 🟡 Minor

Non-null assertions may cause runtime errors if API returns incomplete data.

The code uses non-null assertions (!) on config_id, type, and name properties which are typed as optional in ConfigChange. If the API returns changes without these fields, this will result in undefined values being assigned.

Consider adding fallback values or filtering out incomplete records:

Proposed defensive handling
  const baseChanges = (data?.changes ?? []).map((change) => ({
    ...change,
    config: {
-     id: change.config_id!,
-     type: change.type!,
-     name: change.name!
+     id: change.config_id ?? "",
+     type: change.type ?? "",
+     name: change.name ?? ""
    }
  }));

  const tailedWithConfig = tailedChanges.map((change) => ({
    ...change,
    config: {
-     id: change.config_id!,
-     type: change.type!,
-     name: change.name!
+     id: change.config_id ?? "",
+     type: change.type ?? "",
+     name: change.name ?? ""
    }
  }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const baseChanges = (data?.changes ?? []).map((change) => ({
...change,
config: {
id: changes.config_id!,
type: changes.type!,
name: changes.name!
id: change.config_id!,
type: change.type!,
name: change.name!
}
}));
const totalChanges = data?.total ?? 0;
const tailedWithConfig = tailedChanges.map((change) => ({
...change,
config: {
id: change.config_id!,
type: change.type!,
name: change.name!
}
}));
const baseChanges = (data?.changes ?? []).map((change) => ({
...change,
config: {
id: change.config_id ?? "",
type: change.type ?? "",
name: change.name ?? ""
}
}));
const tailedWithConfig = tailedChanges.map((change) => ({
...change,
config: {
id: change.config_id ?? "",
type: change.type ?? "",
name: change.name ?? ""
}
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/config/details/ConfigDetailsChangesPage.tsx` around lines 80 - 96,
The mapping for baseChanges and tailedWithConfig is using non-null assertions on
change.config_id, change.type, and change.name which can throw if the API
returns incomplete records; update the mapping logic in ConfigDetailsChangesPage
(functions/vars: baseChanges and tailedWithConfig) to either filter out changes
missing required fields (e.g., drop entries where any of config_id/type/name is
null/undefined) or provide safe fallbacks (e.g., id: change.config_id ??
'unknown', type: change.type ?? 'unknown', name: change.name ?? 'Unnamed')
before constructing the config object so runtime errors are avoided.


const tailedIds = new Set(tailedWithConfig.map((c) => c.id));
const baseWithoutTailed = baseChanges.filter((c) => !tailedIds.has(c.id));
const baseIds = new Set(baseChanges.map((c) => c.id));
const newTailedCount = tailedWithConfig.filter(
(c) => !baseIds.has(c.id)
).length;
const changes = [...tailedWithConfig, ...baseWithoutTailed];

const totalChanges = (data?.total ?? 0) + newTailedCount;
const totalChangesPages = Math.ceil(totalChanges / parseInt(pageSize));

if (error) {
Expand All @@ -50,7 +124,12 @@ export function ConfigDetailsChangesPage() {
>
<div className={`flex h-full flex-1 flex-col overflow-y-auto`}>
<div className="flex w-full flex-1 flex-col items-start gap-2 overflow-y-auto">
<ConfigRelatedChangesFilters paramsToReset={["page"]} />
<ConfigRelatedChangesFilters
paramsToReset={["page"]}
extra={
<Toggle label="Live" value={liveTail} onChange={setLiveTail} />
}
/>
<div className="flex w-full flex-1 flex-col overflow-y-auto">
<ConfigChangeTable
data={changes}
Expand Down
Loading