From 76d03398fd5e69f306f51bbdc87e354fa0ec4f11 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:29:12 -0400 Subject: [PATCH] [8.11] Fix API Key flyout double submit (#167468) (#168483) # Backport This will backport the following commits from `main` to `8.11`: - [Fix API Key flyout double submit (#167468)](https://github.com/elastic/kibana/pull/167468) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sid --- .../public/components/form_flyout.tsx | 104 --- .../api_keys/api_keys_grid/api_key_flyout.tsx | 851 +++++++++--------- 2 files changed, 444 insertions(+), 511 deletions(-) delete mode 100644 x-pack/plugins/security/public/components/form_flyout.tsx diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx deleted file mode 100644 index 51ab56a11d225..0000000000000 --- a/x-pack/plugins/security/public/components/form_flyout.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiButtonProps, EuiFlyoutProps } from '@elastic/eui'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiPortal, - EuiTitle, -} from '@elastic/eui'; -import type { FunctionComponent, RefObject } from 'react'; -import React, { useEffect } from 'react'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import { useHtmlId } from './use_html_id'; - -export interface FormFlyoutProps extends Omit { - title: string; - initialFocus?: RefObject; - onCancel(): void; - onSubmit(): void; - submitButtonText: string; - submitButtonColor?: EuiButtonProps['color']; - isLoading?: EuiButtonProps['isLoading']; - isDisabled?: EuiButtonProps['isDisabled']; - isSubmitButtonHidden?: boolean; -} - -export const FormFlyout: FunctionComponent = ({ - title, - submitButtonText, - submitButtonColor, - onCancel, - onSubmit, - isLoading, - isDisabled, - isSubmitButtonHidden, - children, - initialFocus, - ...rest -}) => { - useEffect(() => { - if (initialFocus && initialFocus.current) { - initialFocus.current.focus(); - } - }, [initialFocus]); - - const titleId = useHtmlId('formFlyout', 'title'); - - return ( - - - - -

{title}

-
-
- {children} - - - - - - - - {!isSubmitButtonHidden && ( - - - {submitButtonText} - - - )} - - -
-
- ); -}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx index 6d100f2a8261e..b3792941fc6d0 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx @@ -7,11 +7,17 @@ import type { ExclusiveUnion } from '@elastic/eui'; import { + EuiButton, + EuiButtonEmpty, EuiCallOut, EuiCheckableCard, EuiFieldNumber, EuiFlexGroup, EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, EuiFormFieldset, EuiFormRow, EuiHorizontalRule, @@ -36,10 +42,9 @@ import { ApiKeyBadge, ApiKeyStatus, TimeToolTip, UsernameWithIcon } from './api_ import type { ApiKeyRoleDescriptors } from '../../../../common/model'; import { DocLink } from '../../../components/doc_link'; import { FormField } from '../../../components/form_field'; -import type { FormFlyoutProps } from '../../../components/form_flyout'; -import { FormFlyout } from '../../../components/form_flyout'; import { FormRow } from '../../../components/form_row'; import { useCurrentUser } from '../../../components/use_current_user'; +import { useHtmlId } from '../../../components/use_html_id'; import { useInitialFocus } from '../../../components/use_initial_focus'; import { RolesAPIClient } from '../../roles/roles_api_client'; import { APIKeysAPIClient } from '../api_keys_api_client'; @@ -64,7 +69,7 @@ export interface ApiKeyFormValues { interface CommonApiKeyFlyoutProps { initialValues?: ApiKeyFormValues; - onCancel: FormFlyoutProps['onCancel']; + onCancel(): void; canManageCrossClusterApiKeys?: boolean; readOnly?: boolean; } @@ -175,337 +180,466 @@ export const ApiKeyFlyout: FunctionComponent = ({ const firstFieldRef = useInitialFocus([isLoading]); + const titleId = useHtmlId('formFlyout', 'title'); + const isSubmitButtonHidden = readOnly || (apiKey && !canEdit); + + const isSubmitDisabled = + isLoading || (formik.submitCount > 0 && !formik.isValid) || readOnly || (apiKey && !canEdit); + + const title = apiKey + ? readOnly || !canEdit + ? i18n.translate('xpack.security.accountManagement.apiKeyFlyout.viewTitle', { + defaultMessage: `API key details`, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.updateTitle', { + defaultMessage: `Update API key`, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.createTitle', { + defaultMessage: `Create API key`, + }); + + const submitButtonText = apiKey + ? i18n.translate('xpack.security.accountManagement.apiKeyFlyout.updateSubmitButton', { + defaultMessage: `{isSubmitting, select, true{Updating API key…} other{Update API key}}`, + values: { isSubmitting: formik.isSubmitting }, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.createSubmitButton', { + defaultMessage: `{isSubmitting, select, true{Creating API key…} other{Create API key}}`, + values: { isSubmitting: formik.isSubmitting }, + }); + return ( - 0 && !formik.isValid) || - readOnly || - (apiKey && !canEdit) - } - isSubmitButtonHidden={readOnly || (apiKey && !canEdit)} - size="m" - ownFocus - > - - {apiKey && !readOnly ? ( - !isOwner ? ( - <> - - } - /> - - - ) : hasExpired ? ( - <> - - } - /> - - - ) : null - ) : null} - -
- - } - fullWidth - > - - - - {apiKey ? ( - <> - - - - - } - > - - - - - - } - > - - - - - + + + +

{title}

+
+
+ + + {apiKey && !readOnly ? ( + !isOwner ? ( + <> + } - > - -
-
- - + + + ) : hasExpired ? ( + <> + } - > - - - -
- - ) : canManageCrossClusterApiKeys ? ( + /> + + + ) : null + ) : null} + } fullWidth > - - - - -

- -

-
- - - - - + formik.setFieldValue('type', 'rest')} - checked={formik.values.type === 'rest'} + ), + }} + fullWidth + /> +
+ + {apiKey ? ( + <> + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + ) : canManageCrossClusterApiKeys ? ( + - - - - -

+ } + fullWidth + > + + + + +

+ +

+
+ + -

-
- - - - - - } - onChange={() => formik.setFieldValue('type', 'cross_cluster')} - checked={formik.values.type === 'cross_cluster'} + + + } + onChange={() => formik.setFieldValue('type', 'rest')} + checked={formik.values.type === 'rest'} + /> +
+ + + +

+ +

+
+ + + + + + } + onChange={() => formik.setFieldValue('type', 'cross_cluster')} + checked={formik.values.type === 'cross_cluster'} + /> +
+ +
+ ) : ( + - - - - ) : ( - - } - > - - - )} - + } + > + + + )} + - {formik.values.type === 'cross_cluster' ? ( - - } - helpText={ - + {formik.values.type === 'cross_cluster' ? ( + - - } - fullWidth - > - formik.setFieldValue('access', value)} - validate={(value: string) => { - if (!value) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.accessRequired', - { - defaultMessage: 'Enter access permissions or disable this option.', - } - ); + } + helpText={ + + + + } + fullWidth + > + formik.setFieldValue('access', value)} + validate={(value: string) => { + if (!value) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.accessRequired', + { + defaultMessage: 'Enter access permissions or disable this option.', + } + ); + } + try { + JSON.parse(value); + } catch (e) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } + ); + } + }} + fullWidth + languageId="xjson" + height={200} + /> + + ) : ( + + } - try { - JSON.parse(value); - } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', + checked={formik.values.customPrivileges} + data-test-subj="apiKeysRoleDescriptorsSwitch" + onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} + disabled={readOnly || (apiKey && !canEdit)} + /> + {formik.values.customPrivileges && ( + <> + + + + } - ); - } - }} - fullWidth - languageId="xjson" - height={200} - /> - - ) : ( + fullWidth + data-test-subj="apiKeysRoleDescriptorsCodeEditor" + > + + formik.setFieldValue('role_descriptors', value) + } + validate={(value: string) => { + if (!value) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.roleDescriptorsRequired', + { + defaultMessage: 'Enter role descriptors or disable this option.', + } + ); + } + try { + JSON.parse(value); + } catch (e) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } + ); + } + }} + fullWidth + languageId="xjson" + height={200} + /> + + + + )} + + )} + + {!apiKey && ( + <> + + + + } + checked={formik.values.customExpiration} + onChange={(e) => formik.setFieldValue('customExpiration', e.target.checked)} + disabled={readOnly || !!apiKey} + data-test-subj="apiKeyCustomExpirationSwitch" + /> + {formik.values.customExpiration && ( + <> + + + } + fullWidth + > + + + + + )} + + + )} + } - checked={formik.values.customPrivileges} - data-test-subj="apiKeysRoleDescriptorsSwitch" - onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} + data-test-subj="apiKeysMetadataSwitch" + checked={formik.values.includeMetadata} disabled={readOnly || (apiKey && !canEdit)} + onChange={(e) => formik.setFieldValue('includeMetadata', e.target.checked)} /> - {formik.values.customPrivileges && ( + {formik.values.includeMetadata && ( <> } fullWidth - data-test-subj="apiKeysRoleDescriptorsCodeEditor" > - formik.setFieldValue('role_descriptors', value) - } + value={formik.values.metadata} + onChange={(value: string) => formik.setFieldValue('metadata', value)} validate={(value: string) => { if (!value) { return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.roleDescriptorsRequired', + 'xpack.security.management.apiKeys.apiKeyFlyout.metadataRequired', { - defaultMessage: 'Enter role descriptors or disable this option.', + defaultMessage: 'Enter metadata or disable this option.', } ); } @@ -529,137 +663,40 @@ export const ApiKeyFlyout: FunctionComponent = ({ )} - )} - - {!apiKey && ( - <> - - - - } - checked={formik.values.customExpiration} - onChange={(e) => formik.setFieldValue('customExpiration', e.target.checked)} - disabled={readOnly || !!apiKey} - data-test-subj="apiKeyCustomExpirationSwitch" - /> - {formik.values.customExpiration && ( - <> - - - } - fullWidth - > - - - - - )} - - - )} - - - + + + + + - } - data-test-subj="apiKeysMetadataSwitch" - checked={formik.values.includeMetadata} - disabled={readOnly || (apiKey && !canEdit)} - onChange={(e) => formik.setFieldValue('includeMetadata', e.target.checked)} - /> - {formik.values.includeMetadata && ( - <> - - - - - } - fullWidth + + + {!isSubmitButtonHidden && ( + + - formik.setFieldValue('metadata', value)} - validate={(value: string) => { - if (!value) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.metadataRequired', - { - defaultMessage: 'Enter metadata or disable this option.', - } - ); - } - try { - JSON.parse(value); - } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', - } - ); - } - }} - fullWidth - languageId="xjson" - height={200} - /> - - - + {submitButtonText} + + )} - - -
-
+ + + +
); };