diff --git a/cypress/e2e/organizations/users.cy.ts b/cypress/e2e/organizations/users.cy.ts index e3879b34..45678d36 100644 --- a/cypress/e2e/organizations/users.cy.ts +++ b/cypress/e2e/organizations/users.cy.ts @@ -17,6 +17,8 @@ describe('Org Users page', () => { aliasMutation(req, 'removeUserFromGroup'); aliasMutation(req, 'addGroupToOrganization'); }); + + registerIdleHandler('idle'); }); it('Creates a group', () => { @@ -29,6 +31,7 @@ describe('Org Users page', () => { }); it('Deletes user', () => { + cy.waitForNetworkIdle('@idle', 500); users.doDeleteUser(testData.organizations.users.email); }); diff --git a/src/components/Organizations/GroupMembers/index.js b/src/components/Organizations/GroupMembers/index.js index acb5164b..7df6e937 100644 --- a/src/components/Organizations/GroupMembers/index.js +++ b/src/components/Organizations/GroupMembers/index.js @@ -75,8 +75,6 @@ const GroupMembers = ({ projects, refetch, }) => { - const duRegex = new RegExp('^default-user@' + groupName.replace('project-', '') + '$', 'g'); - const [projectModalOpen, setProjectModalOpen] = useState(false); const [selectedProject, setSelectedProject] = useState(''); @@ -356,6 +354,7 @@ const GroupMembers = ({ defaultViewOptions={{ type: 'user', selected: false, + selectedOnZeroCount: true, }} />
diff --git a/src/components/Organizations/PaginatedTable/PaginatedTable.tsx b/src/components/Organizations/PaginatedTable/PaginatedTable.tsx index 9389fc14..f7252d18 100644 --- a/src/components/Organizations/PaginatedTable/PaginatedTable.tsx +++ b/src/components/Organizations/PaginatedTable/PaginatedTable.tsx @@ -17,12 +17,12 @@ import { TableRow, } from './Styles'; +interface NestedData { + [key: string]: string | Record | string[]; +} + type DataType = { - [key: string]: - | string - | Record - | Record | string[]> - | Array>; + [key: string]: string | NestedData | Record | Array>; name: string; id: string; }; @@ -38,8 +38,13 @@ interface Props { disableUrlMutation?: boolean; defaultViewOptions?: { type: 'group' | 'user'; - selected: boolean; - }; + } & ( + | { selected: true } + | { + selected: false; + selectedOnZeroCount?: boolean; + } + ); numericSortOptions?: { key?: string; displayName: string; @@ -100,9 +105,34 @@ const PaginatedTable: FC = ({ const [unfilteredData, setUnfilteredData] = useState(data); - const [defaultsSelected, setDefaultsSelected] = useState( - (defaultViewOptions && defaultViewOptions.selected) || false - ); + const filteredDataWithoutDefaults = useMemo(() => { + let filtered = unfilteredData; + + if (defaultViewOptions) { + if (defaultViewOptions.type === 'group') { + filtered = filtered.filter(dataItem => dataItem.type !== 'project-default-group'); + } + if (defaultViewOptions.type === 'user') { + filtered = filtered.filter(dataItem => { + //@ts-ignore + const filterItem = dataItem.email ? dataItem.email : (dataItem.user.email as string); + return !(filterItem as string).startsWith('default-user'); + }); + } + } + + return filtered; + }, [defaultViewOptions, unfilteredData]); + + const [defaultsSelected, setDefaultsSelected] = useState(() => { + if (defaultViewOptions?.selected) { + return true; + } + if (defaultViewOptions?.selectedOnZeroCount && filteredDataWithoutDefaults.length === 0) { + return true; + } + return false; + }); useEffect(() => { setUnfilteredData(data); @@ -321,6 +351,31 @@ const PaginatedTable: FC = ({ const startPage = Math.max(currentPage - Math.floor(maxPagination / 2), 1); const endPage = Math.min(startPage + maxPagination - 1, totalPages); + const systemDefaultCount = useMemo(() => { + let count = 0; + + if (defaultViewOptions) { + if (defaultViewOptions?.type === 'group') { + count = unfilteredData.filter(dataItem => dataItem.type === 'project-default-group').length; + } + if (defaultViewOptions?.type === 'user') { + count = unfilteredData.filter(dataItem => { + let filterItem = ''; + + if (dataItem.email) { + filterItem = dataItem.email as string; + } + if (dataItem.user && typeof dataItem.user === 'object' && 'email' in dataItem.user) { + filterItem = dataItem.user.email as string; + } + + return filterItem.startsWith('default-user'); + }).length; + } + } + return count; + }, [defaultViewOptions, unfilteredData]); + return ( @@ -333,7 +388,7 @@ const PaginatedTable: FC = ({ )} {defaultViewOptions ? ( - {defaultViewOptions.type === 'group' ? 'Show system groups' : 'Show default users'} + {defaultViewOptions.type === 'group' ? 'Show system groups' : 'Show default users'} ({systemDefaultCount}) { @@ -24,10 +27,15 @@ const ProjectGroupSkeleton = () => { ); return ( -
- - +
+ + + + + +
+
{[...Array(numberOfFields)].map((_, idx) => groupsSkeleton(idx))}
); diff --git a/src/components/Organizations/ProjectGroupMembers/Styles.tsx b/src/components/Organizations/ProjectGroupMembers/Styles.tsx index 6187f589..3a66cea0 100644 --- a/src/components/Organizations/ProjectGroupMembers/Styles.tsx +++ b/src/components/Organizations/ProjectGroupMembers/Styles.tsx @@ -4,6 +4,15 @@ import styled from 'styled-components'; import { sharedTableStyles } from '../SharedStyles'; export const StyledGroupMembers = styled.div` + .project-wrapper { + margin-top: 20px; + } + .skeleton { + margin-top: 40px; + input { + padding-left: 30px; + } + } .default-group-label { color: ${color.white}; display: inline-block; diff --git a/src/components/Organizations/ProjectGroupMembers/index.js b/src/components/Organizations/ProjectGroupMembers/index.js index c7b14b6b..e61b6670 100644 --- a/src/components/Organizations/ProjectGroupMembers/index.js +++ b/src/components/Organizations/ProjectGroupMembers/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Mutation } from 'react-apollo'; import { EyeOutlined } from '@ant-design/icons'; @@ -8,7 +8,8 @@ import OrgGroupsLink from 'components/link/Organizations/Group'; import gql from 'graphql-tag'; import AddGroupToProject from '../AddGroupToProject'; -import { Checkbox } from '../PaginatedTable/Styles'; +import PaginatedTable from '../PaginatedTable/PaginatedTable'; +import { TableActions, Tag } from '../SharedStyles'; import { StyledGroupMembers } from './Styles'; const REMOVE_GROUP_FROM_PROJECT = gql` @@ -31,87 +32,62 @@ const ProjectGroupMembers = ({ refresh, orgFriendlyName, }) => { - const [searchInput, setSearchInput] = useState(''); - - const [showDefaults, setShowDefaults] = useState(false); - - const filteredGroups = showDefaults ? groups : groups.filter(group => group.type !== 'project-default-group'); - - const filteredMembers = filteredGroups.filter(key => { - const sortByName = key.name.toLowerCase().includes(searchInput.toLowerCase()); - return ['name', 'role', '__typename'].includes(key) ? false : true && sortByName; - }); - - useEffect(() => { - // tick the "show system groups" box if all groups are of that type. - const allDefaults = filteredMembers.every(group => group.type === 'project-default-group'); - if (allDefaults) setShowDefaults(true); - }, []); - - return ( - -
- - - Show system groups - setShowDefaults(checked)} - /> - - - setSearchInput(e.target.value)} - placeholder="Type to search" - disabled={groups.length === 0} - /> -
- -
- {!filteredMembers.length &&
No groups
} - {searchInput && !filteredMembers.length &&
No groups matching "{searchInput}"
} - {filteredMembers.map(group => ( -
-
- - {group.name} - -
-
Members: {group.memberCount}
- -
- {group.type.includes('project-default-group') && ( - + const Columns = [ + { + width: '50%', + key: 'name', + render: g => { + return ( + + + {g.name}{' '} + {g.type === 'project-default-group' && ( + + SYSTEM GROUP + )} -
+ + + ); + }, + }, -
- -
- <> - - - - -
-
- {/* even though we can't prevent users from removing the project default group from the api, we can make it harder to do from the ui */} - {!group.name.includes('project-' + projectName.toLowerCase()) ? ( + { + width: '15%', + key: 'members', + render: g => { + return typeof g.memberCount !== 'undefined' && Members: {g.memberCount} ; + }, + }, + { + width: '35%', + key: 'actions', + render: function (g) { + return ( + + +
+ <> + + + + +
+
+ + {g.type !== 'project-default-group' && ( + <>
{(removeGroupFromProject, { _, called, error }) => { @@ -123,13 +99,13 @@ const ProjectGroupMembers = ({ loading={called} info={{ type: 'group', - deleteName: group.name, + deleteName: g.name, projectName: projectName, }} onRemove={() => { removeGroupFromProject({ variables: { - groupName: group.name, + groupName: g.name, projectName: projectName, }, }).then(refresh); @@ -139,13 +115,31 @@ const ProjectGroupMembers = ({ }}
- ) : ( -
- )} -
-
- ))} + + )} + + ); + }, + }, + ]; + + return ( + +
+
+ { @@ -21,10 +24,15 @@ const ProjectNotificationsSkeleton = () => { ); return ( -
- - +
+ + + + + +
+
{[...Array(numberOfFields)].map((_, idx) => notificationsSkeleton(idx))}
diff --git a/src/components/Organizations/ProjectNotifications/Styles.tsx b/src/components/Organizations/ProjectNotifications/Styles.tsx index f492cdfa..fe2cf0f8 100644 --- a/src/components/Organizations/ProjectNotifications/Styles.tsx +++ b/src/components/Organizations/ProjectNotifications/Styles.tsx @@ -13,9 +13,15 @@ export const StyledProjectNotifications = styled.div` border-radius: 4px; box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); } + .skeleton { + margin-top: 40px; + input { + padding-left: 30px; + } + } .name { font-family: 'roboto', sans-serif; - font-size: 0.8125rem; + font-size: 1rem; height: 100%; display: flex; align-items: center; diff --git a/src/components/Organizations/ProjectNotifications/index.js b/src/components/Organizations/ProjectNotifications/index.js index 6647e062..8bcb7893 100644 --- a/src/components/Organizations/ProjectNotifications/index.js +++ b/src/components/Organizations/ProjectNotifications/index.js @@ -1,13 +1,14 @@ import React, { useState } from 'react'; import { Mutation } from 'react-apollo'; -import { EditOutlined } from '@ant-design/icons'; +import { EditOutlined, SearchOutlined } from '@ant-design/icons'; import { Tooltip } from 'antd'; import RemoveProjectGroupConfirm from 'components/Organizations/RemoveProjectGroupConfirm'; import OrgNotificationsLink from 'components/link/Organizations/Notifications'; import gql from 'graphql-tag'; import AddNotificationToProject from '../AddNotificationToProject'; +import { SearchBar } from '../Orgheader/Styles'; import { TableActions } from '../SharedStyles'; import { StyledProjectNotifications } from './Styles'; @@ -45,17 +46,21 @@ const ProjectNotifications = ({ return ( -
- - setSearchInput(e.target.value)} - placeholder="Type to search" - disabled={notifications.length === 0} - /> +
+ + + + + setSearchInput(e.target.value)} + placeholder="Type to search" + disabled={notifications.length === 0} + /> +
diff --git a/src/components/Organizations/Projects/Styles.tsx b/src/components/Organizations/Projects/Styles.tsx index a8ec3e21..b60467b2 100644 --- a/src/components/Organizations/Projects/Styles.tsx +++ b/src/components/Organizations/Projects/Styles.tsx @@ -157,7 +157,29 @@ export const OrgProjectWrapper = styled.div` padding-inline: 12px; display: flex; flex-direction: column; - + .tableheader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + label { + font-weight: 600; + } + .icon { + transform: translateY(6px); + } + input { + height: 40px; + min-width: 290px; + font-size: 0.95rem; + &::placeholder { + color: ${props => (props.theme.colorScheme === 'dark' ? '#fff' : '#5f6f7a')}; + line-height: 20px; + font-size: 0.95rem; + font-weight: 500; + } + } + } .rightside-button { display: flex; justify-content: flex-end; diff --git a/src/components/Organizations/User/index.tsx b/src/components/Organizations/User/index.tsx index e86be134..fc210da2 100644 --- a/src/components/Organizations/User/index.tsx +++ b/src/components/Organizations/User/index.tsx @@ -339,6 +339,7 @@ const User: FC = ({ user, organizationName, organizationId, refetch, emptyText="No groups" defaultViewOptions={{ selected: false, + selectedOnZeroCount: true, type: 'group', }} /> diff --git a/src/components/Organizations/Users/index.js b/src/components/Organizations/Users/index.js index e3579f8c..641f5d7d 100644 --- a/src/components/Organizations/Users/index.js +++ b/src/components/Organizations/Users/index.js @@ -36,7 +36,6 @@ const DELETE_USER = gql` * The primary list of users. */ const Users = ({ users = [], organization, organizationId, organizationName, refetch, orgFriendlyName }) => { - const [userModalOpen, setUserModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(''); const [addUserModalOpen, setAddUserModalOpen] = useState(false); @@ -52,7 +51,6 @@ const Users = ({ users = [], organization, organizationId, organizationName, ref const closeUserModal = () => { setSelectedUser(''); - setUserModalOpen(false); }; useEffect(() => { @@ -202,6 +200,7 @@ const Users = ({ users = [], organization, organizationId, organizationName, ref defaultViewOptions={{ type: 'user', selected: false, + selectedOnZeroCount: true, }} labelText="Users" emptyText="No Users"