mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-06 22:02:03 +00:00
Compare commits
3 Commits
members-pa
...
ssopage-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a2702c77 | ||
|
|
9a6976e50e | ||
|
|
0f5cab9d68 |
@@ -11,7 +11,7 @@ import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/utils';
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import { InviteTeamMembersProps } from 'container/OrganizationSettings/utils';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { gold } from '@ant-design/colors';
|
||||
import { ExclamationCircleTwoTone } from '@ant-design/icons';
|
||||
import { Space, Typography } from 'antd';
|
||||
|
||||
function DeleteMembersDetails({
|
||||
name,
|
||||
}: DeleteMembersDetailsProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Space direction="horizontal" size="middle" align="start">
|
||||
<ExclamationCircleTwoTone
|
||||
twoToneColor={[gold[6], '#1f1f1f']}
|
||||
style={{
|
||||
fontSize: '1.4rem',
|
||||
}}
|
||||
/>
|
||||
<Space direction="vertical">
|
||||
<Typography>Are you sure you want to delete {name}</Typography>
|
||||
<Typography>
|
||||
This will remove all access from dashboards and other features in SigNoz
|
||||
</Typography>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteMembersDetailsProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default DeleteMembersDetails;
|
||||
@@ -1,167 +0,0 @@
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Select, Space, Tooltip } from 'antd';
|
||||
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import { InputGroup, SelectDrawer, Title } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function EditMembersDetails({
|
||||
emailAddress,
|
||||
name,
|
||||
role,
|
||||
setEmailAddress,
|
||||
setName,
|
||||
setRole,
|
||||
id,
|
||||
}: EditMembersDetailsProps): JSX.Element {
|
||||
const [passwordLink, setPasswordLink] = useState<string>('');
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [state, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const getPasswordLink = (token: string): string =>
|
||||
`${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
||||
setFunc(value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (state.value) {
|
||||
notifications.success({
|
||||
message: t('success'),
|
||||
});
|
||||
}
|
||||
}, [state.error, state.value, t, notifications]);
|
||||
|
||||
const onPasswordChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
setPasswordLink(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onGeneratePasswordHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getResetPasswordToken({
|
||||
userId: id || '',
|
||||
});
|
||||
setPasswordLink(getPasswordLink(response.data.token));
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large">
|
||||
<Space direction="horizontal">
|
||||
<Title>Email address</Title>
|
||||
<Input
|
||||
placeholder="john@signoz.io"
|
||||
readOnly
|
||||
onChange={(event): void =>
|
||||
onChangeHandler(setEmailAddress, event.target.value)
|
||||
}
|
||||
disabled={isLoading}
|
||||
value={emailAddress}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="horizontal">
|
||||
<Title>Name (optional)</Title>
|
||||
<Input
|
||||
placeholder="John"
|
||||
onChange={(event): void => onChangeHandler(setName, event.target.value)}
|
||||
value={name}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="horizontal">
|
||||
<Title>Role</Title>
|
||||
<SelectDrawer
|
||||
value={role}
|
||||
onSelect={(value: unknown): void => {
|
||||
if (typeof value === 'string') {
|
||||
setRole(value as ROLES);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Option value="ADMIN">ADMIN</Option>
|
||||
<Option value="VIEWER">VIEWER</Option>
|
||||
<Option value="EDITOR">EDITOR</Option>
|
||||
</SelectDrawer>
|
||||
</Space>
|
||||
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={onGeneratePasswordHandler}
|
||||
type="primary"
|
||||
>
|
||||
Generate Reset Password link
|
||||
</Button>
|
||||
{passwordLink && (
|
||||
<InputGroup>
|
||||
<Input
|
||||
style={{ width: '100%' }}
|
||||
defaultValue="git@github.com:ant-design/ant-design.git"
|
||||
onChange={onPasswordChangeHandler}
|
||||
value={passwordLink}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Tooltip title="COPY LINK">
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(): void => copyToClipboard(passwordLink)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputGroup>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditMembersDetailsProps {
|
||||
emailAddress: string;
|
||||
name: string;
|
||||
role: ROLES;
|
||||
setEmailAddress: Dispatch<SetStateAction<string>>;
|
||||
setName: Dispatch<SetStateAction<string>>;
|
||||
setRole: Dispatch<SetStateAction<ROLES>>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default EditMembersDetails;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const SelectDrawer = styled(Select)`
|
||||
width: 120px;
|
||||
`;
|
||||
|
||||
export const Title = styled(Typography)`
|
||||
width: 7rem;
|
||||
`;
|
||||
|
||||
export const InputGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'antd';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
|
||||
import { InviteMemberFormValues } from '../PendingInvitesContainer/index';
|
||||
import { InviteMemberFormValues } from '../utils';
|
||||
import { SelectDrawer, SpaceContainer, TitleWrapper } from './styles';
|
||||
|
||||
function InviteTeamMembers({ form, onFinish }: Props): JSX.Element {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import { InviteMemberFormValues } from '../PendingInvitesContainer';
|
||||
import { InviteMemberFormValues } from '../utils';
|
||||
|
||||
export interface InviteUserModalProps {
|
||||
isInviteTeamMemberModalOpen: boolean;
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Space,
|
||||
TableColumnsType as ColumnsType,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import getAll from 'api/v1/user/get';
|
||||
import deleteUser from 'api/v1/user/id/delete';
|
||||
import update from 'api/v1/user/id/update';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import DeleteMembersDetails from '../DeleteMembersDetails';
|
||||
import EditMembersDetails from '../EditMembersDetails';
|
||||
|
||||
function UserFunction({
|
||||
setDataSource,
|
||||
accessLevel,
|
||||
name,
|
||||
email,
|
||||
id,
|
||||
}: UserFunctionProps): JSX.Element {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
|
||||
const onModalToggleHandler = (
|
||||
func: Dispatch<SetStateAction<boolean>>,
|
||||
value: boolean,
|
||||
): void => {
|
||||
func(value);
|
||||
};
|
||||
|
||||
const [emailAddress, setEmailAddress] = useState(email);
|
||||
const [updatedName, setUpdatedName] = useState(name);
|
||||
const [role, setRole] = useState<ROLES>(accessLevel);
|
||||
const { t } = useTranslation(['common']);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState<boolean>(false);
|
||||
const [isUpdateLoading, setIsUpdateLoading] = useState<boolean>(false);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onUpdateDetailsHandler = (): void => {
|
||||
setDataSource((data) => {
|
||||
const index = data.findIndex((e) => e.id === id);
|
||||
if (index !== -1) {
|
||||
const current = data[index];
|
||||
|
||||
const updatedData: DataType[] = [
|
||||
...data.slice(0, index),
|
||||
{
|
||||
...current,
|
||||
name: updatedName,
|
||||
accessLevel: role,
|
||||
email: emailAddress,
|
||||
},
|
||||
...data.slice(index + 1, data.length),
|
||||
];
|
||||
|
||||
return updatedData;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = (): void => {
|
||||
setDataSource((source) => {
|
||||
const index = source.findIndex((e) => e.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
const updatedData: DataType[] = [
|
||||
...source.slice(0, index),
|
||||
...source.slice(index + 1, source.length),
|
||||
];
|
||||
|
||||
return updatedData;
|
||||
}
|
||||
return source;
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setIsDeleteLoading(true);
|
||||
await deleteUser({
|
||||
userId: id,
|
||||
});
|
||||
onDelete();
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
setIsDeleteModalVisible(false);
|
||||
setIsDeleteLoading(false);
|
||||
} catch (error) {
|
||||
setIsDeleteLoading(false);
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onEditMemberDetails = async (): Promise<void> => {
|
||||
try {
|
||||
setIsUpdateLoading(true);
|
||||
await update({
|
||||
userId: id,
|
||||
displayName: updatedName,
|
||||
role,
|
||||
});
|
||||
onUpdateDetailsHandler();
|
||||
|
||||
if (role !== accessLevel) {
|
||||
notifications.success({
|
||||
message: 'User details updated successfully',
|
||||
description: 'The user details have been updated successfully.',
|
||||
});
|
||||
} else {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setIsUpdateLoading(false);
|
||||
setIsModalVisible(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
setIsUpdateLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space direction="horizontal">
|
||||
<Typography.Link
|
||||
onClick={(): void => onModalToggleHandler(setIsModalVisible, true)}
|
||||
>
|
||||
Edit
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
onClick={(): void => onModalToggleHandler(setIsDeleteModalVisible, true)}
|
||||
>
|
||||
Delete
|
||||
</Typography.Link>
|
||||
</Space>
|
||||
<Modal
|
||||
title="Edit member details"
|
||||
className="edit-member-details-modal"
|
||||
open={isModalVisible}
|
||||
onOk={(): void => onModalToggleHandler(setIsModalVisible, false)}
|
||||
onCancel={(): void => onModalToggleHandler(setIsModalVisible, false)}
|
||||
centered
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="back"
|
||||
onClick={(): void => onModalToggleHandler(setIsModalVisible, false)}
|
||||
type="default"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="Invite_team_members"
|
||||
onClick={onEditMemberDetails}
|
||||
type="primary"
|
||||
disabled={isUpdateLoading}
|
||||
loading={isUpdateLoading}
|
||||
>
|
||||
Update Details
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<EditMembersDetails
|
||||
{...{
|
||||
emailAddress,
|
||||
name: updatedName,
|
||||
role,
|
||||
setEmailAddress,
|
||||
setName: setUpdatedName,
|
||||
setRole,
|
||||
id,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Edit member details"
|
||||
open={isDeleteModalVisible}
|
||||
onOk={onDeleteHandler}
|
||||
onCancel={(): void => onModalToggleHandler(setIsDeleteModalVisible, false)}
|
||||
centered
|
||||
confirmLoading={isDeleteLoading}
|
||||
>
|
||||
<DeleteMembersDetails name={name} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Members(): JSX.Element {
|
||||
const { org } = useAppContext();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryFn: () => getAll(),
|
||||
queryKey: ['getOrgUser', org?.[0].id],
|
||||
});
|
||||
|
||||
const [dataSource, setDataSource] = useState<DataType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.data && Array.isArray(data.data)) {
|
||||
const updatedData: DataType[] = data?.data?.map((e) => ({
|
||||
accessLevel: e.role,
|
||||
email: e.email,
|
||||
id: String(e.id),
|
||||
joinedOn: String(e.createdAt),
|
||||
name: e.displayName,
|
||||
}));
|
||||
setDataSource(updatedData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Emails',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Access Level',
|
||||
dataIndex: 'accessLevel',
|
||||
key: 'accessLevel',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: 'Joined On',
|
||||
dataIndex: 'joinedOn',
|
||||
key: 'joinedOn',
|
||||
width: 60,
|
||||
render: (_, record): JSX.Element => {
|
||||
const { joinedOn } = record;
|
||||
return (
|
||||
<Typography>
|
||||
{dayjs(joinedOn).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
width: 80,
|
||||
render: (_, record): JSX.Element => (
|
||||
<UserFunction
|
||||
{...{
|
||||
accessLevel: record.accessLevel,
|
||||
email: record.email,
|
||||
joinedOn: record.joinedOn,
|
||||
name: record.name,
|
||||
id: record.id,
|
||||
setDataSource,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="members-container">
|
||||
<Typography.Title level={3}>
|
||||
Members{' '}
|
||||
{!isLoading && dataSource && (
|
||||
<div className="members-count"> ({dataSource.length}) </div>
|
||||
)}
|
||||
</Typography.Title>
|
||||
{!(error as APIError) && (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
loading={isLoading}
|
||||
bordered
|
||||
/>
|
||||
)}
|
||||
{(error as APIError) && <ErrorContent error={error as APIError} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
accessLevel: ROLES;
|
||||
joinedOn: string;
|
||||
}
|
||||
|
||||
interface UserFunctionProps extends DataType {
|
||||
setDataSource: Dispatch<SetStateAction<DataType[]>>;
|
||||
}
|
||||
|
||||
export default Members;
|
||||
@@ -1,248 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Space,
|
||||
TableColumnsType as ColumnsType,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import get from 'api/v1/invite/get';
|
||||
import deleteInvite from 'api/v1/invite/id/delete';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { PendingInvite } from 'types/api/user/getPendingInvites';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import InviteUserModal from '../InviteUserModal/InviteUserModal';
|
||||
import { TitleWrapper } from './styles';
|
||||
|
||||
function PendingInvitesContainer(): JSX.Element {
|
||||
const [
|
||||
isInviteTeamMemberModalOpen,
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const [state, setText] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const { user } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
notifications.error({
|
||||
message: state.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (state.value) {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [state.error, state.value, t, notifications]);
|
||||
|
||||
const { data, isLoading, error, isError, refetch } = useQuery({
|
||||
queryFn: get,
|
||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
||||
});
|
||||
|
||||
const [dataSource, setDataSource] = useState<DataProps[]>([]);
|
||||
|
||||
const toggleModal = useCallback(
|
||||
(value: boolean): void => {
|
||||
setIsInviteTeamMemberModalOpen(value);
|
||||
if (!value) {
|
||||
form.resetFields();
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const { hash } = useLocation();
|
||||
|
||||
const getParsedInviteData = useCallback(
|
||||
(payload: PendingInvite[] = []) =>
|
||||
payload?.map((data) => ({
|
||||
key: data.createdAt,
|
||||
name: data.name,
|
||||
id: data.id,
|
||||
email: data.email,
|
||||
accessLevel: data.role,
|
||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hash === INVITE_MEMBERS_HASH) {
|
||||
toggleModal(true);
|
||||
}
|
||||
}, [hash, toggleModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.data) {
|
||||
const parsedData = getParsedInviteData(data?.data || []);
|
||||
setDataSource(parsedData);
|
||||
}
|
||||
}, [data, getParsedInviteData]);
|
||||
|
||||
const onRevokeHandler = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await deleteInvite({
|
||||
id,
|
||||
});
|
||||
// remove from the client data
|
||||
const index = dataSource.findIndex((e) => e.id === id);
|
||||
if (index !== -1) {
|
||||
setDataSource([
|
||||
...dataSource.slice(0, index),
|
||||
...dataSource.slice(index + 1, dataSource.length),
|
||||
]);
|
||||
}
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataProps> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Emails',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: 'Access Level',
|
||||
dataIndex: 'accessLevel',
|
||||
key: 'accessLevel',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: 'Invite Link',
|
||||
dataIndex: 'inviteLink',
|
||||
key: 'Invite Link',
|
||||
ellipsis: true,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
width: 80,
|
||||
key: 'Action',
|
||||
render: (_, record): JSX.Element => (
|
||||
<Space direction="horizontal">
|
||||
<Typography.Link onClick={(): Promise<void> => onRevokeHandler(record.id)}>
|
||||
Revoke
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
onClick={(): void => {
|
||||
setText(record.inviteLink);
|
||||
}}
|
||||
>
|
||||
Copy Invite Link
|
||||
</Typography.Link>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="pending-invites-container-wrapper">
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
toggleModal={toggleModal}
|
||||
onClose={refetch}
|
||||
/>
|
||||
|
||||
<div className="pending-invites-container">
|
||||
<TitleWrapper>
|
||||
<Typography.Title level={3}>
|
||||
{t('pending_invites')}
|
||||
{dataSource && (
|
||||
<div className="members-count"> ({dataSource.length})</div>
|
||||
)}
|
||||
</Typography.Title>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
toggleModal(true);
|
||||
}}
|
||||
>
|
||||
{t('invite_members')}
|
||||
</Button>
|
||||
</Space>
|
||||
</TitleWrapper>
|
||||
{!isError && (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
loading={isLoading}
|
||||
bordered
|
||||
/>
|
||||
)}
|
||||
{isError && <ErrorContent error={error as APIError} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface InviteTeamMembersProps {
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
id: string;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
interface DataProps {
|
||||
key: number;
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
accessLevel: ROLES;
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
type Role = 'ADMIN' | 'VIEWER' | 'EDITOR';
|
||||
|
||||
export interface InviteMemberFormValues {
|
||||
members: {
|
||||
email: string;
|
||||
name: string;
|
||||
role: Role;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default PendingInvitesContainer;
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TitleWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -3,8 +3,6 @@ import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import AuthDomain from './AuthDomain';
|
||||
import DisplayName from './DisplayName';
|
||||
import Members from './Members';
|
||||
import PendingInvitesContainer from './PendingInvitesContainer';
|
||||
|
||||
import './OrganizationSettings.styles.scss';
|
||||
|
||||
@@ -23,9 +21,6 @@ function OrganizationSettings(): JSX.Element {
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<PendingInvitesContainer />
|
||||
|
||||
<Members />
|
||||
<AuthDomain />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { act, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import Members from '../Members';
|
||||
|
||||
describe('Organization Settings Page', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('render list of members', async () => {
|
||||
act(() => {
|
||||
render(<Members />);
|
||||
});
|
||||
|
||||
const title = await screen.findByText(/Members/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
|
||||
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
|
||||
});
|
||||
});
|
||||
|
||||
// this is required as our edit/delete logic is dependent on the index and it will break with pagination enabled
|
||||
it('render list of members without pagination', async () => {
|
||||
render(<Members />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
|
||||
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
|
||||
|
||||
expect(
|
||||
document.querySelector('.ant-table-pagination'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
17
frontend/src/container/OrganizationSettings/utils.ts
Normal file
17
frontend/src/container/OrganizationSettings/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface InviteTeamMembersProps {
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
id: string;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
type Role = 'ADMIN' | 'VIEWER' | 'EDITOR';
|
||||
|
||||
export interface InviteMemberFormValues {
|
||||
members: {
|
||||
email: string;
|
||||
name: string;
|
||||
role: Role;
|
||||
}[];
|
||||
}
|
||||
Reference in New Issue
Block a user