mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-10 15:00:47 +01:00
Compare commits
2 Commits
fix/codemi
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9c2d59a5 | ||
|
|
d0a640517a |
@@ -5,10 +5,9 @@ import {
|
||||
useCreateResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useGetResetPasswordToken,
|
||||
useGetRolesByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
@@ -25,15 +24,14 @@ import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useGetUser: jest.fn(),
|
||||
useGetRolesByUserID: jest.fn(),
|
||||
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
|
||||
useDeleteUserRole: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateMyUserV2: jest.fn(),
|
||||
useSetRoleByUserID: jest.fn(),
|
||||
useCreateUserRole: jest.fn(),
|
||||
useGetResetPasswordToken: jest.fn(),
|
||||
useCreateResetPasswordToken: jest.fn(),
|
||||
getGetRolesByUserIDQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}/roles`,
|
||||
getGetUserQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}`,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -194,11 +192,7 @@ describe('EditMemberDrawer', () => {
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useGetRolesByUserID as jest.Mock).mockReturnValue({
|
||||
data: { data: [managedRoles[0]] },
|
||||
isLoading: false,
|
||||
});
|
||||
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
|
||||
(useDeleteUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockRemoveMutateAsync.mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -210,7 +204,7 @@ describe('EditMemberDrawer', () => {
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -312,12 +306,12 @@ describe('EditMemberDrawer', () => {
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adding a new role calls setRole without removing existing ones', async () => {
|
||||
it('adding a new role creates a user role without removing existing ones', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -334,15 +328,14 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-editor' },
|
||||
data: { userId: 'user-1', roleId: managedRoles[1].id },
|
||||
});
|
||||
expect(mockRemoveMutateAsync).not.toHaveBeenCalled();
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('deselecting a role calls removeRole with the role id', async () => {
|
||||
it('deselecting a role deletes the user role by its assignment id', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -361,7 +354,7 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRemoveMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
|
||||
pathParams: { id: 'ur-1' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
.highlights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
padding: 12px 0;
|
||||
|
||||
// Constrain each KeyValueLabel (the grid items) to its cell.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.valueBadge {
|
||||
--badge-font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Truncating text inside a badge
|
||||
.badgeText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.traceLink {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LOG_HIGHLIGHTS } from './config';
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface LogHighlightsProps {
|
||||
log: ILog;
|
||||
}
|
||||
|
||||
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
|
||||
const fields = LOG_HIGHLIGHTS.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
value: field.render(log),
|
||||
})).filter((field) => field.value != null);
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.highlights} data-testid="log-details-highlights">
|
||||
{fields.map((field) => (
|
||||
<KeyValueLabel
|
||||
key={field.key}
|
||||
badgeKey={field.label}
|
||||
badgeValue={field.value}
|
||||
direction="column"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogHighlights;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
to={{ pathname: `/trace/${traceId}` }}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.traceLink}
|
||||
title={traceId}
|
||||
>
|
||||
{traceId}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceIdField;
|
||||
@@ -1,102 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
import TraceIdField from './TraceIdField';
|
||||
|
||||
// Severity badge color mirrors the LogStateIndicator bar
|
||||
const SEVERITY_COLOR: Record<string, BadgeColor> = {
|
||||
[LogType.TRACE]: 'forest',
|
||||
[LogType.DEBUG]: 'aqua',
|
||||
[LogType.INFO]: 'robin',
|
||||
[LogType.WARN]: 'amber',
|
||||
[LogType.ERROR]: 'cherry',
|
||||
[LogType.FATAL]: 'sakura',
|
||||
};
|
||||
|
||||
export interface LogHighlightConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
render: (log: ILog) => ReactNode | null;
|
||||
}
|
||||
|
||||
// Resource/attribute lookup (keys like `service.name` live in resources_string,
|
||||
// occasionally attributes_string). Typed loosely as these are string maps.
|
||||
const getAttr = (log: ILog, key: string): string =>
|
||||
(log.resources_string as unknown as Record<string, string>)?.[key] ||
|
||||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
|
||||
'';
|
||||
|
||||
const valueBadge = (
|
||||
value: string,
|
||||
options?: { prefix?: ReactNode; color?: BadgeColor },
|
||||
): ReactNode => (
|
||||
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
|
||||
{options?.prefix}
|
||||
<span className={styles.badgeText} title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
|
||||
{
|
||||
key: 'service',
|
||||
label: 'SERVICE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.name');
|
||||
return value
|
||||
? valueBadge(value, {
|
||||
prefix: <span className={styles.serviceDot} />,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
label: 'SEVERITY',
|
||||
render: (log): ReactNode | null => {
|
||||
if (!log.severity_text) {
|
||||
return null;
|
||||
}
|
||||
return valueBadge(log.severity_text, {
|
||||
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'namespace',
|
||||
label: 'NAMESPACE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.namespace');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'environment',
|
||||
label: 'ENVIRONMENT',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'deployment.environment');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'traceId',
|
||||
label: 'TRACE ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const traceId = log.trace_id || log.traceId;
|
||||
return traceId ? <TraceIdField traceId={traceId} /> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanId',
|
||||
label: 'SPAN ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const spanId = log.span_id || log.spanID;
|
||||
return spanId ? valueBadge(spanId) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -115,45 +115,6 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Highlights for fields present on the log, omitting absent ones', () => {
|
||||
const logWithMeta = {
|
||||
...mockLog,
|
||||
severity_text: 'ERROR',
|
||||
trace_id: 'trace-abc',
|
||||
resources_string: {
|
||||
'service.name': 'checkout',
|
||||
'deployment.environment': 'production',
|
||||
},
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithMeta });
|
||||
|
||||
const highlights = screen.getByTestId('log-details-highlights');
|
||||
expect(highlights).toHaveTextContent('SEVERITY');
|
||||
expect(highlights).toHaveTextContent('ERROR');
|
||||
expect(highlights).toHaveTextContent('SERVICE');
|
||||
expect(highlights).toHaveTextContent('checkout');
|
||||
expect(highlights).toHaveTextContent('ENVIRONMENT');
|
||||
expect(highlights).toHaveTextContent('production');
|
||||
expect(highlights).toHaveTextContent('TRACE ID');
|
||||
// Absent fields are omitted (no namespace / span id on this log).
|
||||
expect(highlights).not.toHaveTextContent('NAMESPACE');
|
||||
expect(highlights).not.toHaveTextContent('SPAN ID');
|
||||
});
|
||||
|
||||
it('links the trace id highlight to the trace detail in a new tab', () => {
|
||||
const logWithTrace = {
|
||||
...mockLog,
|
||||
trace_id: 'trace-abc',
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithTrace });
|
||||
|
||||
const link = screen.getByRole('link', { name: 'trace-abc' });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
|
||||
});
|
||||
|
||||
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
|
||||
|
||||
@@ -55,7 +55,6 @@ import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
|
||||
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
|
||||
import LogHighlights from './LogHighlights/LogHighlights';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
@@ -400,8 +399,6 @@ function LogDetailInner({
|
||||
<div className="log-overflow-shadow"> </div>
|
||||
</div>
|
||||
|
||||
{isLogDetailsV2 && <LogHighlights log={log} />}
|
||||
|
||||
<div className="tabs-and-search">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
|
||||
@@ -183,14 +183,15 @@ function QuerySearch({
|
||||
isProgrammaticChangeRef.current = true;
|
||||
}
|
||||
|
||||
const changes = view.state.changes({
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
});
|
||||
view.dispatch({
|
||||
changes,
|
||||
selection: { anchor: changes.newLength },
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
selection: {
|
||||
anchor: value.length,
|
||||
},
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -301,66 +301,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
|
||||
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
|
||||
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
||||
const initialExpression = "service.name = 'frontend'";
|
||||
// Filtering on a multi-line log value (CRLF) used to throw
|
||||
// "RangeError: Selection points outside of document".
|
||||
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
|
||||
|
||||
const baseQueryData = {
|
||||
...initialQueriesMap.logs.builder.queryData[0],
|
||||
filter: { expression: initialExpression },
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={baseQueryData}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const editorContent = document.querySelector(
|
||||
CM_EDITOR_SELECTOR,
|
||||
) as HTMLElement;
|
||||
expect(editorContent.textContent || '').toBe(initialExpression);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The programmatic replace dispatched without throwing, and the selection anchor
|
||||
// stayed within the CRLF-normalized document (the bug set it past the end).
|
||||
await waitFor(() => {
|
||||
const spec = dispatchSpy.mock.calls
|
||||
.map(
|
||||
(call) =>
|
||||
call[0] as {
|
||||
selection?: { anchor?: number };
|
||||
changes?: { newLength?: number };
|
||||
},
|
||||
)
|
||||
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
|
||||
expect(spec).toBeDefined();
|
||||
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
|
||||
spec?.changes?.newLength as number,
|
||||
);
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getGetRolesByUserIDQueryKey,
|
||||
useGetRolesByUserID,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { retryOn429 } from 'utils/errorUtils';
|
||||
|
||||
const enum PromiseStatus {
|
||||
Fulfilled = 'fulfilled',
|
||||
Rejected = 'rejected',
|
||||
}
|
||||
|
||||
@@ -33,31 +30,34 @@ export function useMemberRoleManager(
|
||||
userId: string,
|
||||
enabled: boolean,
|
||||
): UseMemberRoleManagerResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useGetRolesByUserID(
|
||||
const { data, isLoading } = useGetUser(
|
||||
{ id: userId },
|
||||
{ query: { enabled: !!userId && enabled } },
|
||||
);
|
||||
|
||||
const userRoles = useMemo(
|
||||
() => data?.data?.userRoles ?? [],
|
||||
[data?.data?.userRoles],
|
||||
);
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
() => userRoles.map((userRole) => userRole.role),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: setRole } = useSetRoleByUserID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: removeRole } = useRemoveUserRoleByUserIDAndRoleID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries(getGetRolesByUserIDQueryKey({ id: userId })),
|
||||
[userId, queryClient],
|
||||
// DELETE /api/v2/user_roles/{id} is keyed by the user_role join row, not the role.
|
||||
const assignmentIdByRoleId = useMemo(
|
||||
() => new Map(userRoles.map((userRole) => [userRole.roleId, userRole.id])),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: createUserRole } = useCreateUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteUserRole } = useDeleteUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
@@ -80,30 +80,33 @@ export function useMemberRoleManager(
|
||||
const allOperations = [
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof setRole> =>
|
||||
setRole({
|
||||
pathParams: { id: userId },
|
||||
data: { name: role.name ?? '' },
|
||||
}),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof removeRole> =>
|
||||
removeRole({ pathParams: { id: userId, roleId: role.id ?? '' } }),
|
||||
run: (): ReturnType<typeof createUserRole> =>
|
||||
createUserRole({ data: { userId, roleId: role.id ?? '' } }),
|
||||
})),
|
||||
...removedRoles
|
||||
.map((role) => ({
|
||||
role,
|
||||
assignmentId: assignmentIdByRoleId.get(role.id ?? ''),
|
||||
}))
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
role: AuthtypesGettableRoleDTO;
|
||||
assignmentId: string;
|
||||
} => !!entry.assignmentId,
|
||||
)
|
||||
.map(({ role, assignmentId }) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteUserRole> =>
|
||||
deleteUserRole({ pathParams: { id: assignmentId } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allOperations.map((op) => op.run()),
|
||||
);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r) => r.status === PromiseStatus.Fulfilled,
|
||||
).length;
|
||||
if (successCount > 0) {
|
||||
await invalidateRoles();
|
||||
}
|
||||
|
||||
const failures: MemberRoleUpdateFailure[] = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === PromiseStatus.Rejected) {
|
||||
@@ -113,7 +116,6 @@ export function useMemberRoleManager(
|
||||
error: result.reason,
|
||||
onRetry: async (): Promise<void> => {
|
||||
await run();
|
||||
await invalidateRoles();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -121,7 +123,7 @@ export function useMemberRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[userId, currentRoles, setRole, removeRole, invalidateRoles],
|
||||
[userId, currentRoles, assignmentIdByRoleId, createUserRole, deleteUserRole],
|
||||
);
|
||||
|
||||
return { currentRoles, isLoading, applyDiff };
|
||||
|
||||
Reference in New Issue
Block a user