Skip to content

Commit

Permalink
feat: create edit user group page
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyWMitchell committed Dec 13, 2024
1 parent 999361a commit 75b4e66
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 3 deletions.
8 changes: 8 additions & 0 deletions cypress/e2e/users-and-user-groups.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,12 @@ describe('Users and User Groups page', () => {
cy.get('[data-ouia-component-id="iam-user-groups-table-actions-dropdown-action-0"]').click();
cy.get('[data-ouia-component-id="add-group-wizard"]').should('exist');
});

it('should be able to open Edit User Group page from row actions', () => {
cy.get('[data-ouia-component-id="user-groups-tab-button"]').click();
cy.get('[data-ouia-component-id^="iam-user-groups-table-table-td-0-7"]').click();
cy.get('[data-ouia-component-id^="iam-user-groups-table-table-td-0-7"] button').contains('Edit user group').click();
cy.url().should('include', '/iam/access-management/users-and-user-groups/edit-group');
cy.get('[data-ouia-component-id="edit-user-group-form"]').should('be.visible');
});
});
5 changes: 5 additions & 0 deletions src/Routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const RemoveServiceAccountFromGroup = lazy(() => import('./smart-components/grou
const QuickstartsTest = lazy(() => import('./smart-components/quickstarts/quickstarts-test'));

const UsersAndUserGroups = lazy(() => import('./smart-components/access-management/users-and-user-groups'));
const EditUserGroup = lazy(() => import('./smart-components/access-management/EditUserGroup'));

const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommonAuthModel }: Record<string, boolean>) => [
{
Expand All @@ -57,6 +58,10 @@ const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommon
},
],
},
{
path: pathnames['users-and-user-groups-edit-group'].path,
element: EditUserGroup,
},
{
path: pathnames.overview.path,
element: isWorkspacesFlag ? WorkspacesOverview : Overview,
Expand Down
2 changes: 1 addition & 1 deletion src/redux/reducers/group-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface GroupStore {
filters: any;
pagination: { count: number };
};
selectedGroup: {
selectedGroup: Group & {
addRoles: any;
members: { meta: PaginationDefaultI; data?: any[] };
serviceAccounts: { meta: PaginationDefaultI; data?: any[] };
Expand Down
114 changes: 114 additions & 0 deletions src/smart-components/access-management/EditUserGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import ContentHeader from '@patternfly/react-component-groups/dist/esm/ContentHeader';
import { PageSection, PageSectionVariants } from '@patternfly/react-core';
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import Messages from '../../Messages';
import { FormRenderer, componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer';
import componentMapper from '@data-driven-forms/pf4-component-mapper/component-mapper';
import { FormTemplate } from '@data-driven-forms/pf4-component-mapper';
import { useDispatch, useSelector } from 'react-redux';
import { fetchGroup, fetchGroups, updateGroup } from '../../redux/actions/group-actions';
import { RBACStore } from '../../redux/store';
import { useNavigate, useParams } from 'react-router-dom';
import { EditGroupUsersAndServiceAccounts } from './EditUserGroupUsersAndServiceAccounts';

export const EditUserGroup: React.FunctionComponent = () => {
const intl = useIntl();
const dispatch = useDispatch();
const params = useParams();
const groupId = params.groupId;
const navigate = useNavigate();

const group = useSelector((state: RBACStore) => state.groupReducer?.selectedGroup);
const allGroups = useSelector((state: RBACStore) => state.groupReducer?.groups?.data || []);

useEffect(() => {
dispatch(fetchGroups({ limit: 1000, offset: 0, orderBy: 'name', usesMetaInURL: true }));
if (groupId) {
dispatch(fetchGroup(groupId));
}
}, [dispatch, groupId]);

const schema = {
fields: [
{
name: 'name',
label: intl.formatMessage(Messages.name),
component: componentTypes.TEXT_FIELD,
validate: [
{ type: validatorTypes.REQUIRED },
(value: string) => {
if (value === group?.name) {
return undefined;
}

const isDuplicate = allGroups.some(
(existingGroup) => existingGroup.name.toLowerCase() === value?.toLowerCase() && existingGroup.uuid !== groupId
);

return isDuplicate ? intl.formatMessage(Messages.groupNameTakenTitle) : undefined;
},
],
initialValue: group?.name,
},
{
name: 'description',
label: intl.formatMessage(Messages.description),
component: componentTypes.TEXTAREA,
initialValue: group?.description,
},
{
name: 'users-and-service-accounts',
component: 'users-and-service-accounts',
groupId: groupId,
},
],
};

const returnToPreviousPage = () => {
navigate(-1);
};

const handleSubmit = async (values: Record<string, any>) => {
if (values.name !== group?.name || values.description !== group?.description) {
dispatch(updateGroup({ uuid: groupId, name: values.name, description: values.description }));
console.log(`Dispatched update group with name: ${values.name} and description: ${values.description}`);
}
if (values['users-and-service-accounts']) {
const { users, serviceAccounts } = values['users-and-service-accounts'];
if (users.added.length > 0) {
console.log(`Users added: ${users.added}`);
}
if (users.removed.length > 0) {
console.log(`Users removed: ${users.removed}`);
}
if (serviceAccounts.added.length > 0) {
console.log(`Service accounts added: ${serviceAccounts.added}`);
}
if (serviceAccounts.removed.length > 0) {
console.log(`Service accounts removed: ${serviceAccounts.removed}`);
}
returnToPreviousPage();
}
};

return (
<React.Fragment>
<ContentHeader title={intl.formatMessage(Messages.usersAndUserGroupsEditUserGroup)} subtitle={''} />
<PageSection data-ouia-component-id="edit-user-group-form" className="pf-v5-u-m-lg-on-lg" variant={PageSectionVariants.light} isWidthLimited>
<FormRenderer
schema={schema}
componentMapper={{
...componentMapper,
'users-and-service-accounts': EditGroupUsersAndServiceAccounts,
}}
onSubmit={handleSubmit}
onCancel={returnToPreviousPage}
FormTemplate={FormTemplate}
/>
</PageSection>
</React.Fragment>
);
};

export default EditUserGroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Pagination } from '@patternfly/react-core';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { DataView, DataViewTable, DataViewToolbar, useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view';
import { useDispatch, useSelector } from 'react-redux';
import { fetchServiceAccountsForGroup } from '../../redux/actions/group-actions';
import { RBACStore } from '../../redux/store';
import { mappedProps } from '../../helpers/shared/helpers';
import { fetchServiceAccounts } from '../../redux/actions/service-account-actions';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ServiceAccountsState } from '../../redux/reducers/service-account-reducer';
import { LAST_PAGE, ServiceAccount } from '../../helpers/service-account/service-account-helper';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups';
import DateFormat from '@redhat-cloud-services/frontend-components/DateFormat';
import { Diff } from './EditUserGroupUsersAndServiceAccounts';

interface EditGroupServiceAccountsTableProps {
groupId?: string;
onChange: (serviceAccounts: Diff) => void;
}

const PER_PAGE_OPTIONS = [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 },
{ title: '100', value: 100 },
];

const reducer = ({ serviceAccountReducer }: { serviceAccountReducer: ServiceAccountsState }) => ({
serviceAccounts: serviceAccountReducer.serviceAccounts,
status: serviceAccountReducer.status,
isLoading: serviceAccountReducer.isLoading,
limit: serviceAccountReducer.limit,
offset: serviceAccountReducer.offset,
});

const EditGroupServiceAccountsTable: React.FunctionComponent<EditGroupServiceAccountsTableProps> = ({ groupId, onChange }) => {
const dispatch = useDispatch();
const pagination = useDataViewPagination({ perPage: 20 });
const { page, perPage, onSetPage, onPerPageSelect } = pagination;
const { auth, getEnvironmentDetails } = useChrome();
const initialServiceAccountIds = useRef<string[]>([]);

const selection = useDataViewSelection({
matchOption: (a, b) => a.id === b.id,
});
const { onSelect, selected } = selection;

useEffect(() => {
return () => {
onSelect(false);
initialServiceAccountIds.current = [];
};
}, []);

const { serviceAccounts, status } = useSelector(reducer);
const calculateTotalCount = () => {
if (!serviceAccounts) return 0;
const currentCount = (page - 1) * perPage + serviceAccounts.length;
return status === LAST_PAGE ? currentCount : currentCount + 1;
};
const totalCount = calculateTotalCount();

const { groupServiceAccounts: groupServiceAccounts } = useSelector((state: RBACStore) => ({
groupServiceAccounts: state.groupReducer?.selectedGroup?.serviceAccounts?.data || [],
}));

const fetchData = useCallback(
async (apiProps: { count: number; limit: number; offset: number; orderBy: string }) => {
if (groupId) {
const { count, limit, offset, orderBy } = apiProps;
const env = getEnvironmentDetails();
const token = await auth.getToken();
dispatch(fetchServiceAccounts({ ...mappedProps({ count, limit, offset, orderBy, token, sso: env?.sso }) }));
dispatch(fetchServiceAccountsForGroup(groupId, {}));
}
},
[dispatch, groupId]
);

useEffect(() => {
fetchData({
limit: perPage,
offset: (page - 1) * perPage,
orderBy: 'username',
count: totalCount || 0,
});
}, [fetchData, page, perPage]);

const processedServiceAccounts = serviceAccounts ? serviceAccounts.slice(0, perPage) : [];
const rows = useMemo(
() =>
processedServiceAccounts.map((account: ServiceAccount) => ({
id: account.uuid,
row: [
account.name,
account.description,
account.clientId,
account.createdBy,
<DateFormat key={`${account.name}-date`} date={account.createdAt} />,
],
})),
[processedServiceAccounts, groupId]
);

useEffect(() => {
// on mount, select the accounts that are in the current group
onSelect(false);
initialServiceAccountIds.current = [];
const initialSelectedServiceAccounts = groupServiceAccounts.map((account) => ({ id: account.clientId }));
onSelect(true, initialSelectedServiceAccounts);
initialServiceAccountIds.current = initialSelectedServiceAccounts.map((account) => account.id);
}, [groupServiceAccounts]);

useEffect(() => {
const selectedServiceAccountIds = selection.selected.map((account) => account.id);
const added = selectedServiceAccountIds.filter((id) => !initialServiceAccountIds.current.includes(id));
const removed = initialServiceAccountIds.current.filter((id) => !selectedServiceAccountIds.includes(id));
onChange({ added, removed });
}, [selection.selected]);

const handleBulkSelect = (value: BulkSelectValue) => {
if (value === BulkSelectValue.none) {
onSelect(false);
} else if (value === BulkSelectValue.page) {
onSelect(true, rows);
} else if (value === BulkSelectValue.nonePage) {
onSelect(false, rows);
}
};

const pageSelected = rows.length > 0 && rows.every((row) => selection.isSelected(row));
const pagePartiallySelected = !pageSelected && rows.some((row) => selection.isSelected(row));

return (
<DataView selection={{ ...selection }}>
<DataViewToolbar
pagination={
<Pagination
isCompact
perPageOptions={PER_PAGE_OPTIONS}
itemCount={totalCount}
page={page}
perPage={perPage}
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
toggleTemplate={() => {
const firstIndex = (page - 1) * perPage + 1;
const lastIndex = Math.min(page * perPage, totalCount);
const totalNumber = status === LAST_PAGE ? (page - 1) * perPage + serviceAccounts.length : 'many';
return (
<React.Fragment>
<b>
{firstIndex} - {lastIndex}
</b>{' '}
of <b>{totalNumber}</b>
</React.Fragment>
);
}}
/>
}
bulkSelect={
<BulkSelect
pageCount={serviceAccounts.length}
selectedCount={selected.length}
totalCount={totalCount}
pageSelected={pageSelected}
pagePartiallySelected={pagePartiallySelected}
onSelect={handleBulkSelect}
/>
}
/>
<DataViewTable variant="compact" columns={['Name', 'Description', 'Client ID', 'Owner', 'Time created']} rows={rows} />
</DataView>
);
};

export default EditGroupServiceAccountsTable;
Loading

0 comments on commit 75b4e66

Please sign in to comment.