diff --git a/web/e2e/settings/integrations.spec.ts b/web/e2e/settings/integrations.spec.ts index ba25524103..99cac30c87 100644 --- a/web/e2e/settings/integrations.spec.ts +++ b/web/e2e/settings/integrations.spec.ts @@ -12,7 +12,6 @@ test("Integration CRUD and searching has succeeded", async ({ reearth, page }) = await reearth.goto("/", { waitUntil: "domcontentloaded" }); await page.getByText("My Integrations").click(); - await page.locator("div").filter({ hasText: "Create new integration" }).nth(4).click(); await page .locator("div") .filter({ hasText: /^Create new integration$/ }) diff --git a/web/e2e/settings/member.spec.ts b/web/e2e/settings/member.spec.ts index 1951405ca7..2d4ac7b297 100644 --- a/web/e2e/settings/member.spec.ts +++ b/web/e2e/settings/member.spec.ts @@ -13,8 +13,8 @@ test.afterEach(async ({ page }) => { test("Searching current members has succeeded", async ({ page }) => { await page.getByText("Member").click(); await expect(page.getByRole("cell", { name: "OWNER" })).toBeVisible(); - await page.getByPlaceholder("search for a member").click(); - await page.getByPlaceholder("search for a member").fill("no member"); + await page.getByPlaceholder("input search text").click(); + await page.getByPlaceholder("input search text").fill("no member"); await page.getByRole("button", { name: "search" }).click(); await expect(page.getByRole("cell", { name: "OWNER" })).toBeHidden(); await page.getByRole("button", { name: "close-circle" }).click(); diff --git a/web/src/components/molecules/Integration/IntegrationTable/index.tsx b/web/src/components/molecules/Integration/IntegrationTable/index.tsx index dfd59bc637..224ca913f5 100644 --- a/web/src/components/molecules/Integration/IntegrationTable/index.tsx +++ b/web/src/components/molecules/Integration/IntegrationTable/index.tsx @@ -26,6 +26,11 @@ interface Props { onIntegrationSettingsModalOpen: (integrationMember: IntegrationMember) => void; setSelection: (input: { selectedRowKeys: Key[] }) => void; onIntegrationRemove: (integrationIds: string[]) => Promise; + page: number; + pageSize: number; + onTableChange: (page: number, pageSize: number) => void; + loading: boolean; + onReload: () => void; } const IntegrationTable: React.FC = ({ @@ -36,6 +41,11 @@ const IntegrationTable: React.FC = ({ onIntegrationSettingsModalOpen, setSelection, onIntegrationRemove, + page, + pageSize, + onTableChange, + loading, + onReload, }) => { const t = useT(); @@ -93,6 +103,15 @@ const IntegrationTable: React.FC = ({ [onSearchTerm, t], ); + const pagination = useMemo( + () => ({ + showSizeChanger: true, + current: page, + pageSize: pageSize, + }), + [page, pageSize], + ); + const rowSelection: TableRowSelection = useMemo( () => ({ selectedRowKeys: selection.selectedRowKeys, @@ -106,30 +125,27 @@ const IntegrationTable: React.FC = ({ [selection, setSelection], ); - const AlertOptions = useCallback( + const alertOptions = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props: any) => { - return ( - - - {t("Deselect")} - - onIntegrationRemove?.(props.selectedRowKeys)}> - {t("Remove")} - - - ); - }, + (props: any) => ( + + + {t("Deselect")} + + onIntegrationRemove(props.selectedRowKeys)}> + {t("Remove")} + + + ), [onIntegrationRemove, t], ); const options = useMemo( () => ({ fullScreen: true, - reload: false, - setting: true, + reload: onReload, }), - [], + [onReload], ); return ( @@ -160,18 +176,24 @@ const IntegrationTable: React.FC = ({ )}> - + + { + onTableChange(pagination.current ?? 1, pagination.pageSize ?? 10); + }} + /> + ); @@ -179,6 +201,7 @@ const IntegrationTable: React.FC = ({ const Wrapper = styled.div` height: 100%; + padding: 16px 16px 0; `; const EmptyTableWrapper = styled.div` @@ -206,8 +229,6 @@ const Title = styled.h1` color: #000; `; -export default IntegrationTable; - const DeselectButton = styled.a` display: flex; align-items: center; @@ -225,3 +246,11 @@ const StyledIcon = styled(Icon)` color: #1890ff; font-size: 18px; `; + +const TableWrapper = styled.div` + background-color: #fff; + border-top: 1px solid #f0f0f0; + height: calc(100% - 72px); +`; + +export default IntegrationTable; diff --git a/web/src/components/molecules/Member/MemberRoleModal/index.tsx b/web/src/components/molecules/Member/MemberRoleModal/index.tsx index 73ddc663da..b5fe2c0f50 100644 --- a/web/src/components/molecules/Member/MemberRoleModal/index.tsx +++ b/web/src/components/molecules/Member/MemberRoleModal/index.tsx @@ -57,7 +57,7 @@ const MemberRoleModal: React.FC = ({ open, member, onClose, onSubmit }) = }}> Promise; + handleMemberRemoveFromWorkspace: (userIds: string[]) => Promise; handleSearchTerm: (term?: string) => void; handleRoleModalOpen: (member: UserMember) => void; handleMemberAddModalOpen: () => void; workspaceUserMembers?: UserMember[]; + selection: { + selectedRowKeys: Key[]; + }; + setSelection: (input: { selectedRowKeys: Key[] }) => void; + page: number; + pageSize: number; + onTableChange: (page: number, pageSize: number) => void; + loading: boolean; + onReload: () => void; } const MemberTable: React.FC = ({ @@ -61,13 +44,20 @@ const MemberTable: React.FC = ({ handleRoleModalOpen, handleMemberAddModalOpen, workspaceUserMembers, + selection, + setSelection, + page, + pageSize, + onTableChange, + loading, + onReload, }) => { const t = useT(); const { confirm } = Modal; const handleMemberDelete = useCallback( - (member: UserMember) => { + (userIds: string[]) => { confirm({ title: t("Are you sure to remove this member?"), icon: , @@ -75,35 +65,124 @@ const MemberTable: React.FC = ({ "Remove this member from workspace means this member will not view any content of this workspace.", ), onOk() { - handleMemberRemoveFromWorkspace(member?.userId); + handleMemberRemoveFromWorkspace(userIds); }, }); }, [confirm, handleMemberRemoveFromWorkspace, t], ); + const columns = [ + { + title: t("Name"), + dataIndex: "name", + key: "name", + width: 256, + minWidth: 256, + }, + { + title: t("Thumbnail"), + dataIndex: "thumbnail", + key: "thumbnail", + width: 128, + minWidth: 128, + }, + { + title: t("Email"), + dataIndex: "email", + key: "email", + width: 256, + minWidth: 256, + }, + { + title: t("Role"), + dataIndex: "role", + key: "role", + width: 128, + minWidth: 128, + }, + { + title: t("Action"), + dataIndex: "action", + key: "action", + width: 128, + minWidth: 128, + }, + ]; + const dataSource = workspaceUserMembers?.map(member => ({ - key: member.userId, + id: member.userId, name: member.user.name, thumbnail: , email: member.user.email, role: member.role, - action: ( - <> - {member.userId !== me?.id && ( - - )} - {member.userId !== me?.id && ( - handleMemberDelete(member)} disabled={!owner}> - {t("Remove")} - - )} - + action: member.userId !== me?.id && ( + handleRoleModalOpen(member)} disabled={!owner}> + {t("Change Role?")} + ), })); + const toolbar: ListToolBarProps = useMemo( + () => ({ + search: ( + { + handleSearchTerm(value); + }} + /> + ), + }), + [handleSearchTerm, t], + ); + + const pagination = useMemo( + () => ({ + showSizeChanger: true, + current: page, + pageSize: pageSize, + }), + [page, pageSize], + ); + + const rowSelection: TableRowSelection = useMemo( + () => ({ + selectedRowKeys: selection.selectedRowKeys, + onChange: (selectedRowKeys: Key[]) => { + setSelection({ + ...selection, + selectedRowKeys: selectedRowKeys, + }); + }, + }), + [selection, setSelection], + ); + + const alertOptions = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any) => ( + + + {t("Deselect")} + + handleMemberDelete(props.selectedRowKeys)}> + {t("Remove")} + + + ), + [handleMemberDelete, t], + ); + + const options = useMemo( + () => ({ + fullScreen: true, + reload: onReload, + }), + [onReload], + ); + return ( = ({ } /> - - + { + onTableChange(pagination.current ?? 1, pagination.pageSize ?? 10); + }} /> - - + ); }; const PaddedContent = styled(Content)` - margin: 16px; - background-color: #fff; - min-height: 100%; + padding: 16px 16px 0; + height: 100%; `; -const ActionHeader = styled(Content)` +const TableWrapper = styled.div` + background-color: #fff; border-top: 1px solid #f0f0f0; - padding: 16px; - display: flex; - justify-content: space-between; + height: calc(100% - 72px); `; -const StyledButton = styled(Button)` - margin-left: 8px; +const RoleButton = styled(Button)` + padding-left: 0; `; -const StyledSearch = styled(Search)` - width: 264px; +const DeselectButton = styled.a` + display: flex; + align-items: center; + gap: 8px; `; -const StyledTable = styled(Table)` - padding: 24px; - overflow-x: auto; +const DeleteButton = styled.a` + color: #ff7875; + :hover { + color: #ff7875b3; + } `; export default MemberTable; diff --git a/web/src/components/molecules/MyIntegrations/Content/index.tsx b/web/src/components/molecules/MyIntegrations/Content/index.tsx index 0f8dbf3661..305e64eb96 100644 --- a/web/src/components/molecules/MyIntegrations/Content/index.tsx +++ b/web/src/components/molecules/MyIntegrations/Content/index.tsx @@ -94,8 +94,9 @@ const MyIntegrationContent: React.FC = ({ }; const MyIntegrationWrapper = styled.div` - min-height: 100%; + min-height: calc(100% - 16px); background-color: #fff; + margin: 16px 16px 0; `; const MyIntegrationTabs = styled(Tabs)` diff --git a/web/src/components/molecules/MyIntegrations/List/index.tsx b/web/src/components/molecules/MyIntegrations/List/index.tsx index ac79d79380..65480b6251 100644 --- a/web/src/components/molecules/MyIntegrations/List/index.tsx +++ b/web/src/components/molecules/MyIntegrations/List/index.tsx @@ -37,8 +37,9 @@ const MyIntegrationList: React.FC = ({ }; const Wrapper = styled.div` + min-height: calc(100% - 16px); background: #fff; - min-height: 100%; + margin: 16px 16px 0; `; const ListWrapper = styled.div` diff --git a/web/src/components/molecules/ProjectOverview/ModelCard.tsx b/web/src/components/molecules/ProjectOverview/ModelCard.tsx index 062778075d..a278bdc8b5 100644 --- a/web/src/components/molecules/ProjectOverview/ModelCard.tsx +++ b/web/src/components/molecules/ProjectOverview/ModelCard.tsx @@ -36,6 +36,7 @@ const ModelCard: React.FC = ({ key: "delete", label: t("Delete"), onClick: () => onModelDeletionModalOpen(model), + danger: true, }, ], [t, model, onModelUpdateModalOpen, onModelDeletionModalOpen], diff --git a/web/src/components/organisms/Settings/Integration/hooks.ts b/web/src/components/organisms/Settings/Integration/hooks.ts index e3ddd4aaa8..9a1bc2e540 100644 --- a/web/src/components/organisms/Settings/Integration/hooks.ts +++ b/web/src/components/organisms/Settings/Integration/hooks.ts @@ -21,7 +21,12 @@ export default (workspaceId?: string) => { const [selection, setSelection] = useState<{ selectedRowKeys: Key[] }>({ selectedRowKeys: [], }); - const { data, refetch } = useGetMeQuery(); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const { data, refetch, loading } = useGetMeQuery({ + fetchPolicy: "cache-and-network", + notifyOnNetworkStatusChange: true, + }); const t = useT(); const workspaces = useMemo(() => data?.me?.workspaces, [data?.me?.workspaces]); @@ -122,32 +127,41 @@ export default (workspaceId?: string) => { const [removeIntegrationFromWorkspaceMutation] = useRemoveIntegrationFromWorkspaceMutation(); const handleIntegrationRemove = useCallback( - (integrationIds: string[]) => - (async () => { - if (!workspaceId) return; - const results = await Promise.all( - integrationIds.map(async integrationId => { - const result = await removeIntegrationFromWorkspaceMutation({ - variables: { workspaceId, integrationId }, - refetchQueries: ["GetMe"], - }); - if (result.errors) { - Notification.error({ message: t("Failed to delete one or more intagrations.") }); - } - }), - ); - if (results) { - Notification.success({ - message: t("One or more integrations were successfully deleted!"), + async (integrationIds: string[]) => { + if (!workspaceId) return; + const results = await Promise.all( + integrationIds.map(async integrationId => { + const result = await removeIntegrationFromWorkspaceMutation({ + variables: { workspaceId, integrationId }, + refetchQueries: ["GetMe"], }); - setSelection({ selectedRowKeys: [] }); - } - })(), + if (result.errors) { + Notification.error({ message: t("Failed to delete one or more intagrations.") }); + } + }), + ); + if (results) { + Notification.success({ + message: t("One or more integrations were successfully deleted!"), + }); + setSelection({ selectedRowKeys: [] }); + } + }, [t, removeIntegrationFromWorkspaceMutation, workspaceId], ); const handleSearchTerm = useCallback((term?: string) => { setSearchTerm(term); + setPage(1); + }, []); + + const handleReload = useCallback(() => { + refetch(); + }, [refetch]); + + const handleTableChange = useCallback((page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); }, []); return { @@ -168,5 +182,10 @@ export default (workspaceId?: string) => { selection, handleSearchTerm, setSelection, + page, + pageSize, + handleTableChange, + loading, + handleReload, }; }; diff --git a/web/src/components/organisms/Settings/Integration/index.tsx b/web/src/components/organisms/Settings/Integration/index.tsx index f86c1808fc..6346966fad 100644 --- a/web/src/components/organisms/Settings/Integration/index.tsx +++ b/web/src/components/organisms/Settings/Integration/index.tsx @@ -12,21 +12,26 @@ const Integration: React.FC = () => { const { integrations, workspaceIntegrationMembers, - handleIntegrationConnect, handleIntegrationConnectModalClose, handleIntegrationConnectModalOpen, addLoading, + handleIntegrationConnect, + handleIntegrationRemove, integrationConnectModalShown, + handleUpdateIntegration, + updateLoading, handleIntegrationSettingsModalClose, handleIntegrationSettingsModalOpen, integrationSettingsModalShown, - handleUpdateIntegration, - updateLoading, selectedIntegrationMember, selection, handleSearchTerm, setSelection, - handleIntegrationRemove, + page, + pageSize, + handleTableChange, + loading, + handleReload, } = useHooks(workspaceId); return ( @@ -39,6 +44,11 @@ const Integration: React.FC = () => { onIntegrationConnectModalOpen={handleIntegrationConnectModalOpen} setSelection={setSelection} onIntegrationRemove={handleIntegrationRemove} + page={page} + pageSize={pageSize} + onTableChange={handleTableChange} + loading={loading} + onReload={handleReload} /> { const [selectedMember, setSelectedMember] = useState(); const [searchTerm, setSearchTerm] = useState(); const [owner, setOwner] = useState(false); + const [selection, setSelection] = useState<{ selectedRowKeys: Key[] }>({ + selectedRowKeys: [], + }); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); const t = useT(); const handleSearchTerm = useCallback((term?: string) => { setSearchTerm(term); + setPage(1); }, []); const [searchedUser, changeSearchedUser] = useState(); const [searchedUserList, changeSearchedUserList] = useState([]); - const { data } = useGetWorkspacesQuery(); + const { data, refetch, loading } = useGetWorkspacesQuery({ + fetchPolicy: "cache-and-network", + notifyOnNetworkStatusChange: true, + }); const me = useMemo( () => ({ id: data?.me?.id, myWorkspace: data?.me?.myWorkspace?.id }), [data?.me?.id, data?.me?.myWorkspace?.id], @@ -58,10 +67,8 @@ export default () => { }, [isOwner]); useEffect(() => { - if (workspaceId && !currentWorkspace) { - setWorkspace(workspaces?.find(workspace => workspace.id === workspaceId)); - } - }, [currentWorkspace, setWorkspace, workspaces, data?.me, workspaceId]); + setWorkspace(workspaces?.find(workspace => workspace.id === workspaceId)); + }, [setWorkspace, workspaces, data?.me, workspaceId]); const [searchUserQuery, { data: searchUserData }] = useGetUserBySearchLazyQuery({ fetchPolicy: "no-cache", @@ -161,19 +168,27 @@ export default () => { const [removeMemberFromWorkspaceMutation] = useRemoveMemberFromWorkspaceMutation(); const handleMemberRemoveFromWorkspace = useCallback( - async (userId: string) => { + async (userIds: string[]) => { if (!workspaceId) return; - const result = await removeMemberFromWorkspaceMutation({ - variables: { workspaceId, userId }, - refetchQueries: ["GetWorkspaces"], - }); - const workspace = result.data?.removeUserFromWorkspace?.workspace; - if (result.errors || !workspace) { - Notification.error({ message: t("Failed to delete member from the workspace.") }); - return; + const results = await Promise.all( + userIds.map(async userId => { + const result = await removeMemberFromWorkspaceMutation({ + variables: { workspaceId, userId }, + refetchQueries: ["GetWorkspaces"], + }); + if (result.errors) { + Notification.error({ + message: t("Failed to remove member(s) from the workspace."), + }); + } + }), + ); + if (results) { + Notification.success({ + message: t("Successfully removed member(s) from the workspace!"), + }); + setSelection({ selectedRowKeys: [] }); } - setWorkspace(fromGraphQLWorkspace(workspace as GQLWorkspace)); - Notification.success({ message: t("Successfully removed member from the workspace!") }); }, [workspaceId, removeMemberFromWorkspaceMutation, setWorkspace, t], ); @@ -198,6 +213,15 @@ export default () => { setSelectedMember(undefined); }, []); + const handleReload = useCallback(() => { + refetch(); + }, [refetch]); + + const handleTableChange = useCallback((page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, []); + return { me, owner, @@ -219,5 +243,12 @@ export default () => { handleMemberAddModalOpen, MemberAddModalShown, workspaceUserMembers, + selection, + setSelection, + page, + pageSize, + handleTableChange, + loading, + handleReload, }; }; diff --git a/web/src/components/organisms/Settings/Members/index.tsx b/web/src/components/organisms/Settings/Members/index.tsx index eeaa4fdc93..4f0599f3eb 100644 --- a/web/src/components/organisms/Settings/Members/index.tsx +++ b/web/src/components/organisms/Settings/Members/index.tsx @@ -26,6 +26,13 @@ const Members: React.FC = () => { handleMemberAddModalOpen, MemberAddModalShown, workspaceUserMembers, + selection, + setSelection, + page, + pageSize, + handleTableChange, + loading, + handleReload, } = useHooks(); return ( @@ -38,6 +45,13 @@ const Members: React.FC = () => { handleRoleModalOpen={handleRoleModalOpen} handleMemberAddModalOpen={handleMemberAddModalOpen} workspaceUserMembers={workspaceUserMembers} + selection={selection} + setSelection={setSelection} + page={page} + pageSize={pageSize} + onTableChange={handleTableChange} + loading={loading} + onReload={handleReload} /> {selectedMember && (