Skip to content
Draft
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
531 changes: 56 additions & 475 deletions api/src/main/webui/src/components/kafka/topics/AdvancedSearch.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { TextInput, TextInputProps } from '@patternfly/react-core';

interface CommitInputProps
extends Omit<TextInputProps, 'value' | 'onChange' | 'onBlur' | 'onKeyDown'> {
value: string;
onCommit: (value: string) => void;
isValid?: (value: string) => boolean;
}

export function CommitInput({
value,
onCommit,
isValid = () => true,
...props
}: CommitInputProps) {
const [draft, setDraft] = useState(value);
const [prevValue, setPrevValue] = useState(value);
if (value !== prevValue) {
setPrevValue(value);
setDraft(value);
}

const valid = isValid(draft);
const commit = () => {
if (valid) onCommit(draft);
};

return (
<TextInput
{...props}
value={draft}
validated={valid ? 'default' : 'error'}
onChange={(_, next) => setDraft(next)}
onBlur={commit}
onKeyDown={(e) => {
if (e.key === 'Enter') commit();
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Fetch-from selector.
*
* Controlled component: the selected fetch option comes in as a prop,
* and changes go out through onFromChange.
*/

import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Menu,
MenuContainer,
MenuContent,
MenuList,
MenuToggle,
SelectOption,
} from '@patternfly/react-core';

export type FromOptions =
| 'latest'
| 'last5'
| 'last10'
| 'last15'
| 'offset'
| 'timestamp';

interface FetchFromSelectorProps {
selectedFrom?: FromOptions;
onFromChange: (from: FromOptions) => void;
isDisabled?: boolean;
ouiaId?: string;
}

export function FetchFromSelector({
selectedFrom = 'latest',
onFromChange,
isDisabled = false,
ouiaId = 'fetch-from-select',
}: FetchFromSelectorProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);

const FROM_OPTIONS: { value: FromOptions; label: string }[] = [
{ value: 'latest', label: t('topics.messages.fetchFrom.latest') },
{ value: 'last5', label: t('topics.messages.fetchFrom.last5') },
{ value: 'last10', label: t('topics.messages.fetchFrom.last10') },
{ value: 'last15', label: t('topics.messages.fetchFrom.last15') },
{ value: 'offset', label: t('topics.messages.fetchFrom.offset') },
{
value: 'timestamp',
label: t('topics.messages.fetchFrom.timestamp'),
},
];

const toggleRef = useRef<HTMLButtonElement | null>(null);
const menuRef = useRef<HTMLDivElement | null>(null);

const selectedOption = FROM_OPTIONS.find(
(option) => option.value === selectedFrom
);

const toggle = (
<MenuToggle
ref={toggleRef}
ouiaId={ouiaId}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
isDisabled={isDisabled}
aria-label={t('topics.messages.fetchFrom.label')}
>
{selectedOption?.label ?? FROM_OPTIONS[0].label}
</MenuToggle>
);

const menu = (
<Menu
ref={menuRef}
onSelect={(_event, itemId) => {
onFromChange(itemId as FromOptions);
setIsOpen(false);
}}
activeItemId={selectedFrom}
>
<MenuContent>
<MenuList>
{FROM_OPTIONS.map((option) => (
<SelectOption
key={option.value}
itemId={option.value}
isSelected={selectedFrom === option.value}
>
{option.label}
</SelectOption>
))}
</MenuList>
</MenuContent>
</Menu>
);

return (
<MenuContainer
toggle={toggle}
toggleRef={toggleRef}
menu={menu}
menuRef={menuRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
onOpenChangeKeys={['Escape']}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from 'react';
import {
MenuToggle,
MenuToggleElement,
Select,
SelectList,
SelectOption,
} from '@patternfly/react-core';

// 50, 100, 150 ... 1000
const LIMIT_OPTIONS = Array.from({ length: 20 }, (_, i) => (i + 1) * 50);

interface LimitSelectProps {
value: number;
onChange: (limit: number) => void;
ouiaId?: string;
}

export function LimitSelect({
value,
onChange,
ouiaId = 'limit-select',
}: LimitSelectProps) {
const [isOpen, setIsOpen] = useState(false);

return (
<Select
isOpen={isOpen}
onOpenChange={setIsOpen}
isScrollable
maxMenuHeight="200px"
selected={value}
onSelect={(_, selection) => {
onChange(selection as number);
setIsOpen(false);
}}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
ouiaId={ouiaId}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
style={{ width: '120px' }}
>
{value}
</MenuToggle>
)}
>
<SelectList>
{LIMIT_OPTIONS.map((option) => (
<SelectOption key={option} value={option}>
{option}
</SelectOption>
))}
</SelectList>
</Select>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { MessageTableFilter } from './MessageTableFilter';
import { FilterState } from './messageFilter';

const meta: Meta<typeof MessageTableFilter> = {
component: MessageTableFilter,
};

export default meta;
type Story = StoryObj<typeof MessageTableFilter>;

const defaultValue: FilterState = {
partition: undefined,
from: 'latest',
offset: undefined,
limit: 50,
query: '',
where: undefined,
};

interface FilterStoryProps {
partitions?: number[];
initialValue?: FilterState;
}

function FilterStory({
partitions = [0, 1, 2, 3, 4],
initialValue = defaultValue,
}: FilterStoryProps) {
const [value, setValue] = useState<FilterState>(initialValue);

return (
<MessageTableFilter
partitions={partitions}
value={value}
onChange={(patch) => setValue((current) => ({ ...current, ...patch }))}
/>
);
}

export const Latest: Story = {
render: () => <FilterStory />,
};

export const Last5Minutes: Story = {
render: () => (
<FilterStory initialValue={{ ...defaultValue, from: 'last5' }} />
),
};

export const Last10Minutes: Story = {
render: () => (
<FilterStory initialValue={{ ...defaultValue, from: 'last10' }} />
),
};

export const Last15Minutes: Story = {
render: () => (
<FilterStory initialValue={{ ...defaultValue, from: 'last15' }} />
),
};

export const FetchFromOffset: Story = {
render: () => (
<FilterStory
initialValue={{ ...defaultValue, from: 'offset', offset: 123 }}
/>
),
};

export const FetchFromOffsetWithoutValue: Story = {
render: () => (
<FilterStory
initialValue={{ ...defaultValue, from: 'offset', offset: undefined }}
/>
),
};

export const FetchFromTimestamp: Story = {
render: () => (
<FilterStory
initialValue={{
...defaultValue,
from: 'timestamp',
query: 'payment',
}}
/>
),
};

export const SelectedPartition: Story = {
render: () => (
<FilterStory
initialValue={{
...defaultValue,
partition: 2,
from: 'last10',
}}
/>
),
};

export const SinglePartition: Story = {
render: () => (
<FilterStory
partitions={[0]}
initialValue={{ ...defaultValue, partition: 0 }}
/>
),
};

export const NoPartitions: Story = {
render: () => <FilterStory partitions={[]} />,
};

export const LargeLimit: Story = {
render: () => (
<FilterStory
initialValue={{
...defaultValue,
limit: 500,
from: 'last10',
}}
/>
),
};
Loading
Loading