From 1cdd2ec0017ede23564824b2eefcdf2b2eedb712 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 Date: Tue, 10 Mar 2026 00:39:26 +0530 Subject: [PATCH] feat: multiple style and functionality fixes --- .../CreateServiceAccountModal.tsx | 57 ++++-- .../RolesSelect/RolesSelect.styles.scss | 25 +++ .../components/RolesSelect/RolesSelect.tsx | 171 ++++++++++++++++++ frontend/src/components/RolesSelect/index.ts | 2 + .../ServiceAccountDrawer/AddKeyModal.tsx | 19 +- .../ServiceAccountDrawer/EditKeyModal.tsx | 97 ++++++---- .../ServiceAccountDrawer/KeysTab.tsx | 35 ++-- .../ServiceAccountDrawer/OverviewTab.tsx | 37 ++-- .../ServiceAccountDrawer.tsx | 13 ++ .../components/ServiceAccountDrawer/utils.ts | 18 ++ .../ServiceAccountsTable.styles.scss | 16 ++ .../ServiceAccountsTable.tsx | 37 ++-- .../MembersSettings/MembersSettings.tsx | 2 +- .../ServiceAccountsSettings.styles.scss | 5 + .../ServiceAccountsSettings.tsx | 48 +++-- .../ServiceAccountsSettings/utils.ts | 7 +- 16 files changed, 454 insertions(+), 135 deletions(-) create mode 100644 frontend/src/components/RolesSelect/RolesSelect.styles.scss create mode 100644 frontend/src/components/RolesSelect/RolesSelect.tsx create mode 100644 frontend/src/components/RolesSelect/index.ts create mode 100644 frontend/src/components/ServiceAccountDrawer/utils.ts diff --git a/frontend/src/components/CreateServiceAccountModal/CreateServiceAccountModal.tsx b/frontend/src/components/CreateServiceAccountModal/CreateServiceAccountModal.tsx index 4a88896d05..f6ef4426af 100644 --- a/frontend/src/components/CreateServiceAccountModal/CreateServiceAccountModal.tsx +++ b/frontend/src/components/CreateServiceAccountModal/CreateServiceAccountModal.tsx @@ -1,10 +1,11 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Button } from '@signozhq/button'; import { DialogFooter, DialogWrapper } from '@signozhq/dialog'; -import { ChevronDown, X } from '@signozhq/icons'; +import { X } from '@signozhq/icons'; import { toast } from '@signozhq/sonner'; -import { Form, Input, Select } from 'antd'; +import { Form, Input } from 'antd'; import { useCreateServiceAccount } from 'api/generated/services/serviceaccount'; +import RolesSelect, { useRoles } from 'components/RolesSelect'; import './CreateServiceAccountModal.styles.scss'; @@ -16,7 +17,7 @@ interface CreateServiceAccountModalProps { interface FormValues { name: string; - email?: string; + email: string; roles: string[]; } @@ -27,8 +28,25 @@ function CreateServiceAccountModal({ }: CreateServiceAccountModalProps): JSX.Element { const [form] = Form.useForm(); const [isSubmitting, setIsSubmitting] = useState(false); + const [submittable, setSubmittable] = useState(false); + + const values = Form.useWatch([], form); + + useEffect(() => { + form + .validateFields({ validateOnly: true }) + .then(() => setSubmittable(true)) + .catch(() => setSubmittable(false)); + }, [values, form]); const { mutateAsync: createServiceAccount } = useCreateServiceAccount(); + const { + roles, + isLoading: rolesLoading, + isError: rolesError, + error: rolesErrorObj, + refetch: refetchRoles, + } = useRoles(); const handleClose = useCallback((): void => { form.resetFields(); @@ -42,8 +60,8 @@ function CreateServiceAccountModal({ await createServiceAccount({ data: { name: values.name.trim(), - email: values.email?.trim() ?? '', - roles: values.roles ?? [], + email: values.email.trim(), + roles: values.roles, }, }); toast.success('Service account created successfully', { richColors: true }); @@ -93,6 +111,10 @@ function CreateServiceAccountModal({ - - + /> @@ -142,7 +169,7 @@ function CreateServiceAccountModal({ color="primary" size="sm" onClick={handleSubmit} - disabled={isSubmitting} + disabled={isSubmitting || !submittable} > {isSubmitting ? 'Creating...' : 'Create Service Account'} diff --git a/frontend/src/components/RolesSelect/RolesSelect.styles.scss b/frontend/src/components/RolesSelect/RolesSelect.styles.scss new file mode 100644 index 0000000000..3047942763 --- /dev/null +++ b/frontend/src/components/RolesSelect/RolesSelect.styles.scss @@ -0,0 +1,25 @@ +.roles-select-error { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + padding: 4px 8px; + color: var(--bg-cherry-500); + font-size: 12px; + + &__msg { + display: flex; + align-items: center; + gap: 6px; + } + + &__retry-btn { + display: flex; + align-items: center; + background: none; + border: none; + cursor: pointer; + padding: 2px; + color: var(--bg-cherry-500); + } +} diff --git a/frontend/src/components/RolesSelect/RolesSelect.tsx b/frontend/src/components/RolesSelect/RolesSelect.tsx new file mode 100644 index 0000000000..edeb8e1276 --- /dev/null +++ b/frontend/src/components/RolesSelect/RolesSelect.tsx @@ -0,0 +1,171 @@ +import { CircleAlert, RefreshCw } from '@signozhq/icons'; +import { Checkbox, Select } from 'antd'; +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { useListRoles } from 'api/generated/services/role'; +import type { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas'; +import APIError from 'types/api/error'; + +import './RolesSelect.styles.scss'; + +export interface RoleOption { + label: string; + value: string; +} + +export function useRoles(): { + roles: RoletypesRoleDTO[]; + isLoading: boolean; + isError: boolean; + error: APIError | undefined; + refetch: () => void; +} { + const { data, isLoading, isError, error, refetch } = useListRoles(); + return { + roles: data?.data ?? [], + isLoading, + isError, + error: convertToApiError(error), + refetch, + }; +} + +export function getRoleOptions(roles: RoletypesRoleDTO[]): RoleOption[] { + return roles.map((role) => ({ + label: role.name ?? '', + value: role.name ?? '', + })); +} + +function ErrorContent({ + error, + onRefetch, +}: { + error?: APIError; + onRefetch?: () => void; +}): JSX.Element { + const errorMessage = error?.message || 'Failed to load roles'; + + return ( +
+ + + {errorMessage} + + {onRefetch && ( + + )} +
+ ); +} + +interface BaseProps { + id?: string; + placeholder?: string; + className?: string; + getPopupContainer?: (trigger: HTMLElement) => HTMLElement; + roles?: RoletypesRoleDTO[]; + loading?: boolean; + isError?: boolean; + error?: APIError; + onRefetch?: () => void; +} + +interface SingleProps extends BaseProps { + mode?: 'single'; + value?: string; + onChange?: (role: string) => void; +} + +interface MultipleProps extends BaseProps { + mode: 'multiple'; + value?: string[]; + onChange?: (roles: string[]) => void; +} + +export type RolesSelectProps = SingleProps | MultipleProps; + +function RolesSelect(props: RolesSelectProps): JSX.Element { + const externalRoles = props.roles; + + const { + data, + isLoading: internalLoading, + isError: internalError, + error: internalErrorObj, + refetch: internalRefetch, + } = useListRoles({ + query: { enabled: externalRoles === undefined }, + }); + + const roles = externalRoles ?? data?.data ?? []; + const options = getRoleOptions(roles); + + const { + mode, + id, + placeholder = 'Select role', + className, + getPopupContainer, + loading = internalLoading, + isError = internalError, + error = convertToApiError(internalErrorObj), + onRefetch = externalRoles === undefined ? internalRefetch : undefined, + } = props; + + const notFoundContent = isError ? ( + + ) : undefined; + + if (mode === 'multiple') { + const { value = [], onChange } = props as MultipleProps; + return ( + + ); +} + +export default RolesSelect; diff --git a/frontend/src/components/RolesSelect/index.ts b/frontend/src/components/RolesSelect/index.ts new file mode 100644 index 0000000000..4bb99d4be9 --- /dev/null +++ b/frontend/src/components/RolesSelect/index.ts @@ -0,0 +1,2 @@ +export type { RoleOption, RolesSelectProps } from './RolesSelect'; +export { default, getRoleOptions, useRoles } from './RolesSelect'; diff --git a/frontend/src/components/ServiceAccountDrawer/AddKeyModal.tsx b/frontend/src/components/ServiceAccountDrawer/AddKeyModal.tsx index 8d5a7c82ae..dae5933fe4 100644 --- a/frontend/src/components/ServiceAccountDrawer/AddKeyModal.tsx +++ b/frontend/src/components/ServiceAccountDrawer/AddKeyModal.tsx @@ -9,7 +9,6 @@ import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group'; import { DatePicker } from 'antd'; import { useCreateServiceAccountKey } from 'api/generated/services/serviceaccount'; import type { ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO } from 'api/generated/services/sigNoz.schemas'; -import { format } from 'date-fns'; import type { Dayjs } from 'dayjs'; import './AddKeyModal.styles.scss'; @@ -105,7 +104,7 @@ function AddKeyModal({ return 'Never'; } try { - return format(expiryDate.toDate(), 'MMM d, yyyy'); + return expiryDate.format('MMM D, YYYY'); } catch { return 'Never'; } @@ -143,7 +142,7 @@ function AddKeyModal({
- + Expiration - +
setExpiryDate(date)} style={{ width: '100%', height: 32 }} @@ -222,7 +224,7 @@ function AddKeyModal({ {phase === 'created' && createdKey && (
- + API Key
{createdKey.key}
diff --git a/frontend/src/components/ServiceAccountDrawer/EditKeyModal.tsx b/frontend/src/components/ServiceAccountDrawer/EditKeyModal.tsx index 4b76c1c99e..8d4931d2e9 100644 --- a/frontend/src/components/ServiceAccountDrawer/EditKeyModal.tsx +++ b/frontend/src/components/ServiceAccountDrawer/EditKeyModal.tsx @@ -11,11 +11,12 @@ import { useUpdateServiceAccountKey, } from 'api/generated/services/serviceaccount'; import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; -import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { format } from 'date-fns'; -import dayjs, { type Dayjs } from 'dayjs'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { useTimezone } from 'providers/Timezone'; +import { formatLastUsed } from './utils'; + import './EditKeyModal.styles.scss'; interface EditKeyModalProps { @@ -28,6 +29,7 @@ interface EditKeyModalProps { type ExpiryMode = 'none' | 'date'; +// eslint-disable-next-line sonarjs/cognitive-complexity function EditKeyModal({ open, accountId, @@ -68,7 +70,9 @@ function EditKeyModal({ const { mutateAsync: revokeKey } = useRevokeServiceAccountKey(); const handleSave = useCallback(async (): Promise => { - if (!keyItem || !isDirty) {return;} + if (!keyItem || !isDirty) { + return; + } setIsSaving(true); try { await updateKey({ @@ -82,10 +86,20 @@ function EditKeyModal({ } finally { setIsSaving(false); } - }, [keyItem, isDirty, localName, currentExpiresAt, accountId, updateKey, onSuccess]); + }, [ + keyItem, + isDirty, + localName, + currentExpiresAt, + accountId, + updateKey, + onSuccess, + ]); const handleRevoke = useCallback(async (): Promise => { - if (!keyItem) {return;} + if (!keyItem) { + return; + } setIsRevoking(true); try { await revokeKey({ @@ -101,25 +115,18 @@ function EditKeyModal({ } }, [keyItem, accountId, revokeKey, onSuccess]); - const formatLastUsed = useCallback( - (lastUsed: Date | null | undefined): string => { - if (!lastUsed) {return '—';} - try { - return formatTimezoneAdjustedTimestamp( - String(lastUsed), - DATE_TIME_FORMATS.DASH_DATETIME, - ); - } catch { - return '—'; - } - }, + const handleFormatLastUsed = useCallback( + (lastUsed: Date | null | undefined): string => + formatLastUsed(lastUsed, formatTimezoneAdjustedTimestamp), [formatTimezoneAdjustedTimestamp], ); const expiryDisplayLabel = (): string => { - if (expiryMode === 'none' || !localDate) {return 'Never';} + if (expiryMode === 'none' || !localDate) { + return 'Never'; + } try { - return format(localDate.toDate(), 'MMM d, yyyy'); + return localDate.format('MMM D, YYYY'); } catch { return 'Never'; } @@ -153,24 +160,33 @@ function EditKeyModal({ {/* Key (read-only masked) */}
- -
- - {keyItem?.key ?? '—'} - + +
+ {keyItem?.key ?? '—'}
{/* Expiration toggle */}
- -
+ +
- @@ -255,7 +272,9 @@ function EditKeyModal({ { - if (!isOpen) {setIsRevokeConfirmOpen(false);} + if (!isOpen) { + setIsRevokeConfirmOpen(false); + } }} title={`Revoke ${keyItem?.name ?? 'key'}?`} width="narrow" diff --git a/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx b/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx index 7e13ff344c..6a0cccb5d9 100644 --- a/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx +++ b/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx @@ -9,11 +9,11 @@ import { useRevokeServiceAccountKey, } from 'api/generated/services/serviceaccount'; import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; -import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { format } from 'date-fns'; +import dayjs from 'dayjs'; import { useTimezone } from 'providers/Timezone'; import EditKeyModal from './EditKeyModal'; +import { formatLastUsed } from './utils'; interface KeysTabProps { accountId: string; @@ -25,15 +25,15 @@ function formatExpiry(expiresAt: number): JSX.Element { if (expiresAt === 0) { return Never; } - const expiryDate = new Date(expiresAt * 1000); - if (expiryDate < new Date()) { + const expiryDate = dayjs.unix(expiresAt); + if (expiryDate.isBefore(dayjs())) { return ( - {format(expiryDate, 'MMM d, yyyy')} + {expiryDate.format('MMM D, YYYY')} ); } - return {format(expiryDate, 'MMM d, yyyy')}; + return {expiryDate.format('MMM D, YYYY')}; } function KeysTab({ @@ -88,20 +88,9 @@ function KeysTab({ refetch(); }, [refetch]); - const formatLastUsed = useCallback( - (lastUsed: Date | null | undefined): string => { - if (!lastUsed) { - return '—'; - } - try { - return formatTimezoneAdjustedTimestamp( - String(lastUsed), - DATE_TIME_FORMATS.DASH_DATETIME, - ); - } catch { - return '—'; - } - }, + const handleFormatLastUsed = useCallback( + (lastUsed: Date | null | undefined): string => + formatLastUsed(lastUsed, formatTimezoneAdjustedTimestamp), [formatTimezoneAdjustedTimestamp], ); @@ -157,7 +146,7 @@ function KeysTab({ {formatExpiry(keyItem.expires_at)} - {formatLastUsed(keyItem.last_used ?? null)} + {handleFormatLastUsed(keyItem.last_used ?? null)} @@ -201,8 +190,8 @@ function KeysTab({ disableOutsideClick={false} >

- Revoking this key will permanently invalidate it. Any systems using - this key will lose access immediately. + Revoking this key will permanently invalidate it. Any systems using this + key will lose access immediately.

) : ( - + /> )}
diff --git a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx index 997b422a0f..1fd0fe1b17 100644 --- a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx +++ b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx @@ -17,6 +17,7 @@ import { useUpdateServiceAccount, useUpdateServiceAccountStatus, } from 'api/generated/services/serviceaccount'; +import { useRoles } from 'components/RolesSelect'; import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils'; import AddKeyModal from './AddKeyModal'; @@ -65,6 +66,13 @@ function ServiceAccountDrawer({ (localName !== (account.name ?? '') || JSON.stringify(localRoles) !== JSON.stringify(account.roles ?? [])); + const { + roles: availableRoles, + isLoading: rolesLoading, + isError: rolesError, + error: rolesErrorObj, + refetch: refetchRoles, + } = useRoles(); const { mutateAsync: updateAccount } = useUpdateServiceAccount(); const { mutateAsync: updateStatus } = useUpdateServiceAccountStatus(); @@ -182,6 +190,11 @@ function ServiceAccountDrawer({ localRoles={localRoles} onRolesChange={setLocalRoles} isDisabled={isDisabled} + availableRoles={availableRoles} + rolesLoading={rolesLoading} + rolesError={rolesError} + rolesErrorObj={rolesErrorObj} + onRefetchRoles={refetchRoles} /> )} {activeTab === 'keys' && account && ( diff --git a/frontend/src/components/ServiceAccountDrawer/utils.ts b/frontend/src/components/ServiceAccountDrawer/utils.ts new file mode 100644 index 0000000000..20a43642ce --- /dev/null +++ b/frontend/src/components/ServiceAccountDrawer/utils.ts @@ -0,0 +1,18 @@ +import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; + +export function formatLastUsed( + lastUsed: Date | null | undefined, + formatTimezoneAdjustedTimestamp: (ts: string, format: string) => string, +): string { + if (!lastUsed) { + return '—'; + } + const d = new Date(lastUsed); + if (Number.isNaN(d.getTime())) { + return '—'; + } + return formatTimezoneAdjustedTimestamp( + d.toISOString(), + DATE_TIME_FORMATS.DASH_DATETIME, + ); +} diff --git a/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.styles.scss b/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.styles.scss index b2ae0dbf6f..a76cc7ef5d 100644 --- a/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.styles.scss +++ b/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.styles.scss @@ -168,6 +168,22 @@ } } +.sa-tooltip { + .ant-tooltip-inner { + background-color: var(--bg-slate-500); + color: var(--foreground); + font-size: var(--font-size-xs); + line-height: normal; + padding: var(--padding-2) var(--padding-3); + border-radius: 4px; + text-align: left; + } + + .ant-tooltip-arrow-content { + background-color: var(--bg-slate-500); + } +} + .lightMode { .sa-table { .ant-table-tbody { diff --git a/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.tsx b/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.tsx index 4729d1c851..82205b6056 100644 --- a/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.tsx +++ b/frontend/src/components/ServiceAccountsTable/ServiceAccountsTable.tsx @@ -46,13 +46,21 @@ function RolesCell({ roles }: { roles: string[] }): JSX.Element { } const first = roles[0]; const overflow = roles.length - 1; + const tooltipContent = roles.slice(1).join(', '); + return (
{first} {overflow > 0 && ( - - +{overflow} - + + + +{overflow} + + )}
); @@ -67,7 +75,7 @@ function StatusBadge({ status }: { status: string }): JSX.Element { ); } return ( - + DISABLED ); @@ -80,8 +88,8 @@ function ServiceAccountsEmptyState({ }): JSX.Element { return (
- - 🤖 + + 🧐 {searchQuery ? (

@@ -132,27 +140,14 @@ function ServiceAccountsTable({ title: 'Roles', dataIndex: 'roles', key: 'roles', - width: 200, + width: 420, render: (roles: string[]): JSX.Element => , }, - { - title: 'Permissions', - key: 'permissions', - width: 240, - render: (): JSX.Element => , - }, - { - title: 'Keys', - key: 'keys', - width: 96, - align: 'right' as const, - render: (): JSX.Element => , - }, { title: 'Status', dataIndex: 'status', key: 'status', - width: 96, + width: 120, align: 'right' as const, className: 'sa-status-cell', render: (status: string): JSX.Element => , diff --git a/frontend/src/container/MembersSettings/MembersSettings.tsx b/frontend/src/container/MembersSettings/MembersSettings.tsx index d90fb00507..34dbbcb802 100644 --- a/frontend/src/container/MembersSettings/MembersSettings.tsx +++ b/frontend/src/container/MembersSettings/MembersSettings.tsx @@ -119,7 +119,7 @@ function MembersSettings(): JSX.Element { return; } const maxPage = Math.ceil(filteredMembers.length / PAGE_SIZE); - if (currentPage > maxPage) { + if (currentPage > maxPage || currentPage < 1) { setPage(maxPage); } }, [filteredMembers.length, currentPage, setPage]); diff --git a/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.styles.scss b/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.styles.scss index d31e29a712..faf08e8dc3 100644 --- a/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.styles.scss +++ b/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.styles.scss @@ -50,6 +50,11 @@ } } +.sa-status-badge { + color: var(--l3-foreground); + border-color: var(--border); +} + .sa-settings-filter-trigger { display: flex; align-items: center; diff --git a/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.tsx b/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.tsx index e3a2e7d464..d74b9815fb 100644 --- a/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.tsx +++ b/frontend/src/container/ServiceAccountsSettings/ServiceAccountsSettings.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { Button } from '@signozhq/button'; import { Check, ChevronDown, Plus } from '@signozhq/icons'; @@ -11,7 +11,7 @@ import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccount import ServiceAccountsTable from 'components/ServiceAccountsTable/ServiceAccountsTable'; import useUrlQuery from 'hooks/useUrlQuery'; -import { FilterMode, ServiceAccountRow } from './utils'; +import { FilterMode, ServiceAccountRow, ServiceAccountStatus } from './utils'; import './ServiceAccountsSettings.styles.scss'; @@ -27,7 +27,10 @@ function ServiceAccountsSettings(): JSX.Element { const [searchQuery, setSearchQuery] = useState(''); const [filterMode, setFilterMode] = useState(FilterMode.All); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [selectedAccount, setSelectedAccount] = useState(null); + const [ + selectedAccount, + setSelectedAccount, + ] = useState(null); const { data: serviceAccountsData, @@ -50,12 +53,18 @@ function ServiceAccountsSettings(): JSX.Element { ); const activeCount = useMemo( - () => allAccounts.filter((a) => a.status?.toUpperCase() === 'ACTIVE').length, + () => + allAccounts.filter( + (a) => a.status?.toUpperCase() === ServiceAccountStatus.Active, + ).length, [allAccounts], ); const disabledCount = useMemo( - () => allAccounts.filter((a) => a.status?.toUpperCase() !== 'ACTIVE').length, + () => + allAccounts.filter( + (a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active, + ).length, [allAccounts], ); @@ -63,16 +72,22 @@ function ServiceAccountsSettings(): JSX.Element { let result = allAccounts; if (filterMode === FilterMode.Active) { - result = result.filter((a) => a.status?.toUpperCase() === 'ACTIVE'); + result = result.filter( + (a) => a.status?.toUpperCase() === ServiceAccountStatus.Active, + ); } else if (filterMode === FilterMode.Disabled) { - result = result.filter((a) => a.status?.toUpperCase() !== 'ACTIVE'); + result = result.filter( + (a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active, + ); } if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); result = result.filter( (a) => - a.name?.toLowerCase().includes(q) || a.email?.toLowerCase().includes(q), + a.name?.toLowerCase().includes(q) || + a.email?.toLowerCase().includes(q) || + a.roles?.some((role: string) => role.toLowerCase().includes(q)), ); } @@ -92,6 +107,17 @@ function ServiceAccountsSettings(): JSX.Element { [history, urlQuery], ); + useEffect(() => { + if (filteredAccounts.length === 0) { + return; + } + + const maxPage = Math.max(1, Math.ceil(filteredAccounts.length / PAGE_SIZE)); + if (currentPage > maxPage || currentPage < 1) { + setPage(maxPage); + } + }, [filteredAccounts.length, currentPage, setPage]); + const totalCount = allAccounts.length; const filterMenuItems: MenuProps['items'] = [ @@ -167,15 +193,15 @@ function ServiceAccountsSettings(): JSX.Element {

Service Accounts

Service accounts are used for machine-to-machine authentication via API - keys.{' '} - Learn more - + */}

diff --git a/frontend/src/container/ServiceAccountsSettings/utils.ts b/frontend/src/container/ServiceAccountsSettings/utils.ts index 24ac1b74d8..2e102a5915 100644 --- a/frontend/src/container/ServiceAccountsSettings/utils.ts +++ b/frontend/src/container/ServiceAccountsSettings/utils.ts @@ -4,12 +4,17 @@ export enum FilterMode { Disabled = 'disabled', } +export enum ServiceAccountStatus { + Active = 'ACTIVE', + Disabled = 'DISABLED', +} + export interface ServiceAccountRow { id: string; name: string; email: string; roles: string[]; - status: string; + status: ServiceAccountStatus | string; createdAt: string | null; updatedAt: string | null; }