feat: multiple style and functionality fixes

This commit is contained in:
SagarRajput-7
2026-03-10 00:39:26 +05:30
parent 0bac99742e
commit 1cdd2ec001
16 changed files with 454 additions and 135 deletions

View File

@@ -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<FormValues>();
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({
<Form.Item
name="email"
label="Email Address"
rules={[
{ required: true, message: 'Email Address is required' },
{ type: 'email', message: 'Please enter a valid email address' },
]}
className="create-sa-form__item"
>
<Input
@@ -106,21 +128,26 @@ function CreateServiceAccountModal({
authentication.
</p>
<Form.Item name="roles" label="Roles" className="create-sa-form__item">
<Select
<Form.Item
name="roles"
label="Roles"
rules={[{ required: true, message: 'At least one role is required' }]}
className="create-sa-form__item"
>
<RolesSelect
mode="multiple"
roles={roles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={refetchRoles}
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
className="create-sa-form__select"
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.create-sa-modal') as HTMLElement) ||
document.body
}
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
/>
</Form.Item>
</Form>
</div>
@@ -142,7 +169,7 @@ function CreateServiceAccountModal({
color="primary"
size="sm"
onClick={handleSubmit}
disabled={isSubmitting}
disabled={isSubmitting || !submittable}
>
{isSubmitting ? 'Creating...' : 'Create Service Account'}
</Button>

View File

@@ -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);
}
}

View File

@@ -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 (
<div className="roles-select-error">
<span className="roles-select-error__msg">
<CircleAlert size={12} />
{errorMessage}
</span>
{onRefetch && (
<button
type="button"
onClick={(e): void => {
e.stopPropagation();
onRefetch();
}}
className="roles-select-error__retry-btn"
title="Retry"
>
<RefreshCw size={12} />
</button>
)}
</div>
);
}
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 ? (
<ErrorContent error={error} onRefetch={onRefetch} />
) : undefined;
if (mode === 'multiple') {
const { value = [], onChange } = props as MultipleProps;
return (
<Select
id={id}
mode="multiple"
value={value}
onChange={onChange}
placeholder={placeholder}
className={className}
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
style={{ pointerEvents: 'none' }}
>
{option.label}
</Checkbox>
)}
getPopupContainer={getPopupContainer}
/>
);
}
const { value, onChange } = props as SingleProps;
return (
<Select
id={id}
value={value}
onChange={onChange}
placeholder={placeholder}
className={className}
loading={loading}
notFoundContent={notFoundContent}
options={options}
getPopupContainer={getPopupContainer}
/>
);
}
export default RolesSelect;

View File

@@ -0,0 +1,2 @@
export type { RoleOption, RolesSelectProps } from './RolesSelect';
export { default, getRoleOptions, useRoles } from './RolesSelect';

View File

@@ -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({
</div>
<div className="add-key-modal__field">
<label className="add-key-modal__label">Expiration</label>
<span className="add-key-modal__label">Expiration</span>
<ToggleGroup
type="single"
value={expiryMode}
@@ -174,9 +173,12 @@ function AddKeyModal({
{expiryMode === 'date' && (
<div className="add-key-modal__field">
<label className="add-key-modal__label">Expiration Date</label>
<label className="add-key-modal__label" htmlFor="expiry-date">
Expiration Date
</label>
<div className="add-key-modal__datepicker">
<DatePicker
id="expiry-date"
value={expiryDate}
onChange={(date): void => setExpiryDate(date)}
style={{ width: '100%', height: 32 }}
@@ -222,7 +224,7 @@ function AddKeyModal({
{phase === 'created' && createdKey && (
<div className="add-key-modal__form">
<div className="add-key-modal__field">
<label className="add-key-modal__label">API Key</label>
<span className="add-key-modal__label">API Key</span>
<div className="add-key-modal__key-display">
<span className="add-key-modal__key-text">{createdKey.key}</span>
<Button
@@ -252,12 +254,7 @@ function AddKeyModal({
<div className="add-key-modal__footer">
<span />
<div className="add-key-modal__footer-right">
<Button
variant="solid"
color="primary"
size="sm"
onClick={handleClose}
>
<Button variant="solid" color="primary" size="sm" onClick={handleClose}>
Done
</Button>
</div>

View File

@@ -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<void> => {
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<void> => {
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) */}
<div className="edit-key-modal__field">
<label className="edit-key-modal__label">Key</label>
<div className="edit-key-modal__key-display">
<span className="edit-key-modal__key-text">
{keyItem?.key ?? '—'}
</span>
<label className="edit-key-modal__label" htmlFor="edit-key-display">
Key
</label>
<div id="edit-key-display" className="edit-key-modal__key-display">
<span className="edit-key-modal__key-text">{keyItem?.key ?? '—'}</span>
<LockKeyhole size={12} className="edit-key-modal__lock-icon" />
</div>
</div>
{/* Expiration toggle */}
<div className="edit-key-modal__field">
<label className="edit-key-modal__label">Expiration</label>
<div className="edit-key-modal__expiry-toggle">
<label className="edit-key-modal__label" htmlFor="edit-key-expiry-toggle">
Expiration
</label>
<div
id="edit-key-expiry-toggle"
className="edit-key-modal__expiry-toggle"
>
<Button
variant={expiryMode === 'none' ? 'solid' : 'ghost'}
color="secondary"
size="sm"
className={`edit-key-modal__expiry-toggle-btn${expiryMode === 'none' ? ' edit-key-modal__expiry-toggle-btn--active' : ''}`}
className={`edit-key-modal__expiry-toggle-btn${
expiryMode === 'none'
? ' edit-key-modal__expiry-toggle-btn--active'
: ''
}`}
onClick={(): void => {
setExpiryMode('none');
setLocalDate(null);
@@ -182,7 +198,11 @@ function EditKeyModal({
variant={expiryMode === 'date' ? 'solid' : 'ghost'}
color="secondary"
size="sm"
className={`edit-key-modal__expiry-toggle-btn${expiryMode === 'date' ? ' edit-key-modal__expiry-toggle-btn--active' : ''}`}
className={`edit-key-modal__expiry-toggle-btn${
expiryMode === 'date'
? ' edit-key-modal__expiry-toggle-btn--active'
: ''
}`}
onClick={(): void => setExpiryMode('date')}
>
Set Expiration Date
@@ -192,8 +212,10 @@ function EditKeyModal({
{expiryMode === 'date' && (
<div className="edit-key-modal__field">
<label className="edit-key-modal__label">Expiration Date</label>
<div className="edit-key-modal__datepicker">
<label className="edit-key-modal__label" htmlFor="edit-key-datepicker">
Expiration Date
</label>
<div id="edit-key-datepicker" className="edit-key-modal__datepicker">
<DatePicker
value={localDate}
onChange={(date): void => setLocalDate(date)}
@@ -213,7 +235,7 @@ function EditKeyModal({
<div className="edit-key-modal__meta">
<span className="edit-key-modal__meta-label">Last Used</span>
<Badge color="vanilla">
{formatLastUsed(keyItem?.last_used ?? null)}
{handleFormatLastUsed(keyItem?.last_used ?? null)}
</Badge>
</div>
@@ -228,12 +250,7 @@ function EditKeyModal({
Revoke Key
</button>
<div className="edit-key-modal__footer-right">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={onClose}
>
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
<X size={12} />
Cancel
</Button>
@@ -255,7 +272,9 @@ function EditKeyModal({
<DialogWrapper
open={isRevokeConfirmOpen}
onOpenChange={(isOpen): void => {
if (!isOpen) {setIsRevokeConfirmOpen(false);}
if (!isOpen) {
setIsRevokeConfirmOpen(false);
}
}}
title={`Revoke ${keyItem?.name ?? 'key'}?`}
width="narrow"

View File

@@ -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 <span className="keys-tab__expiry--never">Never</span>;
}
const expiryDate = new Date(expiresAt * 1000);
if (expiryDate < new Date()) {
const expiryDate = dayjs.unix(expiresAt);
if (expiryDate.isBefore(dayjs())) {
return (
<span className="keys-tab__expiry--expired">
{format(expiryDate, 'MMM d, yyyy')}
{expiryDate.format('MMM D, YYYY')}
</span>
);
}
return <span>{format(expiryDate, 'MMM d, yyyy')}</span>;
return <span>{expiryDate.format('MMM D, YYYY')}</span>;
}
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)}
</span>
<span className="keys-tab__col-last-used">
{formatLastUsed(keyItem.last_used ?? null)}
{handleFormatLastUsed(keyItem.last_used ?? null)}
</span>
<span className="keys-tab__col-action">
<Tooltip title="Revoke Key">
@@ -201,8 +190,8 @@ function KeysTab({
disableOutsideClick={false}
>
<p className="delete-dialog__body">
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.
</p>
<DialogFooter className="delete-dialog__footer">
<Button

View File

@@ -1,12 +1,13 @@
import { useCallback } from 'react';
import { Badge } from '@signozhq/badge';
import { ChevronDown, LockKeyhole } from '@signozhq/icons';
import { LockKeyhole } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { Select } from 'antd';
import type { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { capitalize } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
interface OverviewTabProps {
account: ServiceAccountRow;
@@ -15,6 +16,11 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: RoletypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;
onRefetchRoles?: () => void;
}
function OverviewTab({
@@ -24,6 +30,11 @@ function OverviewTab({
localRoles,
onRolesChange,
isDisabled,
availableRoles,
rolesLoading,
rolesError,
rolesErrorObj,
onRefetchRoles,
}: OverviewTabProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -86,7 +97,7 @@ function OverviewTab({
{localRoles.length > 0 ? (
localRoles.map((r) => (
<Badge key={r} color="vanilla">
{capitalize(r)}
{r}
</Badge>
))
) : (
@@ -96,22 +107,22 @@ function OverviewTab({
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
) : (
<Select
<RolesSelect
id="sa-roles"
mode="multiple"
roles={availableRoles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={onRefetchRoles}
value={localRoles}
onChange={(roles): void => onRolesChange(roles as string[])}
className="sa-drawer__role-select"
suffixIcon={<ChevronDown size={14} />}
onChange={onRolesChange}
placeholder="Select roles"
className="sa-drawer__role-select"
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.sa-drawer') as HTMLElement) || document.body
}
>
<Select.Option value="ADMIN">{capitalize('ADMIN')}</Select.Option>
<Select.Option value="EDITOR">{capitalize('EDITOR')}</Select.Option>
<Select.Option value="VIEWER">{capitalize('VIEWER')}</Select.Option>
</Select>
/>
)}
</div>

View File

@@ -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 && (

View File

@@ -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,
);
}

View File

@@ -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 {

View File

@@ -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 (
<div className="sa-roles-cell">
<Badge color="vanilla">{first}</Badge>
{overflow > 0 && (
<Badge color="vanilla" variant="outline">
+{overflow}
</Badge>
<Tooltip
title={tooltipContent}
overlayClassName="sa-tooltip"
overlayStyle={{ maxWidth: '600px' }}
>
<Badge color="vanilla" variant="outline" className="sa-status-badge">
+{overflow}
</Badge>
</Tooltip>
)}
</div>
);
@@ -67,7 +75,7 @@ function StatusBadge({ status }: { status: string }): JSX.Element {
);
}
return (
<Badge color="vanilla" variant="outline">
<Badge color="vanilla" variant="outline" className="sa-status-badge">
DISABLED
</Badge>
);
@@ -80,8 +88,8 @@ function ServiceAccountsEmptyState({
}): JSX.Element {
return (
<div className="sa-empty-state">
<span className="sa-empty-state__emoji" role="img" aria-label="robot">
🤖
<span className="sa-empty-state__emoji" role="img" aria-label="monocle face">
🧐
</span>
{searchQuery ? (
<p className="sa-empty-state__text">
@@ -132,27 +140,14 @@ function ServiceAccountsTable({
title: 'Roles',
dataIndex: 'roles',
key: 'roles',
width: 200,
width: 420,
render: (roles: string[]): JSX.Element => <RolesCell roles={roles} />,
},
{
title: 'Permissions',
key: 'permissions',
width: 240,
render: (): JSX.Element => <span className="sa-dash"></span>,
},
{
title: 'Keys',
key: 'keys',
width: 96,
align: 'right' as const,
render: (): JSX.Element => <span className="sa-dash"></span>,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 96,
width: 120,
align: 'right' as const,
className: 'sa-status-cell',
render: (status: string): JSX.Element => <StatusBadge status={status} />,

View File

@@ -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]);

View File

@@ -50,6 +50,11 @@
}
}
.sa-status-badge {
color: var(--l3-foreground);
border-color: var(--border);
}
.sa-settings-filter-trigger {
display: flex;
align-items: center;

View File

@@ -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>(FilterMode.All);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [selectedAccount, setSelectedAccount] = useState<ServiceAccountRow | null>(null);
const [
selectedAccount,
setSelectedAccount,
] = useState<ServiceAccountRow | null>(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 {
<h1 className="sa-settings__title">Service Accounts</h1>
<p className="sa-settings__subtitle">
Service accounts are used for machine-to-machine authentication via API
keys.{' '}
<a
keys. {/* Todo: to add doc links */}
{/* <a
href="https://signoz.io/docs/service-accounts"
target="_blank"
rel="noopener noreferrer"
className="sa-settings__learn-more"
>
Learn more
</a>
</a> */}
</p>
</div>

View File

@@ -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;
}