From f1648f62b7f4a50d10ec337723575b7d0808d1c8 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Mon, 15 Jan 2024 05:29:54 -0600 Subject: [PATCH] [Serverless Search] refactor(security): update create api key flyout markup (#170074) ## Summary Updating the markup for the security plugin create API key flyout to match the flyout used by serverless search getting started page. Update API key with roles image Update API key with metadata image ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Sander Philipse Co-authored-by: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../api_keys/api_keys_grid/api_key_flyout.tsx | 785 +++++++++++------- x-pack/plugins/security/tsconfig.json | 8 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../functional/apps/api_keys/home_page.ts | 20 +- .../functional/page_objects/api_keys_page.ts | 4 + 7 files changed, 489 insertions(+), 340 deletions(-) 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 d2346147da3d1..18bc6db281939 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 @@ -18,28 +18,31 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiFormFieldset, EuiFormRow, EuiHorizontalRule, + EuiIcon, + EuiPanel, EuiSkeletonText, EuiSpacer, EuiSwitch, EuiText, EuiTitle, + useEuiTheme, } from '@elastic/eui'; import { Form, FormikProvider, useFormik } from 'formik'; import moment from 'moment-timezone'; import type { FunctionComponent } from 'react'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { CodeEditorField } from '@kbn/code-editor'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { FormattedDate, FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public'; import type { CategorizedApiKey } from './api_keys_grid_page'; -import { ApiKeyBadge, ApiKeyStatus, TimeToolTip, UsernameWithIcon } from './api_keys_grid_page'; +import { ApiKeyBadge, ApiKeyStatus, TimeToolTip } from './api_keys_grid_page'; import type { ApiKeyRoleDescriptors } from '../../../../common/model'; import { DocLink } from '../../../components/doc_link'; import { FormField } from '../../../components/form_field'; @@ -56,6 +59,27 @@ import type { UpdateAPIKeyResult, } from '../api_keys_api_client'; +const TypeLabel = () => ( + +); + +const NameLabel = () => ( + +); + +const invalidJsonError = i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } +); + export interface ApiKeyFormValues { name: string; type: string; @@ -89,10 +113,10 @@ export type ApiKeyFlyoutProps = ExclusiveUnion = ({ canManageCrossClusterApiKeys = false, readOnly = false, }) => { + const { euiTheme } = useEuiTheme(); const { services } = useKibana(); const { value: currentUser, loading: isLoadingCurrentUser } = useCurrentUser(); const [{ value: roles, loading: isLoadingRoles }, getRoles] = useAsyncFn( () => new RolesAPIClient(services.http!).getRoles(), [services.http] ); + const [responseError, setResponseError] = useState(undefined); const formik = useFormik({ onSubmit: async (values) => { @@ -143,7 +169,9 @@ export const ApiKeyFlyout: FunctionComponent = ({ onSuccess?.(createApiKeyResponse); } + setResponseError(undefined); } catch (error) { + setResponseError(error.body); throw error; } }, @@ -209,6 +237,12 @@ export const ApiKeyFlyout: FunctionComponent = ({ values: { isSubmitting: formik.isSubmitting }, }); + let expirationDate: Date | undefined; + if (formik.values.customExpiration) { + expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + parseInt(formik.values.expiration, 10)); + } + return ( @@ -223,6 +257,22 @@ export const ApiKeyFlyout: FunctionComponent = ({ + {responseError && ( + <> + + } + > + {responseError.message} + + + + )} {apiKey && !readOnly ? ( !isOwner ? ( <> @@ -252,243 +302,406 @@ export const ApiKeyFlyout: FunctionComponent = ({ ) : null ) : null} - - - } - fullWidth - > - - - - {apiKey ? ( - <> - - - - - } - > + + {apiKey ? ( + <> + +

+ +

+
+ + + + + + + + + + {apiKey.name} + + + + + + + + + + + + + + + + {apiKey.username} + + + + + + + + + + + + + + -
-
- - - } - > - - - - - - } - > + +
+ + + + + + + + + + + - - - - - } - > + + + + + + + + + + + + + - - - - - ) : canManageCrossClusterApiKeys ? ( - - } - fullWidth - > - - - - -

- -

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

+ +

+
+
+
+ + +

+ +

+
+ + } fullWidth> + - - - + {canManageCrossClusterApiKeys ? ( + } fullWidth> + + + + +

+ +

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

+ +

+
+ + + + + + } + onChange={() => formik.setFieldValue('type', 'cross_cluster')} + checked={formik.values.type === 'cross_cluster'} + /> +
+
+
+ ) : ( + }> + + + )} + + )} + + + {!apiKey && ( + <> + +
+ - -

- -

-
- - + +

- - +

+
} - onChange={() => formik.setFieldValue('type', 'cross_cluster')} - checked={formik.values.type === 'cross_cluster'} + checked={Boolean(formik.values.customExpiration)} + disabled={readOnly || !!apiKey} + onChange={(e) => formik.setFieldValue('customExpiration', e.target.checked)} /> - - - - ) : ( - - } - > - - + + +

+ +

+
+
+ {formik.values.customExpiration && ( + <> + + + + + ), + }} + /> + } + > + + + + )} +
+ + )} - - {formik.values.type === 'cross_cluster' ? ( - - } - helpText={ - + +
+ + + + + + +

+ {i18n.translate( + 'xpack.security.accountManagement.apiKeyFlyout.accessPermissions.title', + { + defaultMessage: 'Access Permissions', + } + )} +

+
+
+
+
+ -
- } - 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 invalidJsonError; + } + }} + fullWidth + languageId="xjson" + height={200} + /> +
+ + ) : ( + +
+ +

+ {i18n.translate( + 'xpack.security.accountManagement.apiKeyFlyout.privileges.title', + { + defaultMessage: 'Control security privileges', + } + )} +

+ } - try { - JSON.parse(value); - } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + checked={formik.values.customPrivileges} + data-test-subj="apiKeysRoleDescriptorsSwitch" + onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} + disabled={readOnly || (apiKey && !canEdit)} + /> + + +

+ {i18n.translate( + 'xpack.security.accountManagement.apiKeyFlyout.privileges.description', { - defaultMessage: 'Enter valid JSON.', + defaultMessage: + 'Control access to specific Elasticsearch APIs and resources using predefined roles or custom privileges per API key.', } - ); - } - }} - fullWidth - languageId="xjson" - height={200} - /> - - ) : ( - - - } - checked={formik.values.customPrivileges} - data-test-subj="apiKeysRoleDescriptorsSwitch" - onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} - disabled={readOnly || (apiKey && !canEdit)} - /> + )} +

+
+
{formik.values.customPrivileges && ( <> - + = ({ try { JSON.parse(value); } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', - } - ); + return invalidJsonError; } }} fullWidth @@ -543,89 +751,42 @@ export const ApiKeyFlyout: FunctionComponent = ({ 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 - > - + +
+ +

+ - - - - )} - - - )} - - - - } - data-test-subj="apiKeysMetadataSwitch" - checked={formik.values.includeMetadata} - disabled={readOnly || (apiKey && !canEdit)} - onChange={(e) => formik.setFieldValue('includeMetadata', 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.includeMetadata && ( <> - + = ({ try { JSON.parse(value); } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', - } - ); + return invalidJsonError; } }} fullWidth @@ -679,10 +835,9 @@ export const ApiKeyFlyout: FunctionComponent = ({ height={200} /> - )} -
+
diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 0b54e40c228e7..5a28a341b94da 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -3,7 +3,12 @@ "compilerOptions": { "outDir": "target/types", }, - "include": ["common/**/*", "public/**/*", "server/**/*", "__mocks__/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "__mocks__/**/*" + ], "kbn_references": [ "@kbn/cloud-plugin", "@kbn/features-plugin", @@ -66,6 +71,7 @@ "@kbn/security-plugin-types-common", "@kbn/security-plugin-types-public", "@kbn/security-plugin-types-server", + "@kbn/kibana-utils-plugin", "@kbn/code-editor", "@kbn/code-editor-mock", ], diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index dcf92f6e1670a..fcbc20c1a8c79 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -30825,11 +30825,7 @@ "xpack.security.accountManagement.apiKeyFlyout.createTitle": "Créer une clé d'API", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeDescription": "Autorise les clusters distants à se connecter à votre cluster local.", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeLabel": "Clé d'API inter-clusters", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationInputLabel": "Durée de vie", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationLabel": "Délai d'expiration", - "xpack.security.accountManagement.apiKeyFlyout.customPrivilegesLabel": "Limiter les privilèges", "xpack.security.accountManagement.apiKeyFlyout.expirationUnit": "jours", - "xpack.security.accountManagement.apiKeyFlyout.includeMetadataLabel": "Inclure les métadonnées", "xpack.security.accountManagement.apiKeyFlyout.metadataHelpText": "Découvrez comment structurer les métadonnées.", "xpack.security.accountManagement.apiKeyFlyout.nameLabel": "Nom", "xpack.security.accountManagement.apiKeyFlyout.ownerLabel": "Propriétaire", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5cf4410604fc4..62908af4bd9c1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -30824,11 +30824,7 @@ "xpack.security.accountManagement.apiKeyFlyout.createTitle": "APIキーを作成", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeDescription": "リモートクラスターがローカルクラスターに接続できるようにします。", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeLabel": "クラスター横断APIキー", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationInputLabel": "寿命", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationLabel": "時間の後に有効期限切れ", - "xpack.security.accountManagement.apiKeyFlyout.customPrivilegesLabel": "権限を制限", "xpack.security.accountManagement.apiKeyFlyout.expirationUnit": "日", - "xpack.security.accountManagement.apiKeyFlyout.includeMetadataLabel": "メタデータを含む", "xpack.security.accountManagement.apiKeyFlyout.metadataHelpText": "メタデータを構成する方法を参照してください。", "xpack.security.accountManagement.apiKeyFlyout.nameLabel": "名前", "xpack.security.accountManagement.apiKeyFlyout.ownerLabel": "所有者", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c39642226aaa2..c1550eab55885 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -30806,11 +30806,7 @@ "xpack.security.accountManagement.apiKeyFlyout.createTitle": "创建 API 密钥", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeDescription": "允许远程集群连接到本地集群。", "xpack.security.accountManagement.apiKeyFlyout.crossClusterTypeLabel": "跨集群 API 密钥", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationInputLabel": "寿命", - "xpack.security.accountManagement.apiKeyFlyout.customExpirationLabel": "有效时间", - "xpack.security.accountManagement.apiKeyFlyout.customPrivilegesLabel": "限制权限", "xpack.security.accountManagement.apiKeyFlyout.expirationUnit": "天", - "xpack.security.accountManagement.apiKeyFlyout.includeMetadataLabel": "包括元数据", "xpack.security.accountManagement.apiKeyFlyout.metadataHelpText": "了解如何结构化元数据。", "xpack.security.accountManagement.apiKeyFlyout.nameLabel": "名称", "xpack.security.accountManagement.apiKeyFlyout.ownerLabel": "所有者", diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 316a2e32c47fd..7eb37c4665554 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -170,9 +170,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Update API key'); - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); + // Verify name input box is not present + expect(await pageObjects.apiKeys.isApiKeyNamePresent()).to.be(false); // Status should be displayed const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); @@ -278,9 +277,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); + // Verify name input box is not present + expect(await pageObjects.apiKeys.isApiKeyNamePresent()).to.be(false); // Status should be displayed const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); @@ -324,9 +322,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); + // Verify name input box is not present + expect(await pageObjects.apiKeys.isApiKeyNamePresent()).to.be(false); // Status should be displayed const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); @@ -365,9 +362,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); + // Verify name input box is not present + expect(await pageObjects.apiKeys.isApiKeyNamePresent()).to.be(false); // Status should be displayed const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts index 0875774470018..8f74f927b976f 100644 --- a/x-pack/test/functional/page_objects/api_keys_page.ts +++ b/x-pack/test/functional/page_objects/api_keys_page.ts @@ -45,6 +45,10 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) { return await testSubjects.find('apiKeyNameInput'); }, + async isApiKeyNamePresent() { + return await testSubjects.exists('apiKeyNameInput'); + }, + async setApiKeyCustomExpiration(expirationTime: string) { return await testSubjects.setValue('apiKeyCustomExpirationInput', expirationTime); },