Skip to content

Commit

Permalink
feat(invite-users): add invite users modal via data driven forms
Browse files Browse the repository at this point in the history
  • Loading branch information
karelhala committed Nov 14, 2024
1 parent 64f76a2 commit 5aece2e
Show file tree
Hide file tree
Showing 12 changed files with 486 additions and 103 deletions.
76 changes: 76 additions & 0 deletions src/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,67 @@ export default defineMessages({
defaultMessage:
'The organization administrator role is the highest permission level with full access to content and features. This is the only role that can manage users.',
},
inviteUsersFormManageSupportCasesFieldTitle: {
id: 'inviteUsersFormManageSupportCasesFieldTitle',
description: 'Invite users form manage support cases field title',
defaultMessage: 'Organization administrators',
},
inviteUsersFormManageSupportCasesFieldDescription: {
id: 'inviteUsersFormManageSupportCasesFieldDescription',
description: 'Invite users form manage support cases field description',
defaultMessage:
'The organization administrator role is the highest permission level with full access to content and features. This is the only role that can manage users.',
},
inviteUsersFormDownloadSoftwareUpdatesFieldTitle: {
id: 'inviteUsersFormDownloadSoftwareUpdatesFieldTitle',
description: 'Invite users form download software and updates field title',
defaultMessage: 'Download software and updates',
},
inviteUsersFormDownloadSoftwareUpdatesFieldDescription: {
id: 'inviteUsersFormDownloadSoftwareUpdatesFieldDescription',
description: 'Invite users form download software and updates field description',
defaultMessage: 'User can download software and updates from the Red Hat Customer Portal.',
},
inviteUsersFormManageSubscriptionsFieldTitle: {
id: 'inviteUsersFormManageSubscriptionsFieldTitle',
description: 'Invite users form manage subscriptions field title',
defaultMessage: 'Manage your subscriptions',
},
inviteUsersFormManageSubscriptionsFieldDescription: {
id: 'inviteUsersFormManageSubscriptionsFieldDescription',
description: 'Invite users form manage subscriptions field description',
defaultMessage: 'Grants user access to subscription management via Red Hat Subscription Management in the Red Hat Customer Portal.',
},
inviteUsersFormManageSubscriptionsViewEditUsersOnlyTitle: {
id: 'inviteUsersFormManageSubscriptionsViewEditUsersOnlyTitle',
description: 'Invite users form manage subscriptions field View edit Users only title',
defaultMessage: 'View/Edit user’s only',
},
inviteUsersFormManageSubscriptionsViewEditUsersOnlyDescription: {
id: 'inviteUsersFormManageSubscriptionsViewEditUsersOnlyDescription',
description: 'Invite users form manage subscriptions field View edit Users only description',
defaultMessage: 'User can view and edit only the systems that they have registered in the account.',
},
inviteUsersFormManageSubscriptionsViewAllTitle: {
id: 'inviteUsersFormManageSubscriptionsViewAllTitle',
description: 'Invite users form manage subscriptions field view all option title',
defaultMessage: 'User can view and edit only the systems that they have registered in the account.',
},
inviteUsersFormManageSubscriptionsViewAllDescription: {
id: 'inviteUsersFormManageSubscriptionsViewAllDescription',
description: 'Invite users form manage subscriptions field view all option description',
defaultMessage: 'User can view (but not edit) all systems and Subscription Management Applications in the account.',
},
inviteUsersFormManageSubscriptionsViewEditAllTitle: {
id: 'inviteUsersFormManageSubscriptionsViewEditAllTitle',
description: 'Invite users form manage subscriptions field View/Edit all option title',
defaultMessage: 'View/Edit all',
},
inviteUsersFormManageSubscriptionsViewEditAllDescription: {
id: 'inviteUsersFormManageSubscriptionsViewEditAllDescription',
description: 'Invite users form manage subscriptions field View/Edit all option description',
defaultMessage: 'User can view and edit all systems and Subscription Management Applications in the account.',
},
inviteUsersFormEmailsFieldTitle: {
id: 'inviteUsersFormEmailsFieldTitle',
description: 'Invite users form emails field title',
Expand All @@ -38,6 +99,21 @@ export default defineMessages({
description: 'Invite users form emails field description',
defaultMessage: 'Enter up to 50 e-mail addresses separated by commas or returns.',
},
inviteUsersFormEmailsFieldError: {
id: 'inviteUsersFormEmailsFieldError',
description: 'Invite users form emails field error message is one email address is not valid.',
defaultMessage: 'Some of the email addresses you provided are not valid',
},
inviteUsersMessageTitle: {
id: 'inviteUsersMessageTitle',
description: 'Message to be sent to each email',
defaultMessage: 'Send a message with the invite',
},
inviteUsersCustomerPortalPermissions: {
id: 'inviteUsersCustomerPortalPermissions',
description: 'Customer portal permissions title',
defaultMessage: 'Customer Portal access permissions',
},
inviteUsersCancelled: {
id: 'inviteUsersCancelled',
description: 'Invite users cancelled notification description',
Expand Down
16 changes: 12 additions & 4 deletions src/Routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Users = lazy(() => import('./smart-components/user/users'));
const UserDetail = lazy(() => import('./smart-components/user/user'));
const AddUserToGroup = lazy(() => import('./smart-components/user/add-user-to-group/add-user-to-group'));
const InviteUsersModal = lazy(() => import('./smart-components/user/invite-users/invite-users-modal'));
const InviteUsersModalCommonAuth = lazy(() => import('./smart-components/user/invite-users/invite-users-modal-common-auth'));

const Roles = lazy(() => import('./smart-components/role/roles'));
const Role = lazy(() => import('./smart-components/role/role'));
Expand Down Expand Up @@ -44,10 +45,16 @@ const QuickstartsTest = lazy(() => import('./smart-components/quickstarts/quicks

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

const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag }: Record<string, boolean>) => [
const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommonAuthModel }: Record<string, boolean>) => [
{
path: pathnames['users-and-user-groups'].path,
element: UsersAndUserGroups,
childRoutes: [
isCommonAuthModel && {
path: pathnames['invite-group-users'].path,
element: InviteUsersModalCommonAuth,
},
],
},
{
path: pathnames.overview.path,
Expand Down Expand Up @@ -79,9 +86,9 @@ const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag }: Record
path: pathnames.users.path,
element: Users,
childRoutes: [
isITLess && {
(isITLess || isCommonAuthModel) && {
path: pathnames['invite-users'].path,
element: InviteUsersModal,
element: isCommonAuthModel ? InviteUsersModalCommonAuth : InviteUsersModal,
},
],
},
Expand Down Expand Up @@ -267,6 +274,7 @@ const Routing = () => {
const location = useLocation();
const { updateDocumentTitle, isBeta } = useChrome();
const isITLess = useFlag('platform.rbac.itless');
const isCommonAuthModel = useFlag('platform.rbac.common-auth-model');
const enableServiceAccounts =
(isBeta() && useFlag('platform.rbac.group-service-accounts')) || (!isBeta() && useFlag('platform.rbac.group-service-accounts.stable'));
const isWorkspacesFlag = useFlag('platform.rbac.workspaces');
Expand All @@ -287,7 +295,7 @@ const Routing = () => {
}
}, [location.pathname, updateDocumentTitle]);

const routes = getRoutes({ enableServiceAccounts, isITLess, isWorkspacesFlag });
const routes = getRoutes({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommonAuthModel });
const renderedRoutes = useMemo(() => renderRoutes(routes as never), [routes]);

const { getBundle, getApp } = useChrome();
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/user/user-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { getLastPageOffset, isOffsetValid } from '../shared/pagination';
import { getPrincipalApi } from '../shared/user-login';
import { isInt, isStage, isITLessProd } from '../../itLessConfig';

export const MANAGE_SUBSCRIPTIONS_VIEW_EDIT_USER = 'view-edit-user';
export const MANAGE_SUBSCRIPTIONS_VIEW_ALL = 'view-all';
export const MANAGE_SUBSCRIPTIONS_VIEW_EDIT_ALL = 'view-edit-all';

const principalApi = getPrincipalApi();

const principalStatusApiMap = {
Expand Down
21 changes: 12 additions & 9 deletions src/presentational-components/shared/AppLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link, LinkProps, To } from 'react-router-dom';

interface AppLinkProps extends LinkProps {
linkBasename?: string;
to: To;
}

export const mergeToBasename = (to: To, basename = '/iam/user-access') => {
Expand All @@ -18,15 +19,17 @@ export const mergeToBasename = (to: To, basename = '/iam/user-access') => {
};
};

const AppLink: React.FC<AppLinkProps> = React.forwardRef((props: AppLinkProps, ref: LegacyRef<HTMLSpanElement>) => {
const { getBundle, getApp } = useChrome();
const defaultBasename = `/${getBundle()}/${getApp()}`;
return (
<span ref={ref}>
<Link {...props} to={mergeToBasename(props.to, props.linkBasename || defaultBasename)} />
</span>
);
});
const AppLink: React.FC<React.PropsWithChildren<AppLinkProps & { className?: string }>> = React.forwardRef(
(props: React.PropsWithChildren<AppLinkProps>, ref: LegacyRef<HTMLSpanElement>) => {
const { getBundle, getApp } = useChrome();
const defaultBasename = `/${getBundle()}/${getApp()}`;
return (
<span ref={ref}>
<Link {...props} to={mergeToBasename(props.to, props.linkBasename || defaultBasename)} />
</span>
);
}
);

AppLink.displayName = 'AppLink';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ interface MainTableProps {
setFilterValue: (value: FilterProps) => void;
pagination: { limit?: number; offset?: number; count?: number; noBottom?: boolean };
fetchData: (config: FetchDataProps) => void;
toolbarButtons?: () => React.ReactNode[];
toolbarButtons?: () => (React.JSX.Element | React.ReactNode)[];
filterPlaceholder?: string;
filters: Array<{
value: string | number | Array<unknown>;
Expand Down
53 changes: 40 additions & 13 deletions src/smart-components/access-management/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import React, { useEffect, useCallback, useState, Fragment, useMemo } from 'react';
import React, { useEffect, useCallback, useState, Fragment, useMemo, Suspense } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useDataViewSelection, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction';
import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { Button, Pagination, ButtonVariant } from '@patternfly/react-core';
import { Pagination, ButtonVariant } from '@patternfly/react-core';
import { ActionsColumn } from '@patternfly/react-table';
import { fetchUsers } from '../../redux/actions/user-actions';
import { mappedProps } from '../../helpers/shared/helpers';
import { RBACStore } from '../../redux/store';
import { User } from '../../redux/reducers/user-reducer';
import { useIntl } from 'react-intl';
import messages from '../../Messages';
import { useSearchParams } from 'react-router-dom';
import { Outlet, useSearchParams } from 'react-router-dom';
import { WarningModal } from '@patternfly/react-component-groups';
import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
import paths from '../../utilities/pathnames';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import useAppNavigate from '../../hooks/useAppNavigate';

const COLUMNS: string[] = ['Username', 'Email', 'First name', 'Last name', 'Status', 'Org admin'];

Expand All @@ -35,11 +40,13 @@ interface UsersTableProps {
}

const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick, focusedUser }) => {
const { getBundle, getApp } = useChrome();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<User | undefined>();
const dispatch = useDispatch();
const intl = useIntl();
const { trigger } = useDataViewEventsContext();
const appNavigate = useAppNavigate(`/${getBundle()}/${getApp()}`);

const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent, user: User) => {
setCurrentUser(user);
Expand Down Expand Up @@ -176,21 +183,41 @@ const UsersTable: React.FunctionComponent<UsersTableProps> = ({ onAddUserClick,
}
pagination={React.cloneElement(paginationComponent, { isCompact: true })}
actions={
<Button
variant="primary"
onClick={() => {
onAddUserClick(selected);
}}
isDisabled={selected.length === 0}
ouiaId={`${OUIA_ID}-add-user-button`}
>
{intl.formatMessage(messages['usersAndUserGroupsAddToGroup'])}
</Button>
<ResponsiveActions breakpoint="lg" ouiaId="example-actions">
<ResponsiveAction
isPersistent
onClick={() => {
onAddUserClick(selected);
}}
variant="primary"
isDisabled={selected.length === 0}
ouiaId={`${OUIA_ID}-add-user-button`}
>
{intl.formatMessage(messages['usersAndUserGroupsAddToGroup'])}
</ResponsiveAction>
<ResponsiveAction
variant="primary"
onClick={() => {
appNavigate(paths['invite-group-users'].link);
}}
>
{intl.formatMessage(messages.inviteUsers)}
</ResponsiveAction>
</ResponsiveActions>
}
/>
<DataViewTable variant="compact" aria-label="Users Table" ouiaId={`${OUIA_ID}-table`} columns={COLUMNS} rows={rows} />
<DataViewToolbar ouiaId={`${OUIA_ID}-footer-toolbar`} pagination={paginationComponent} />
</DataView>
<Suspense>
<Outlet
context={{
fetchData: () => {
appNavigate(paths['users-and-user-groups'].link);
},
}}
/>
</Suspense>
</Fragment>
);
};
Expand Down
75 changes: 75 additions & 0 deletions src/smart-components/common/expandable-checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { useFormApi } from '@data-driven-forms/react-form-renderer';
import useFieldApi, { UseFieldApiConfig } from '@data-driven-forms/react-form-renderer/use-field-api';
import { ExpandableSection, Checkbox, FormGroup, Radio, Tooltip } from '@patternfly/react-core';
import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/outlined-question-circle-icon';

export type ExpandableCheckboxItemType = {
title: string;
description: string;
name: string;
options?: { title: string; description: string; name: string }[];
};

const ExpandableCheckbox: React.FC<UseFieldApiConfig> = (props) => {
const formOptions = useFormApi();
const { items, input, ...rest } = useFieldApi(props);
console.log(formOptions, rest, formOptions.getState().values, 'this is formOptions!');
return (
<FormGroup>
{items?.map((item: ExpandableCheckboxItemType, key: number) => (
<Checkbox
inputClassName="pf-v5-u-mt-xs"
key={key}
isChecked={!!formOptions.getState().values[input.name]?.[item.name]}
onChange={(_e, value) => {
const newVal = value && item.options ? item.options?.[0]?.name : value;
input.onChange({
...formOptions.getState().values[input.name],
[item.name]: newVal,
});
}}
label={
<ExpandableSection toggleText={item.title} isIndented>
{item.description}
{item.options && (
<FormGroup role="radiogroup" fieldId={`${item.name}-radiogroup`}>
{item.options?.map((option, key) => (
<Radio
key={key}
name={`${item.name}-radio`}
isChecked={formOptions.getState().values[input.name]?.[item.name] === option.name}
onChange={() => {
input.onChange({
...formOptions.getState().values[input.name],
[item.name]: option.name,
});
}}
label={
<React.Fragment>
{option.title}{' '}
{option.description && (
<Tooltip content={option.description}>
<OutlinedQuestionCircleIcon />
</Tooltip>
)}
</React.Fragment>
}
id={`${item.name}-option-${key}`}
/>
))}
</FormGroup>
)}
</ExpandableSection>
}
id={`${item.name}-checkbox`}
name={item.name}
/>
))}
</FormGroup>
);
};

// const FieldListenerWrapper: React.FC<UseFieldApiConfig> = (props) => <FormSpy subscription={{ values: true }}>{() => <ExpandableCheckbox {...props}/>}</FormSpy>;

export default ExpandableCheckbox;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import FormButtons from './FormButtons';
import FormTemplate from '@data-driven-forms/pf4-component-mapper/form-template';
import TextField from '@data-driven-forms/pf4-component-mapper/text-field';
import Textarea from '@data-driven-forms/pf4-component-mapper/textarea';
import ReactFormRender from '@data-driven-forms/react-form-renderer/form-renderer';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import FormTemplateCommonProps from '@data-driven-forms/common/form-template';

const FormRenderer = ({ formTemplateProps, ...props }) => (
const FormRenderer: React.FC<{ formTemplateProps?: FormTemplateCommonProps } & FormTemplateCommonProps> = ({ formTemplateProps, ...props }) => (
<ReactFormRender
componentMapper={{
[componentTypes.TEXT_FIELD]: TextField,
Expand All @@ -18,8 +18,4 @@ const FormRenderer = ({ formTemplateProps, ...props }) => (
/>
);

FormRenderer.propTypes = {
formTemplateProps: PropTypes.object,
};

export default FormRenderer;
Loading

0 comments on commit 5aece2e

Please sign in to comment.