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
3 changes: 0 additions & 3 deletions frontend/src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
useSidebar,
} from 'components/redpanda-ui/components/sidebar';
import { ChevronsLeft, ChevronsRight, ChevronUp, LogOut, Settings } from 'lucide-react';
Expand Down Expand Up @@ -268,8 +267,6 @@ export function AppSidebar() {
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>

<SidebarRail />
</Sidebar>
);
}
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/components/misc/login-complete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { ApiError, UserData } from '../../state/rest-interfaces';
import { uiState } from '../../state/ui-state';
import { getBasePath } from '../../utils/env';
import fetchWithTimeout from '../../utils/fetch-with-timeout';
import { queryToObj } from '../../utils/query-helper';

class LoginCompletePage extends Component<{ provider: string }> {
componentDidMount() {
Expand All @@ -30,14 +29,16 @@ class LoginCompletePage extends Component<{ provider: string }> {
async completeLogin(provider: string, location: Location) {
const query = location.search;

const queryObj = queryToObj(query);
if (queryObj.error || queryObj.error_description) {
const searchParams = new URLSearchParams(query);
const error = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
if (error || errorDescription) {
let errorString = '';
if (queryObj.error) {
errorString += `Error: ${queryObj.error}\n`;
if (error) {
errorString += `Error: ${error}\n`;
}
if (queryObj.error_description) {
errorString += `Description: ${queryObj.error_description}\n`;
if (errorDescription) {
errorString += `Description: ${errorDescription}\n`;
}
uiState.loginError = errorString.trim();
appGlobal.historyReplace(`${getBasePath()}/login`);
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/misc/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ const authenticationApi = observable({

const AUTH_ELEMENTS: Partial<Record<AuthenticationMethod, React.FC>> = {
[AuthenticationMethod.NONE]: observer(() => {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const { searchStr } = useLocation();
const searchParams = new URLSearchParams(searchStr);
// Don't auto-redirect if there was an auth error - user needs to see the login page
const hasError = searchParams.has('error_code') || authenticationApi.methodsErrorResponse !== null;

Expand Down Expand Up @@ -218,8 +218,8 @@ const AUTH_ELEMENTS: Partial<Record<AuthenticationMethod, React.FC>> = {
};

const LoginPage = observer(() => {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const { searchStr } = useLocation();
const searchParams = new URLSearchParams(searchStr);

useEffect(() => {
authenticationApi.refreshAuthenticationMethods();
Expand Down
14 changes: 6 additions & 8 deletions frontend/src/components/pages/connect/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import { create } from '@bufbuild/protobuf';
import { Badge, Box, DataTable, Stack, Tooltip } from '@redpanda-data/ui';
import { useLocation } from '@tanstack/react-router';
import ErrorResult from 'components/misc/error-result';
import { Link, Text } from 'components/redpanda-ui/components/typography';
import { WaitingRedpanda } from 'components/redpanda-ui/components/waiting-redpanda';
Expand Down Expand Up @@ -78,21 +77,20 @@ const getDefaultView = (defaultView: string): { initialTab: ConnectView; redpand
}
};

const WrapKafkaConnectOverview: FunctionComponent<{ matchedPath: string }> = (props) => {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const defaultTab = searchParams.get('defaultTab') || '';

const WrapKafkaConnectOverview: FunctionComponent<{
matchedPath: string;
defaultTab?: ConnectView;
}> = (props) => {
const { data: kafkaConnectors, isLoading: isLoadingKafkaConnectors } = useKafkaConnectConnectorsQuery();

const isKafkaConnectEnabled = kafkaConnectors?.isConfigured === true;

return (
<KafkaConnectOverview
defaultView={defaultTab}
defaultView={props.defaultTab ?? ''}
isKafkaConnectEnabled={isKafkaConnectEnabled}
isLoadingKafkaConnectors={isLoadingKafkaConnectors}
{...props}
matchedPath={props.matchedPath}
/>
);
};
Expand Down
35 changes: 21 additions & 14 deletions frontend/src/components/pages/consumers/group-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ import { api } from '../../../state/backend-api';
import type { GroupDescription, GroupMemberDescription } from '../../../state/rest-interfaces';
import { Features } from '../../../state/supported-features';
import { uiSettings } from '../../../state/ui';
import { editQuery, queryToObj } from '../../../utils/query-helper';
import { Button, DefaultSkeleton, IconButton, numberToThousandsString } from '../../../utils/tsx-utils';
import PageContent from '../../misc/page-content';
import { ShortNum } from '../../misc/short-num';
import { Statistic } from '../../misc/statistic';
import { PageComponent, type PageInitHelper, type PageProps } from '../page';
import AclList from '../topics/Tab.Acl/acl-list';

type GroupSearchParams = {
q?: string;
withLag?: boolean;
};

const DEFAULT_MATCH_ALL_REGEX = /.*/s;
const QUICK_SEARCH_REGEX_CACHE = new Map<string, RegExp>();

Expand All @@ -71,21 +75,29 @@ function getQuickSearchRegex(pattern: string): RegExp {
return regExp;
}

type GroupDetailsProps = {
groupId: string;
search: GroupSearchParams;
onSearchChange: (updates: Partial<GroupSearchParams>) => void;
};

@observer
class GroupDetails extends PageComponent<{ groupId: string }> {
class GroupDetails extends PageComponent<GroupDetailsProps> {
@observable edittingOffsets: GroupOffset[] | null = null;
@observable editedTopic: string | null = null;
@observable editedPartition: number | null = null;

@observable deletingMode: GroupDeletingMode = 'group';
@observable deletingOffsets: GroupOffset[] | null = null;

@observable quickSearch = queryToObj(window.location.search).q;
@observable showWithLagOnly = Boolean(queryToObj(window.location.search).withLag);
@observable quickSearch = '';
@observable showWithLagOnly = false;

constructor(p: Readonly<PageProps<{ groupId: string }>>) {
constructor(p: Readonly<PageProps<GroupDetailsProps>>) {
super(p);
makeObservable(this);
this.quickSearch = p.search?.q ?? '';
this.showWithLagOnly = p.search?.withLag ?? false;
}

initPage(p: PageInitHelper): void {
Expand Down Expand Up @@ -118,21 +130,16 @@ class GroupDetails extends PageComponent<{ groupId: string }> {
placeholderText="Filter by member"
searchText={this.quickSearch}
setSearchText={(filterText) => {
editQuery((query) => {
this.quickSearch = filterText;
const q = String(filterText);
query.q = q;
});
this.quickSearch = filterText;
this.props.onSearchChange({ q: filterText });
}}
width={300}
/>
<Checkbox
isChecked={this.showWithLagOnly}
onChange={(e) => {
editQuery((query) => {
this.showWithLagOnly = e.target.checked;
query.withLag = this.showWithLagOnly ? 'true' : null;
});
this.showWithLagOnly = e.target.checked;
this.props.onSearchChange({ withLag: e.target.checked });
}}
>
Only show topics with lag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function MotionHighlight<T extends string>(props: MotionHighlightProps<T>) {
defaultValue,
onValueChange,
className,
transition = { type: 'spring', stiffness: 350, damping: 35 },
transition = { type: 'tween', ease: 'easeOut', duration: 0.2 },
hover = false,
enabled = true,
controlledItems,
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/redpanda-ui/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ function Sidebar({
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-400 ease-[cubic-bezier(0.7,-0.15,0.25,1.15)]',
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-300 ease-out',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
Expand All @@ -262,7 +262,7 @@ function Sidebar({
/>
<div
className={cn(
'fixed inset-y-0 z-10 flex max-md:hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-400 ease-[cubic-bezier(0.75,0,0.25,1)]',
'fixed inset-y-0 z-10 flex max-md:hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-300 ease-out',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
Expand Down Expand Up @@ -723,7 +723,7 @@ function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
return (
<ul
className={cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-1 py-0.5',
'group-data-[collapsible=icon]:hidden',
className
)}
Expand Down Expand Up @@ -764,7 +764,7 @@ const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubB
<Comp
aria-disabled={props['aria-disabled']}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-0.5 text-sidebar-foreground outline-hidden ring-sidebar-ring focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 [&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 [&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/redpanda-ui/components/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function TooltipContent({
className,
side = 'top',
sideOffset = 4,
transition = { type: 'spring', stiffness: 300, damping: 25 },
transition = { type: 'tween', ease: 'easeOut', duration: 0.15 },
arrow = true,
children,
testId,
Expand All @@ -104,7 +104,7 @@ function TooltipContent({
exit={{ opacity: 0, scale: 0, ...initialPosition }}
transition={transition}
className={cn(
'relative bg-primary text-primary-foreground shadow-md w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance',
'relative bg-base-800 dark:bg-base-700 text-white shadow-md w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance',
className,
)}
>
Expand All @@ -113,7 +113,7 @@ function TooltipContent({
{arrow && (
<TooltipPrimitive.Arrow
data-slot="tooltip-content-arrow"
className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]"
className="bg-base-800 fill-base-800 dark:bg-base-700 dark:fill-base-700 z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]"
/>
)}
</motion.div>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/redpanda-ui/style/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
--color-sidebar-accent: oklch(1 0 0 / 8%);
--color-sidebar-accent-foreground: var(--color-base-100);
--color-sidebar-border: var(--color-base-600);
--color-sidebar-ring: var(--color-primary-500);
--color-sidebar-ring: var(--color-secondary-400);
--color-selection: oklch(0.8462 0.027 262.33);
--color-selection-foreground: var(--color-base-800);
--color-sidebar-selection: oklch(0.5547 0.087 264.14);
Expand Down Expand Up @@ -127,7 +127,7 @@
--color-sidebar-accent: oklch(0 0 0 / 24%);
--color-sidebar-accent-foreground: var(--color-base-100);
--color-sidebar-border: var(--color-base-500);
--color-sidebar-ring: var(--color-primary-500);
--color-sidebar-ring: var(--color-secondary-400);
--color-selection: oklch(0.8462 0.027 262.33);
--color-selection-foreground: var(--color-base-200);
--color-sidebar-selection: oklch(0.5547 0.087 264.14);
Expand Down
37 changes: 22 additions & 15 deletions frontend/src/hooks/use-pagination-params.test.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,47 @@
import { renderHook } from '@testing-library/react';
import { connectQueryWrapper } from 'test-utils';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import type { PropsWithChildren } from 'react';

import usePaginationParams from './use-pagination-params';

const createWrapper =
(searchParams?: Record<string, string>) =>
({ children }: PropsWithChildren) => <NuqsTestingAdapter searchParams={searchParams}>{children}</NuqsTestingAdapter>;

describe('usePaginationParams', () => {
test('returns default values when URL parameters are absent', () => {
const { wrapper } = connectQueryWrapper();
// Assuming a total data length of 100 for testing
const totalDataLength = 100;
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), { wrapper });
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), {
wrapper: createWrapper(),
});
expect(result.current.pageSize).toBe(10);
expect(result.current.pageIndex).toBe(0);
});

test('parses pageSize and pageIndex from URL parameters', () => {
const { wrapper } = connectQueryWrapper();
const totalDataLength = 100;
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), { wrapper });
// Without setting initial URL, defaults apply
expect(result.current.pageSize).toBe(10);
expect(result.current.pageIndex).toBe(0);
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), {
wrapper: createWrapper({ pageSize: '20', page: '2' }),
});
expect(result.current.pageSize).toBe(20);
expect(result.current.pageIndex).toBe(2);
});

test('uses defaultPageSize when pageSize is not in URL', () => {
const { wrapper } = connectQueryWrapper();
const totalDataLength = 150;
const { result } = renderHook(() => usePaginationParams(totalDataLength, 15), { wrapper });
const { result } = renderHook(() => usePaginationParams(totalDataLength, 15), {
wrapper: createWrapper(),
});
expect(result.current.pageSize).toBe(15);
expect(result.current.pageIndex).toBe(0);
});

test('returns default pageIndex when URL page param would exceed total pages', () => {
const { wrapper } = connectQueryWrapper();
test('returns bounded pageIndex when URL page param would exceed total pages', () => {
const totalDataLength = 50; // Only 5 pages available with pageSize 10
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), { wrapper });
const { result } = renderHook(() => usePaginationParams(totalDataLength, 10), {
wrapper: createWrapper({ page: '10' }), // Page 10 exceeds available pages
});
expect(result.current.pageSize).toBe(10);
expect(result.current.pageIndex).toBe(0);
expect(result.current.pageIndex).toBe(4); // Bounded to max valid page (5 pages = index 0-4)
});
});
17 changes: 7 additions & 10 deletions frontend/src/hooks/use-pagination-params.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useLocation } from '@tanstack/react-router';
import { DEFAULT_TABLE_PAGE_SIZE } from 'components/constants';
import { parseAsInteger, useQueryState } from 'nuqs';
import { useMemo } from 'react';

/**
* Custom hook for parsing pagination parameters from the URL search query.
*
* This hook extracts 'pageSize' and 'pageIndex' parameters from the URL search query.
* This hook extracts 'pageSize' and 'pageIndex' parameters from the URL search query
* using nuqs for type-safe URL state management.
* If these parameters are not present in the URL, it falls back to default values.
* 'pageSize' defaults to the value passed as an argument, or 10 if not provided.
* 'pageSize' defaults to the value passed as an argument, or DEFAULT_TABLE_PAGE_SIZE if not provided.
* 'pageIndex' defaults to 0 if not present in the URL.
*
* @param {number} totalDataLength - The total length of the data to paginate over.
Expand All @@ -22,22 +23,18 @@ const usePaginationParams = (
totalDataLength: number,
defaultPageSize: number = DEFAULT_TABLE_PAGE_SIZE
): { pageSize: number; pageIndex: number } => {
const location = useLocation();
const search = location.searchStr ?? '';
const [pageSize] = useQueryState('pageSize', parseAsInteger.withDefault(defaultPageSize));
const [pageIndex] = useQueryState('page', parseAsInteger.withDefault(0));

return useMemo(() => {
const searchParams = new URLSearchParams(search);
const pageSize = searchParams.has('pageSize') ? Number(searchParams.get('pageSize')) : defaultPageSize;
const pageIndex = searchParams.has('page') ? Number(searchParams.get('page')) : 0;
const totalPages = Math.ceil(totalDataLength / pageSize);

const boundedPageIndex = Math.max(0, Math.min(pageIndex, totalPages - 1));

return {
pageSize,
pageIndex: boundedPageIndex,
};
}, [search, defaultPageSize, totalDataLength]);
}, [pageSize, pageIndex, totalDataLength]);
};

export default usePaginationParams;
Loading
Loading