mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-23 00:47:31 +00:00
Compare commits
3 Commits
invite-val
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43509681fa | ||
|
|
ff5fcc0e98 | ||
|
|
122d88c4d2 |
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -103,9 +103,10 @@ function K8sClustersList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -101,9 +101,10 @@ function K8sJobsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { safeParseJSON } from './commonUtils';
|
||||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
||||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||||
import { IEntityColumn } from './utils';
|
import { IEntityColumn } from './utils';
|
||||||
@@ -58,9 +59,10 @@ function K8sHeader({
|
|||||||
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
||||||
let { filters } = currentQuery.builder.queryData[0];
|
let { filters } = currentQuery.builder.queryData[0];
|
||||||
if (urlFilters) {
|
if (urlFilters) {
|
||||||
const decoded = decodeURIComponent(urlFilters);
|
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
filters = parsed;
|
filters = parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -104,9 +104,10 @@ function K8sNamespacesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -99,9 +99,10 @@ function K8sNodesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -92,9 +92,10 @@ function K8sPodsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sVolumesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Table, Tooltip, Typography } from 'antd';
|
import { Table, Tooltip, Typography } from 'antd';
|
||||||
import { Progress } from 'antd/lib';
|
import { Progress } from 'antd/lib';
|
||||||
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
|
|||||||
return uniqueFilters;
|
return uniqueFilters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const safeParseJSON = <T,>(value: string): T | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing JSON from URL parameter:', e);
|
||||||
|
// TODO: Should we capture this error in Sentry?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getOrderByFromParams = (
|
export const getOrderByFromParams = (
|
||||||
searchParams: URLSearchParams,
|
searchParams: URLSearchParams,
|
||||||
returnNullAsDefault = false,
|
returnNullAsDefault = false,
|
||||||
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
|
|||||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||||
);
|
);
|
||||||
if (orderByFromParams) {
|
if (orderByFromParams) {
|
||||||
const decoded = decodeURIComponent(orderByFromParams);
|
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
|
||||||
const parsed = JSON.parse(decoded);
|
orderByFromParams,
|
||||||
return parsed as { columnName: string; order: 'asc' | 'desc' };
|
);
|
||||||
|
if (parsed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (returnNullAsDefault) {
|
if (returnNullAsDefault) {
|
||||||
return null;
|
return null;
|
||||||
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
|
|||||||
): IBuilderQuery['filters'] | null => {
|
): IBuilderQuery['filters'] | null => {
|
||||||
const filtersFromParams = searchParams.get(queryKey);
|
const filtersFromParams = searchParams.get(queryKey);
|
||||||
if (filtersFromParams) {
|
if (filtersFromParams) {
|
||||||
try {
|
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
|
||||||
const decoded = decodeURIComponent(filtersFromParams);
|
|
||||||
const parsed = JSON.parse(decoded);
|
|
||||||
return parsed as IBuilderQuery['filters'];
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import inviteUsers from 'api/v1/invite/bulk/create';
|
import inviteUsers from 'api/v1/invite/bulk/create';
|
||||||
import AuthError from 'components/AuthError/AuthError';
|
import AuthError from 'components/AuthError/AuthError';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { cloneDeep, debounce } from 'lodash-es';
|
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -65,7 +65,7 @@ function InviteTeamMembers({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (teamMembers === null) {
|
if (isEmpty(teamMembers)) {
|
||||||
const initialTeamMembers = Array.from({ length: 3 }, () => ({
|
const initialTeamMembers = Array.from({ length: 3 }, () => ({
|
||||||
...defaultTeamMember,
|
...defaultTeamMember,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@@ -88,10 +88,7 @@ function InviteTeamMembers({
|
|||||||
setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id));
|
setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMemberTouched = (member: TeamMember): boolean =>
|
// Validation function to check all users
|
||||||
member.email.trim() !== '' ||
|
|
||||||
Boolean(member.role && member.role.trim() !== '');
|
|
||||||
|
|
||||||
const validateAllUsers = (): boolean => {
|
const validateAllUsers = (): boolean => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
let hasEmailErrors = false;
|
let hasEmailErrors = false;
|
||||||
@@ -99,9 +96,7 @@ function InviteTeamMembers({
|
|||||||
|
|
||||||
const updatedEmailValidity: Record<string, boolean> = {};
|
const updatedEmailValidity: Record<string, boolean> = {};
|
||||||
|
|
||||||
const touchedMembers = teamMembersToInvite?.filter(isMemberTouched) ?? [];
|
teamMembersToInvite?.forEach((member) => {
|
||||||
|
|
||||||
touchedMembers?.forEach((member) => {
|
|
||||||
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email);
|
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email);
|
||||||
const roleValid = Boolean(member.role && member.role.trim() !== '');
|
const roleValid = Boolean(member.role && member.role.trim() !== '');
|
||||||
|
|
||||||
@@ -155,12 +150,12 @@ function InviteTeamMembers({
|
|||||||
|
|
||||||
const handleNext = (): void => {
|
const handleNext = (): void => {
|
||||||
if (validateAllUsers()) {
|
if (validateAllUsers()) {
|
||||||
setTeamMembers(teamMembersToInvite?.filter(isMemberTouched) ?? []);
|
setTeamMembers(teamMembersToInvite || []);
|
||||||
setHasInvalidEmails(false);
|
setHasInvalidEmails(false);
|
||||||
setHasInvalidRoles(false);
|
setHasInvalidRoles(false);
|
||||||
setInviteError(null);
|
setInviteError(null);
|
||||||
sendInvites({
|
sendInvites({
|
||||||
invites: teamMembersToInvite?.filter(isMemberTouched) ?? [],
|
invites: teamMembersToInvite || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -235,12 +230,12 @@ function InviteTeamMembers({
|
|||||||
|
|
||||||
const getValidationErrorMessage = (): string => {
|
const getValidationErrorMessage = (): string => {
|
||||||
if (hasInvalidEmails && hasInvalidRoles) {
|
if (hasInvalidEmails && hasInvalidRoles) {
|
||||||
return 'Please enter valid emails and select roles for team members';
|
return 'Please enter valid emails and select roles for all team members';
|
||||||
}
|
}
|
||||||
if (hasInvalidEmails) {
|
if (hasInvalidEmails) {
|
||||||
return 'Please enter valid emails for team members';
|
return 'Please enter valid emails for all team members';
|
||||||
}
|
}
|
||||||
return 'Please select roles for team members';
|
return 'Please select roles for all team members';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDoLater = (): void => {
|
const handleDoLater = (): void => {
|
||||||
@@ -251,10 +246,7 @@ function InviteTeamMembers({
|
|||||||
onNext();
|
onNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasInvites =
|
|
||||||
(teamMembersToInvite?.filter(isMemberTouched) ?? []).length > 0;
|
|
||||||
const isButtonDisabled = isSendingInvites || isLoading;
|
const isButtonDisabled = isSendingInvites || isLoading;
|
||||||
const isInviteButtonDisabled = isButtonDisabled || !hasInvites;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="questions-container">
|
<div className="questions-container">
|
||||||
@@ -364,11 +356,9 @@ function InviteTeamMembers({
|
|||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
className={`onboarding-next-button ${
|
className={`onboarding-next-button ${isButtonDisabled ? 'disabled' : ''}`}
|
||||||
isInviteButtonDisabled ? 'disabled' : ''
|
|
||||||
}`}
|
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={isInviteButtonDisabled}
|
disabled={isButtonDisabled}
|
||||||
suffixIcon={
|
suffixIcon={
|
||||||
isButtonDisabled ? (
|
isButtonDisabled ? (
|
||||||
<Loader2 className="animate-spin" size={12} />
|
<Loader2 className="animate-spin" size={12} />
|
||||||
@@ -377,7 +367,7 @@ function InviteTeamMembers({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Send Invites
|
Complete
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,480 +0,0 @@
|
|||||||
import { rest, server } from 'mocks-server/server';
|
|
||||||
import {
|
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
userEvent,
|
|
||||||
waitFor,
|
|
||||||
} from 'tests/test-utils';
|
|
||||||
|
|
||||||
import InviteTeamMembers from '../InviteTeamMembers';
|
|
||||||
|
|
||||||
jest.mock('api/common/logEvent', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockNotificationSuccess = jest.fn() as jest.MockedFunction<
|
|
||||||
(args: { message: string }) => void
|
|
||||||
>;
|
|
||||||
const mockNotificationError = jest.fn() as jest.MockedFunction<
|
|
||||||
(args: { message: string }) => void
|
|
||||||
>;
|
|
||||||
|
|
||||||
jest.mock('hooks/useNotifications', () => ({
|
|
||||||
useNotifications: (): any => ({
|
|
||||||
notifications: {
|
|
||||||
success: mockNotificationSuccess,
|
|
||||||
error: mockNotificationError,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const INVITE_USERS_ENDPOINT = '*/api/v1/invite/bulk';
|
|
||||||
|
|
||||||
interface TeamMember {
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
name: string;
|
|
||||||
frontendBaseUrl: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InviteRequestBody {
|
|
||||||
invites: { email: string; role: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RenderProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
teamMembers?: TeamMember[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockOnNext = jest.fn() as jest.MockedFunction<() => void>;
|
|
||||||
const mockSetTeamMembers = jest.fn() as jest.MockedFunction<
|
|
||||||
(members: TeamMember[]) => void
|
|
||||||
>;
|
|
||||||
|
|
||||||
function renderComponent({
|
|
||||||
isLoading = false,
|
|
||||||
teamMembers = null,
|
|
||||||
}: RenderProps = {}): ReturnType<typeof render> {
|
|
||||||
return render(
|
|
||||||
<InviteTeamMembers
|
|
||||||
isLoading={isLoading}
|
|
||||||
teamMembers={teamMembers}
|
|
||||||
setTeamMembers={mockSetTeamMembers}
|
|
||||||
onNext={mockOnNext}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectRole(
|
|
||||||
user: ReturnType<typeof userEvent.setup>,
|
|
||||||
selectIndex: number,
|
|
||||||
optionLabel: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const placeholders = screen.getAllByText(/select roles/i);
|
|
||||||
await user.click(placeholders[selectIndex]);
|
|
||||||
const optionContent = await screen.findByText(optionLabel);
|
|
||||||
fireEvent.click(optionContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('InviteTeamMembers', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
rest.post(INVITE_USERS_ENDPOINT, (_, res, ctx) =>
|
|
||||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
server.resetHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Initial rendering', () => {
|
|
||||||
it('renders the page header, column labels, default rows, and action buttons', () => {
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByRole('heading', { name: /invite your team/i }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText(/signoz is a lot more useful with collaborators/i),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getAllByPlaceholderText(/e\.g\. john@signoz\.io/i),
|
|
||||||
).toHaveLength(3);
|
|
||||||
expect(screen.getByText('Email address')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Roles')).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole('button', { name: /send invites/i }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole('button', { name: /i'll do this later/i }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables both action buttons while isLoading is true', () => {
|
|
||||||
renderComponent({ isLoading: true });
|
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /send invites/i })).toBeDisabled();
|
|
||||||
expect(
|
|
||||||
screen.getByRole('button', { name: /i'll do this later/i }),
|
|
||||||
).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Row management', () => {
|
|
||||||
it('adds a new empty row when "Add another" is clicked', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getAllByPlaceholderText(/e\.g\. john@signoz\.io/i),
|
|
||||||
).toHaveLength(3);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /add another/i }));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getAllByPlaceholderText(/e\.g\. john@signoz\.io/i),
|
|
||||||
).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('removes the correct row when its trash icon is clicked', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const emailInputs = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(emailInputs[0], 'first@example.com');
|
|
||||||
await screen.findByDisplayValue('first@example.com');
|
|
||||||
|
|
||||||
await user.click(
|
|
||||||
screen.getAllByRole('button', { name: /remove team member/i })[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByDisplayValue('first@example.com'),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getAllByPlaceholderText(/e\.g\. john@signoz\.io/i),
|
|
||||||
).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hides remove buttons when only one row remains', async () => {
|
|
||||||
renderComponent();
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
|
|
||||||
let removeButtons = screen.getAllByRole('button', {
|
|
||||||
name: /remove team member/i,
|
|
||||||
});
|
|
||||||
while (removeButtons.length > 0) {
|
|
||||||
await user.click(removeButtons[0]);
|
|
||||||
removeButtons = screen.queryAllByRole('button', {
|
|
||||||
name: /remove team member/i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.queryByRole('button', { name: /remove team member/i }),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Inline email validation', () => {
|
|
||||||
it('shows an inline error after typing an invalid email and clears it when a valid email is entered', async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
const user = userEvent.setup({
|
|
||||||
advanceTimers: (ms) => jest.advanceTimersByTime(ms),
|
|
||||||
});
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.type(firstInput, 'not-an-email');
|
|
||||||
jest.advanceTimersByTime(600);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/invalid email address/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.clear(firstInput);
|
|
||||||
await user.type(firstInput, 'good@example.com');
|
|
||||||
jest.advanceTimersByTime(600);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/invalid email address/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not show an inline error when the field is cleared back to empty', async () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
const user = userEvent.setup({
|
|
||||||
advanceTimers: (ms) => jest.advanceTimersByTime(ms),
|
|
||||||
});
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(firstInput, 'a');
|
|
||||||
await user.clear(firstInput);
|
|
||||||
jest.advanceTimersByTime(600);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/invalid email address/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Validation callout on Complete', () => {
|
|
||||||
it('shows the correct callout message for each combination of email/role validity', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const removeButtons = screen.getAllByRole('button', {
|
|
||||||
name: /remove team member/i,
|
|
||||||
});
|
|
||||||
await user.click(removeButtons[0]);
|
|
||||||
await user.click(
|
|
||||||
screen.getAllByRole('button', { name: /remove team member/i })[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.type(firstInput, 'bad-email');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
/please enter valid emails and select roles for team members/i,
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please select roles for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await selectRole(user, 0, 'Viewer');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText(/please enter valid emails for team members/i),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please select roles for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails and select roles/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.clear(firstInput);
|
|
||||||
await user.type(firstInput, 'valid@example.com');
|
|
||||||
await user.click(screen.getByRole('button', { name: /add another/i }));
|
|
||||||
const allInputs = screen.getAllByPlaceholderText(/e\.g\. john@signoz\.io/i);
|
|
||||||
await user.type(allInputs[1], 'norole@example.com');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText(/please select roles for team members/i),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails and select roles/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('treats whitespace as untouched, clears the callout on fix-and-resubmit, and clears role error on role select', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const removeButtons = screen.getAllByRole('button', {
|
|
||||||
name: /remove team member/i,
|
|
||||||
});
|
|
||||||
await user.click(removeButtons[0]);
|
|
||||||
await user.click(
|
|
||||||
screen.getAllByRole('button', { name: /remove team member/i })[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.type(firstInput, ' ');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText(/please select roles/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.clear(firstInput);
|
|
||||||
await user.type(firstInput, 'bad-email');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
/please enter valid emails and select roles for team members/i,
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please select roles for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.clear(firstInput);
|
|
||||||
await user.type(firstInput, 'good@example.com');
|
|
||||||
await selectRole(user, 0, 'Admin');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails and select roles/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please enter valid emails for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/please select roles for team members/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(mockOnNext).toHaveBeenCalledTimes(1), {
|
|
||||||
timeout: 1200,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables the Send Invites button when all rows are untouched (empty)', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const sendInvitesBtn = screen.getByRole('button', { name: /send invites/i });
|
|
||||||
expect(sendInvitesBtn).toBeDisabled();
|
|
||||||
|
|
||||||
// Type something to make a row touched
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(firstInput, 'a');
|
|
||||||
|
|
||||||
expect(sendInvitesBtn).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('API integration', () => {
|
|
||||||
it('only sends touched (non-empty) rows — empty rows are excluded from the invite payload', async () => {
|
|
||||||
let capturedBody: InviteRequestBody | null = null;
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
rest.post(INVITE_USERS_ENDPOINT, async (req, res, ctx) => {
|
|
||||||
capturedBody = await req.json<InviteRequestBody>();
|
|
||||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(firstInput, 'only@example.com');
|
|
||||||
await selectRole(user, 0, 'Admin');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(capturedBody).not.toBeNull();
|
|
||||||
expect(capturedBody?.invites).toHaveLength(1);
|
|
||||||
expect(capturedBody?.invites[0]).toMatchObject({
|
|
||||||
email: 'only@example.com',
|
|
||||||
role: 'ADMIN',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await waitFor(() => expect(mockOnNext).toHaveBeenCalled(), {
|
|
||||||
timeout: 1200,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the invite API, shows a success notification, and calls onNext after the 1 s delay', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(firstInput, 'alice@example.com');
|
|
||||||
await selectRole(user, 0, 'Admin');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockNotificationSuccess).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ message: 'Invites sent successfully!' }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(
|
|
||||||
() => {
|
|
||||||
expect(mockOnNext).toHaveBeenCalledTimes(1);
|
|
||||||
},
|
|
||||||
{ timeout: 1200 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an API error container when the invite request fails', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.post(INVITE_USERS_ENDPOINT, (_, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(500),
|
|
||||||
ctx.json({
|
|
||||||
errors: [{ code: 'INTERNAL_ERROR', msg: 'Something went wrong' }],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
const [firstInput] = screen.getAllByPlaceholderText(
|
|
||||||
/e\.g\. john@signoz\.io/i,
|
|
||||||
);
|
|
||||||
await user.type(firstInput, 'fail@example.com');
|
|
||||||
await selectRole(user, 0, 'Viewer');
|
|
||||||
await user.click(screen.getByRole('button', { name: /send invites/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(document.querySelector('.auth-error-container')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.type(firstInput, 'x');
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.auth-error-container'),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user