mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-10 16:40:34 +01:00
Compare commits
3 Commits
nv/dashboa
...
v0.132.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd6852fa6 | ||
|
|
b5567d1550 | ||
|
|
2017660eb8 |
@@ -2,7 +2,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
@@ -134,18 +134,17 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<AuthZTooltip checks={[SACreatePermission]}>
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-sa-form"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Create Service Account
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<AuthZButton
|
||||
checks={[SACreatePermission]}
|
||||
type="submit"
|
||||
form="create-sa-form"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Create Service Account
|
||||
</AuthZButton>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ function OtherFiltersSkeleton(): JSX.Element {
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className="qf-other-filters-skeleton"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { DatePicker } from 'antd';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import {
|
||||
APIKeyCreatePermission,
|
||||
buildSAAttachPermission,
|
||||
@@ -109,24 +109,21 @@ function KeyFormPhase({
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[
|
||||
APIKeyCreatePermission,
|
||||
buildSAAttachPermission(accountId ?? ''),
|
||||
]}
|
||||
enabled={!!accountId}
|
||||
authZEnabled={!!accountId}
|
||||
type="submit"
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Create Key
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
Create Key
|
||||
</AuthZButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
@@ -84,20 +84,17 @@ function DeleteAccountModal(): JSX.Element {
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[buildSADeletePermission(accountId ?? '')]}
|
||||
enabled={!!accountId}
|
||||
authZEnabled={!!accountId}
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isDeleting}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isDeleting}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<Trash2 size={12} />
|
||||
Delete
|
||||
</AuthZButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { DatePicker } from 'antd';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import {
|
||||
buildAPIKeyDeletePermission,
|
||||
@@ -158,38 +159,36 @@ function EditKeyForm({
|
||||
</form>
|
||||
|
||||
<div className="edit-key-modal__footer">
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(keyItem?.id ?? ''),
|
||||
buildSADetachPermission(accountId ?? ''),
|
||||
]}
|
||||
enabled={!!accountId && !!keyItem?.id}
|
||||
authZEnabled={!!accountId && !!keyItem?.id}
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={onRevokeClick}
|
||||
>
|
||||
<Button variant="link" color="destructive" onClick={onRevokeClick}>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</AuthZButton>
|
||||
<div className="edit-key-modal__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
|
||||
enabled={!!accountId && !!keyItem?.id}
|
||||
authZEnabled={!!accountId && !!keyItem?.id}
|
||||
type="submit"
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
Save Changes
|
||||
</AuthZButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { KeyRound, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton, Table, Tooltip } from 'antd';
|
||||
import { Pagination, Skeleton, Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
|
||||
import {
|
||||
APIKeyCreatePermission,
|
||||
APIKeyListPermission,
|
||||
buildAPIKeyDeletePermission,
|
||||
buildSAAttachPermission,
|
||||
buildSADetachPermission,
|
||||
@@ -24,10 +25,10 @@ interface KeysTabProps {
|
||||
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
|
||||
isLoading: boolean;
|
||||
isDisabled?: boolean;
|
||||
canUpdate?: boolean;
|
||||
accountId?: string;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
interface BuildColumnsParams {
|
||||
@@ -113,29 +114,26 @@ function buildColumns({
|
||||
render: (_, record): JSX.Element => {
|
||||
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
|
||||
return (
|
||||
<AuthZTooltip
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</AuthZTooltip>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<AuthZButton
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
authZEnabled={!isDisabled && !!accountId}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
>
|
||||
<X size={12} />
|
||||
</AuthZButton>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -149,6 +147,7 @@ function KeysTab({
|
||||
accountId = '',
|
||||
currentPage,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
}: KeysTabProps): JSX.Element {
|
||||
const [, setIsAddKeyOpen] = useQueryState(
|
||||
'add-key',
|
||||
@@ -212,21 +211,18 @@ function KeysTab({
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
authZEnabled={!isDisabled && !!accountId}
|
||||
variant="link"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsAddKeyOpen(true);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsAddKeyOpen(true);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
+ Add your first key
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
+ Add your first key
|
||||
</AuthZButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -278,6 +274,24 @@ function KeysTab({
|
||||
})}
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={keys.length}
|
||||
showTotal={(total: number, range: number[]): JSX.Element => (
|
||||
<>
|
||||
<span className="sa-drawer__pagination-range">
|
||||
{range[0]} — {range[1]}
|
||||
</span>
|
||||
<span className="sa-drawer__pagination-total"> of {total}</span>
|
||||
</>
|
||||
)}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage
|
||||
onChange={onPageChange}
|
||||
className="sa-drawer__keys-pagination"
|
||||
/>
|
||||
|
||||
<EditKeyModal keyItem={editKey} />
|
||||
|
||||
<RevokeKeyModal />
|
||||
@@ -285,4 +299,7 @@ function KeysTab({
|
||||
);
|
||||
}
|
||||
|
||||
export default KeysTab;
|
||||
export default withAuthZContent(KeysTab, {
|
||||
checks: [APIKeyListPermission],
|
||||
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
|
||||
});
|
||||
|
||||
@@ -6,15 +6,20 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
|
||||
import RolesSelect from 'components/RolesSelect';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
|
||||
import { buildSAUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import {
|
||||
buildSAReadPermission,
|
||||
buildSAUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import SaveErrorItem from './SaveErrorItem';
|
||||
import type { SaveError } from './utils';
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
interface OverviewTabProps {
|
||||
account: ServiceAccountRow;
|
||||
@@ -23,7 +28,6 @@ interface OverviewTabProps {
|
||||
localRoles: string[];
|
||||
onRolesChange: (v: string[]) => void;
|
||||
isDisabled: boolean;
|
||||
canUpdate?: boolean;
|
||||
availableRoles: AuthtypesRoleDTO[];
|
||||
rolesLoading?: boolean;
|
||||
rolesError?: boolean;
|
||||
@@ -39,7 +43,6 @@ function OverviewTab({
|
||||
localRoles,
|
||||
onRolesChange,
|
||||
isDisabled,
|
||||
canUpdate = true,
|
||||
availableRoles,
|
||||
rolesLoading,
|
||||
rolesError,
|
||||
@@ -86,23 +89,22 @@ function OverviewTab({
|
||||
<label className="sa-drawer__label" htmlFor="sa-name">
|
||||
Name
|
||||
</label>
|
||||
{isDisabled || !canUpdate ? (
|
||||
<AuthZTooltip
|
||||
checks={[buildSAUpdatePermission(account.id)]}
|
||||
enabled={!isDisabled && !canUpdate}
|
||||
>
|
||||
{isDisabled ? (
|
||||
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
|
||||
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
|
||||
<span className="sa-drawer__input-text">{localName || '—'}</span>
|
||||
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
|
||||
</div>
|
||||
</AuthZTooltip>
|
||||
) : (
|
||||
<Input
|
||||
id="sa-name"
|
||||
value={localName}
|
||||
onChange={(e): void => onNameChange(e.target.value)}
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
|
||||
<Input
|
||||
id="sa-name"
|
||||
value={localName}
|
||||
onChange={(e): void => onNameChange(e.target.value)}
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
</AuthZTooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -220,4 +222,9 @@ function OverviewTab({
|
||||
);
|
||||
}
|
||||
|
||||
export default OverviewTab;
|
||||
export default withAuthZContent(OverviewTab, {
|
||||
checks: (props): ReturnType<typeof buildSAReadPermission>[] => [
|
||||
buildSAReadPermission(props.account.id),
|
||||
],
|
||||
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import {
|
||||
buildAPIKeyDeletePermission,
|
||||
buildSADetachPermission,
|
||||
@@ -45,23 +45,20 @@ export function RevokeKeyFooter({
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(keyId ?? ''),
|
||||
buildSADetachPermission(accountId ?? ''),
|
||||
]}
|
||||
enabled={!!accountId && !!keyId}
|
||||
authZEnabled={!!accountId && !!keyId}
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</AuthZButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -111,7 +108,7 @@ function RevokeKeyModal(): JSX.Element {
|
||||
}
|
||||
|
||||
function handleCancel(): void {
|
||||
setRevokeKeyId(null);
|
||||
void setRevokeKeyId(null);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Pagination, Skeleton } from 'antd';
|
||||
import { Skeleton } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getListServiceAccountsQueryKey,
|
||||
@@ -16,7 +22,6 @@ import {
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
|
||||
import { useRoles } from 'components/RolesSelect';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import {
|
||||
@@ -28,15 +33,13 @@ import {
|
||||
RoleUpdateFailure,
|
||||
useServiceAccountRoleManager,
|
||||
} from 'hooks/serviceAccount/useServiceAccountRoleManager';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import {
|
||||
APIKeyCreatePermission,
|
||||
APIKeyListPermission,
|
||||
buildSAAttachPermission,
|
||||
buildSADeletePermission,
|
||||
buildSAReadPermission,
|
||||
buildSAUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -47,7 +50,6 @@ import {
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
import KeysTab from './KeysTab';
|
||||
@@ -70,14 +72,12 @@ function toSaveApiError(err: unknown): APIError {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ServiceAccountDrawer({
|
||||
onSuccess,
|
||||
}: ServiceAccountDrawerProps): JSX.Element {
|
||||
const [selectedAccountId, setSelectedAccountId] = useQueryState(
|
||||
SA_QUERY_PARAMS.ACCOUNT,
|
||||
);
|
||||
const open = !!selectedAccountId;
|
||||
const [activeTab, setActiveTab] = useQueryState(
|
||||
SA_QUERY_PARAMS.TAB,
|
||||
parseAsStringEnum<ServiceAccountDrawerTab>(
|
||||
@@ -100,28 +100,14 @@ function ServiceAccountDrawer({
|
||||
SA_QUERY_PARAMS.DELETE_SA,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
|
||||
const [localName, setLocalName] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { permissions: drawerPermissions, isLoading: isAuthZLoading } = useAuthZ(
|
||||
selectedAccountId
|
||||
? [
|
||||
buildSAReadPermission(selectedAccountId),
|
||||
buildSAUpdatePermission(selectedAccountId),
|
||||
buildSADeletePermission(selectedAccountId),
|
||||
APIKeyListPermission,
|
||||
]
|
||||
: [],
|
||||
{ enabled: !!selectedAccountId },
|
||||
);
|
||||
|
||||
const canRead =
|
||||
drawerPermissions?.[buildSAReadPermission(selectedAccountId ?? '')]
|
||||
?.isGranted ?? false;
|
||||
const open = !!selectedAccountId;
|
||||
|
||||
const {
|
||||
data: accountData,
|
||||
@@ -131,7 +117,7 @@ function ServiceAccountDrawer({
|
||||
refetch: refetchAccount,
|
||||
} = useGetServiceAccount(
|
||||
{ id: selectedAccountId ?? '' },
|
||||
{ query: { enabled: canRead && !!selectedAccountId } },
|
||||
{ query: { enabled: !!selectedAccountId } },
|
||||
);
|
||||
|
||||
const account = useMemo(
|
||||
@@ -145,7 +131,7 @@ function ServiceAccountDrawer({
|
||||
isLoading: isRolesLoading,
|
||||
applyDiff,
|
||||
} = useServiceAccountRoleManager(selectedAccountId ?? '', {
|
||||
enabled: canRead && !!selectedAccountId,
|
||||
enabled: !!selectedAccountId,
|
||||
});
|
||||
|
||||
const roleSessionRef = useRef<string | null>(null);
|
||||
@@ -194,16 +180,9 @@ function ServiceAccountDrawer({
|
||||
refetch: refetchRoles,
|
||||
} = useRoles();
|
||||
|
||||
const canListKeys =
|
||||
drawerPermissions?.[APIKeyListPermission]?.isGranted ?? false;
|
||||
|
||||
const canUpdate =
|
||||
drawerPermissions?.[buildSAUpdatePermission(selectedAccountId ?? '')]
|
||||
?.isGranted ?? true;
|
||||
|
||||
const { data: keysData, isLoading: keysLoading } = useListServiceAccountKeys(
|
||||
{ id: selectedAccountId ?? '' },
|
||||
{ query: { enabled: !!selectedAccountId && canListKeys } },
|
||||
{ query: { enabled: !!selectedAccountId } },
|
||||
);
|
||||
const keys = keysData?.data ?? [];
|
||||
|
||||
@@ -217,7 +196,6 @@ function ServiceAccountDrawer({
|
||||
}
|
||||
}, [keysLoading, keys.length, keysPage, setKeysPage]);
|
||||
|
||||
// the retry for this mutation is safe due to the api being idempotent on backend
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
|
||||
|
||||
const retryNameUpdate = useCallback(async (): Promise<void> => {
|
||||
@@ -375,23 +353,71 @@ function ServiceAccountDrawer({
|
||||
]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
void setIsDeleteOpen(null);
|
||||
void setIsAddKeyOpen(null);
|
||||
void setSelectedAccountId(null);
|
||||
setSaveErrors([]);
|
||||
void setActiveTab(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
setSaveErrors([]);
|
||||
void setIsAddKeyOpen(null);
|
||||
void setIsDeleteOpen(null);
|
||||
void setSelectedAccountId(null);
|
||||
}, [
|
||||
setSelectedAccountId,
|
||||
setActiveTab,
|
||||
setKeysPage,
|
||||
setEditKeyId,
|
||||
setIsAddKeyOpen,
|
||||
setIsDeleteOpen,
|
||||
setSelectedAccountId,
|
||||
]);
|
||||
|
||||
const drawerContent = (
|
||||
const footer = useMemo(
|
||||
() =>
|
||||
activeTab === ServiceAccountDrawerTab.Overview && !isDeleted && open ? (
|
||||
<div className="sa-drawer__footer">
|
||||
<AuthZButton
|
||||
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
|
||||
authZEnabled={!!selectedAccountId}
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete Service Account
|
||||
</AuthZButton>
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
<AuthZButton
|
||||
checks={[buildSAUpdatePermission(selectedAccountId ?? '')]}
|
||||
authZEnabled={!!selectedAccountId}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</AuthZButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
[
|
||||
activeTab,
|
||||
isDeleted,
|
||||
open,
|
||||
selectedAccountId,
|
||||
isSaving,
|
||||
isDirty,
|
||||
handleClose,
|
||||
handleSave,
|
||||
setIsDeleteOpen,
|
||||
],
|
||||
);
|
||||
|
||||
const body = (
|
||||
<div className="sa-drawer__layout">
|
||||
<div className="sa-drawer__tabs">
|
||||
<ToggleGroupSimple
|
||||
@@ -433,26 +459,23 @@ function ServiceAccountDrawer({
|
||||
]}
|
||||
/>
|
||||
{activeTab === ServiceAccountDrawerTab.Keys && (
|
||||
<AuthZTooltip
|
||||
<AuthZButton
|
||||
checks={[
|
||||
APIKeyCreatePermission,
|
||||
buildSAAttachPermission(selectedAccountId ?? ''),
|
||||
]}
|
||||
enabled={!isDeleted && !!selectedAccountId}
|
||||
authZEnabled={!isDeleted && !!selectedAccountId}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Add Key
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<Plus size={12} />
|
||||
Add Key
|
||||
</AuthZButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -461,9 +484,7 @@ function ServiceAccountDrawer({
|
||||
activeTab === ServiceAccountDrawerTab.Keys ? ' sa-drawer__body--keys' : ''
|
||||
}`}
|
||||
>
|
||||
{(isAuthZLoading || isAccountLoading) && (
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
)}
|
||||
{isAccountLoading && <Skeleton active paragraph={{ rows: 6 }} />}
|
||||
{isAccountError && (
|
||||
<ErrorInPlace
|
||||
error={toAPIError(
|
||||
@@ -472,141 +493,73 @@ function ServiceAccountDrawer({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!isAuthZLoading &&
|
||||
!isAccountLoading &&
|
||||
!isAccountError &&
|
||||
selectedAccountId && (
|
||||
<>
|
||||
{activeTab === ServiceAccountDrawerTab.Overview &&
|
||||
(canRead && account ? (
|
||||
<OverviewTab
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
localRoles={localRoles}
|
||||
onRolesChange={(roles): void => {
|
||||
setLocalRoles(roles);
|
||||
clearRoleErrors();
|
||||
}}
|
||||
isDisabled={isDeleted}
|
||||
canUpdate={canUpdate}
|
||||
availableRoles={availableRoles}
|
||||
rolesLoading={rolesLoading}
|
||||
rolesError={rolesError}
|
||||
rolesErrorObj={rolesErrorObj}
|
||||
onRefetchRoles={refetchRoles}
|
||||
saveErrors={saveErrors}
|
||||
/>
|
||||
) : (
|
||||
<PermissionDeniedCallout permissionName="serviceaccount:read" />
|
||||
))}
|
||||
{activeTab === ServiceAccountDrawerTab.Keys &&
|
||||
(canListKeys ? (
|
||||
<KeysTab
|
||||
keys={keys}
|
||||
isLoading={keysLoading}
|
||||
isDisabled={isDeleted}
|
||||
canUpdate={canUpdate}
|
||||
accountId={selectedAccountId}
|
||||
currentPage={keysPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
) : (
|
||||
<PermissionDeniedCallout permissionName="factor-api-key:list" />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{!isAccountLoading && !isAccountError && (
|
||||
<>
|
||||
{activeTab === ServiceAccountDrawerTab.Overview &&
|
||||
(account ? (
|
||||
<OverviewTab
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
localRoles={localRoles}
|
||||
onRolesChange={(roles): void => {
|
||||
setLocalRoles(roles);
|
||||
clearRoleErrors();
|
||||
}}
|
||||
isDisabled={isDeleted}
|
||||
availableRoles={availableRoles}
|
||||
rolesLoading={rolesLoading}
|
||||
rolesError={rolesError}
|
||||
rolesErrorObj={rolesErrorObj}
|
||||
onRefetchRoles={refetchRoles}
|
||||
saveErrors={saveErrors}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton active />
|
||||
))}
|
||||
{activeTab === ServiceAccountDrawerTab.Keys && (
|
||||
<KeysTab
|
||||
keys={keys}
|
||||
isLoading={keysLoading}
|
||||
isDisabled={isDeleted}
|
||||
accountId={selectedAccountId ?? ''}
|
||||
currentPage={keysPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
onPageChange={(page): void => {
|
||||
void setKeysPage(page);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<div className="sa-drawer__footer">
|
||||
{activeTab === ServiceAccountDrawerTab.Keys ? (
|
||||
<Pagination
|
||||
current={keysPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={keys.length}
|
||||
showTotal={(total: number, range: number[]): JSX.Element => (
|
||||
<>
|
||||
<span className="sa-drawer__pagination-range">
|
||||
{range[0]} — {range[1]}
|
||||
</span>
|
||||
<span className="sa-drawer__pagination-total"> of {total}</span>
|
||||
</>
|
||||
)}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage
|
||||
onChange={(page): void => {
|
||||
void setKeysPage(page);
|
||||
}}
|
||||
className="sa-drawer__keys-pagination"
|
||||
/>
|
||||
) : (
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
title="Service Account Details"
|
||||
className="sa-drawer"
|
||||
width="wide"
|
||||
footer={footer}
|
||||
>
|
||||
{open && (
|
||||
<>
|
||||
{!isDeleted && (
|
||||
<AuthZTooltip
|
||||
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
|
||||
enabled={!!selectedAccountId}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete Service Account
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
)}
|
||||
{!isDeleted && (
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{body}
|
||||
<DeleteAccountModal />
|
||||
<AddKeyModal />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
title="Service Account Details"
|
||||
className="sa-drawer"
|
||||
width="wide"
|
||||
footer={footer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
|
||||
<DeleteAccountModal />
|
||||
|
||||
<AddKeyModal />
|
||||
</>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
@@ -59,6 +60,7 @@ describe('AddKeyModal', () => {
|
||||
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(201), ctx.json(createdKeyResponse)),
|
||||
),
|
||||
setupAuthzAdmin(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -61,6 +62,7 @@ describe('EditKeyModal (URL-controlled)', () => {
|
||||
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
setupAuthzAdmin(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -35,7 +36,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
|
||||
{
|
||||
id: 'key-2',
|
||||
name: 'Staging Key',
|
||||
expiresAt: 1924905600, // 2030-12-31
|
||||
expiresAt: 1924948800, // 2030-12-31 12:00 UTC (noon to avoid timezone issues)
|
||||
lastObservedAt: '2026-03-10T10:00:00Z',
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
@@ -47,6 +48,7 @@ const defaultProps = {
|
||||
isDisabled: false,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
onPageChange: jest.fn(),
|
||||
};
|
||||
|
||||
function renderKeysTab(
|
||||
@@ -67,6 +69,7 @@ describe('KeysTab', () => {
|
||||
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
setupAuthzAdmin(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -74,9 +77,12 @@ describe('KeysTab', () => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders loading state', () => {
|
||||
it('renders loading state', async () => {
|
||||
renderKeysTab({ isLoading: true });
|
||||
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
// Wait for authz to complete, then check for skeleton
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty state when no keys and clicking add sets add-key param', async () => {
|
||||
@@ -91,9 +97,9 @@ describe('KeysTab', () => {
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/No keys. Start by creating one./i),
|
||||
).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/No keys. Start by creating one./i),
|
||||
).resolves.toBeInTheDocument();
|
||||
const addBtn = screen.getByRole('button', { name: /\+ Add your first key/i });
|
||||
await user.click(addBtn);
|
||||
expect(onUrlUpdate).toHaveBeenCalledWith(
|
||||
@@ -103,10 +109,12 @@ describe('KeysTab', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders table with keys', () => {
|
||||
it('renders table with keys', async () => {
|
||||
renderKeysTab();
|
||||
|
||||
expect(screen.getByText('Production Key')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('Production Key'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('Staging Key')).toBeInTheDocument();
|
||||
expect(screen.getByText('Never')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dec 31, 2030')).toBeInTheDocument();
|
||||
@@ -122,7 +130,7 @@ describe('KeysTab', () => {
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
const row = screen.getByText('Production Key').closest('tr');
|
||||
const row = (await screen.findByText('Production Key')).closest('tr');
|
||||
if (!row) {
|
||||
throw new Error('Row not found');
|
||||
}
|
||||
@@ -146,6 +154,8 @@ describe('KeysTab', () => {
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
// Wait for authz to complete and table to render
|
||||
await screen.findByText('Production Key');
|
||||
const revokeBtns = screen
|
||||
.getAllByRole('button')
|
||||
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
|
||||
@@ -163,7 +173,8 @@ describe('KeysTab', () => {
|
||||
|
||||
renderKeysTab();
|
||||
|
||||
// Seed the keys cache so RevokeKeyModal can read the key name
|
||||
// Wait for authz to complete and table to render
|
||||
await screen.findByText('Production Key');
|
||||
const revokeBtns = screen
|
||||
.getAllByRole('button')
|
||||
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
|
||||
@@ -177,9 +188,11 @@ describe('KeysTab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('disables actions when isDisabled is true', () => {
|
||||
it('disables actions when isDisabled is true', async () => {
|
||||
renderKeysTab({ isDisabled: true });
|
||||
|
||||
// Wait for authz to complete and table to render
|
||||
await screen.findByText('Production Key');
|
||||
const revokeBtns = screen
|
||||
.getAllByRole('button')
|
||||
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
@@ -32,30 +31,6 @@ const activeAccountResponse = {
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
};
|
||||
|
||||
jest.mock('@signozhq/ui/drawer', () => ({
|
||||
...jest.requireActual('@signozhq/ui/drawer'),
|
||||
DrawerWrapper: ({
|
||||
children,
|
||||
footer,
|
||||
open,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
open: boolean;
|
||||
}): JSX.Element | null =>
|
||||
open ? (
|
||||
<div>
|
||||
{children}
|
||||
{footer}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
function renderDrawer(
|
||||
searchParams: Record<string, string> = { account: 'sa-1' },
|
||||
): ReturnType<typeof render> {
|
||||
@@ -118,7 +93,7 @@ describe('ServiceAccountDrawer — permissions', () => {
|
||||
renderDrawer();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceaccount:read/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/read:serviceaccount/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,7 +115,7 @@ describe('ServiceAccountDrawer — permissions', () => {
|
||||
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/factor-api-key:list/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
@@ -7,30 +6,6 @@ import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import ServiceAccountDrawer from '../ServiceAccountDrawer';
|
||||
|
||||
jest.mock('@signozhq/ui/drawer', () => ({
|
||||
...jest.requireActual('@signozhq/ui/drawer'),
|
||||
DrawerWrapper: ({
|
||||
children,
|
||||
footer,
|
||||
open,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
open: boolean;
|
||||
}): JSX.Element | null =>
|
||||
open ? (
|
||||
<div>
|
||||
{children}
|
||||
{footer}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.07px;
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-8);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
|
||||
@@ -7,19 +7,48 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useNavigationBlocker } from 'hooks/useNavigationBlocker';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { withAuthZPage } from 'lib/authz/components/withAuthZ/withAuthZPage';
|
||||
import { RouterContext } from 'lib/authz/components/withAuthZ/withAuthZ';
|
||||
import {
|
||||
buildRoleReadPermission,
|
||||
buildRoleUpdatePermission,
|
||||
RoleCreatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import PermissionEditor from './components/PermissionEditor';
|
||||
import { useCreateEditRolePageActions } from './useCreateEditRolePageActions';
|
||||
import { useNavigationBlocker } from 'hooks/useNavigationBlocker';
|
||||
|
||||
import styles from './CreateEditRolePage.module.scss';
|
||||
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
function CreateEditRolePage(): JSX.Element {
|
||||
function authzCheckFn(
|
||||
_props: object,
|
||||
router: RouterContext,
|
||||
): BrandedPermission[] {
|
||||
const match = router.matchPath<{ roleId: string }>(ROUTES.ROLE_DETAILS);
|
||||
const roleId = match?.roleId ?? 'new';
|
||||
const roleName = router.searchParams.get('name') ?? '';
|
||||
const isCreateMode = roleId === 'new';
|
||||
|
||||
if (isCreateMode) {
|
||||
return [RoleCreatePermission];
|
||||
}
|
||||
if (roleName) {
|
||||
return [
|
||||
buildRoleReadPermission(roleName),
|
||||
buildRoleUpdatePermission(roleName),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function CreateEditRolePageContent(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const urlQuery = useUrlQuery();
|
||||
@@ -47,9 +76,6 @@ function CreateEditRolePage(): JSX.Element {
|
||||
saveError,
|
||||
validationErrors,
|
||||
isCreateMode,
|
||||
hasRequiredPermission,
|
||||
isAuthZLoading,
|
||||
deniedPermission,
|
||||
loadError,
|
||||
} = useCreateEditRolePageActions(roleId, roleName);
|
||||
|
||||
@@ -81,10 +107,6 @@ function CreateEditRolePage(): JSX.Element {
|
||||
roleName,
|
||||
]);
|
||||
|
||||
if (!hasRequiredPermission && !isAuthZLoading) {
|
||||
return <PermissionDeniedFullPage permissionName={deniedPermission} />;
|
||||
}
|
||||
|
||||
if (!isRolesEnabled && !isFeatureGateLoading) {
|
||||
return (
|
||||
<div
|
||||
@@ -127,7 +149,7 @@ function CreateEditRolePage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthZLoading || (isLoading && !isCreateMode) || isFeatureGateLoading) {
|
||||
if ((isLoading && !isCreateMode) || isFeatureGateLoading) {
|
||||
return (
|
||||
<div className={styles.createEditRolePage}>
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
@@ -195,7 +217,12 @@ function CreateEditRolePage(): JSX.Element {
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
<AuthZButton
|
||||
checks={
|
||||
isCreateMode
|
||||
? [RoleCreatePermission]
|
||||
: [buildRoleUpdatePermission(roleName)]
|
||||
}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSaveAndNavigate}
|
||||
@@ -204,7 +231,7 @@ function CreateEditRolePage(): JSX.Element {
|
||||
data-testid="save-button"
|
||||
>
|
||||
{isCreateMode ? 'Create role' : 'Save changes'}
|
||||
</Button>
|
||||
</AuthZButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,4 +317,11 @@ function CreateEditRolePage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateEditRolePage;
|
||||
export default withAuthZPage(CreateEditRolePageContent, {
|
||||
checks: authzCheckFn,
|
||||
fallbackOnLoading: (
|
||||
<div className={styles.createEditRolePage}>
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
setupAuthzAdmin,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderCreatePage(
|
||||
@@ -71,7 +68,9 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -80,7 +79,9 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderCreatePage({ activeLicense: invalidLicense });
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -92,16 +93,19 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
await expect(screen.findByText('Create Role')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows back button when feature disabled', () => {
|
||||
it('shows back button when feature disabled', async () => {
|
||||
renderCreatePage({ activeLicense: invalidLicense });
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('back button is enabled when feature disabled', () => {
|
||||
it('back button is enabled when feature disabled', async () => {
|
||||
renderCreatePage({ activeLicense: invalidLicense });
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).not.toBeDisabled();
|
||||
const cancelButton = await screen.findByTestId('cancel-button');
|
||||
expect(cancelButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +122,9 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -127,7 +133,9 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderEditPage(ROLE_ID, ROLE_NAME, { activeLicense: invalidLicense });
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { mockUseAuthZDenyAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDenyAll,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderCreatePage(): ReturnType<typeof render> {
|
||||
@@ -31,7 +32,7 @@ function renderCreatePage(): ReturnType<typeof render> {
|
||||
describe('CreateRolePage - AuthZ', () => {
|
||||
describe('permission denied', () => {
|
||||
it('shows PermissionDeniedFullPage when create permission denied', async () => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
renderCreatePage();
|
||||
|
||||
@@ -43,19 +44,31 @@ describe('CreateRolePage - AuthZ', () => {
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows skeleton while checking permissions', () => {
|
||||
mockUseAuthZ.mockReturnValue({
|
||||
isLoading: true,
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
server.use(
|
||||
rest.post('*/api/v1/authz/check', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(200),
|
||||
ctx.status(200),
|
||||
ctx.json({ data: [], status: 'success' }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
renderCreatePage();
|
||||
|
||||
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('permission granted', () => {
|
||||
it('renders create page when create permission granted', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
renderCreatePage();
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('role-name-input'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,27 +3,22 @@ import ROUTES from 'constants/routes';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const rolesApiBase = '*/api/v1/roles';
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderCreatePage(): ReturnType<typeof render> {
|
||||
return render(
|
||||
async function renderCreatePage(): Promise<ReturnType<typeof render>> {
|
||||
const result = render(
|
||||
<Switch>
|
||||
<Route path={ROUTES.ROLES_SETTINGS} exact>
|
||||
<div data-testid="roles-list-redirect" />
|
||||
@@ -35,61 +30,63 @@ function renderCreatePage(): ReturnType<typeof render> {
|
||||
undefined,
|
||||
{ initialRoute: '/settings/roles/new' },
|
||||
);
|
||||
await screen.findByTestId('create-edit-role-page');
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('CreateRolePage', () => {
|
||||
describe('initial render', () => {
|
||||
it('renders create role page with testId', () => {
|
||||
renderCreatePage();
|
||||
it('renders create role page with testId', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
expect(screen.getByTestId('create-edit-role-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows breadcrumb with "Create role" as current page', () => {
|
||||
renderCreatePage();
|
||||
it('shows breadcrumb with "Create role" as current page', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const page = screen.getByTestId('create-edit-role-page');
|
||||
const breadcrumbs = within(page).getAllByText('Create role');
|
||||
expect(breadcrumbs.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('renders empty name input', () => {
|
||||
renderCreatePage();
|
||||
it('renders empty name input', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
expect(nameInput).toHaveValue('');
|
||||
});
|
||||
|
||||
it('renders empty description input', () => {
|
||||
renderCreatePage();
|
||||
it('renders empty description input', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
expect(descInput).toHaveValue('');
|
||||
});
|
||||
|
||||
it('name input is enabled in create mode', () => {
|
||||
renderCreatePage();
|
||||
it('name input is enabled in create mode', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
expect(nameInput).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('save button shows "Create role" text', () => {
|
||||
renderCreatePage();
|
||||
it('save button shows "Create role" text', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
expect(saveBtn).toHaveTextContent('Create role');
|
||||
});
|
||||
|
||||
it('save button is disabled when no changes', () => {
|
||||
renderCreatePage();
|
||||
it('save button is disabled when no changes', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not show unsaved indicator initially', () => {
|
||||
renderCreatePage();
|
||||
it('does not show unsaved indicator initially', async () => {
|
||||
await renderCreatePage();
|
||||
|
||||
expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -98,7 +95,7 @@ describe('CreateRolePage', () => {
|
||||
describe('form interactions', () => {
|
||||
it('enables save button when name is entered', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'test-role');
|
||||
@@ -109,7 +106,7 @@ describe('CreateRolePage', () => {
|
||||
|
||||
it('shows unsaved indicator when form modified', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'my-role');
|
||||
@@ -121,7 +118,7 @@ describe('CreateRolePage', () => {
|
||||
|
||||
it('enables save button when description is entered', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Some description');
|
||||
@@ -134,7 +131,7 @@ describe('CreateRolePage', () => {
|
||||
describe('cancel action', () => {
|
||||
it('navigates to roles list on cancel', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const cancelBtn = screen.getByTestId('cancel-button');
|
||||
await user.click(cancelBtn);
|
||||
@@ -163,7 +160,7 @@ describe('CreateRolePage', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'my-custom-role');
|
||||
@@ -200,7 +197,7 @@ describe('CreateRolePage', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Description only');
|
||||
@@ -218,7 +215,7 @@ describe('CreateRolePage', () => {
|
||||
|
||||
it('shows error banner with "Role name is required" when saving with empty name', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Description only');
|
||||
@@ -237,7 +234,7 @@ describe('CreateRolePage', () => {
|
||||
|
||||
it('clears error banner when user starts typing in name field', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Description only');
|
||||
@@ -270,7 +267,7 @@ describe('CreateRolePage', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'duplicate-role');
|
||||
@@ -291,7 +288,7 @@ describe('CreateRolePage', () => {
|
||||
describe('validation errors', () => {
|
||||
it('shows validation error when Only Selected has no items', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
await renderCreatePage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
mockUseAuthZDenyAll,
|
||||
mockUseAuthZGrantByPrefix,
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDenyAll,
|
||||
setupAuthzDeny,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { buildRoleUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const EDIT_ROLE_ID = 'test-role-123';
|
||||
const EDIT_ROLE_NAME = 'test-role';
|
||||
const rolesApiBase = '*/api/v1/roles';
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
id: EDIT_ROLE_ID,
|
||||
name: EDIT_ROLE_NAME,
|
||||
description: 'Test role description',
|
||||
type: 'custom',
|
||||
transactionGroups: [],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderEditPage(): ReturnType<typeof render> {
|
||||
@@ -37,7 +58,7 @@ function renderEditPage(): ReturnType<typeof render> {
|
||||
describe('EditRolePage - AuthZ', () => {
|
||||
describe('permission denied', () => {
|
||||
it('shows PermissionDeniedFullPage when read permission denied', async () => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
renderEditPage();
|
||||
|
||||
@@ -47,7 +68,7 @@ describe('EditRolePage - AuthZ', () => {
|
||||
});
|
||||
|
||||
it('shows PermissionDeniedFullPage when update permission denied but read granted', async () => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantByPrefix('read'));
|
||||
server.use(setupAuthzDeny(buildRoleUpdatePermission(EDIT_ROLE_NAME)));
|
||||
|
||||
renderEditPage();
|
||||
|
||||
@@ -55,36 +76,35 @@ describe('EditRolePage - AuthZ', () => {
|
||||
screen.findByText(/You are not authorized/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks both read and update permissions for edit mode', () => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
|
||||
|
||||
renderEditPage();
|
||||
|
||||
expect(mockUseAuthZ).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('read'),
|
||||
expect.stringContaining('update'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows skeleton while checking permissions', () => {
|
||||
mockUseAuthZ.mockReturnValue({
|
||||
isLoading: true,
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
it('shows skeleton while checking permissions', async () => {
|
||||
server.use(
|
||||
rest.post('*/api/v1/authz/check', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(200),
|
||||
ctx.status(200),
|
||||
ctx.json({ data: [], status: 'success' }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
renderEditPage();
|
||||
|
||||
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('permission granted', () => {
|
||||
it('renders edit page when both read and update permissions granted', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
renderEditPage();
|
||||
|
||||
await expect(
|
||||
screen.findByText(`Role - ${EDIT_ROLE_NAME}`),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,10 @@ import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const CUSTOM_ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
|
||||
const rolesApiBase = '*/api/v1/roles';
|
||||
|
||||
@@ -32,8 +28,8 @@ const roleWithTransactionGroups = {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(
|
||||
setupAuthzAdmin(),
|
||||
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(roleWithTransactionGroups)),
|
||||
),
|
||||
@@ -41,7 +37,6 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
@@ -116,9 +111,9 @@ describe('EditRolePage', () => {
|
||||
|
||||
renderEditPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
});
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to view page when cancel clicked in error state', async () => {
|
||||
@@ -132,10 +127,8 @@ describe('EditRolePage', () => {
|
||||
|
||||
renderEditPage();
|
||||
|
||||
await waitFor(async () => {
|
||||
const cancelButton = await screen.findByTestId('cancel-button');
|
||||
await user.click(cancelButton);
|
||||
});
|
||||
const cancelButton = await screen.findByTestId('cancel-button');
|
||||
await user.click(cancelButton);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('role-details-redirect'),
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderPage(): ReturnType<typeof render> {
|
||||
@@ -37,13 +34,13 @@ function renderPage(): ReturnType<typeof render> {
|
||||
|
||||
async function switchToJsonMode(): Promise<void> {
|
||||
const user = userEvent.setup();
|
||||
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
|
||||
const jsonRadio = await screen.findByTestId('permission-editor-mode-json');
|
||||
await user.click(jsonRadio);
|
||||
}
|
||||
|
||||
async function switchToInteractiveMode(): Promise<void> {
|
||||
const user = userEvent.setup();
|
||||
const interactiveRadio = screen.getByTestId(
|
||||
const interactiveRadio = await screen.findByTestId(
|
||||
'permission-editor-mode-interactive',
|
||||
);
|
||||
await user.click(interactiveRadio);
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
async function expandAllCards(): Promise<void> {
|
||||
const user = userEvent.setup();
|
||||
const expandButton = screen.getByTestId('expand-all-button');
|
||||
const expandButton = await screen.findByTestId('expand-all-button');
|
||||
await user.click(expandButton);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
function renderPage(): ReturnType<typeof render> {
|
||||
return render(
|
||||
async function renderPage(): Promise<ReturnType<typeof render>> {
|
||||
const result = render(
|
||||
<TooltipProvider>
|
||||
<Switch>
|
||||
<Route path={ROUTES.ROLES_SETTINGS} exact>
|
||||
@@ -39,18 +36,20 @@ function renderPage(): ReturnType<typeof render> {
|
||||
undefined,
|
||||
{ initialRoute: '/settings/roles/new' },
|
||||
);
|
||||
await screen.findByTestId('permission-editor');
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('PermissionEditor', () => {
|
||||
describe('mode toggle', () => {
|
||||
it('renders permission editor with testId', () => {
|
||||
renderPage();
|
||||
it('renders permission editor with testId', async () => {
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getByTestId('permission-editor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults to interactive mode', () => {
|
||||
renderPage();
|
||||
it('defaults to interactive mode', async () => {
|
||||
await renderPage();
|
||||
|
||||
const interactiveRadio = screen.getByTestId(
|
||||
'permission-editor-mode-interactive',
|
||||
@@ -60,7 +59,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('switches to JSON mode when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
|
||||
await user.click(jsonRadio);
|
||||
@@ -71,7 +70,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('switches back to interactive mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
|
||||
await user.click(jsonRadio);
|
||||
@@ -87,8 +86,8 @@ describe('PermissionEditor', () => {
|
||||
});
|
||||
|
||||
describe('resource cards', () => {
|
||||
it('renders all resource cards', () => {
|
||||
renderPage();
|
||||
it('renders all resource cards', async () => {
|
||||
await renderPage();
|
||||
|
||||
expect(
|
||||
screen.getByTestId('resource-card-factor-api-key'),
|
||||
@@ -99,8 +98,8 @@ describe('PermissionEditor', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('resource cards are collapsed by default', () => {
|
||||
renderPage();
|
||||
it('resource cards are collapsed by default', async () => {
|
||||
await renderPage();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
@@ -112,7 +111,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('expands resource card when header clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
@@ -126,7 +125,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('collapses expanded resource card when header clicked again', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
@@ -140,7 +139,7 @@ describe('PermissionEditor', () => {
|
||||
});
|
||||
|
||||
it('shows granted count in resource card header', async () => {
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
await expect(
|
||||
@@ -151,7 +150,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
describe('action toggles', () => {
|
||||
it('renders action toggles for each available action', async () => {
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -170,7 +169,7 @@ describe('PermissionEditor', () => {
|
||||
});
|
||||
|
||||
it('defaults all actions to None scope', async () => {
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -188,7 +187,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('changes scope to All when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -209,7 +208,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('updates granted count when scope changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -228,7 +227,7 @@ describe('PermissionEditor', () => {
|
||||
describe('Only Selected scope', () => {
|
||||
it('shows item input selector when Only Selected is chosen', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -245,7 +244,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('adds item when typed and Enter pressed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -263,7 +262,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('adds item when Add button clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -284,7 +283,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('adds multiple items separated by comma', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -304,7 +303,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('adds multiple items separated by space', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -324,7 +323,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('does not add duplicate items', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -344,7 +343,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('removes item when X clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -367,7 +366,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('shows Add button disabled when input is empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -385,7 +384,7 @@ describe('PermissionEditor', () => {
|
||||
describe('scope change confirmation dialog', () => {
|
||||
it('shows confirm dialog when leaving Only Selected with items', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -407,7 +406,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('clears items when confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -434,7 +433,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('keeps items when cancelled', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -461,7 +460,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('does not show dialog when leaving Only Selected with no items', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -480,7 +479,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
describe('verbs without Only Selected option', () => {
|
||||
it('does not show Only Selected for list verb', async () => {
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -501,8 +500,8 @@ describe('PermissionEditor', () => {
|
||||
});
|
||||
|
||||
describe('collapse/expand all resources', () => {
|
||||
it('shows expand/collapse toggle group', () => {
|
||||
renderPage();
|
||||
it('shows expand/collapse toggle group', async () => {
|
||||
await renderPage();
|
||||
|
||||
expect(screen.getByTestId('toggle-all-group')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('expand-all-button')).toBeInTheDocument();
|
||||
@@ -510,7 +509,7 @@ describe('PermissionEditor', () => {
|
||||
});
|
||||
|
||||
it('expands all cards when expand button clicked', async () => {
|
||||
renderPage();
|
||||
await renderPage();
|
||||
await expandAllCards();
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
@@ -524,7 +523,7 @@ describe('PermissionEditor', () => {
|
||||
describe('resource card error states', () => {
|
||||
it('shows error border on collapsed card with validation error', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
@@ -554,7 +553,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('hides error border when card is expanded', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
@@ -591,7 +590,7 @@ describe('PermissionEditor', () => {
|
||||
|
||||
it('clears validation error when permission is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
useRolePermissions,
|
||||
useUpdateRolePermissions,
|
||||
} from '../hooks/useRolePermissions';
|
||||
import { useRoleAuthZ } from '../hooks/useRoleAuthZ';
|
||||
import {
|
||||
useRoleUnsavedChanges,
|
||||
type RoleFormData,
|
||||
@@ -43,9 +42,6 @@ interface UseCreateEditRolePageCallbacksResult {
|
||||
saveError: APIError | null;
|
||||
clearSaveError: () => void;
|
||||
validationErrors: Set<string>;
|
||||
hasRequiredPermission: boolean;
|
||||
isAuthZLoading: boolean;
|
||||
deniedPermission: string;
|
||||
}
|
||||
|
||||
export function useCreateEditRolePageActions(
|
||||
@@ -55,23 +51,6 @@ export function useCreateEditRolePageActions(
|
||||
const history = useHistory();
|
||||
const isCreateMode = roleId === 'new';
|
||||
|
||||
const {
|
||||
hasCreatePermission,
|
||||
hasReadPermission,
|
||||
hasUpdatePermission,
|
||||
isAuthZLoading,
|
||||
} = useRoleAuthZ(roleName);
|
||||
|
||||
const deniedPermission = useMemo(() => {
|
||||
if (isCreateMode) {
|
||||
return 'role:create';
|
||||
}
|
||||
if (roleName) {
|
||||
return `role:${roleName}:update`;
|
||||
}
|
||||
return `role:<missing-rule-name>:update`;
|
||||
}, [isCreateMode, roleName]);
|
||||
|
||||
const [formData, setFormData] = useState<RoleFormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -261,10 +240,5 @@ export function useCreateEditRolePageActions(
|
||||
saveError,
|
||||
clearSaveError,
|
||||
validationErrors,
|
||||
hasRequiredPermission: isCreateMode
|
||||
? hasCreatePermission
|
||||
: hasReadPermission && hasUpdatePermission,
|
||||
isAuthZLoading,
|
||||
deniedPermission,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import APIError from 'types/api/error';
|
||||
interface UseDeleteRoleModalProps {
|
||||
roleId?: string | null;
|
||||
isManaged: boolean;
|
||||
hasDeletePermission: boolean;
|
||||
onDeleteSuccess?: () => void;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,7 @@ interface UseDeleteRoleModalResult {
|
||||
export function useDeleteRoleModal(
|
||||
props: UseDeleteRoleModalProps,
|
||||
): UseDeleteRoleModalResult {
|
||||
const { roleId, isManaged, hasDeletePermission, onDeleteSuccess } = props;
|
||||
const { roleId, isManaged, onDeleteSuccess } = props;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteTargetRoleId, setDeleteTargetRoleId] = useState<string | null>(
|
||||
@@ -79,10 +78,8 @@ export function useDeleteRoleModal(
|
||||
|
||||
const isDeleteModalOpen = deleteTargetRoleId !== null;
|
||||
|
||||
const isDeleteDisabled = isManaged || !hasDeletePermission;
|
||||
const deleteDisabledReason = isManaged
|
||||
? 'Managed roles cannot be deleted'
|
||||
: 'You do not have permission to delete this role';
|
||||
const isDeleteDisabled = isManaged;
|
||||
const deleteDisabledReason = 'Managed roles cannot be deleted';
|
||||
|
||||
return {
|
||||
isDeleteModalOpen,
|
||||
|
||||
@@ -5,12 +5,11 @@ import { Pagination, Skeleton } from 'antd';
|
||||
import { useListRoles } from 'api/generated/services/role';
|
||||
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { withAuthZPage } from 'lib/authz/components/withAuthZ/withAuthZPage';
|
||||
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { RoleType } from 'types/roles';
|
||||
@@ -24,23 +23,14 @@ type DisplayItem =
|
||||
| { type: 'section'; label: string; count?: number }
|
||||
| { type: 'role'; role: AuthtypesRoleDTO };
|
||||
|
||||
interface RolesListingTableProps {
|
||||
interface RolesListContentProps {
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
function RolesListingTable({
|
||||
searchQuery,
|
||||
}: RolesListingTableProps): JSX.Element {
|
||||
function RolesListContent({ searchQuery }: RolesListContentProps): JSX.Element {
|
||||
const { isRolesEnabled } = useRolesFeatureGate();
|
||||
|
||||
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
|
||||
RoleListPermission,
|
||||
]);
|
||||
const hasListPermission = listPerms?.[RoleListPermission]?.isGranted ?? false;
|
||||
|
||||
const { data, isLoading, isError, error } = useListRoles({
|
||||
query: { enabled: hasListPermission },
|
||||
});
|
||||
const { data, isLoading, isError, error } = useListRoles();
|
||||
const { formatTimezoneAdjustedTimestampOptional } = useTimezone();
|
||||
const history = useHistory();
|
||||
const urlQuery = useUrlQuery();
|
||||
@@ -155,11 +145,7 @@ function RolesListingTable({
|
||||
</>
|
||||
);
|
||||
|
||||
if (!hasListPermission && listPerms !== null) {
|
||||
return <PermissionDeniedFullPage permissionName="role:list" />;
|
||||
}
|
||||
|
||||
if (isAuthZLoading || isLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.rolesListingTable}>
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
@@ -281,4 +267,11 @@ function RolesListingTable({
|
||||
);
|
||||
}
|
||||
|
||||
export default RolesListingTable;
|
||||
export default withAuthZPage<RolesListContentProps>(RolesListContent, {
|
||||
checks: [RoleListPermission],
|
||||
fallbackOnLoading: (
|
||||
<div className={styles.rolesListingTable}>
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RoleCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import {
|
||||
RoleCreatePermission,
|
||||
RoleListPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
|
||||
import RolesListingTable from './RolesComponents/RolesListingTable';
|
||||
@@ -37,24 +40,25 @@ function RolesSettings(): JSX.Element {
|
||||
</div>
|
||||
<div className={styles.rolesSettingsContent}>
|
||||
<div className={styles.rolesSettingsToolbar}>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<AuthZTooltip checks={[RoleListPermission]}>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</AuthZTooltip>
|
||||
{isRolesEnabled && (
|
||||
<AuthZTooltip checks={[RoleCreatePermission]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.roleSettingsToolbarButton}
|
||||
onClick={(): void => history.push(ROUTES.ROLE_CREATE)}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Custom role
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
<AuthZButton
|
||||
checks={[RoleCreatePermission]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.roleSettingsToolbarButton}
|
||||
onClick={(): void => history.push(ROUTES.ROLE_CREATE)}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Custom role
|
||||
</AuthZButton>
|
||||
)}
|
||||
</div>
|
||||
<RolesListingTable searchQuery={searchQuery} />
|
||||
|
||||
@@ -10,11 +10,17 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import { useGetRole } from 'api/generated/services/role';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import { useDeleteRoleModal } from 'container/RolesSettings/DeleteRoleModal/useDeleteRoleModal';
|
||||
import { useRoleAuthZ } from 'container/RolesSettings/hooks/useRoleAuthZ';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { transformApiToRolePermissions } from 'container/RolesSettings/hooks/useRolePermissions';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import { withAuthZPage } from 'lib/authz/components/withAuthZ/withAuthZPage';
|
||||
import { RouterContext } from 'lib/authz/components/withAuthZ/withAuthZ';
|
||||
import {
|
||||
buildRoleDeletePermission,
|
||||
buildRoleReadPermission,
|
||||
buildRoleUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
import { RoleType } from 'types/roles';
|
||||
@@ -27,7 +33,7 @@ import { useViewRolePageActions } from './useViewRolePageActions';
|
||||
|
||||
import styles from './ViewRolePage.module.scss';
|
||||
|
||||
function ViewRolePage(): JSX.Element {
|
||||
function ViewRolePageContent(): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestampOptional } = useTimezone();
|
||||
const { isRolesEnabled, isLoading: isFeatureGateLoading } =
|
||||
useRolesFeatureGate();
|
||||
@@ -45,26 +51,15 @@ function ViewRolePage(): JSX.Element {
|
||||
handleTabChange,
|
||||
} = useViewRolePageActions();
|
||||
|
||||
const {
|
||||
hasReadPermission,
|
||||
readRolePermission,
|
||||
hasUpdatePermission,
|
||||
updateRolePermission,
|
||||
hasDeletePermission,
|
||||
isAuthZLoading,
|
||||
} = useRoleAuthZ(roleName);
|
||||
|
||||
const { data, isLoading, error } = useGetRole(
|
||||
{ id: roleId ?? '' },
|
||||
{ query: { enabled: !!roleId && hasReadPermission } },
|
||||
{ query: { enabled: !!roleId } },
|
||||
);
|
||||
const role = data?.data;
|
||||
const isManaged = role?.type === RoleType.MANAGED;
|
||||
|
||||
const {
|
||||
isDeleteModalOpen,
|
||||
isDeleteDisabled,
|
||||
deleteDisabledReason,
|
||||
deleteError,
|
||||
handleOpenDeleteModal,
|
||||
handleCloseDeleteModal,
|
||||
@@ -72,7 +67,6 @@ function ViewRolePage(): JSX.Element {
|
||||
} = useDeleteRoleModal({
|
||||
roleId,
|
||||
isManaged: isManaged ?? false,
|
||||
hasDeletePermission,
|
||||
onDeleteSuccess: handleCancel,
|
||||
});
|
||||
|
||||
@@ -144,12 +138,6 @@ function ViewRolePage(): JSX.Element {
|
||||
],
|
||||
);
|
||||
|
||||
if (!hasReadPermission && !isAuthZLoading) {
|
||||
return (
|
||||
<PermissionDeniedFullPage permissionName={readRolePermission.object} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isRolesEnabled && !isFeatureGateLoading) {
|
||||
return (
|
||||
<div className={styles.viewRolePage} data-testid="view-role-page">
|
||||
@@ -187,7 +175,7 @@ function ViewRolePage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthZLoading || isLoading || isFeatureGateLoading) {
|
||||
if (isLoading || isFeatureGateLoading) {
|
||||
return (
|
||||
<div className={styles.viewRolePage}>
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
@@ -244,47 +232,55 @@ function ViewRolePage(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className={styles.viewRolePageActions}>
|
||||
<TooltipSimple
|
||||
title={isDeleteDisabled ? deleteDisabledReason : 'Open delete modal'}
|
||||
>
|
||||
<Button
|
||||
{isManaged ? (
|
||||
<TooltipSimple title="Managed roles cannot be deleted">
|
||||
<Button
|
||||
variant="link"
|
||||
color="destructive"
|
||||
disabled
|
||||
data-testid="delete-button"
|
||||
className={styles.deleteButton}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
<AuthZButton
|
||||
checks={[buildRoleDeletePermission(roleName)]}
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={handleOpenDeleteModal}
|
||||
disabled={isDeleteDisabled}
|
||||
data-testid="delete-button"
|
||||
className={styles.deleteButton}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</AuthZButton>
|
||||
)}
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<TooltipSimple
|
||||
title={
|
||||
isManaged
|
||||
? 'Managed roles cannot be updated'
|
||||
: hasUpdatePermission
|
||||
? 'Open update page'
|
||||
: `You are not authorized to perform ${updateRolePermission.object}`
|
||||
}
|
||||
>
|
||||
<Button
|
||||
{isManaged ? (
|
||||
<TooltipSimple title="Managed roles cannot be updated">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled
|
||||
data-testid="save-button"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
<AuthZButton
|
||||
checks={[buildRoleUpdatePermission(roleName)]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
data-testid="save-button"
|
||||
disabled={isManaged || !hasUpdatePermission}
|
||||
onClick={handleRedirectToUpdate}
|
||||
style={
|
||||
isManaged || !hasUpdatePermission
|
||||
? { pointerEvents: 'auto' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</AuthZButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -336,4 +332,14 @@ function ViewRolePage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewRolePage;
|
||||
export default withAuthZPage(ViewRolePageContent, {
|
||||
checks: (_props: object, router: RouterContext) => {
|
||||
const roleName = router.searchParams.get('name') ?? '';
|
||||
return roleName ? [buildRoleReadPermission(roleName)] : [];
|
||||
},
|
||||
fallbackOnLoading: (
|
||||
<div className={styles.viewRolePage}>
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('ViewRolePage - Actions', () => {
|
||||
{ initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME) },
|
||||
);
|
||||
|
||||
const cancelBtn = screen.getByTestId('cancel-button');
|
||||
const cancelBtn = await screen.findByTestId('cancel-button');
|
||||
await user.click(cancelBtn);
|
||||
|
||||
await expect(
|
||||
@@ -61,7 +61,10 @@ describe('ViewRolePage - Actions', () => {
|
||||
{ initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME) },
|
||||
);
|
||||
|
||||
const updateBtn = screen.getByTestId('save-button');
|
||||
const updateBtn = await screen.findByTestId('save-button');
|
||||
await waitFor(() => {
|
||||
expect(updateBtn).not.toBeDisabled();
|
||||
});
|
||||
await user.click(updateBtn);
|
||||
|
||||
await expect(
|
||||
@@ -76,7 +79,10 @@ describe('ViewRolePage - Actions', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
const deleteBtn = screen.getByTestId('delete-button');
|
||||
const deleteBtn = await screen.findByTestId('delete-button');
|
||||
await waitFor(() => {
|
||||
expect(deleteBtn).not.toBeDisabled();
|
||||
});
|
||||
await user.click(deleteBtn);
|
||||
|
||||
await expect(
|
||||
@@ -105,7 +111,11 @@ describe('ViewRolePage - Actions', () => {
|
||||
{ initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME) },
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('delete-button'));
|
||||
const deleteBtn = await screen.findByTestId('delete-button');
|
||||
await waitFor(() => {
|
||||
expect(deleteBtn).not.toBeDisabled();
|
||||
});
|
||||
await user.click(deleteBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByText(/Are you sure you want to delete the role/),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
customRoleResponse,
|
||||
managedRoleResponse,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
mockUseAuthZDenyAll,
|
||||
mockUseAuthZGrantAll,
|
||||
mockUseAuthZGrantByPrefix,
|
||||
AUTHZ_CHECK_URL,
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDenyAll,
|
||||
setupAuthzGrantByPrefix,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
@@ -25,25 +27,15 @@ import {
|
||||
mockPermissionsData,
|
||||
} from './testUtils';
|
||||
|
||||
const mockUseAuthZGrantReadDeleteDenied = mockUseAuthZGrantByPrefix(
|
||||
'read',
|
||||
'update',
|
||||
);
|
||||
const mockUseAuthZGrantReadUpdateDenied = mockUseAuthZGrantByPrefix(
|
||||
'read',
|
||||
'delete',
|
||||
);
|
||||
|
||||
describe('ViewRolePage - AuthZ', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('permission denied', () => {
|
||||
it('shows permission denied page when read permission denied', async () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZDenyAll);
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: undefined,
|
||||
@@ -63,10 +55,8 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
});
|
||||
|
||||
describe('update button visibility', () => {
|
||||
it('enables Update button when update permission granted', () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
it('enables Update button when update permission granted', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -92,13 +82,13 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('save-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables Update button when update permission denied', () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantReadUpdateDenied);
|
||||
it('disables Update button when update permission denied', async () => {
|
||||
server.use(setupAuthzGrantByPrefix('read', 'delete'));
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -124,13 +114,13 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables Update button when role is managed', () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
it('disables Update button when role is managed', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: managedRoleResponse,
|
||||
@@ -160,15 +150,15 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows managed role tooltip when update button hovered on managed role', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: managedRoleResponse,
|
||||
@@ -198,7 +188,7 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const updateButton = screen.getByTestId('save-button');
|
||||
const updateButton = await screen.findByTestId('save-button');
|
||||
await user.hover(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -208,12 +198,8 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows authorization tooltip when update permission denied', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantReadUpdateDenied);
|
||||
it('disables and shows denial attribute when update permission denied', async () => {
|
||||
server.use(setupAuthzGrantByPrefix('read', 'delete'));
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -239,22 +225,17 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const updateButton = screen.getByTestId('save-button');
|
||||
await user.hover(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||
/You are not authorized to perform/,
|
||||
);
|
||||
const updateButton = screen.getByTestId('save-button');
|
||||
expect(updateButton).toBeDisabled();
|
||||
expect(updateButton).toHaveAttribute('data-denied-permissions');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete button visibility', () => {
|
||||
it('disables Delete button when delete permission denied', () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantReadDeleteDenied);
|
||||
it('disables Delete button when delete permission denied', async () => {
|
||||
server.use(setupAuthzGrantByPrefix('read', 'update'));
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -280,88 +261,81 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('delete-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Delete button when delete permission granted', () => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof roleApi.useGetRole>);
|
||||
|
||||
jest.spyOn(useRolePermissionsModule, 'useRolePermissions').mockReturnValue({
|
||||
data: mockPermissionsData,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useRolePermissionsModule.useRolePermissions>);
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
</TooltipProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('delete-button')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows permission denied tooltip when delete permission denied', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantReadDeleteDenied);
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof roleApi.useGetRole>);
|
||||
|
||||
jest.spyOn(useRolePermissionsModule, 'useRolePermissions').mockReturnValue({
|
||||
data: mockPermissionsData,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useRolePermissionsModule.useRolePermissions>);
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
</TooltipProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
},
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByTestId('delete-button');
|
||||
await user.hover(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||
'You do not have permission to delete this role',
|
||||
);
|
||||
expect(screen.getByTestId('delete-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('enables Delete button when delete permission granted', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof roleApi.useGetRole>);
|
||||
|
||||
jest.spyOn(useRolePermissionsModule, 'useRolePermissions').mockReturnValue({
|
||||
data: mockPermissionsData,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useRolePermissionsModule.useRolePermissions>);
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
</TooltipProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('delete-button')).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables and shows denial attribute when delete permission denied', async () => {
|
||||
server.use(setupAuthzGrantByPrefix('read', 'update'));
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof roleApi.useGetRole>);
|
||||
|
||||
jest.spyOn(useRolePermissionsModule, 'useRolePermissions').mockReturnValue({
|
||||
data: mockPermissionsData,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useRolePermissionsModule.useRolePermissions>);
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
</TooltipProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteButton = screen.getByTestId('delete-button');
|
||||
expect(deleteButton).toBeDisabled();
|
||||
expect(deleteButton).toHaveAttribute('data-denied-permissions');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows managed role tooltip when role is managed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: managedRoleResponse,
|
||||
@@ -391,7 +365,7 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByTestId('delete-button');
|
||||
const deleteButton = await screen.findByTestId('delete-button');
|
||||
await user.hover(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -404,15 +378,9 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows skeleton while checking permissions', () => {
|
||||
jest.spyOn(useAuthZModule, 'useAuthZ').mockReturnValue({
|
||||
isLoading: true,
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => res(ctx.delay('infinite'))),
|
||||
);
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: undefined,
|
||||
|
||||
@@ -38,28 +38,32 @@ describe('ViewRolePage - Custom Role', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Update button for custom roles', () => {
|
||||
it('shows Update button for custom roles', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeInTheDocument();
|
||||
await expect(screen.findByTestId('save-button')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Cancel button', () => {
|
||||
it('shows Cancel button', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Delete button', () => {
|
||||
it('shows Delete button', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('delete-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('delete-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders created/updated timestamps labels', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
@@ -16,13 +16,12 @@ import {
|
||||
|
||||
describe('ViewRolePage - Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows fallback for missing description', async () => {
|
||||
@@ -53,7 +52,7 @@ describe('ViewRolePage - Edge Cases', () => {
|
||||
await expect(screen.findByText('Description')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows fallback for invalid timestamps', () => {
|
||||
it('shows fallback for invalid timestamps', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: {
|
||||
status: 'success',
|
||||
@@ -79,11 +78,14 @@ describe('ViewRolePage - Edge Cases', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('view-role-page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
const dashes = screen.getAllByText('—');
|
||||
expect(dashes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('shows fallback for undefined timestamps', () => {
|
||||
it('shows fallback for undefined timestamps', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: {
|
||||
status: 'success',
|
||||
@@ -109,6 +111,9 @@ describe('ViewRolePage - Edge Cases', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('view-role-page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
const dashes = screen.getAllByText('—');
|
||||
expect(dashes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
@@ -18,16 +18,15 @@ import {
|
||||
|
||||
describe('ViewRolePage - Error State', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('displays error component when API has error but role data exists', () => {
|
||||
it('displays error component when API has error but role data exists', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
@@ -46,7 +45,9 @@ describe('ViewRolePage - Error State', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
expect(document.querySelector('.error-in-place')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.error-in-place')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays error state with title when API fails without role data', async () => {
|
||||
@@ -64,10 +65,12 @@ describe('ViewRolePage - Error State', () => {
|
||||
await expect(
|
||||
screen.findByText('Failed to load role'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(document.querySelector('.error-in-place')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.error-in-place')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows back button on error state', () => {
|
||||
it('shows back button on error state', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
@@ -79,7 +82,9 @@ describe('ViewRolePage - Error State', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to roles list when back button clicked on error state', async () => {
|
||||
@@ -105,7 +110,7 @@ describe('ViewRolePage - Error State', () => {
|
||||
{ initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME) },
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByTestId('cancel-button');
|
||||
const cancelButton = await screen.findByTestId('cancel-button');
|
||||
await user.click(cancelButton);
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { defaultFeatureFlags, render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
setupAuthzAdmin,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
@@ -17,9 +17,7 @@ import {
|
||||
|
||||
describe('ViewRolePage - Feature Gate', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: undefined,
|
||||
@@ -31,6 +29,7 @@ describe('ViewRolePage - Feature Gate', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('feature disabled', () => {
|
||||
@@ -46,7 +45,9 @@ describe('ViewRolePage - Feature Gate', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -58,28 +59,34 @@ describe('ViewRolePage - Feature Gate', () => {
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows back button when feature disabled', () => {
|
||||
it('shows back button when feature disabled', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('back button is enabled when feature disabled', () => {
|
||||
it('back button is enabled when feature disabled', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).not.toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-button')).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
@@ -13,13 +13,12 @@ import {
|
||||
|
||||
describe('ViewRolePage - Loading State', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows skeleton while fetching role', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('ViewRolePage - Managed Role', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('disables Delete button for managed roles', () => {
|
||||
it('disables Delete button for managed roles', async () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
@@ -30,10 +30,12 @@ describe('ViewRolePage - Managed Role', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('delete-button')).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('delete-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables Update button for managed roles', () => {
|
||||
it('disables Update button for managed roles', async () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
@@ -44,10 +46,12 @@ describe('ViewRolePage - Managed Role', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('save-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('still shows Cancel button for managed roles', () => {
|
||||
it('still shows Cancel button for managed roles', async () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ViewRolePage />
|
||||
@@ -58,6 +62,8 @@ describe('ViewRolePage - Managed Role', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('cancel-button'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, within } from 'tests/test-utils';
|
||||
|
||||
@@ -17,8 +17,15 @@ import {
|
||||
mockPermissionsData,
|
||||
} from './testUtils';
|
||||
|
||||
async function waitForPageReady(): Promise<void> {
|
||||
await expect(
|
||||
screen.findByTestId('view-role-page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
}
|
||||
|
||||
async function expandAllCards(): Promise<void> {
|
||||
const user = userEvent.setup();
|
||||
await waitForPageReady();
|
||||
const expandButton = screen.getByTestId('expand-all-button');
|
||||
await user.click(expandButton);
|
||||
}
|
||||
@@ -30,6 +37,7 @@ describe('ViewRolePage - Permission Overview', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders Transaction Groups section label', async () => {
|
||||
@@ -42,19 +50,21 @@ describe('ViewRolePage - Permission Overview', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders permission overview container', () => {
|
||||
it('renders permission overview container', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('permission-overview')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows resource permission cards', () => {
|
||||
it('shows resource permission cards', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(
|
||||
screen.getByTestId('resource-section-factor-api-key'),
|
||||
).toBeInTheDocument();
|
||||
@@ -64,11 +74,12 @@ describe('ViewRolePage - Permission Overview', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays granted count for each resource', () => {
|
||||
it('displays granted count for each resource', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(
|
||||
screen.getByTestId('granted-count-factor-api-key'),
|
||||
).toBeInTheDocument();
|
||||
@@ -77,16 +88,15 @@ describe('ViewRolePage - Permission Overview', () => {
|
||||
|
||||
describe('ViewRolePage - Permission Overview Loading State', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows skeleton when permissions are loading', () => {
|
||||
it('shows skeleton when permissions are loading', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
@@ -105,22 +115,22 @@ describe('ViewRolePage - Permission Overview Loading State', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('permission-overview-loading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Permission Overview Error State', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows error when permissions fail to load', () => {
|
||||
it('shows error when permissions fail to load', async () => {
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
isLoading: false,
|
||||
@@ -139,19 +149,19 @@ describe('ViewRolePage - Permission Overview Error State', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('permission-overview-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Scope: ALL permissions', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows "All" badge for actions with ALL scope', async () => {
|
||||
@@ -182,7 +192,7 @@ describe('ViewRolePage - Scope: ALL permissions', () => {
|
||||
expect(screen.getByTestId('scope-badge-create')).toHaveTextContent('All');
|
||||
});
|
||||
|
||||
it('shows full granted count when all actions are ALL', () => {
|
||||
it('shows full granted count when all actions are ALL', async () => {
|
||||
mockHooksWithPermissions({
|
||||
...mockPermissionsData,
|
||||
resources: [
|
||||
@@ -205,6 +215,7 @@ describe('ViewRolePage - Scope: ALL permissions', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('granted-count-role')).toHaveTextContent(
|
||||
'3 / 3 granted',
|
||||
);
|
||||
@@ -212,8 +223,13 @@ describe('ViewRolePage - Scope: ALL permissions', () => {
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Scope: NONE permissions', () => {
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows "None" badge for actions with NONE scope', async () => {
|
||||
@@ -244,7 +260,7 @@ describe('ViewRolePage - Scope: NONE permissions', () => {
|
||||
expect(screen.getByTestId('scope-badge-delete')).toHaveTextContent('None');
|
||||
});
|
||||
|
||||
it('shows zero granted count when all actions are NONE', () => {
|
||||
it('shows zero granted count when all actions are NONE', async () => {
|
||||
mockHooksWithPermissions({
|
||||
...mockPermissionsData,
|
||||
resources: [
|
||||
@@ -268,6 +284,7 @@ describe('ViewRolePage - Scope: NONE permissions', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('granted-count-factor-api-key')).toHaveTextContent(
|
||||
'0 / 4 granted',
|
||||
);
|
||||
@@ -275,8 +292,13 @@ describe('ViewRolePage - Scope: NONE permissions', () => {
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Scope: ONLY_SELECTED permissions', () => {
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('shows "Only selected" badge with count', async () => {
|
||||
@@ -340,7 +362,7 @@ describe('ViewRolePage - Scope: ONLY_SELECTED permissions', () => {
|
||||
await expect(screen.findByText('key-def-456')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('counts ONLY_SELECTED as granted in count', () => {
|
||||
it('counts ONLY_SELECTED as granted in count', async () => {
|
||||
mockHooksWithPermissions({
|
||||
...mockPermissionsData,
|
||||
resources: [
|
||||
@@ -362,6 +384,7 @@ describe('ViewRolePage - Scope: ONLY_SELECTED permissions', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('granted-count-serviceaccount')).toHaveTextContent(
|
||||
'1 / 2 granted',
|
||||
);
|
||||
@@ -408,8 +431,13 @@ describe('ViewRolePage - Scope: ONLY_SELECTED permissions', () => {
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Mixed permission scopes', () => {
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders all three scope types in single resource card', async () => {
|
||||
@@ -458,7 +486,7 @@ describe('ViewRolePage - Mixed permission scopes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders multiple resources with different scope combinations', () => {
|
||||
it('renders multiple resources with different scope combinations', async () => {
|
||||
mockHooksWithPermissions({
|
||||
...mockPermissionsData,
|
||||
resources: [
|
||||
@@ -502,6 +530,7 @@ describe('ViewRolePage - Mixed permission scopes', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('granted-count-factor-api-key')).toHaveTextContent(
|
||||
'2 / 2 granted',
|
||||
);
|
||||
@@ -515,8 +544,13 @@ describe('ViewRolePage - Mixed permission scopes', () => {
|
||||
});
|
||||
|
||||
describe('ViewRolePage - Unknown resources', () => {
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders unknown resource with fallback label', async () => {
|
||||
@@ -540,6 +574,7 @@ describe('ViewRolePage - Unknown resources', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(
|
||||
screen.getByTestId('resource-section-future-resource'),
|
||||
).toBeInTheDocument();
|
||||
@@ -576,7 +611,7 @@ describe('ViewRolePage - Unknown resources', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles resource with empty actions', () => {
|
||||
it('handles resource with empty actions', async () => {
|
||||
mockHooksWithPermissions({
|
||||
...mockPermissionsData,
|
||||
resources: [
|
||||
@@ -595,6 +630,7 @@ describe('ViewRolePage - Unknown resources', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(
|
||||
screen.getByTestId('resource-section-empty-resource'),
|
||||
).toBeInTheDocument();
|
||||
@@ -611,13 +647,15 @@ describe('ViewRolePage - View mode toggle', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders Interactive/JSON toggle', () => {
|
||||
it('renders Interactive/JSON toggle', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('permission-view-mode-list')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('permission-view-mode-json')).toBeInTheDocument();
|
||||
});
|
||||
@@ -629,6 +667,7 @@ describe('ViewRolePage - View mode toggle', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
expect(screen.getByTestId('permission-overview')).toBeInTheDocument();
|
||||
|
||||
const jsonToggle = screen.getByTestId('permission-view-mode-json');
|
||||
@@ -645,6 +684,7 @@ describe('ViewRolePage - JSON Viewer Copy Button', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders copy button in JSON view', async () => {
|
||||
@@ -654,6 +694,7 @@ describe('ViewRolePage - JSON Viewer Copy Button', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
const jsonToggle = screen.getByTestId('permission-view-mode-json');
|
||||
await user.click(jsonToggle);
|
||||
|
||||
@@ -669,6 +710,7 @@ describe('ViewRolePage - JSON Viewer Copy Button', () => {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
});
|
||||
|
||||
await waitForPageReady();
|
||||
const jsonToggle = screen.getByTestId('permission-view-mode-json');
|
||||
await user.click(jsonToggle);
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
CoretypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
customRoleResponse,
|
||||
managedRoleResponse,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
|
||||
@@ -79,9 +79,7 @@ export const mockPermissionsData = {
|
||||
};
|
||||
|
||||
export function mockHooksForCustomRole(): void {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -99,9 +97,7 @@ export function mockHooksForCustomRole(): void {
|
||||
}
|
||||
|
||||
export function mockHooksWithPermissions(permissions: unknown): void {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: customRoleResponse,
|
||||
@@ -119,9 +115,7 @@ export function mockHooksWithPermissions(permissions: unknown): void {
|
||||
}
|
||||
|
||||
export function mockHooksForManagedRole(): void {
|
||||
jest
|
||||
.spyOn(useAuthZModule, 'useAuthZ')
|
||||
.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
|
||||
data: managedRoleResponse,
|
||||
|
||||
@@ -11,23 +11,21 @@ import {
|
||||
userEvent,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDeny,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
|
||||
import RolesSettings from '../RolesSettings';
|
||||
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const rolesApiURL = 'http://localhost/api/v1/roles';
|
||||
|
||||
describe('RolesSettings', () => {
|
||||
beforeEach(() => {
|
||||
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
|
||||
server.use(
|
||||
setupAuthzAdmin(),
|
||||
rest.get(rolesApiURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
@@ -35,7 +33,6 @@ describe('RolesSettings', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
@@ -273,4 +270,18 @@ describe('RolesSettings', () => {
|
||||
// Total dashes expected: 2 (for both dates)
|
||||
expect(dashFallback.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('disables search input when user lacks list permission', async () => {
|
||||
server.use(
|
||||
setupAuthzDeny(RoleListPermission),
|
||||
rest.get(rolesApiURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<RolesSettings />);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText('Search for roles...');
|
||||
expect(searchInput).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
buildRoleDeletePermission,
|
||||
buildRoleReadPermission,
|
||||
buildRoleUpdatePermission,
|
||||
RoleCreatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
ParsedPermissionObject,
|
||||
parsePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/utils';
|
||||
|
||||
interface UseRoleAuthZResult {
|
||||
readRolePermission: ParsedPermissionObject;
|
||||
updateRolePermission: ParsedPermissionObject;
|
||||
deleteRolePermission: ParsedPermissionObject;
|
||||
hasCreatePermission: boolean;
|
||||
hasReadPermission: boolean;
|
||||
hasUpdatePermission: boolean;
|
||||
hasDeletePermission: boolean;
|
||||
isAuthZLoading: boolean;
|
||||
}
|
||||
|
||||
export function useRoleAuthZ(roleName: string): UseRoleAuthZResult {
|
||||
const readRolePermissionName = buildRoleReadPermission(roleName);
|
||||
const updateRolePermissionName = buildRoleUpdatePermission(roleName);
|
||||
const deleteRolePermissionName = buildRoleDeletePermission(roleName);
|
||||
|
||||
const permissionsToCheck = useMemo(() => {
|
||||
const perms = [RoleCreatePermission];
|
||||
if (roleName) {
|
||||
perms.push(
|
||||
readRolePermissionName,
|
||||
updateRolePermissionName,
|
||||
deleteRolePermissionName,
|
||||
);
|
||||
}
|
||||
return perms;
|
||||
}, [
|
||||
roleName,
|
||||
readRolePermissionName,
|
||||
updateRolePermissionName,
|
||||
deleteRolePermissionName,
|
||||
]);
|
||||
|
||||
const { permissions, isLoading: isAuthZLoading } =
|
||||
useAuthZ(permissionsToCheck);
|
||||
|
||||
const hasCreatePermission = useMemo(() => {
|
||||
if (permissions === null) {
|
||||
return false;
|
||||
}
|
||||
return permissions[RoleCreatePermission]?.isGranted ?? false;
|
||||
}, [permissions]);
|
||||
|
||||
const hasReadPermission = useMemo(() => {
|
||||
if (!roleName || permissions === null) {
|
||||
return true;
|
||||
}
|
||||
return permissions[buildRoleReadPermission(roleName)]?.isGranted ?? true;
|
||||
}, [permissions, roleName]);
|
||||
|
||||
const hasUpdatePermission = useMemo(() => {
|
||||
if (!roleName || permissions === null) {
|
||||
return true;
|
||||
}
|
||||
return permissions[buildRoleUpdatePermission(roleName)]?.isGranted ?? true;
|
||||
}, [permissions, roleName]);
|
||||
|
||||
const hasDeletePermission = useMemo(() => {
|
||||
if (!roleName || permissions === null) {
|
||||
return true;
|
||||
}
|
||||
return permissions[buildRoleDeletePermission(roleName)]?.isGranted ?? true;
|
||||
}, [permissions, roleName]);
|
||||
|
||||
return {
|
||||
readRolePermission: parsePermission(readRolePermissionName),
|
||||
updateRolePermission: parsePermission(updateRolePermissionName),
|
||||
deleteRolePermission: parsePermission(deleteRolePermissionName),
|
||||
hasCreatePermission,
|
||||
hasReadPermission,
|
||||
hasUpdatePermission,
|
||||
hasDeletePermission,
|
||||
isAuthZLoading,
|
||||
};
|
||||
}
|
||||
@@ -28,7 +28,7 @@ describe('ServiceAccountsSettings — FGA', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('shows PermissionDeniedFullPage when list permission is denied', async () => {
|
||||
it('shows denied callout when list permission is denied', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
@@ -47,14 +47,40 @@ describe('ServiceAccountsSettings — FGA', () => {
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Uh-oh! You are not authorized'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/is not authorized to perform/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows page header and disables search when list permission is denied', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
authzMockResponse(
|
||||
payload,
|
||||
payload.map(() => false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/is not authorized to perform/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Service Accounts')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText('Search by name or email...'),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows table when list permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
@@ -78,7 +104,7 @@ describe('ServiceAccountsSettings — FGA', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('Uh-oh! You are not authorized'),
|
||||
screen.queryByText(/is not authorized to perform/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
padding-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
&__search {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||
import { invalidateListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccountDrawer';
|
||||
import ServiceAccountsTable, {
|
||||
PAGE_SIZE,
|
||||
@@ -17,7 +20,6 @@ import {
|
||||
SACreatePermission,
|
||||
SAListPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -38,6 +40,13 @@ import {
|
||||
import './ServiceAccountsSettings.styles.scss';
|
||||
|
||||
function ServiceAccountsSettings(): JSX.Element {
|
||||
const queryClient = useQueryClient();
|
||||
const { permissions: authzPermissions, isLoading: isAuthZLoading } = useAuthZ([
|
||||
SAListPermission,
|
||||
]);
|
||||
const canListServiceAccounts =
|
||||
authzPermissions?.[SAListPermission]?.isGranted ?? false;
|
||||
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
|
||||
const [currentPage, setPage] = useQueryState(
|
||||
SA_QUERY_PARAMS.PAGE,
|
||||
parseAsInteger.withDefault(1),
|
||||
@@ -52,25 +61,19 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
FilterMode.All,
|
||||
),
|
||||
);
|
||||
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
|
||||
const [, setIsCreateModalOpen] = useQueryState(
|
||||
SA_QUERY_PARAMS.CREATE_SA,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
|
||||
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
|
||||
SAListPermission,
|
||||
]);
|
||||
|
||||
const hasListPermission = listPerms?.[SAListPermission]?.isGranted ?? false;
|
||||
|
||||
const {
|
||||
data: serviceAccountsData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
refetch: handleCreateSuccess,
|
||||
} = useListServiceAccounts({ query: { enabled: hasListPermission } });
|
||||
} = useListServiceAccounts({ query: { enabled: canListServiceAccounts } });
|
||||
|
||||
const controlsDisabled = isAuthZLoading || !canListServiceAccounts;
|
||||
|
||||
const allAccounts = useMemo(
|
||||
(): ServiceAccountRow[] =>
|
||||
@@ -199,9 +202,9 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
if (options?.closeDrawer) {
|
||||
void setSelectedAccountId(null);
|
||||
}
|
||||
void handleCreateSuccess();
|
||||
void invalidateListServiceAccounts(queryClient);
|
||||
},
|
||||
[handleCreateSuccess, setSelectedAccountId],
|
||||
[queryClient, setSelectedAccountId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -223,31 +226,32 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAuthZLoading || isLoading ? (
|
||||
<Spinner height="50vh" />
|
||||
) : !hasListPermission ? (
|
||||
<PermissionDeniedFullPage permissionName="serviceaccount:list" />
|
||||
) : (
|
||||
<div className="sa-settings__list-section">
|
||||
<div className="sa-settings__controls">
|
||||
<DropdownMenuSimple
|
||||
menu={{ items: filterMenuItems }}
|
||||
className="sa-settings-filter-dropdown"
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
className="sa-settings-filter-trigger"
|
||||
<div className="sa-settings__list-section">
|
||||
<div className="sa-settings__controls">
|
||||
<AuthZTooltip checks={[SAListPermission]}>
|
||||
<span>
|
||||
<DropdownMenuSimple
|
||||
menu={{ items: filterMenuItems }}
|
||||
className="sa-settings-filter-dropdown"
|
||||
>
|
||||
<span>{filterLabel}</span>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className="sa-settings-filter-trigger__chevron"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuSimple>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
className="sa-settings-filter-trigger"
|
||||
disabled={controlsDisabled}
|
||||
>
|
||||
<span>{filterLabel}</span>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className="sa-settings-filter-trigger__chevron"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuSimple>
|
||||
</span>
|
||||
</AuthZTooltip>
|
||||
|
||||
<div className="sa-settings__search">
|
||||
<div className="sa-settings__search">
|
||||
<AuthZTooltip checks={[SAListPermission]}>
|
||||
<Input
|
||||
type="search"
|
||||
name="service-accounts-search"
|
||||
@@ -258,23 +262,25 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
void setPage(1);
|
||||
}}
|
||||
className="sa-settings-search-input"
|
||||
disabled={controlsDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AuthZTooltip checks={[SACreatePermission]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsCreateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
New Service Account
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
</div>
|
||||
|
||||
<AuthZButton
|
||||
checks={[SACreatePermission]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsCreateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
New Service Account
|
||||
</AuthZButton>
|
||||
</div>
|
||||
|
||||
<AuthZGuardContent checks={[SAListPermission]}>
|
||||
{isError ? (
|
||||
<ErrorInPlace
|
||||
error={toAPIError(
|
||||
@@ -289,8 +295,8 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AuthZGuardContent>
|
||||
</div>
|
||||
|
||||
<CreateServiceAccountModal />
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
@@ -14,46 +13,6 @@ const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
|
||||
jest.mock('@signozhq/ui/drawer', () => ({
|
||||
...jest.requireActual('@signozhq/ui/drawer'),
|
||||
DrawerWrapper: ({
|
||||
children,
|
||||
footer,
|
||||
open,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
open: boolean;
|
||||
}): JSX.Element | null =>
|
||||
open ? (
|
||||
<div>
|
||||
{children}
|
||||
{footer}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/dialog', () => ({
|
||||
...jest.requireActual('@signozhq/ui/dialog'),
|
||||
DialogWrapper: ({
|
||||
children,
|
||||
open,
|
||||
title,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
open: boolean;
|
||||
title?: string;
|
||||
}): JSX.Element | null =>
|
||||
open ? (
|
||||
<div role="dialog" aria-label={title}>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockServiceAccountsAPI = [
|
||||
{
|
||||
id: 'sa-1',
|
||||
@@ -173,11 +132,11 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /View service account CI Bot/i,
|
||||
}),
|
||||
);
|
||||
const viewButton = await screen.findByRole('button', {
|
||||
name: /View service account CI Bot/i,
|
||||
});
|
||||
|
||||
fireEvent.click(viewButton);
|
||||
|
||||
await expect(
|
||||
screen.findByRole('button', { name: /Delete Service Account/i }),
|
||||
|
||||
172
tests/e2e/helpers/quick-filters.ts
Normal file
172
tests/e2e/helpers/quick-filters.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
|
||||
import { authToken } from './common';
|
||||
|
||||
export const LOGS_EXPLORER_PATH = '/logs/logs-explorer';
|
||||
|
||||
const SETTINGS_PANEL_TITLE = 'Edit quick filters';
|
||||
const SETTINGS_ICON_TEST_ID = 'settings-icon';
|
||||
const ANNOUNCEMENT_DISMISSED_KEY = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT';
|
||||
/**
|
||||
* Conservative lower bound for the open panel width. The panel is 342px in
|
||||
* QuickFiltersSettings.styles.scss; 300 leaves slack for sub-pixel rounding.
|
||||
*/
|
||||
const SETTINGS_PANEL_MIN_WIDTH_PX = 300;
|
||||
|
||||
// Pages that already had the announcement-dismissal init script registered.
|
||||
// addInitScript re-runs on every navigation, so registering once per Page keeps
|
||||
// the callback from stacking up across repeated helper calls.
|
||||
const announcementDismissed = new WeakSet<Page>();
|
||||
|
||||
export type QuickFilterSignal = 'logs' | 'traces';
|
||||
|
||||
export interface QuickFilterDefinition {
|
||||
key: string;
|
||||
dataType: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CUSTOM_FILTERS: QuickFilterDefinition[] = [
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'deployment.environment',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
];
|
||||
|
||||
function settingsPanel(page: Page): Locator {
|
||||
return page
|
||||
.locator('.quick-filters-settings')
|
||||
.filter({ hasText: SETTINGS_PANEL_TITLE });
|
||||
}
|
||||
|
||||
function settingsGearContainer(page: Page): Locator {
|
||||
return page.getByTestId(SETTINGS_ICON_TEST_ID).locator('..');
|
||||
}
|
||||
|
||||
/** Minimal filter set so `isDynamicFilters` is true and the settings gear renders. */
|
||||
export async function seedCustomFiltersViaApi(
|
||||
page: Page,
|
||||
signal: QuickFilterSignal,
|
||||
filters: QuickFilterDefinition[] = DEFAULT_CUSTOM_FILTERS,
|
||||
): Promise<void> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.put('/api/v1/orgs/me/filters', {
|
||||
data: { signal, filters },
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`PUT /api/v1/orgs/me/filters ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the one-off announcement tooltip so it cannot intercept the gear click.
|
||||
* Registered via addInitScript (not page.evaluate) so it runs in the app's own
|
||||
* origin on every navigation — evaluate() before goto() would set localStorage
|
||||
* on the pre-navigation about:blank origin, which the app never reads. Must be
|
||||
* called before the first goto(). Guarded to once per Page to avoid stacking.
|
||||
*/
|
||||
export async function dismissQuickFiltersAnnouncement(
|
||||
page: Page,
|
||||
): Promise<void> {
|
||||
if (announcementDismissed.has(page)) {
|
||||
return;
|
||||
}
|
||||
await page.addInitScript((key) => {
|
||||
// Runs in the browser; Node's tsc has no DOM globals on this callback.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).localStorage.setItem(key, 'false');
|
||||
}, ANNOUNCEMENT_DISMISSED_KEY);
|
||||
announcementDismissed.add(page);
|
||||
}
|
||||
|
||||
export async function gotoExplorerWithQuickFilters(
|
||||
page: Page,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
await dismissQuickFiltersAnnouncement(page);
|
||||
await page.goto(path);
|
||||
await page.getByTestId(SETTINGS_ICON_TEST_ID).waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
/** Settings drawer is closed — content unmounted and gear not active. */
|
||||
export async function assertQuickFiltersSettingsDrawerClosed(
|
||||
page: Page,
|
||||
): Promise<void> {
|
||||
await expect(page.getByText(SETTINGS_PANEL_TITLE)).toBeHidden();
|
||||
await expect(settingsGearContainer(page)).not.toHaveClass(/active/);
|
||||
// Collapsed panel keeps the `hidden` class (width: 0). Assert the class, not
|
||||
// toBeVisible() — a zero-width element is never "visible" to Playwright.
|
||||
await expect(page.locator('.quick-filters-settings')).toHaveClass(/hidden/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the settings gear. Pair with `assertQuickFiltersSettingsDrawerOpen` for
|
||||
* visibility/layout checks that guard overflow clipping regressions.
|
||||
*/
|
||||
export async function openQuickFiltersSettingsPanel(page: Page): Promise<void> {
|
||||
await assertQuickFiltersSettingsDrawerClosed(page);
|
||||
await page.getByTestId(SETTINGS_ICON_TEST_ID).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the customization drawer is open, painted on screen, and populated with
|
||||
* the seeded filters — not merely present in the DOM.
|
||||
*/
|
||||
export async function assertQuickFiltersSettingsDrawerOpen(
|
||||
page: Page,
|
||||
seededFilters: QuickFilterDefinition[] = DEFAULT_CUSTOM_FILTERS,
|
||||
): Promise<void> {
|
||||
const panel = settingsPanel(page);
|
||||
const filtersColumn = page.locator('.quick-filters');
|
||||
|
||||
await expect(settingsGearContainer(page)).toHaveClass(/active/);
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel).not.toHaveClass(/hidden/);
|
||||
await expect(page.getByText(SETTINGS_PANEL_TITLE)).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Search for a filter...')).toBeVisible();
|
||||
await expect(panel.locator('.qf-header-icon')).toBeVisible();
|
||||
|
||||
const addedList = panel.locator('.qf-added-filters-list');
|
||||
await expect(panel.getByText('ADDED FILTERS')).toBeVisible();
|
||||
for (const filter of seededFilters) {
|
||||
await expect(addedList.getByText(filter.key, { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
// Footer only mounts after edits — absent on first open.
|
||||
await expect(panel.getByRole('button', { name: 'Discard' })).toBeHidden();
|
||||
await expect(panel.getByRole('button', { name: 'Save changes' })).toBeHidden();
|
||||
|
||||
// OTHER FILTERS loads asynchronously from attribute/aggregate suggestions.
|
||||
await expect(panel.locator('.qf-other-filters-skeleton')).toHaveCount(0);
|
||||
await expect(panel.getByText('OTHER FILTERS')).toBeVisible();
|
||||
await expect(
|
||||
panel.locator('.qf-other-filters-list .other-filters-item').first(),
|
||||
).toBeVisible();
|
||||
|
||||
const [panelBox, columnBox] = await Promise.all([
|
||||
panel.boundingBox(),
|
||||
filtersColumn.boundingBox(),
|
||||
]);
|
||||
expect(panelBox?.width ?? 0).toBeGreaterThanOrEqual(
|
||||
SETTINGS_PANEL_MIN_WIDTH_PX,
|
||||
);
|
||||
expect(columnBox).not.toBeNull();
|
||||
expect(panelBox!.x).toBeGreaterThanOrEqual(columnBox!.x);
|
||||
}
|
||||
|
||||
/** Close via the drawer header X and confirm state resets. */
|
||||
export async function closeQuickFiltersSettingsPanel(
|
||||
page: Page,
|
||||
): Promise<void> {
|
||||
await settingsPanel(page).locator('.qf-header-icon').click();
|
||||
await assertQuickFiltersSettingsDrawerClosed(page);
|
||||
}
|
||||
24
tests/e2e/tests/quick-filters/settings.spec.ts
Normal file
24
tests/e2e/tests/quick-filters/settings.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { test, expect } from '../../fixtures/auth';
|
||||
import {
|
||||
assertQuickFiltersSettingsDrawerOpen,
|
||||
closeQuickFiltersSettingsPanel,
|
||||
DEFAULT_CUSTOM_FILTERS,
|
||||
gotoExplorerWithQuickFilters,
|
||||
LOGS_EXPLORER_PATH,
|
||||
openQuickFiltersSettingsPanel,
|
||||
seedCustomFiltersViaApi,
|
||||
} from '../../helpers/quick-filters';
|
||||
|
||||
// smoke tests for settings drawer in quick filters
|
||||
test.describe('Quick Filters — settings drawer', () => {
|
||||
test('TC-01 logs explorer — settings gear opens a visible, populated drawer', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await seedCustomFiltersViaApi(page, 'logs', DEFAULT_CUSTOM_FILTERS);
|
||||
await gotoExplorerWithQuickFilters(page, LOGS_EXPLORER_PATH);
|
||||
await openQuickFiltersSettingsPanel(page);
|
||||
await assertQuickFiltersSettingsDrawerOpen(page, DEFAULT_CUSTOM_FILTERS);
|
||||
await closeQuickFiltersSettingsPanel(page);
|
||||
await expect(page.locator('.quick-filters')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user