Compare commits

..

1 Commits

5 changed files with 88 additions and 53 deletions

View File

@@ -327,11 +327,6 @@ function App(): JSX.Element {
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {
return null;
}
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,
});

View File

@@ -15,8 +15,8 @@ interface OverviewTabProps {
account: ServiceAccountRow;
localName: string;
onNameChange: (v: string) => void;
localRole: string;
onRoleChange: (v: string | undefined) => void;
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
@@ -30,8 +30,8 @@ function OverviewTab({
account,
localName,
onNameChange,
localRole,
onRoleChange,
localRoles,
onRolesChange,
isDisabled,
availableRoles,
rolesLoading,
@@ -94,10 +94,15 @@ function OverviewTab({
{isDisabled ? (
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<div className="sa-drawer__disabled-roles">
{localRole ? (
<Badge color="vanilla">
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
</Badge>
{localRoles.length > 0 ? (
localRoles.map((roleId) => {
const role = availableRoles.find((r) => r.id === roleId);
return (
<Badge key={roleId} color="vanilla">
{role?.name ?? roleId}
</Badge>
);
})
) : (
<span className="sa-drawer__input-text"></span>
)}
@@ -107,14 +112,15 @@ function OverviewTab({
) : (
<RolesSelect
id="sa-roles"
mode="multiple"
roles={availableRoles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={onRefetchRoles}
value={localRole}
onChange={onRoleChange}
placeholder="Select role"
value={localRoles}
onChange={onRolesChange}
placeholder="Select roles"
/>
)}
</div>

View File

@@ -11,9 +11,7 @@ import {
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getGetServiceAccountRolesQueryKey,
getListServiceAccountsQueryKey,
useDeleteServiceAccountRole,
useGetServiceAccount,
useListServiceAccountKeys,
useUpdateServiceAccount,
@@ -40,7 +38,7 @@ import {
useQueryState,
} from 'nuqs';
import APIError from 'types/api/error';
import { retryOn429, toAPIError } from 'utils/errorUtils';
import { toAPIError } from 'utils/errorUtils';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
@@ -95,7 +93,7 @@ function ServiceAccountDrawer({
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRole, setLocalRole] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
@@ -143,7 +141,7 @@ function ServiceAccountDrawer({
if (!account?.id) {
roleSessionRef.current = null;
} else if (account.id !== roleSessionRef.current && !isRolesLoading) {
setLocalRole(currentRoles[0]?.id ?? '');
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
roleSessionRef.current = account.id;
}
}, [account?.id, currentRoles, isRolesLoading]);
@@ -154,7 +152,13 @@ function ServiceAccountDrawer({
const isDirty =
account !== null &&
(localName !== (account.name ?? '') ||
localRole !== (currentRoles[0]?.id ?? ''));
JSON.stringify([...localRoles].sort()) !==
JSON.stringify(
currentRoles
.map((r) => r.id)
.filter(Boolean)
.sort(),
));
const {
roles: availableRoles,
@@ -182,27 +186,6 @@ function ServiceAccountDrawer({
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
mutation: {
retry: retryOn429,
},
});
const executeRolesOperation = useCallback(
async (accountId: string): Promise<RoleUpdateFailure[]> => {
if (localRole === '' && currentRoles[0]?.id) {
await deleteRole({
pathParams: { id: accountId, rid: currentRoles[0].id },
});
await queryClient.invalidateQueries(
getGetServiceAccountRolesQueryKey({ id: accountId }),
);
return [];
}
return applyDiff([localRole].filter(Boolean), availableRoles);
},
[localRole, currentRoles, availableRoles, applyDiff, deleteRole, queryClient],
);
const retryNameUpdate = useCallback(async (): Promise<void> => {
if (!account) {
@@ -270,7 +253,7 @@ function ServiceAccountDrawer({
const retryRolesUpdate = useCallback(async (): Promise<void> => {
try {
const failures = await executeRolesOperation(selectedAccountId ?? '');
const failures = await applyDiff([...localRoles], availableRoles);
if (failures.length === 0) {
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
} else {
@@ -286,7 +269,7 @@ function ServiceAccountDrawer({
),
);
}
}, [selectedAccountId, executeRolesOperation, failuresToSaveErrors]);
}, [localRoles, availableRoles, applyDiff, failuresToSaveErrors]);
const handleSave = useCallback(async (): Promise<void> => {
if (!account || !isDirty) {
@@ -305,7 +288,7 @@ function ServiceAccountDrawer({
const [nameResult, rolesResult] = await Promise.allSettled([
namePromise,
executeRolesOperation(account.id),
applyDiff([...localRoles], availableRoles),
]);
const errors: SaveError[] = [];
@@ -346,8 +329,10 @@ function ServiceAccountDrawer({
account,
isDirty,
localName,
localRoles,
availableRoles,
updateMutateAsync,
executeRolesOperation,
applyDiff,
refetchAccount,
onSuccess,
queryClient,
@@ -446,9 +431,9 @@ function ServiceAccountDrawer({
account={account}
localName={localName}
onNameChange={handleNameChange}
localRole={localRole}
onRoleChange={(role): void => {
setLocalRole(role ?? '');
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}

View File

@@ -147,7 +147,7 @@ describe('ServiceAccountDrawer', () => {
});
});
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
it('adding a role fires POST for the new role and no DELETE for existing roles', async () => {
const roleSpy = jest.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -167,6 +167,7 @@ describe('ServiceAccountDrawer', () => {
await screen.findByDisplayValue('CI Bot');
// Add signoz-viewer while keeping signoz-admin selected
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-viewer'));
@@ -184,6 +185,43 @@ describe('ServiceAccountDrawer', () => {
});
});
it('removing a role fires DELETE for the removed role and no POST', async () => {
const roleSpy = jest.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
roleSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
deleteSpy();
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
renderDrawer();
await screen.findByDisplayValue('CI Bot');
// Remove the signoz-admin tag from the multi-select
const adminTag = await screen.findByTitle('signoz-admin');
const removeBtn = adminTag.querySelector(
'.ant-select-selection-item-remove',
) as Element;
await user.click(removeBtn);
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
await waitFor(() => {
expect(deleteSpy).toHaveBeenCalled();
expect(roleSpy).not.toHaveBeenCalled();
});
});
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });

View File

@@ -3,6 +3,7 @@ import { useQueryClient } from 'react-query';
import {
getGetServiceAccountRolesQueryKey,
useCreateServiceAccountRole,
useDeleteServiceAccountRole,
useGetServiceAccountRoles,
} from 'api/generated/services/serviceaccount';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
@@ -44,6 +45,9 @@ export function useServiceAccountRoleManager(
const { mutateAsync: createRole } = useCreateServiceAccountRole({
mutation: { retry: retryOn429 },
});
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
mutation: { retry: retryOn429 },
});
const invalidateRoles = useCallback(
() =>
@@ -68,14 +72,21 @@ export function useServiceAccountRoleManager(
const addedRoles = availableRoles.filter(
(r) => r.id && desiredRoleIds.has(r.id) && !currentRoleIds.has(r.id),
);
const removedRoles = currentRoles.filter(
(r) => r.id && !desiredRoleIds.has(r.id),
);
// TODO: re-enable deletes once BE for this is streamlined
const allOperations = [
...addedRoles.map((role) => ({
role,
run: (): ReturnType<typeof createRole> =>
createRole({ pathParams: { id: accountId }, data: { id: role.id } }),
})),
...removedRoles.map((role) => ({
role,
run: (): ReturnType<typeof deleteRole> =>
deleteRole({ pathParams: { id: accountId, rid: role.id ?? '' } }),
})),
];
const results = await Promise.allSettled(
@@ -106,7 +117,7 @@ export function useServiceAccountRoleManager(
return failures;
},
[accountId, currentRoles, createRole, invalidateRoles],
[accountId, currentRoles, createRole, deleteRole, invalidateRoles],
);
return {