From 033866ba84845e53c84e8704b9fe6d8e79948d62 Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Wed, 3 Jan 2024 19:04:47 +0100 Subject: [PATCH] SONAR-21384 Migrate the user tokens modal to MIUI --- .../js/apps/account/__tests__/Account-it.tsx | 2 +- .../js/apps/users/__tests__/UsersApp-it.tsx | 32 +++- .../js/apps/users/components/TokensForm.tsx | 162 +++++++++++------- .../apps/users/components/TokensFormItem.tsx | 119 ++++++++----- .../apps/users/components/TokensFormModal.tsx | 37 ++-- .../users/components/TokensFormNewToken.tsx | 22 +-- 6 files changed, 230 insertions(+), 144 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx index b12ec3fbfa0..44209c8012d 100644 --- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx +++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx @@ -316,7 +316,7 @@ describe('security page', () => { expect( await screen.findByText(`users.tokens.new_token_created.${newTokenName}`), ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'copy_to_clipboard' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument(); const lastTokenCreated = tokenMock.getLastToken(); // eslint-disable-next-line jest/no-conditional-in-test diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index 33a0be26b46..7057c0b9bd7 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -58,6 +58,7 @@ const ui = { scmAddButton: byRole('button', { name: 'add_verb' }), createUserDialogButton: byRole('button', { name: 'create' }), cancelButton: byRole('button', { name: 'cancel' }), + closeButton: byRole('button', { name: 'close' }), reloadButton: byRole('button', { name: 'reload' }), doneButton: byRole('button', { name: 'done' }), changeButton: byRole('button', { name: 'change_verb' }), @@ -107,7 +108,7 @@ const ui = { unselectedFilter: byRole('radio', { name: 'unselected' }), getGroups: () => within(ui.dialogGroups.get()).getAllByRole('checkbox'), - dialogTokens: byRole('dialog', { name: 'users.tokens' }), + dialogTokens: byRole('dialog', { name: /users.user_X_tokens/ }), dialogPasswords: byRole('dialog', { name: 'my_profile.password.title' }), dialogUpdateUser: byRole('dialog', { name: 'users.update_user' }), dialogCreateUser: byRole('dialog', { name: 'users.create_user' }), @@ -136,6 +137,7 @@ const ui = { githubProvisioningSuccess: byText(/synchronization_successful/), githubProvisioningWarning: byText(/synchronization_successful.with_warning/), githubProvisioningAlert: byText(/synchronization_failed_short/), + expiresInSelector: byRole('combobox', { name: 'users.tokens.expires_in' }), }; beforeEach(() => { @@ -568,27 +570,38 @@ describe('in manage mode', () => { const getTokensList = () => ui.dialogTokens.byRole('row').getAll(); - expect(getTokensList()).toHaveLength(3); + expect(getTokensList()).toHaveLength(3); // header + 2 mocked tokens await user.type(ui.tokenNameInput.get(), 'test'); await user.click(ui.generateButton.get()); - // Not deleted because there is already token with name test + // Not created because there is already a token with the name "test" expect(screen.queryByText('users.tokens.new_token_created.test')).not.toBeInTheDocument(); - expect(getTokensList()).toHaveLength(3); + expect(getTokensList()).toHaveLength(3); // header + 2 mocked tokens expect(ui.sureButton.query()).not.toBeInTheDocument(); await user.click(ui.revokeButton('test').get()); expect(await ui.sureButton.find()).toBeInTheDocument(); await user.click(ui.sureButton.get()); - expect(getTokensList()).toHaveLength(2); + expect(getTokensList()).toHaveLength(2); // header + "local-scanner" token + expect(screen.queryByText('users.no_tokens')).not.toBeInTheDocument(); + expect(ui.sureButton.query()).not.toBeInTheDocument(); + await user.click(ui.revokeButton('local-scanner').get()); + expect(await ui.sureButton.find()).toBeInTheDocument(); + await user.click(ui.sureButton.get()); + + expect(getTokensList()).toHaveLength(2); // header + "No tokens" + expect(await screen.findByText('users.no_tokens')).toBeInTheDocument(); + + await selectEvent.select(ui.expiresInSelector.get(), 'users.tokens.expiration.0'); await user.click(ui.generateButton.get()); - expect(getTokensList()).toHaveLength(3); + expect(getTokensList()).toHaveLength(2); // header + "test" token + expect(screen.queryByText('users.no_tokens')).not.toBeInTheDocument(); expect(await screen.findByText('users.tokens.new_token_created.test')).toBeInTheDocument(); - await user.click(ui.doneButton.get()); + await user.click(ui.closeButton.get()); expect(ui.dialogTokens.query()).not.toBeInTheDocument(); }); @@ -700,16 +713,17 @@ it('accessibility', async () => { await user.click(ui.cancelButton.get()); // user tokens dialog should be accessible - user.click( + await user.click( await ui.aliceRow .byRole('button', { name: 'users.update_tokens_for_x.Alice Merveille', }) .find(), ); + expect(await ui.dialogTokens.find()).toBeInTheDocument(); await expect(await ui.dialogTokens.find()).toHaveNoA11yViolations(); - await user.click(ui.doneButton.get()); + await user.click(ui.closeButton.get()); // user password dialog should be accessible await user.click(await ui.aliceUpdateButton.find()); diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx index 64f279e5af5..c270565839b 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx @@ -17,12 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { + ButtonPrimary, + ContentCell, + GreySeparator, + InputField, + InputSelect, + Table, + TableRow, +} from 'design-system'; import { isEmpty } from 'lodash'; import * as React from 'react'; import { getScannableProjects } from '../../../api/components'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import Select, { LabelValueSelectOption } from '../../../components/controls/Select'; -import { SubmitButton } from '../../../components/controls/buttons'; +import { LabelValueSelectOption } from '../../../components/controls/Select'; import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; import { @@ -39,13 +48,15 @@ import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem'; import TokensFormNewToken from './TokensFormNewToken'; interface Props { + currentUser: CurrentUser; deleteConfirmation: TokenDeleteConfirmation; - login: string; displayTokenTypeInput: boolean; - currentUser: CurrentUser; + login: string; } -export function TokensForm(props: Props) { +const COLUMN_WIDTHS = ['auto', 'auto', 'auto', 'auto', 'auto', 'auto', '5%']; + +export function TokensForm(props: Readonly) { const { currentUser, deleteConfirmation, displayTokenTypeInput, login } = props; const { data: tokens, isLoading: loading } = useUserTokensQuery(login); const [newToken, setNewToken] = React.useState<{ name: string; token: string }>(); @@ -53,9 +64,11 @@ export function TokensForm(props: Props) { const [newTokenType, setNewTokenType] = React.useState(); const [projects, setProjects] = React.useState([]); const [selectedProject, setSelectedProject] = React.useState(); + const [newTokenExpiration, setNewTokenExpiration] = React.useState( TokenExpiration.OneMonth, ); + const [tokenExpirationOptions, setTokenExpirationOptions] = React.useState<{ value: TokenExpiration; label: string }[]>(EXPIRATION_OPTIONS); @@ -70,6 +83,7 @@ export function TokensForm(props: Props) { value: TokenType.Global, }); } + if (!isEmpty(projects)) { value.unshift({ label: translate('users.tokens', TokenType.Project), @@ -100,7 +114,9 @@ export function TokensForm(props: Props) { label: project.name, value: project.key, })); + setProjects(projects); + setSelectedProject(projects.length === 1 ? projects[0] : undefined); }) .catch(() => {}); @@ -111,8 +127,8 @@ export function TokensForm(props: Props) { event.preventDefault(); generate({ - name: newTokenName, login, + name: newTokenName, type: newTokenType, ...(newTokenType === TokenType.Project && selectedProject !== undefined && { @@ -141,6 +157,7 @@ export function TokensForm(props: Props) { if (generating || newTokenName.length <= 0) { return true; } + if (newTokenType === TokenType.Project) { return !selectedProject?.value; } @@ -152,16 +169,16 @@ export function TokensForm(props: Props) { setNewTokenName(evt.currentTarget.value); }; - const handleNewTokenTypeChange = ({ value }: { value: TokenType }) => { - setNewTokenType(value); + const handleNewTokenTypeChange = (newTokenType: { value: unknown } | null) => { + setNewTokenType(newTokenType?.value as TokenType); }; const handleProjectChange = (selectedProject: LabelValueSelectOption) => { setSelectedProject(selectedProject); }; - const handleNewTokenExpirationChange = ({ value }: { value: TokenExpiration }) => { - setNewTokenExpiration(value); + const handleNewTokenExpirationChange = (newTokenExpiration: { value: unknown } | null) => { + setNewTokenExpiration(newTokenExpiration?.value as TokenExpiration); }; const customSpinner = ( @@ -172,17 +189,30 @@ export function TokensForm(props: Props) { ); + const tableHeader = ( + + {translate('name')} + {translate('my_account.token_type')} + {translate('my_account.project_name')} + {translate('my_account.tokens_last_usage')} + {translate('created')} + {translate('my_account.tokens.expiration')} + + ); + return ( <> -

{translate('users.tokens.generate')}

-
-
-