Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@
*/

import { Box, GridItem, Tag, TagCloseButton, TagLabel } from '@redpanda-data/ui';
import { observer } from 'mobx-react';
import type { FC } from 'react';
import { MdOutlineSettings } from 'react-icons/md';

import type { FilterEntry } from '../../../../../state/ui';
import { uiState } from '../../../../../state/ui-state';

export const MessageSearchFilterBar: FC<{ onEdit: (filter: FilterEntry) => void }> = observer(({ onEdit }) => {
const settings = uiState.topicSettings.searchParams;
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand the change was to replace the global state by sessions storage, could we use zustand to manage this global state and use the persistence layer https://redpandadata.slack.com/archives/C063X3UEWCA/p1762753166216419

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't follow, what is the advantage of this? Session storage is persisted per reloads and only gets erased once user close the tab. Having zustand in between does't really make sense. UX doesn't want to have any kind of persistance here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes my point is if I want to use a global state that happens to be stored in Session Storage, so maybe if there is a known pattern of handling this global state it will be easier to maintain as opposed to a solution only for this page.

interface MessageSearchFilterBarProps {
filters: FilterEntry[];
onEdit: (filter: FilterEntry) => void;
onToggle: (filterId: string) => void;
onRemove: (filterId: string) => void;
}

export const MessageSearchFilterBar: FC<MessageSearchFilterBarProps> = ({ filters, onEdit, onToggle, onRemove }) => {
return (
<GridItem display="flex" gridColumn="-1/1" justifyContent="space-between">
<Box columnGap="8px" display="inline-flex" flexWrap="wrap" rowGap="2px" width="calc(100% - 200px)">
{/* Existing Tags List */}
{settings.filters?.map((e) => (
{filters?.map((e) => (
<Tag
className={e.isActive ? 'filterTag' : 'filterTag filterTagDisabled'}
key={e.id}
Expand All @@ -43,16 +46,16 @@ export const MessageSearchFilterBar: FC<{ onEdit: (filter: FilterEntry) => void
display="inline-flex"
height="100%"
mx="2"
onClick={() => (e.isActive = !e.isActive)}
onClick={() => onToggle(e.id)}
px="6px"
textDecoration={e.isActive ? '' : 'line-through'}
>
{e.name || e.code || 'New Filter'}
</TagLabel>
<TagCloseButton m="0" onClick={() => settings.filters.remove(e)} opacity={1} px="1" />
<TagCloseButton m="0" onClick={() => onRemove(e.id)} opacity={1} px="1" />
</Tag>
))}
</Box>
</GridItem>
);
});
};
41 changes: 30 additions & 11 deletions frontend/src/components/pages/topics/Tab.Messages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { IsDev } from '../../../../utils/env';
import { sanitizeString, wrapFilterFragment } from '../../../../utils/filter-helper';
import { onPaginationChange } from '../../../../utils/pagination';
import { sortingParser } from '../../../../utils/sorting-parser';
import { getTopicFilters, setTopicFilters } from '../../../../utils/topic-filters-session';
import {
Label,
navigatorClipboardErrorHandler,
Expand Down Expand Up @@ -363,6 +364,14 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {

const [quickSearch, setQuickSearch] = useQueryState('q', parseAsString.withDefault(''));

// Filters with session storage (NOT in URL)
const [filters, setFilters] = useState<FilterEntry[]>(() => getTopicFilters(props.topic.topicName));

// Sync filters to session storage whenever they change
useEffect(() => {
setTopicFilters(props.topic.topicName, filters);
}, [filters, props.topic.topicName]);

// Deserializer settings with URL state management
const [keyDeserializer, setKeyDeserializer] = useQueryStateWithCallback<PayloadEncoding>(
{
Expand Down Expand Up @@ -485,7 +494,8 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
const functionNames: string[] = [];
const functions: string[] = [];

const filteredSearchParams = (currentSearchParams?.filters ?? []).filter(
// Use filters from URL state instead of localStorage
const filteredSearchParams = filters.filter(
(searchParam) => searchParam.isActive && searchParam.code && searchParam.transpiledCode
);

Expand Down Expand Up @@ -557,14 +567,16 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
getSearchParams,
keyDeserializer,
valueDeserializer,
filters,
]);

// Convert searchFunc to useCallback
const searchFunc = useCallback(
(source: 'auto' | 'manual') => {
// Create search params signature
// Create search params signature (includes filters to detect changes)
const currentSearchParams = getSearchParams(props.topic.topicName);
const searchParams = `${startOffset} ${maxResults} ${partitionID} ${currentSearchParams?.startTimestamp ?? uiState.topicSettings.searchParams.startTimestamp} ${keyDeserializer} ${valueDeserializer}`;
const filtersSignature = filters.map((f) => `${f.id}:${f.isActive}:${f.transpiledCode}`).join('|');
const searchParams = `${startOffset} ${maxResults} ${partitionID} ${currentSearchParams?.startTimestamp ?? uiState.topicSettings.searchParams.startTimestamp} ${keyDeserializer} ${valueDeserializer} ${filtersSignature}`;

if (searchParams === currentSearchRunRef.current && source === 'auto') {
// biome-ignore lint/suspicious/noConsole: intentional console usage
Expand Down Expand Up @@ -615,6 +627,7 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
props.topic.topicName,
keyDeserializer,
valueDeserializer,
filters,
]
);

Expand Down Expand Up @@ -997,7 +1010,6 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
isDisabled={!canUseFilters}
onClick={() => {
const filter = new FilterEntry();
filter.isNew = true;
setCurrentJSFilter(filter);
}}
>
Expand Down Expand Up @@ -1080,9 +1092,16 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {

{/* Filter Tags */}
<MessageSearchFilterBar
filters={filters}
onEdit={(filter) => {
setCurrentJSFilter(filter);
}}
onRemove={(filterId) => {
setFilters(filters.filter((f) => f.id !== filterId));
}}
onToggle={(filterId) => {
setFilters(filters.map((f) => (f.id === filterId ? { ...f, isActive: !f.isActive } : f)));
}}
/>

<GridItem display="flex" gap={4} gridColumn="1/-1" mt={4}>
Expand Down Expand Up @@ -1120,14 +1139,14 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
currentFilter={currentJSFilter}
onClose={() => setCurrentJSFilter(null)}
onSave={(filter) => {
if (filter.isNew) {
uiState.topicSettings.searchParams.filters.push(filter);
filter.isNew = false;
// Check if filter exists in the array by ID
const existingIndex = filters.findIndex((f) => f.id === filter.id);
if (existingIndex >= 0) {
// Update existing filter
setFilters(filters.map((f) => (f.id === filter.id ? filter : f)));
} else {
const idx = uiState.topicSettings.searchParams.filters.findIndex((x) => x.id === filter.id);
if (idx !== -1) {
uiState.topicSettings.searchParams.filters.splice(idx, 1, filter);
}
// Add new filter
setFilters([...filters, filter]);
}
searchFunc('manual');
}}
Expand Down
80 changes: 80 additions & 0 deletions frontend/src/utils/topic-filters-session.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

could we validate the session storage works in secure environments, I assume it does. But I would like to double check

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

what do you mean by secure env?

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant https, I think the browser might require and extra config. it's possible that the sessionStorage object handles this for you.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright 2025 Redpanda Data, Inc.
*
* Use of this software is governed by the Business Source License
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
*
* As of the Change Date specified in that file, in accordance with
* the Business Source License, use of this software will be governed
* by the Apache License, Version 2.0
*/

import type { FilterEntry } from '../state/ui';

const SESSION_STORAGE_KEY = 'topic-filters';

interface TopicFiltersMap {
[topicName: string]: FilterEntry[];
}

/**
* Get all filters from sessionStorage
*/
function getAllFilters(): TopicFiltersMap {
try {
const stored = sessionStorage.getItem(SESSION_STORAGE_KEY);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is sessionStorage a global variable ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's a globally accessible object we can rely on

if (!stored) {
return {};
}
return JSON.parse(stored) as TopicFiltersMap;
} catch (error) {
// biome-ignore lint/suspicious/noConsole: intentional console usage for debugging
console.warn('Failed to parse filters from sessionStorage:', error);
return {};
}
}

/**
* Save all filters to sessionStorage
*/
function saveAllFilters(filters: TopicFiltersMap): void {
try {
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(filters));
} catch (error) {
// biome-ignore lint/suspicious/noConsole: intentional console usage for debugging
console.warn('Failed to save filters to sessionStorage:', error);
}
}

/**
* Get filters for a specific topic
*/
export function getTopicFilters(topicName: string): FilterEntry[] {
const allFilters = getAllFilters();
return allFilters[topicName] ?? [];
}

/**
* Set filters for a specific topic
*/
export function setTopicFilters(topicName: string, filters: FilterEntry[]): void {
const allFilters = getAllFilters();
allFilters[topicName] = filters;
saveAllFilters(allFilters);
}

/**
* Clear filters for a specific topic
*/
export function clearTopicFilters(topicName: string): void {
const allFilters = getAllFilters();
delete allFilters[topicName];
saveAllFilters(allFilters);
}

/**
* Clear all filters from sessionStorage
*/
export function clearAllTopicFilters(): void {
sessionStorage.removeItem(SESSION_STORAGE_KEY);
}