Skip to content

Commit 53a9ecd

Browse files
committed
Table skeletons & improved loading in security page
1 parent 292718e commit 53a9ecd

8 files changed

Lines changed: 349 additions & 246 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright 2022 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import type { ReactNode } from 'react';
13+
import { DefaultSkeleton } from 'utils/tsx-utils';
14+
15+
import { Alert, AlertDescription, AlertTitle } from '../redpanda-ui/components/alert';
16+
17+
type Props = {
18+
isLoading: boolean;
19+
isError: boolean;
20+
error?: { message?: string } | null;
21+
errorTitle?: string;
22+
children: ReactNode;
23+
skeleton?: ReactNode;
24+
};
25+
26+
export const QueryResult = ({
27+
isLoading,
28+
isError,
29+
error,
30+
errorTitle = 'Failed to load data',
31+
children,
32+
skeleton = DefaultSkeleton,
33+
}: Props) => {
34+
if (isLoading) {
35+
return skeleton;
36+
}
37+
38+
if (isError) {
39+
return (
40+
<Alert variant="destructive">
41+
<AlertTitle>{errorTitle}</AlertTitle>
42+
<AlertDescription>{error?.message ?? 'An unexpected error occurred.'}</AlertDescription>
43+
</Alert>
44+
);
45+
}
46+
47+
return <>{children}</>;
48+
};

frontend/src/components/pages/security/security-page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ export function SecurityPage({ tab }: SecurityPageProps) {
7878
];
7979
}, [activeTab, activeTabData.label]);
8080

81-
const setActiveTab = (newTab: SecurityTab) => {
82-
if (newTab === 'users') navigate({ to: '/security/users', replace: true });
83-
else if (newTab === 'roles') navigate({ to: '/security/roles', replace: true });
84-
else navigate({ to: '/security/permissions-list', replace: true });
81+
const routes: Record<SecurityTab, string> = {
82+
users: '/security/users',
83+
roles: '/security/roles',
84+
permissions: '/security/permissions-list',
85+
};
86+
87+
const setActiveTab = (securityTab: SecurityTab) => {
88+
navigate({ to: routes[securityTab], replace: true });
8589
};
8690

8791
const visibleTabs = tabs.filter((t) => !t.requiresFeature || t.requiresFeature());
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright 2022 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
export const getCreateUserButtonProps = (
13+
isAdminApiConfigured: boolean,
14+
featureCreateUser: boolean,
15+
canManageUsers: boolean | undefined
16+
) => {
17+
const hasRBAC = canManageUsers !== undefined;
18+
19+
return {
20+
disabled: !(isAdminApiConfigured && featureCreateUser) || (hasRBAC && canManageUsers === false),
21+
tooltip: [
22+
!isAdminApiConfigured && 'The Redpanda Admin API is not configured.',
23+
!featureCreateUser && "Your cluster doesn't support this feature.",
24+
hasRBAC && canManageUsers === false && 'You need RedpandaCapability.MANAGE_REDPANDA_USERS permission.',
25+
]
26+
.filter(Boolean)
27+
.join(' '),
28+
};
29+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright 2022 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { Skeleton } from 'components/redpanda-ui/components/skeleton';
13+
14+
/**
15+
* Skeleton that mimics the 3-column user table:
16+
* [name] [role tags ...] [action icon]
17+
*/
18+
export const TableSkeleton = () => (
19+
<div className="my-4 rounded-md border p-4">
20+
{/* Header */}
21+
<div className="flex items-center gap-4 px-4 py-3">
22+
<Skeleton className="flex-1" size="sm" variant="text" />
23+
<Skeleton size="sm" variant="text" width="md" />
24+
<Skeleton size="sm" variant="text" width="xs" />
25+
</div>
26+
{/* Rows */}
27+
{Array.from({ length: 4 }).map((_, i) => (
28+
<div className="flex items-center gap-4 px-4 py-3" key={i}>
29+
<Skeleton className="flex-1" size="sm" variant="text" width="sm" />
30+
<div className="flex items-center gap-2">
31+
<Skeleton className="h-5 w-20 rounded-full" />
32+
<Skeleton className="h-5 w-16 rounded-full" />
33+
</div>
34+
<Skeleton className="h-6 w-6 rounded" />
35+
</div>
36+
))}
37+
</div>
38+
);

frontend/src/components/pages/security/tabs/acls-tab.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export const AclsTab: FC = () => {
9595
principalGroups?.filter((g) => g.principalType === 'User' || g.principalType === 'Group') || [];
9696
const groups = filterByName(aclPrincipalGroups, searchQuery, (g) => g.principalName);
9797

98-
if (isError && error) {
98+
if (isError) {
9999
return <ErrorResult error={error} />;
100100
}
101101

@@ -105,30 +105,29 @@ export const AclsTab: FC = () => {
105105

106106
return (
107107
<div className="flex flex-col gap-4">
108-
<div>
108+
<p>
109109
This tab displays all access control lists (ACLs), grouped by principal and host. A principal represents any
110110
entity that can be authenticated, such as a user, service, or system (for example, a SASL-SCRAM user, OIDC
111111
identity, or mTLS client). The ACLs tab shows only the permissions directly granted to each principal. For a
112112
complete view of all permissions, including permissions granted through roles, see the Permissions List tab.
113-
</div>
113+
</p>
114114
{Boolean(featureRolesApi) && (
115-
<Alert icon={<InfoIcon />} variant="warning">
115+
<Alert icon={<InfoIcon />} variant="info">
116116
<AlertDescription>
117117
Roles are a more flexible and efficient way to manage user permissions, especially with complex
118118
organizational hierarchies or large numbers of users.
119119
</AlertDescription>
120120
</Alert>
121121
)}
122-
<SearchField
123-
placeholderText="Filter by name"
124-
searchText={searchQuery ?? ''}
125-
setSearchText={(x) => setSearchQuery(x)}
126-
width="300px"
127-
/>
128-
<Section>
129-
<AlertDeleteFailed aclFailed={aclFailed} onClose={() => setAclFailed(null)} />
130-
122+
<div className="flex items-center justify-between gap-4">
123+
<SearchField
124+
placeholderText="Filter by name or regex..."
125+
searchText={searchQuery ?? ''}
126+
setSearchText={(x) => setSearchQuery(x)}
127+
width="300px"
128+
/>
131129
<Button
130+
aria-label="Create ACL"
132131
data-testid="create-acls"
133132
onClick={() => {
134133
navigate({
@@ -137,8 +136,11 @@ export const AclsTab: FC = () => {
137136
});
138137
}}
139138
>
140-
Create ACLs
139+
Create ACL
141140
</Button>
141+
</div>
142+
<Section>
143+
<AlertDeleteFailed aclFailed={aclFailed} onClose={() => setAclFailed(null)} />
142144

143145
<div className="py-4">
144146
<DataTable<{
@@ -263,6 +265,7 @@ export const AclsTab: FC = () => {
263265
},
264266
]}
265267
data={groups}
268+
emptyText={searchQuery ? 'No ACLs match your search' : 'No ACLs yet'}
266269
pagination
267270
sorting
268271
/>

frontend/src/components/pages/security/tabs/permissions-list-tab.tsx

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import { create } from '@bufbuild/protobuf';
1313
import { DataTable, SearchField } from '@redpanda-data/ui';
1414
import { Link } from '@tanstack/react-router';
15-
import { TrashIcon } from 'components/icons';
15+
import { InfoIcon, TrashIcon } from 'components/icons';
1616
import { parseAsString } from 'nuqs';
1717
import {
1818
ACL_Operation,
@@ -48,29 +48,11 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../
4848
import { type PrincipalEntry, usePrincipalList } from '../hooks/use-principal-list';
4949
import { useSecurityBreadcrumbs } from '../hooks/use-security-breadcrumbs';
5050
import { AlertDeleteFailed } from '../shared/alert-delete-failed';
51+
import { getCreateUserButtonProps } from '../shared/create-user-button-props';
5152
import { DeleteUserConfirmModal } from '../shared/delete-user-confirm-modal';
5253
import { filterByName } from '../shared/filter-by-name';
5354
import { UserRoleTags } from '../shared/user-role-tags';
5455

55-
const getCreateUserButtonProps = (
56-
isAdminApiConfigured: boolean,
57-
featureCreateUser: boolean,
58-
canManageUsers: boolean | undefined
59-
) => {
60-
const hasRBAC = canManageUsers !== undefined;
61-
62-
return {
63-
disabled: !(isAdminApiConfigured && featureCreateUser) || (hasRBAC && canManageUsers === false),
64-
tooltip: [
65-
!isAdminApiConfigured && 'The Redpanda Admin API is not configured.',
66-
!featureCreateUser && "Your cluster doesn't support this feature.",
67-
hasRBAC && canManageUsers === false && 'You need RedpandaCapability.MANAGE_REDPANDA_USERS permission.',
68-
]
69-
.filter(Boolean)
70-
.join(' '),
71-
};
72-
};
73-
7456
const PermissionsListActions = ({
7557
entry,
7658
canDeleteUser,
@@ -221,12 +203,27 @@ export const PermissionsListTab: FC = () => {
221203

222204
return (
223205
<div className="flex flex-col gap-4">
224-
<div>
206+
<p>
225207
This page provides a detailed overview of all effective permissions for each principal, including those derived
226208
from assigned roles. While the ACLs tab shows permissions directly granted to principals, this tab also
227209
incorporates roles that may assign additional permissions to a principal. This gives you a complete picture of
228210
what each principal can do within your cluster.
229-
</div>
211+
</p>
212+
<Alert icon={<InfoIcon />} variant="info">
213+
<AlertDescription>
214+
<p>
215+
To grant permissions, use the{' '}
216+
<Link className="font-medium underline" to="/security/acls">
217+
ACLs
218+
</Link>{' '}
219+
or{' '}
220+
<Link className="font-medium underline" to="/security/roles">
221+
Roles
222+
</Link>{' '}
223+
tabs.
224+
</p>
225+
</AlertDescription>
226+
</Alert>
230227

231228
<SearchField
232229
placeholderText="Filter by name"
@@ -296,6 +293,7 @@ export const PermissionsListTab: FC = () => {
296293
]}
297294
data={usersFiltered}
298295
emptyAction={(() => {
296+
if (searchQuery) return;
299297
const { disabled, tooltip } = getCreateUserButtonProps(
300298
isAdminApiConfigured,
301299
featureCreateUser,
@@ -313,12 +311,12 @@ export const PermissionsListTab: FC = () => {
313311
Create user
314312
</Button>
315313
</TooltipTrigger>
316-
{tooltip && <TooltipContent>{tooltip}</TooltipContent>}
314+
{Boolean(tooltip) && <TooltipContent>{tooltip}</TooltipContent>}
317315
</Tooltip>
318316
</TooltipProvider>
319317
);
320318
})()}
321-
emptyText="No principals yet"
319+
emptyText={searchQuery ? 'No principals match your search' : 'No principals yet'}
322320
pagination
323321
sorting
324322
/>

0 commit comments

Comments
 (0)