Skip to content

Commit

Permalink
Merge pull request #1635 from florkbr/pull-in-rhecosystem-changes
Browse files Browse the repository at this point in the history
  • Loading branch information
fhlavac authored Jul 31, 2024
2 parents 81d1f66 + d354f56 commit 15ea84d
Show file tree
Hide file tree
Showing 18 changed files with 1,084 additions and 22 deletions.
8 changes: 8 additions & 0 deletions config/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ global.matchMedia =
};

Object.assign(global, { TextDecoder, TextEncoder });

// TODO several tests do not wrap components in a flag provider and assume this returns false
jest.mock('@unleash/proxy-client-react', () => {
return {
__esModule: true,
useFlag: () => false,
};
});
107 changes: 107 additions & 0 deletions src/Messages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,93 @@
import { defineMessages } from 'react-intl';

export default defineMessages({
inviteUsers: {
id: 'inviteUsers',
description: 'Invite users',
defaultMessage: 'Invite users',
},
inviteUsersTitle: {
id: 'inviteUsersTitle',
description: 'Invite users modal title',
defaultMessage: 'Invite New Users',
},
inviteUsersDescription: {
id: 'inviteUsersDescription',
description: 'Invite users modal description',
defaultMessage:
'Invite users to create a Red Hat login with your organization. Your e-mail address will be included in the invite as a point of contact.',
},
inviteUsersFormIsAdminFieldTitle: {
id: 'inviteUsersFormIsAdminFieldTitle',
description: 'Invite users form is admin field title',
defaultMessage: 'Organization Administrators',
},
inviteUsersFormIsAdminFieldDescription: {
id: 'inviteUsersFormIsAdminFieldDescription',
description: 'Invite users form is admin 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.',
},
inviteUsersFormEmailsFieldTitle: {
id: 'inviteUsersFormEmailsFieldTitle',
description: 'Invite users form emails field title',
defaultMessage: 'Enter the e-mail addresses of the users you would like to invite',
},
inviteUsersFormEmailsFieldDescription: {
id: 'inviteUsersFormEmailsFieldDescription',
description: 'Invite users form emails field description',
defaultMessage: 'Enter up to 50 e-mail addresses separated by commas or returns.',
},
inviteUsersCancelled: {
id: 'inviteUsersCancelled',
description: 'Invite users cancelled notification description',
defaultMessage: 'Invite users process was canceled by the user.',
},
inviteUsersButton: {
id: 'inviteUsersButton',
description: 'Invite users button text',
defaultMessage: 'Invite new users',
},
inviteUsersErrorTitle: {
id: 'inviteUsersErrorTitle',
description: 'Invite users error notification title',
defaultMessage: 'Failed inviting all users',
},
inviteUsersErrorDescription: {
id: 'inviteUsersErrorDescription',
description: 'Invite users error notification description',
defaultMessage: 'Failed inviting users.',
},
activateUsersButton: {
id: 'activateUsersButton',
description: 'activate users button text',
defaultMessage: 'Activate users',
},
deactivateUsersButton: {
id: 'deactivateUsersButton',
description: 'deactivate users button text',
defaultMessage: 'Deactivate users',
},
deactivateUsersConfirmationModalTitle: {
id: 'deactivateUsersConfirmationModalTitle',
description: 'deactivate users confirmation modal title text',
defaultMessage: 'Deactivate users',
},
deactivateUsersConfirmationModalDescription: {
id: 'deactivateUsersConfirmationModalDescription',
description: 'deactivate users confirmation modal description text',
defaultMessage: 'Are you sure you want to deactivate the user(s) below from your Red Hat organization?',
},
deactivateUsersConfirmationModalCheckboxText: {
id: 'deactivateUsersConfirmationModalCheckboxText',
description: 'deactivate users confirmation modal checkbox text',
defaultMessage: 'Yes, I confirm that I want to remove these users',
},
deactivateUsersConfirmationButton: {
id: 'deactivateUsersConfirmationButton',
description: 'deactivate users confirmation button text',
defaultMessage: 'Deactivate user(s)',
},
notApplicable: {
id: 'notApplicable',
description: 'Not applicable text for resource definitions',
Expand Down Expand Up @@ -222,6 +309,26 @@ export default defineMessages({
description: 'Edit group error notification description',
defaultMessage: 'The group was not updated successfuly.',
},
editUserSuccessTitle: {
id: 'editUserSuccessTitle',
description: 'Edit user success notification title',
defaultMessage: 'Success updating user',
},
editUserSuccessDescription: {
id: 'editUserSuccessDescription',
description: 'Edit user success notification description',
defaultMessage: 'The user was updated successfully.',
},
editUserErrorTitle: {
id: 'editUserErrorTitle',
description: 'Edit user error notification title',
defaultMessage: 'Failed updating user',
},
editUserErrorDescription: {
id: 'editUserErrorDescription',
description: 'Edit user error notification description',
defaultMessage: 'The user was not updated successfuly.',
},
removeGroupSuccess: {
id: 'removeGroupSuccess',
description: 'Remove group success notification title',
Expand Down
11 changes: 8 additions & 3 deletions src/Routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Overview = lazy(() => import('./smart-components/overview/overview'));
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 Roles = lazy(() => import('./smart-components/role/roles'));
const Role = lazy(() => import('./smart-components/role/role'));
Expand All @@ -37,7 +38,7 @@ const AddGroupServiceAccounts = lazy(() => import('./smart-components/group/serv
const RemoveServiceAccountFromGroup = lazy(() => import('./smart-components/group/service-account/remove-group-service-accounts'));
const QuickstartsTest = lazy(() => import('./smart-components/quickstarts/quickstarts-test'));

const getRoutes = ({ enableServiceAccounts }: Record<string, boolean>) => [
const getRoutes = ({ enableServiceAccounts, isITLess }: Record<string, boolean>) => [
{
path: pathnames.overview.path,
element: Overview,
Expand All @@ -60,7 +61,10 @@ const getRoutes = ({ enableServiceAccounts }: Record<string, boolean>) => [
path: pathnames.users.path,
element: Users,
},

isITLess && {
path: pathnames['invite-users'].path,
element: InviteUsersModal,
},
{
path: pathnames['role-detail'].path,
element: Role,
Expand Down Expand Up @@ -233,6 +237,7 @@ const renderRoutes = (routes: RouteType[] = []) =>
const Routing = () => {
const location = useLocation();
const { updateDocumentTitle, isBeta } = useChrome();
const isITLess = useFlag('platform.rbac.itless');
const enableServiceAccounts =
(isBeta() && useFlag('platform.rbac.group-service-accounts')) || (!isBeta() && useFlag('platform.rbac.group-service-accounts.stable'));

Expand All @@ -252,7 +257,7 @@ const Routing = () => {
}
}, [location.pathname, updateDocumentTitle]);

const routes = getRoutes({ enableServiceAccounts });
const routes = getRoutes({ enableServiceAccounts, isITLess });
const renderedRoutes = useMemo(() => renderRoutes(routes as never), [routes]);
return (
<Suspense fallback={<AppPlaceholder />}>
Expand Down
16 changes: 16 additions & 0 deletions src/helpers/shared/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,19 @@ export const getDateFormat = (date) => {
const monthAgo = new Date(Date.now());
return Date.parse(date) < monthAgo.setMonth(monthAgo.getMonth() - 1) ? 'onlyDate' : 'relative';
};

export const isExternalIdp = (token = '') => {
let roles = [''];
let tokenArray = token.split('.');
if (tokenArray.length > 1) {
let token1 = window.atob(tokenArray[1]);
if (token1) {
roles = JSON.parse(token1)?.realm_access?.roles;
if (roles.includes('external-idp')) {
return true;
}
}
}

return false;
};
112 changes: 112 additions & 0 deletions src/helpers/user/user-helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getLastPageOffset, isOffsetValid } from '../shared/pagination';
import { getPrincipalApi } from '../shared/user-login';
import { isInt, isStage, isITLessProd } from '../../itLessConfig';

const principalApi = getPrincipalApi();

Expand All @@ -8,6 +9,117 @@ const principalStatusApiMap = {
Inactive: 'disabled',
All: 'all',
};

const getBaseUrl = (url) => {
if (isInt) {
return url.int;
} else if (isStage) {
return url.stage;
} else if (isITLessProd) {
return url.prod;
} else {
return '';
}
};

async function fetchBaseUrl() {
try {
// TODO move to env var defined in cluster surfaced through chrome service
const response = await fetch('/apps/rbac/env.json');
const jsonData = await response.json();
return jsonData;
} catch (error) {
console.log(error);
}
}

const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
};

function handleResponse(response, resolve, reject) {
if (response.ok && response.status !== 206) {
resolve(response);
} else if (response.ok && response.status === 206) {
response.json().then((body) => {
reject(body);
});
} else {
reject(response);
}
}

function handleError(error, reject) {
reject(new Error(error.message));
}

export async function addUsers(usersData = { emails: [], isAdmin: undefined }) {
const requestOpts = {
method: 'POST',
headers,
body: JSON.stringify({
emails: usersData.emails,
isAdmin: usersData.isAdmin,
}),
};
const url = await fetchBaseUrl();
const baseUrl = getBaseUrl(url);
let promise = new Promise((resolve, reject) => {
return fetch(`${baseUrl}/user/invite`, requestOpts)
.then(
(response) => handleResponse(response, resolve, reject),
(error) => handleError(error, reject)
)
.catch((error) => handleError(error, reject));
});

return promise;
}

export async function updateUserIsOrgAdminStatus(user) {
let requestOpts = {
method: 'PUT',
headers,
};

const url = await fetchBaseUrl();
const baseUrl = getBaseUrl(url);

let promise = new Promise((resolve, reject) => {
return fetch(`${baseUrl}/user/${user.id}/admin/${user.is_org_admin}`, requestOpts)
.then(
(response) => handleResponse(response, resolve, reject),
(error) => handleError(error, reject)
)
.catch((error) => handleError(error, reject));
});

return promise;
}

export async function updateUsers(users) {
let requestOpts = {
method: 'PUT',
headers,
body: JSON.stringify({ users: users }),
};

const url = await fetchBaseUrl();
const baseUrl = getBaseUrl(url);

let promise = new Promise((resolve, reject) => {
return fetch(`${baseUrl}/change-users-status`, requestOpts)
.then(
(response) => handleResponse(response, resolve, reject),
(error) => handleError(error, reject)
)
.catch((error) => handleError(error, reject));
});

return promise;
}

export async function fetchUsers({ limit, offset = 0, orderBy, filters = {}, usesMetaInURL, matchCriteria = 'partial' }) {
const { username, email, status = [] } = filters;
const sortOrder = orderBy === '-username' ? 'desc' : 'asc';
Expand Down
4 changes: 4 additions & 0 deletions src/itLessConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const isEphem = insights.chrome.getEnvironment() === 'ephem';
export const isInt = insights.chrome.getEnvironment() === 'int';
export const isStage = insights.chrome.getEnvironment() === 'frhStage';
export const isITLessProd = insights.chrome.getEnvironment() === 'frh';
4 changes: 3 additions & 1 deletion src/presentational-components/shared/ActiveUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import PermissionsContext from '../../utilities/permissions-context';
import messages from '../../Messages';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { useFlag } from '@unleash/proxy-client-react';

const ActiveUser = ({ linkDescription, linkTitle }) => {
const intl = useIntl();
const chrome = useChrome();
const env = chrome.getEnvironment();
const prefix = chrome.isProd() ? '' : `${env}.`;
const { orgAdmin } = useContext(PermissionsContext);
return orgAdmin ? (
const isITLess = useFlag('platform.rbac.itless');
return !isITLess && orgAdmin ? (
<Text className="pf-v5-u-mt-0" component={TextVariants.h7}>
{`${intl.formatMessage(messages.usersDescription)} `}
{linkDescription}
Expand Down
8 changes: 7 additions & 1 deletion src/presentational-components/shared/table-toolbar-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
import { useIntl } from 'react-intl';
import propTypes from 'prop-types';
import messages from '../../Messages';
import { useFlag } from '@unleash/proxy-client-react';
import { TableVariant } from '@patternfly/react-table';
import { Table, TableHeader, TableBody } from '@patternfly/react-table/deprecated';
import TableToolbar from '@redhat-cloud-services/frontend-components/TableToolbar';
Expand Down Expand Up @@ -59,8 +60,10 @@ export const TableToolbarView = ({
tableId,
containerRef,
textFilterRef,
toolbarChildren,
}) => {
const intl = useIntl();
const isITLess = useFlag('platform.rbac.itless');
const renderEmpty = () => ({
title: (
<EmptyWithAction
Expand Down Expand Up @@ -130,6 +133,7 @@ export const TableToolbarView = ({
tableId={tableId}
containerRef={containerRef}
textFilterRef={textFilterRef}
toolbarChildren={toolbarChildren}
/>
{isLoading ? (
<SkeletonTable
Expand All @@ -146,7 +150,7 @@ export const TableToolbarView = ({
{...(isSelectable &&
rows?.length > 0 && {
onSelect: (_e, isSelected, _idx, { uuid, cells: [name], requires }) =>
setCheckedItems(selectedRows([{ uuid, name, requires }], isSelected)),
setCheckedItems(selectedRows([{ uuid, name, requires, ...(isITLess && { username: data[_idx]?.username }) }], isSelected)),
})}
{...(isExpandable && { onExpand })}
rows={rows?.length > 0 ? rows : [{ fullWidth: true, cells: [renderEmpty()] }]}
Expand Down Expand Up @@ -234,6 +238,7 @@ TableToolbarView.propTypes = {
noDataDescription: propTypes.arrayOf(propTypes.node),
filters: propTypes.array,
tableId: propTypes.string.isRequired,
toolbarChildren: propTypes.func,
};

TableToolbarView.defaultProps = {
Expand All @@ -245,4 +250,5 @@ TableToolbarView.defaultProps = {
hideFilterChips: false,
checkedRows: [],
hideHeader: false,
toolbarChildren: () => null,
};
Loading

0 comments on commit 15ea84d

Please sign in to comment.