From 1ff1e2c97a28e57f4dc1009b996cea8563eeb5ca Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 24 Feb 2021 10:02:17 +0000 Subject: [PATCH 01/22] Added ability to create API keys --- .../public/code_editor/code_editor.tsx | 24 +- .../public/code_editor/editor_theme.ts | 2 +- .../kibana_react/public/code_editor/index.tsx | 50 ++- .../plugins/security/common/model/api_key.ts | 4 + x-pack/plugins/security/common/model/index.ts | 2 +- .../security/public/components/breadcrumb.tsx | 19 +- .../public/components/copy_code_field.tsx | 56 +++ .../public/components/use_initial_focus.ts | 34 ++ .../api_keys/api_keys_api_client.mock.ts | 1 + .../api_keys/api_keys_api_client.ts | 29 +- .../api_keys_grid/api_keys_empty_prompt.tsx | 110 ++++++ .../api_keys_grid/api_keys_grid_page.test.tsx | 8 +- .../api_keys_grid/api_keys_grid_page.tsx | 303 ++++++++++----- .../api_keys_grid/create_api_key_flyout.tsx | 355 ++++++++++++++++++ .../api_keys/api_keys_management_app.test.tsx | 9 +- .../api_keys/api_keys_management_app.tsx | 93 +++-- .../public/management/management_service.ts | 2 +- .../edit_user/change_password_flyout.tsx | 5 + .../users/edit_user/confirm_disable_users.tsx | 2 +- .../management/users/users_management_app.tsx | 13 +- .../security/server/routes/api_keys/create.ts | 47 +++ .../security/server/routes/api_keys/index.ts | 2 + 22 files changed, 1001 insertions(+), 169 deletions(-) create mode 100644 x-pack/plugins/security/public/components/copy_code_field.tsx create mode 100644 x-pack/plugins/security/public/components/use_initial_focus.ts create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx create mode 100644 x-pack/plugins/security/server/routes/api_keys/create.ts diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index cb96f077b219b9..dba31afabe9bc3 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -9,7 +9,6 @@ import React from 'react'; import ReactResizeDetector from 'react-resize-detector'; import MonacoEditor from 'react-monaco-editor'; - import { monaco } from '@kbn/monaco'; import { LIGHT_THEME, DARK_THEME } from './editor_theme'; @@ -152,20 +151,33 @@ export class CodeEditor extends React.Component { const { languageId, value, onChange, width, height, options } = this.props; return ( - + <> - + ); } diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index b5d4627a5d89a3..a8aee34609c3e3 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -87,7 +87,7 @@ export function createTheme( ], colors: { 'editor.foreground': euiTheme.euiColorDarkestShade, - 'editor.background': euiTheme.euiFormBackgroundColor, + 'editor.background': '#00000000', // transparent 'editorLineNumber.foreground': euiTheme.euiColorDarkShade, 'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade, 'editorIndentGuide.background': euiTheme.euiColorLightShade, diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index 1607e2b2c11be0..0ba75149a15ec3 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -7,23 +7,53 @@ */ import React from 'react'; -import { EuiDelayRender, EuiLoadingContent } from '@elastic/eui'; +import { EuiErrorBoundary, EuiLoadingContent, EuiFormControlLayout } from '@elastic/eui'; +import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { useUiSetting } from '../ui_settings'; import type { Props } from './code_editor'; const LazyBaseEditor = React.lazy(() => import('./code_editor')); -const Fallback = () => ( - - - -); - export const CodeEditor: React.FunctionComponent = (props) => { + const { width, height, options } = props; + const darkMode = useUiSetting('theme:darkMode'); + const theme = darkMode ? euiThemeDark : euiThemeLight; + + // TODO: Render EuiTextArea as fallback when lazy loading Monaco fails, once EuiErrorBoundary allows return ( - }> - - + + + ); }; diff --git a/x-pack/plugins/security/common/model/api_key.ts b/x-pack/plugins/security/common/model/api_key.ts index 08f8378d145cea..1298e61bc0b8ec 100644 --- a/x-pack/plugins/security/common/model/api_key.ts +++ b/x-pack/plugins/security/common/model/api_key.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { Role } from './role'; + export interface ApiKey { id: string; name: string; @@ -19,3 +21,5 @@ export interface ApiKeyToInvalidate { id: string; name: string; } + +export type RoleDescriptors = Record; diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index bca8b69d03fca1..221dc524c36956 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { ApiKey, ApiKeyToInvalidate } from './api_key'; +export { ApiKey, ApiKeyToInvalidate, RoleDescriptors } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { AuthenticationProvider, shouldProviderUseLoginForm } from './authentication_provider'; diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx index 4462e2bce6abc7..968ddb8db66d09 100644 --- a/x-pack/plugins/security/public/components/breadcrumb.tsx +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -10,6 +10,7 @@ import type { FunctionComponent } from 'react'; import React, { createContext, useContext, useEffect, useRef } from 'react'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ChromeStart } from '../../../../../src/core/public'; interface BreadcrumbsContext { parents: BreadcrumbProps[]; @@ -81,8 +82,8 @@ export const BreadcrumbsProvider: FunctionComponent = if (onChange) { onChange(breadcrumbs); } else if (services.chrome) { - services.chrome.setBreadcrumbs(breadcrumbs); - services.chrome.docTitle.change(getDocTitle(breadcrumbs)); + const setBreadcrumbs = createBreadcrumbsChangeHandler(services.chrome); + setBreadcrumbs(breadcrumbs); } }; @@ -138,3 +139,17 @@ export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) .reverse() .map(({ text }) => text); } + +export function createBreadcrumbsChangeHandler( + chrome: ChromeStart, + setBreadcrumbs = chrome.setBreadcrumbs +) { + return (breadcrumbs: BreadcrumbProps[]) => { + setBreadcrumbs(breadcrumbs); + if (breadcrumbs.length === 0) { + chrome.docTitle.reset(); + } else { + chrome.docTitle.change(getDocTitle(breadcrumbs)); + } + }; +} diff --git a/x-pack/plugins/security/public/components/copy_code_field.tsx b/x-pack/plugins/security/public/components/copy_code_field.tsx new file mode 100644 index 00000000000000..bb1b66015338e7 --- /dev/null +++ b/x-pack/plugins/security/public/components/copy_code_field.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { + EuiButtonIcon, + EuiCode, + EuiCopy, + EuiFieldTextProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormControlLayout, +} from '@elastic/eui'; + +export interface CopyCodeFieldProps extends Omit { + value: string; +} + +export const CopyCodeField: FunctionComponent = (props) => ( + + {(copyText) => ( + + )} + + } + readOnly + > + + + + {props.value} + + + + +); + +CopyCodeField.defaultProps = { + readOnly: true, +}; diff --git a/x-pack/plugins/security/public/components/use_initial_focus.ts b/x-pack/plugins/security/public/components/use_initial_focus.ts new file mode 100644 index 00000000000000..7705eba7e1a771 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_initial_focus.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useRef, useEffect, DependencyList } from 'react'; + +/** + * Creates a ref for an HTML element, which will be focussed on mount. + * + * @example + * ```typescript + * const firstInput = useInitialFocus(); + * + * + * ``` + * + * @example + * ```typescript + * const firstInput = useInitialFocus([showField]); + * + * {showField ? : undefined} + * ``` + */ +export function useInitialFocus(deps: DependencyList = []) { + const inputRef = useRef(null); + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, deps); + return inputRef; +} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts index cfb20229d3f6be..1ba35a20a5e5f6 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts @@ -10,5 +10,6 @@ export const apiKeysAPIClientMock = { checkPrivileges: jest.fn(), getApiKeys: jest.fn(), invalidateApiKeys: jest.fn(), + createApiKey: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 318837f0913279..83d3a0e576cfa5 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -7,23 +7,38 @@ import type { HttpStart } from 'src/core/public'; -import type { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; +import type { ApiKey, ApiKeyToInvalidate, Role } from '../../../common/model'; -interface CheckPrivilegesResponse { +export interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; isAdmin: boolean; canManage: boolean; } -interface InvalidateApiKeysResponse { +export interface InvalidateApiKeysResponse { itemsInvalidated: ApiKeyToInvalidate[]; errors: any[]; } -interface GetApiKeysResponse { +export interface GetApiKeysResponse { apiKeys: ApiKey[]; } +export interface CreateApiKeyRequest { + name: string; + expiration?: string; + role_descriptors?: { + [key in string]: Role['elasticsearch']; + }; +} + +export interface CreateApiKeyResponse { + id: string; + name: string; + expiration: number; + api_key: string; +} + const apiKeysUrl = '/internal/security/api_key'; export class APIKeysAPIClient { @@ -42,4 +57,10 @@ export class APIKeysAPIClient { body: JSON.stringify({ apiKeys, isAdmin }), }); } + + public async createApiKey(apiKey: CreateApiKeyRequest) { + return await this.http.post(apiKeysUrl, { + body: JSON.stringify(apiKey), + }); + } } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx new file mode 100644 index 00000000000000..40881b6b061c1f --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLink } from '../../../components/doc_link'; + +export interface ApiKeysEmptyPromptProps { + error?: Error; +} + +export const ApiKeysEmptyPrompt: FunctionComponent = ({ + error, + children, +}) => { + if (error) { + if (doesErrorIndicateAPIKeysAreDisabled(error)) { + return ( + +

+ +

+

+ + + +

+ + } + /> + ); + } + + if (doesErrorIndicateUserHasNoPermissionsToManageAPIKeys(error)) { + return ( + + +

+ } + /> + ); + } + + return ( + + +

+ } + actions={children} + /> + ); + } + + return ( + + + + } + body={ +

+ +

+ } + actions={children} + /> + ); +}; + +function doesErrorIndicateAPIKeysAreDisabled(error: Record) { + const message = error.body?.message || ''; + return message.indexOf('disabled.feature="api_keys"') !== -1; +} + +function doesErrorIndicateUserHasNoPermissionsToManageAPIKeys(error: Record) { + return error.body?.statusCode === 403; +} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ff9fbad5c05b52..ab025adf7e6352 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -13,6 +13,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { coreMock } from 'src/core/public/mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { createMemoryHistory } from 'history'; import type { APIKeysAPIClient } from '../api_keys_api_client'; import { apiKeysAPIClientMock } from '../index.mock'; @@ -68,9 +69,14 @@ describe('APIKeysGridPage', () => { const coreStart = coreMock.createStart(); const renderView = () => { + const history = createMemoryHistory({ initialEntries: ['/create'] }); return mountWithIntl( - + ); }; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 62ca51be2ede8d..05dc16b8b50b4c 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -7,14 +7,18 @@ import type { EuiBasicTableColumn, EuiInMemoryTableProps } from '@elastic/eui'; import { - EuiBadge, + EuiHealth, EuiButton, EuiButtonIcon, + EuiButtonEmpty, + EuiShowFor, + EuiHideFor, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPageContent, + EuiIcon, EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, @@ -29,17 +33,24 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import { Route } from 'react-router-dom'; +import { History } from 'history'; import type { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import type { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; -import type { APIKeysAPIClient } from '../api_keys_api_client'; -import { EmptyPrompt } from './empty_prompt'; -import { InvalidateProvider } from './invalidate_provider'; -import { NotEnabled } from './not_enabled'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { Breadcrumb } from '../../../components/breadcrumb'; +import { CopyCodeField } from '../../../components/copy_code_field'; +import type { APIKeysAPIClient, CreateApiKeyResponse } from '../api_keys_api_client'; import { PermissionDenied } from './permission_denied'; +import { ApiKeysEmptyPrompt } from './api_keys_empty_prompt'; +import { CreateApiKeyFlyout } from './create_api_key_flyout'; +import { NotEnabled } from './not_enabled'; +import { InvalidateProvider } from './invalidate_provider'; interface Props { + history: History; notifications: NotificationsStart; apiKeysAPIClient: PublicMethodsOf; } @@ -53,6 +64,7 @@ interface State { apiKeys: ApiKey[]; selectedItems: ApiKey[]; error: any; + createdApiKey?: CreateApiKeyResponse; } const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss'; @@ -77,6 +89,26 @@ export class APIKeysGridPage extends Component { } public render() { + return ( +
+ + + { + this.props.history.push({ pathname: '/' }); + this.reloadApiKeys(); + this.setState({ createdApiKey: apiKey }); + }} + onCancel={() => this.props.history.push({ pathname: '/' })} + /> + + + {this.renderContent()} +
+ ); + } + + public renderContent() { const { isLoadingApp, isLoadingTable, @@ -139,29 +171,18 @@ export class APIKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + + + + + ); } - const description = ( - -

- {isAdmin ? ( - - ) : ( - - )} -

-
- ); - return ( @@ -174,10 +195,57 @@ export class APIKeysGridPage extends Component { /> - {description} + +

+ {isAdmin ? ( + + ) : ( + + )} +

+
+ + + + +
+ {this.state.createdApiKey && !this.state.isLoadingTable && ( + <> + +

+ +

+ +
+ + + )} + {this.renderTable()}
); @@ -195,8 +263,8 @@ export class APIKeysGridPage extends Component { const sorting = { sort: { - field: 'expiration', - direction: 'asc', + field: 'creation', + direction: 'desc', }, } as const; @@ -244,19 +312,6 @@ export class APIKeysGridPage extends Component { }} ) : undefined, - toolsRight: ( - this.reloadApiKeys()} - data-test-subj="reloadButton" - > - - - ), box: { incremental: true, }, @@ -306,22 +361,20 @@ export class APIKeysGridPage extends Component { return ( <> - {isAdmin ? ( + {!isAdmin ? ( <> } - color="success" + color="primary" iconType="user" - size="s" - data-test-subj="apiKeyAdminDescriptionCallOut" + data-test-subj="apiKeyManageOwnKeysCallOut" /> - - + ) : undefined} @@ -349,9 +402,11 @@ export class APIKeysGridPage extends Component { }; private getColumnConfig = () => { - const { isAdmin } = this.state; + const { isAdmin, isLoadingTable } = this.state; + + let config: Array> = []; - let config: Array> = [ + config = config.concat([ { field: 'name', name: i18n.translate('xpack.security.management.apiKeys.table.nameColumnName', { @@ -359,7 +414,7 @@ export class APIKeysGridPage extends Component { }), sortable: true, }, - ]; + ]); if (isAdmin) { config = config.concat([ @@ -369,6 +424,16 @@ export class APIKeysGridPage extends Component { defaultMessage: 'User', }), sortable: true, + render: (username: string) => ( + + + + + + {username} + + + ), }, { field: 'realm', @@ -376,6 +441,9 @@ export class APIKeysGridPage extends Component { defaultMessage: 'Realm', }), sortable: true, + mobileOptions: { + show: false, + }, }, ]); } @@ -387,49 +455,58 @@ export class APIKeysGridPage extends Component { defaultMessage: 'Created', }), sortable: true, - render: (creationDateMs: number) => moment(creationDateMs).format(DATE_FORMAT), - }, - { - field: 'expiration', - name: i18n.translate('xpack.security.management.apiKeys.table.expirationDateColumnName', { - defaultMessage: 'Expires', - }), - sortable: true, - render: (expirationDateMs: number) => { - if (expirationDateMs === undefined) { - return ( - - {i18n.translate( - 'xpack.security.management.apiKeys.table.expirationDateNeverMessage', - { - defaultMessage: 'Never', - } - )} - - ); - } - - return moment(expirationDateMs).format(DATE_FORMAT); - }, + render: (expiration: number) => ( + + {moment(expiration).fromNow()} + + ), }, { name: i18n.translate('xpack.security.management.apiKeys.table.statusColumnName', { defaultMessage: 'Status', }), render: ({ expiration }: any) => { - const now = Date.now(); + if (!expiration) { + return ( + + + + ); + } - if (now > expiration) { - return Expired; + if (Date.now() > expiration) { + return ( + + + + ); } - return Active; + return ( + + + + + + ); }, }, { - name: i18n.translate('xpack.security.management.apiKeys.table.actionsColumnName', { - defaultMessage: 'Actions', - }), actions: [ { render: ({ name, id }: any) => { @@ -443,28 +520,42 @@ export class APIKeysGridPage extends Component { > {(invalidateApiKeyPrompt) => { return ( - - + + + invalidateApiKeyPrompt([{ id, name }], this.onApiKeysInvalidated) + } + data-test-subj="invalidateApiKeyButton" + /> + + + + invalidateApiKeyPrompt([{ id, name }], this.onApiKeysInvalidated) } - )} - iconType="minusInCircle" - color="danger" - data-test-subj="invalidateApiKeyButton" - onClick={() => - invalidateApiKeyPrompt([{ id, name }], this.onApiKeysInvalidated) - } - /> - + data-test-subj="invalidateApiKeyButton" + > + + + + ); }} @@ -516,7 +607,11 @@ export class APIKeysGridPage extends Component { }; private reloadApiKeys = () => { - this.setState({ apiKeys: [], isLoadingApp: false, isLoadingTable: true }); + this.setState({ + isLoadingApp: false, + isLoadingTable: true, + createdApiKey: undefined, + }); this.loadApiKeys(); }; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx new file mode 100644 index 00000000000000..1314ac96fe1d85 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -0,0 +1,355 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, FunctionComponent } from 'react'; +import { + EuiCallOut, + EuiFieldNumber, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormFieldset, + EuiFormRow, + EuiIcon, + EuiLoadingContent, + EuiSpacer, + EuiSwitch, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana, CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; +import { + APIKeysAPIClient, + CreateApiKeyRequest, + CreateApiKeyResponse, +} from '../api_keys_api_client'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { FormFlyout, FormFlyoutProps } from '../../../components/form_flyout'; +import { DocLink } from '../../../components/doc_link'; +import { useCurrentUser } from '../../../components/use_current_user'; +import { useInitialFocus } from '../../../components/use_initial_focus'; +import { RolesAPIClient } from '../../roles/roles_api_client'; +import { RoleDescriptors } from '../../../../common/model'; + +export interface ApiKeyFormValues { + name: string; + expiration: string; + customExpiration: boolean; + customPrivileges: boolean; + role_descriptors: string; +} + +export interface CreateApiKeyFlyoutProps { + defaultValues?: ApiKeyFormValues; + onSuccess?: (apiKey: CreateApiKeyResponse) => void; + onCancel: FormFlyoutProps['onCancel']; +} + +const defaultDefaultValues: ApiKeyFormValues = { + name: '', + expiration: '', + customExpiration: false, + customPrivileges: false, + role_descriptors: JSON.stringify( + { + 'role-a': { + cluster: ['all'], + indices: [ + { + names: ['index-a*'], + privileges: ['read'], + }, + ], + }, + 'role-b': { + cluster: ['all'], + indices: [ + { + names: ['index-b*'], + privileges: ['all'], + }, + ], + }, + }, + null, + 2 + ), +}; + +export const CreateApiKeyFlyout: FunctionComponent = ({ + onSuccess, + onCancel, + defaultValues = defaultDefaultValues, +}) => { + const { services } = useKibana(); + const { value: currentUser, loading: isLoadingCurrentUser } = useCurrentUser(); + const [{ value: roles, loading: isLoadingRoles }, getRoles] = useAsyncFn( + () => new RolesAPIClient(services.http!).getRoles(), + [services.http] + ); + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + try { + const apiKey = await new APIKeysAPIClient(services.http!).createApiKey(mapValues(values)); + onSuccess?.(apiKey); + } catch (error) { + throw error; + } + }, + validate, + defaultValues, + }); + const isLoading = isLoadingCurrentUser || isLoadingRoles; + + useEffect(() => { + getRoles(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (currentUser && roles) { + const userPermissions = currentUser.roles.reduce((accumulator, roleName) => { + const role = roles.find((role) => role.name === roleName)!; + if (role) { + accumulator[role.name] = role.elasticsearch; + } + return accumulator; + }, {}); + if (!form.touched.role_descriptors) { + form.setValue('role_descriptors', JSON.stringify(userPermissions, null, 2)); + } + } + }, [currentUser, roles]); + + const firstFieldRef = useInitialFocus([isLoading]); + + return ( + + {form.submitError && ( + <> + + {(form.submitError as any).body?.message || form.submitError.message} + + + + )} + + {isLoading ? ( + + ) : ( + + + + + + + + + {currentUser?.username} + + + + + + + + + + + + form.setValue('customPrivileges', e.target.checked)} + /> + {form.values.customPrivileges && ( + <> + + + + + } + error={form.errors.role_descriptors} + isInvalid={form.touched.role_descriptors && !!form.errors.role_descriptors} + > + form.setValue('role_descriptors', value)} + languageId="xjson" + height={200} + /> + + + + )} + + + + + form.setValue('customExpiration', e.target.checked)} + /> + {form.values.customExpiration && ( + <> + + + + + + + )} + + + {/* Hidden submit button is required for enter key to trigger form submission */} + + + )} + + ); +}; + +export function validate(values: ApiKeyFormValues) { + const errors: ValidationErrors = {}; + + if (!values.name) { + errors.name = i18n.translate('xpack.security.management.apiKeys.createApiKey.nameRequired', { + defaultMessage: 'Enter a name.', + }); + } + + if (values.customExpiration && !values.expiration) { + errors.expiration = i18n.translate( + 'xpack.security.management.apiKeys.createApiKey.expirationRequired', + { + defaultMessage: 'Enter a duration or disable this option.', + } + ); + } + + if (values.customPrivileges) { + if (!values.role_descriptors) { + errors.role_descriptors = i18n.translate( + 'xpack.security.management.apiKeys.createApiKey.roleDescriptorsRequired', + { + defaultMessage: 'Enter role descriptors or disable this option.', + } + ); + } else { + try { + JSON.parse(values.role_descriptors); + } catch (e) { + errors.role_descriptors = i18n.translate( + 'xpack.security.management.apiKeys.createApiKey.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } + ); + } + } + } + + return errors; +} + +export function mapValues(values: ApiKeyFormValues): CreateApiKeyRequest { + return { + name: values.name, + expiration: values.customExpiration && values.expiration ? `${values.expiration}d` : undefined, + role_descriptors: + values.customPrivileges && values.role_descriptors + ? JSON.parse(values.role_descriptors) + : undefined, + }; +} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx index bada8c5c7ce4cf..f5d39f95318399 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx @@ -8,15 +8,17 @@ jest.mock('./api_keys_grid', () => ({ APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, })); -import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; import { apiKeysManagementApp } from './api_keys_management_app'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; +import { securityMock } from '../../mocks'; describe('apiKeysManagementApp', () => { it('create() returns proper management app descriptor', () => { const { getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); - expect(apiKeysManagementApp.create({ getStartServices: getStartServices as any })) + expect(apiKeysManagementApp.create({ authc, getStartServices: getStartServices as any })) .toMatchInlineSnapshot(` Object { "id": "api_keys", @@ -29,6 +31,7 @@ describe('apiKeysManagementApp', () => { it('mount() works for the `grid` page', async () => { const { getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); const startServices = await getStartServices(); const docTitle = startServices[0].chrome.docTitle; @@ -37,7 +40,7 @@ describe('apiKeysManagementApp', () => { const setBreadcrumbs = jest.fn(); const unmount = await apiKeysManagementApp - .create({ getStartServices: () => Promise.resolve(startServices) as any }) + .create({ authc, getStartServices: () => Promise.resolve(startServices) as any }) .mount({ basePath: '/some-base-path', element: container, diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 8fa52ba7e2edd2..e027ade447d973 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -5,63 +5,98 @@ * 2.0. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; - +import { Router } from 'react-router-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import type { StartServicesAccessor } from 'src/core/public'; -import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; - +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import type { PluginStartDependencies } from '../../plugin'; +import { AuthenticationServiceSetup } from '../../authentication'; +import { PluginStartDependencies } from '../../plugin'; +import { + BreadcrumbsProvider, + BreadcrumbsChangeHandler, + Breadcrumb, + createBreadcrumbsChangeHandler, +} from '../../components/breadcrumb'; +import { AuthenticationProvider } from '../../components/use_current_user'; interface CreateParams { + authc: AuthenticationServiceSetup; getStartServices: StartServicesAccessor; } export const apiKeysManagementApp = Object.freeze({ id: 'api_keys', - create({ getStartServices }: CreateParams) { - const title = i18n.translate('xpack.security.management.apiKeysTitle', { - defaultMessage: 'API Keys', - }); + create({ authc, getStartServices }: CreateParams) { return { id: this.id, order: 30, - title, - async mount({ element, setBreadcrumbs }) { - setBreadcrumbs([ - { - text: title, - href: `/`, - }, - ]); - - const [[core], { APIKeysGridPage }, { APIKeysAPIClient }] = await Promise.all([ + title: i18n.translate('xpack.security.management.apiKeysTitle', { + defaultMessage: 'API keys', + }), + async mount({ element, setBreadcrumbs, history }) { + const [[coreStart], { APIKeysGridPage }, { APIKeysAPIClient }] = await Promise.all([ getStartServices(), import('./api_keys_grid'), import('./api_keys_api_client'), ]); - core.chrome.docTitle.change(title); - render( - - + + - - , + + , element ); return () => { - core.chrome.docTitle.reset(); unmountComponentAtNode(element); }; }, } as RegisterManagementAppArgs; }, }); + +export interface ProvidersProps { + services: CoreStart; + history: History; + authc: AuthenticationServiceSetup; + onChange?: BreadcrumbsChangeHandler; +} + +export const Providers: FunctionComponent = ({ + services, + history, + authc, + onChange, + children, +}) => ( + + + + + {children} + + + + +); diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts index 7809a45db16605..af1b05e64e37c9 100644 --- a/x-pack/plugins/security/public/management/management_service.ts +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -47,7 +47,7 @@ export class ManagementService { this.securitySection.registerApp( rolesManagementApp.create({ fatalErrors, license, getStartServices }) ); - this.securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + this.securitySection.registerApp(apiKeysManagementApp.create({ authc, getStartServices })); this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); } diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx index 01b387c9e1fc28..4412ed561691ee 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -24,6 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useInitialFocus } from '../../../components/use_initial_focus'; import { FormFlyout } from '../../../components/form_flyout'; import { useCurrentUser } from '../../../components/use_current_user'; import type { ValidationErrors } from '../../../components/use_form'; @@ -147,6 +148,8 @@ export const ChangePasswordFlyout: FunctionComponent defaultValues, }); + const firstFieldRef = useInitialFocus([isLoading]); + return ( defaultValue={form.values.current_password} isInvalid={form.touched.current_password && !!form.errors.current_password} autoComplete="current-password" + inputRef={firstFieldRef} /> ) : null} @@ -263,6 +267,7 @@ export const ChangePasswordFlyout: FunctionComponent defaultValue={form.values.password} isInvalid={form.touched.password && !!form.errors.password} autoComplete="new-password" + inputRef={isCurrentUser ? undefined : firstFieldRef} /> =

diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 3e18734cbf3683..4467cedf6b77e3 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -19,8 +19,12 @@ import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import type { AuthenticationServiceSetup } from '../../authentication'; -import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb'; -import { Breadcrumb, BreadcrumbsProvider, getDocTitle } from '../../components/breadcrumb'; +import { + BreadcrumbsProvider, + BreadcrumbsChangeHandler, + Breadcrumb, + createBreadcrumbsChangeHandler, +} from '../../components/breadcrumb'; import { AuthenticationProvider } from '../../components/use_current_user'; import type { PluginStartDependencies } from '../../plugin'; import { tryDecodeURIComponent } from '../url_utils'; @@ -64,10 +68,7 @@ export const usersManagementApp = Object.freeze({ services={coreStart} history={history} authc={authc} - onChange={(breadcrumbs) => { - setBreadcrumbs(breadcrumbs); - coreStart.chrome.docTitle.change(getDocTitle(breadcrumbs)); - }} + onChange={createBreadcrumbsChangeHandler(coreStart.chrome, setBreadcrumbs)} > { + try { + const apiKey = await getAuthenticationService().apiKeys.create(request, request.body); + + if (!apiKey) { + return response.badRequest({ body: { message: `API Keys are not available` } }); + } + + return response.ok({ body: apiKey }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index e6a8711bdf19e6..f726e2c12cfe86 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -8,12 +8,14 @@ import type { RouteDefinitionParams } from '../'; import { defineEnabledApiKeysRoutes } from './enabled'; import { defineGetApiKeysRoutes } from './get'; +import { defineCreateApiKeyRoutes } from './create'; import { defineInvalidateApiKeysRoutes } from './invalidate'; import { defineCheckPrivilegesRoutes } from './privileges'; export function defineApiKeysRoutes(params: RouteDefinitionParams) { defineEnabledApiKeysRoutes(params); defineGetApiKeysRoutes(params); + defineCreateApiKeyRoutes(params); defineCheckPrivilegesRoutes(params); defineInvalidateApiKeysRoutes(params); } From 6c373bbc2227b145c49196915d81e42221b94ef5 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 25 Feb 2021 09:33:55 +0000 Subject: [PATCH 02/22] Remove hard coded colours --- .../public/components/copy_code_field.tsx | 68 +++++++++++-------- .../public/components/use_initial_focus.ts | 2 + .../security/public/components/use_theme.ts | 26 +++++++ .../api_keys_grid/api_keys_grid_page.tsx | 2 +- 4 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/security/public/components/use_theme.ts diff --git a/x-pack/plugins/security/public/components/copy_code_field.tsx b/x-pack/plugins/security/public/components/copy_code_field.tsx index bb1b66015338e7..b2cfc4a444bb24 100644 --- a/x-pack/plugins/security/public/components/copy_code_field.tsx +++ b/x-pack/plugins/security/public/components/copy_code_field.tsx @@ -14,42 +14,50 @@ import { EuiFlexItem, EuiFormControlLayout, } from '@elastic/eui'; +import { useTheme } from './use_theme'; export interface CopyCodeFieldProps extends Omit { value: string; } -export const CopyCodeField: FunctionComponent = (props) => ( - - {(copyText) => ( - - )} - - } - readOnly - > - = (props) => { + const theme = useTheme(); + + return ( + + {(copyText) => ( + + )} + + } + readOnly > - - - {props.value} - - - - -); + + + + {props.value} + + + + + ); +}; CopyCodeField.defaultProps = { readOnly: true, diff --git a/x-pack/plugins/security/public/components/use_initial_focus.ts b/x-pack/plugins/security/public/components/use_initial_focus.ts index 7705eba7e1a771..f51ac614ee8eae 100644 --- a/x-pack/plugins/security/public/components/use_initial_focus.ts +++ b/x-pack/plugins/security/public/components/use_initial_focus.ts @@ -16,6 +16,8 @@ import { useRef, useEffect, DependencyList } from 'react'; * * ``` * + * Pass in a dependency list to focus conditionally rendered components: + * * @example * ```typescript * const firstInput = useInitialFocus([showField]); diff --git a/x-pack/plugins/security/public/components/use_theme.ts b/x-pack/plugins/security/public/components/use_theme.ts new file mode 100644 index 00000000000000..b2e7482c1b95d7 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_theme.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; + +/** + * Returns correct EUI theme depending on dark mode setting. + * + * @example + * ```typescript + * const theme = useTheme(); + * + * + * {props.value} + * + * ``` + */ +export function useTheme() { + const darkMode = useUiSetting('theme:darkMode'); + return darkMode ? euiThemeDark : euiThemeLight; +} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 05dc16b8b50b4c..4b46f0ec8dfa20 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -212,7 +212,7 @@ export class APIKeysGridPage extends Component { - + Date: Tue, 2 Mar 2021 18:22:15 +0000 Subject: [PATCH 03/22] Added unit tests --- .../kibana_react/public/code_editor/index.tsx | 40 ++- .../public/components/confirm_modal.tsx | 84 ------ .../public/components/copy_code_field.tsx | 9 +- .../public/components/use_initial_focus.ts | 5 +- .../security/public/components/use_theme.ts | 5 +- .../api_keys_grid_page.test.tsx.snap | 243 ---------------- .../api_keys_grid/api_keys_grid_page.test.tsx | 270 +++++++++--------- .../api_keys_grid/api_keys_grid_page.tsx | 57 ++-- .../users/edit_user/confirm_delete_users.tsx | 17 +- .../users/edit_user/confirm_disable_users.tsx | 17 +- .../users/edit_user/confirm_enable_users.tsx | 15 +- 11 files changed, 213 insertions(+), 549 deletions(-) delete mode 100644 x-pack/plugins/security/public/components/confirm_modal.tsx delete mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index 0ba75149a15ec3..774769e5000264 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -7,7 +7,12 @@ */ import React from 'react'; -import { EuiErrorBoundary, EuiLoadingContent, EuiFormControlLayout } from '@elastic/eui'; +import { + EuiDelayRender, + EuiErrorBoundary, + EuiLoadingContent, + EuiFormControlLayout, +} from '@elastic/eui'; import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { useUiSetting } from '../ui_settings'; @@ -20,37 +25,30 @@ export const CodeEditor: React.FunctionComponent = (props) => { const darkMode = useUiSetting('theme:darkMode'); const theme = darkMode ? euiThemeDark : euiThemeLight; + const style = { + width, + height, + backgroundColor: options?.readOnly + ? theme.euiFormBackgroundReadOnlyColor + : theme.euiFormBackgroundColor, + }; - // TODO: Render EuiTextArea as fallback when lazy loading Monaco fails, once EuiErrorBoundary allows return ( diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx deleted file mode 100644 index 80c2008642d049..00000000000000 --- a/x-pack/plugins/security/public/components/confirm_modal.tsx +++ /dev/null @@ -1,84 +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, EuiModalProps } from '@elastic/eui'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; -import type { FunctionComponent } from 'react'; -import React from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export interface ConfirmModalProps extends Omit { - confirmButtonText: string; - confirmButtonColor?: EuiButtonProps['color']; - isLoading?: EuiButtonProps['isLoading']; - isDisabled?: EuiButtonProps['isDisabled']; - onCancel(): void; - onConfirm(): void; -} - -/** - * Component that renders a confirmation modal similar to `EuiConfirmModal`, except that - * it adds `isLoading` prop, which renders a loading spinner and disables action buttons. - */ -export const ConfirmModal: FunctionComponent = ({ - children, - confirmButtonColor: buttonColor, - confirmButtonText, - isLoading, - isDisabled, - onCancel, - onConfirm, - title, - ...rest -}) => ( - - - {title} - - {children} - - - - - - - - - - {confirmButtonText} - - - - - -); diff --git a/x-pack/plugins/security/public/components/copy_code_field.tsx b/x-pack/plugins/security/public/components/copy_code_field.tsx index b2cfc4a444bb24..4a6c1672a84d7a 100644 --- a/x-pack/plugins/security/public/components/copy_code_field.tsx +++ b/x-pack/plugins/security/public/components/copy_code_field.tsx @@ -1,10 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiCode, @@ -30,6 +32,9 @@ export const CopyCodeField: FunctionComponent = (props) => { {(copyText) => ( - } -> -

-
- - - - API keys not enabled in Elasticsearch - - -
- -
- - - , - } - } - > - Contact your system administrator and refer to the - - - - docs - - - - - - - - (opens in a new tab or window) - - - - - - to enable API keys. - -
-
-
-
-`; - -exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` - - -
- - -
- - -

- } - iconType="securityApp" - title={ -

- -

- } - > -
- - - - -
- - - - -

- - You need permission to manage API keys - -

-
- -
- - -
-

- - Contact your system administrator. - -

-
-
- - -
- -
- - -
- - -`; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ab025adf7e6352..c51e83458af46d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -5,189 +5,177 @@ * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; -import type { ReactWrapper } from 'enzyme'; import React from 'react'; - -import { mountWithIntl } from '@kbn/test/jest'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { coreMock } from 'src/core/public/mocks'; -import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { + render, + fireEvent, + waitFor, + within, + waitForElementToBeRemoved, +} from '@testing-library/react'; import { createMemoryHistory } from 'history'; - -import type { APIKeysAPIClient } from '../api_keys_api_client'; -import { apiKeysAPIClientMock } from '../index.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { securityMock } from '../../../mocks'; +import { Providers } from '../api_keys_management_app'; import { APIKeysGridPage } from './api_keys_grid_page'; -import { NotEnabled } from './not_enabled'; -import { PermissionDenied } from './permission_denied'; - -const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); - -const waitForRender = async ( - wrapper: ReactWrapper, - condition: (wrapper: ReactWrapper) => boolean -) => { - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - await Promise.resolve(); - wrapper.update(); - if (condition(wrapper)) { - resolve(); - } - }, 10); - - setTimeout(() => { - clearInterval(interval); - reject(new Error('waitForRender timeout after 2000ms')); - }, 2000); - }); -}; +import { apiKeysAPIClientMock } from '../index.mock'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const coreStart = coreMock.createStart(); + +const apiClientMock = apiKeysAPIClientMock.create(); +apiClientMock.checkPrivileges.mockResolvedValue({ + areApiKeysEnabled: true, + canManage: true, + isAdmin: true, +}); +apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], +}); + +const authc = securityMock.createSetup().authc; +authc.getCurrentUser.mockResolvedValue( + mockAuthenticatedUser({ + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }) +); describe('APIKeysGridPage', () => { - let apiClientMock: jest.Mocked>; - beforeEach(() => { - apiClientMock = apiKeysAPIClientMock.create(); - apiClientMock.checkPrivileges.mockResolvedValue({ - isAdmin: true, - areApiKeysEnabled: true, - canManage: true, - }); - apiClientMock.getApiKeys.mockResolvedValue({ - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'my-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], - }); - }); + it('loads and displays API keys', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }); - const coreStart = coreMock.createStart(); - const renderView = () => { - const history = createMemoryHistory({ initialEntries: ['/create'] }); - return mountWithIntl( - + const { getByText } = render( + - + ); - }; - it('renders a loading state when fetching API keys', async () => { - expect(renderView().find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); + getByText(/my-api-key/); }); - it('renders a callout when API keys are not enabled', async () => { - apiClientMock.checkPrivileges.mockResolvedValue({ - isAdmin: true, - canManage: true, + it('displays callout when API keys are disabled', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }); + apiClientMock.checkPrivileges.mockResolvedValueOnce({ areApiKeysEnabled: false, + canManage: true, + isAdmin: true, }); - const wrapper = renderView(); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(NotEnabled).length > 0; - }); + const { getByText } = render( + + + + ); - expect(wrapper.find(NotEnabled).find(EuiCallOut)).toMatchSnapshot(); + await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); + getByText(/API keys not enabled/); }); - it('renders permission denied if user does not have required permissions', async () => { - apiClientMock.checkPrivileges.mockResolvedValue({ + it('displays error when user does not have required permissions', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }); + apiClientMock.checkPrivileges.mockResolvedValueOnce({ + areApiKeysEnabled: true, canManage: false, isAdmin: false, - areApiKeysEnabled: true, - }); - - const wrapper = renderView(); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(PermissionDenied).length > 0; }); - expect(wrapper.find(PermissionDenied)).toMatchSnapshot(); - }); - - it('renders error callout if error fetching API keys', async () => { - apiClientMock.getApiKeys.mockRejectedValue(mock500()); - - const wrapper = renderView(); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(EuiCallOut).length > 0; - }); + const { getByText } = render( + + + + ); - expect(wrapper.find('EuiCallOut[data-test-subj="apiKeysError"]')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); + getByText(/You need permission to manage API keys/); }); - describe('Admin view', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - wrapper = renderView(); + it('displays error when fetching API keys fails', async () => { + apiClientMock.getApiKeys.mockRejectedValueOnce({ + body: { error: 'Internal Server Error', message: '', statusCode: 500 }, }); + const history = createMemoryHistory({ initialEntries: ['/'] }); - it('renders a callout indicating the user is an administrator', async () => { - const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; + const { getByText } = render( + + + + ); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(calloutEl).length > 0; - }); + await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); + getByText(/Could not load API keys/); + }); - expect(wrapper.find(calloutEl).text()).toEqual('You are an API Key administrator.'); - }); + it('creates user when submitting form and redirects back', async () => { + const history = createMemoryHistory({ initialEntries: ['/create'] }); + coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); + coreStart.http.post.mockResolvedValue({ api_key: 's3Cr3t_aP1-K3Y=' }); - it('renders the correct description text', async () => { - const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; + const { findByRole, findByText } = render( + + + + ); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(descriptionEl).length > 0; - }); + const dialog = await findByRole('dialog'); - expect(wrapper.find(descriptionEl).text()).toEqual( - 'View and invalidate API keys. An API key sends requests on behalf of a user.' - ); - }); - }); + fireEvent.click(await findByRole('button', { name: 'Create API key' })); - describe('Non-admin view', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - apiClientMock.checkPrivileges.mockResolvedValue({ - isAdmin: false, - canManage: true, - areApiKeysEnabled: true, - }); + const alert = await findByRole('alert'); + within(alert).getByText(/Enter a name/i); - wrapper = renderView(); + fireEvent.change(await within(dialog).findByLabelText('Name'), { + target: { value: 'Test' }, }); - it('does NOT render a callout indicating the user is an administrator', async () => { - const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; - const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; + fireEvent.click(await findByRole('button', { name: 'Create API key' })); - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(descriptionEl).length > 0; + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', { + body: JSON.stringify({ name: 'Test' }), }); - - expect(wrapper.find(calloutEl).length).toEqual(0); + expect(history.location.pathname).toBe('/'); }); - it('renders the correct description text', async () => { - const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; - - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(descriptionEl).length > 0; - }); - - expect(wrapper.find(descriptionEl).text()).toEqual( - 'View and invalidate your API keys. An API key sends requests on your behalf.' - ); - }); + await findByText('s3Cr3t_aP1-K3Y='); }); }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 4b46f0ec8dfa20..b9fb890f4cffbe 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -122,7 +122,7 @@ export class APIKeysGridPage extends Component { if (isLoadingApp) { return ( - + { } if (error) { - const { - body: { error: errorTitle, message, statusCode }, - } = error; - return ( - + - } - color="danger" - iconType="alert" - data-test-subj="apiKeysError" - > - {statusCode}: {errorTitle} - {message} - + + ); } @@ -372,31 +363,23 @@ export class APIKeysGridPage extends Component { } color="primary" iconType="user" - data-test-subj="apiKeyManageOwnKeysCallOut" /> ) : undefined} - { - { - return { - 'data-test-subj': 'apiKeyRow', - }; - }} - /> - } + ); }; @@ -404,7 +387,7 @@ export class APIKeysGridPage extends Component { private getColumnConfig = () => { const { isAdmin, isLoadingTable } = this.state; - let config: Array> = []; + let config: Array> = []; config = config.concat([ { diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx index b3c6f80812fabd..83f2af421bd454 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -5,17 +5,16 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ConfirmModal } from '../../../components/confirm_modal'; -import { UserAPIClient } from '../user_api_client'; +import { UserAPIClient } from '..'; export interface ConfirmDeleteUsersProps { usernames: string[]; @@ -54,13 +53,19 @@ export const ConfirmDeleteUsers: FunctionComponent = ({ }, [services.http]); return ( - = ({ values: { count: usernames.length, isLoading: state.loading }, } )} - confirmButtonColor="danger" + buttonColor="danger" isLoading={state.loading} > @@ -94,6 +99,6 @@ export const ConfirmDeleteUsers: FunctionComponent = ({ />

-
+ ); }; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx index 9e205f903465e2..3bd02f5539cf63 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -5,17 +5,16 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ConfirmModal } from '../../../components/confirm_modal'; -import { UserAPIClient } from '../user_api_client'; +import { UserAPIClient } from '..'; export interface ConfirmDisableUsersProps { usernames: string[]; @@ -58,13 +57,19 @@ export const ConfirmDisableUsers: FunctionComponent = }, [services.http]); return ( - = values: { count: usernames.length, isLoading: state.loading }, }) } - confirmButtonColor={isSystemUser ? 'danger' : undefined} + buttonColor={isSystemUser ? 'danger' : undefined} isLoading={state.loading} > {isSystemUser ? ( @@ -117,6 +122,6 @@ export const ConfirmDisableUsers: FunctionComponent = )} )} - + ); }; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx index 24364d7b56d99c..b95bd3f706f209 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -5,17 +5,16 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ConfirmModal } from '../../../components/confirm_modal'; -import { UserAPIClient } from '../user_api_client'; +import { UserAPIClient } from '..'; export interface ConfirmEnableUsersProps { usernames: string[]; @@ -54,13 +53,19 @@ export const ConfirmEnableUsers: FunctionComponent = ({ }, [services.http]); return ( - = ({ )} - + ); }; From 65c4cd2c297d159f7c64ed69b50adb7ff961ac54 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 3 Mar 2021 09:48:12 +0000 Subject: [PATCH 04/22] Fix linting errors --- .../plugins/security/common/model/api_key.ts | 2 +- .../security/public/components/breadcrumb.tsx | 2 +- .../public/components/copy_code_field.tsx | 5 ++-- .../public/components/use_initial_focus.ts | 5 ++-- .../api_keys_grid/api_keys_empty_prompt.tsx | 8 +++--- .../api_keys_grid/api_keys_grid_page.tsx | 2 +- .../api_keys_grid/create_api_key_flyout.tsx | 27 ++++++++++--------- .../api_keys/api_keys_management_app.tsx | 15 ++++++----- .../management/users/users_management_app.tsx | 2 +- .../security/server/routes/api_keys/create.ts | 7 ++--- 10 files changed, 41 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/security/common/model/api_key.ts b/x-pack/plugins/security/common/model/api_key.ts index 1298e61bc0b8ec..65298bd15c0118 100644 --- a/x-pack/plugins/security/common/model/api_key.ts +++ b/x-pack/plugins/security/common/model/api_key.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Role } from './role'; +import type { Role } from './role'; export interface ApiKey { id: string; diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx index 968ddb8db66d09..eb890bcd46e4fd 100644 --- a/x-pack/plugins/security/public/components/breadcrumb.tsx +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -10,7 +10,7 @@ import type { FunctionComponent } from 'react'; import React, { createContext, useContext, useEffect, useRef } from 'react'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { ChromeStart } from '../../../../../src/core/public'; +import type { ChromeStart } from '../../../../../src/core/public'; interface BreadcrumbsContext { parents: BreadcrumbProps[]; diff --git a/x-pack/plugins/security/public/components/copy_code_field.tsx b/x-pack/plugins/security/public/components/copy_code_field.tsx index 4a6c1672a84d7a..87b4b97ca483af 100644 --- a/x-pack/plugins/security/public/components/copy_code_field.tsx +++ b/x-pack/plugins/security/public/components/copy_code_field.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; +import type { EuiFieldTextProps } from '@elastic/eui'; import { EuiButtonIcon, EuiCode, EuiCopy, - EuiFieldTextProps, EuiFlexGroup, EuiFlexItem, EuiFormControlLayout, diff --git a/x-pack/plugins/security/public/components/use_initial_focus.ts b/x-pack/plugins/security/public/components/use_initial_focus.ts index 1ff0ad6741d7f8..88b423ceefd5a7 100644 --- a/x-pack/plugins/security/public/components/use_initial_focus.ts +++ b/x-pack/plugins/security/public/components/use_initial_focus.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { useRef, useEffect, DependencyList } from 'react'; +import type { DependencyList } from 'react'; +import { useRef, useEffect } from 'react'; /** * Creates a ref for an HTML element, which will be focussed on mount. @@ -32,6 +33,6 @@ export function useInitialFocus(deps: DependencyList = [] if (inputRef.current) { inputRef.current.focus(); } - }, deps); + }, deps); // eslint-disable-line react-hooks/exhaustive-deps return inputRef; } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx index 40881b6b061c1f..c8925292467b71 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx @@ -1,10 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 React, { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLink } from '../../../components/doc_link'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index b9fb890f4cffbe..0fc4561dd3ee42 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -34,7 +34,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { Route } from 'react-router-dom'; -import { History } from 'history'; +import type { History } from 'history'; import type { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index 1314ac96fe1d85..183572d82d2c36 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -1,10 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 React, { useEffect, FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React, { useEffect } from 'react'; import { EuiCallOut, EuiFieldNumber, @@ -24,18 +26,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { useKibana, CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; -import { - APIKeysAPIClient, - CreateApiKeyRequest, - CreateApiKeyResponse, -} from '../api_keys_api_client'; -import { useForm, ValidationErrors } from '../../../components/use_form'; -import { FormFlyout, FormFlyoutProps } from '../../../components/form_flyout'; +import type { CreateApiKeyRequest, CreateApiKeyResponse } from '../api_keys_api_client'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import type { ValidationErrors } from '../../../components/use_form'; +import { useForm } from '../../../components/use_form'; +import type { FormFlyoutProps } from '../../../components/form_flyout'; +import { FormFlyout } from '../../../components/form_flyout'; import { DocLink } from '../../../components/doc_link'; import { useCurrentUser } from '../../../components/use_current_user'; import { useInitialFocus } from '../../../components/use_initial_focus'; import { RolesAPIClient } from '../../roles/roles_api_client'; -import { RoleDescriptors } from '../../../../common/model'; +import type { RoleDescriptors } from '../../../../common/model'; export interface ApiKeyFormValues { name: string; @@ -114,7 +115,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ useEffect(() => { if (currentUser && roles) { const userPermissions = currentUser.roles.reduce((accumulator, roleName) => { - const role = roles.find((role) => role.name === roleName)!; + const role = roles.find((r) => r.name === roleName)!; if (role) { accumulator[role.name] = role.elasticsearch; } @@ -124,7 +125,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ form.setValue('role_descriptors', JSON.stringify(userPermissions, null, 2)); } } - }, [currentUser, roles]); + }, [currentUser, roles]); // eslint-disable-line react-hooks/exhaustive-deps const firstFieldRef = useInitialFocus([isLoading]); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index e027ade447d973..e23124b023c84e 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -5,20 +5,21 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router } from 'react-router-dom'; -import { History } from 'history'; +import type { History } from 'history'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; -import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import type { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; +import type { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { AuthenticationServiceSetup } from '../../authentication'; -import { PluginStartDependencies } from '../../plugin'; +import type { AuthenticationServiceSetup } from '../../authentication'; +import type { PluginStartDependencies } from '../../plugin'; +import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb'; import { BreadcrumbsProvider, - BreadcrumbsChangeHandler, Breadcrumb, createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 4467cedf6b77e3..eb9a33c7f5f9bc 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -19,9 +19,9 @@ import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import type { AuthenticationServiceSetup } from '../../authentication'; +import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb'; import { BreadcrumbsProvider, - BreadcrumbsChangeHandler, Breadcrumb, createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts index 5337dff47240ad..22ed8c73bb015c 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.ts @@ -1,13 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { schema } from '@kbn/config-schema'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../errors'; -import { RouteDefinitionParams } from '..'; +import type { RouteDefinitionParams } from '..'; export function defineCreateApiKeyRoutes({ router, From d834d94bd3367805efa290e434c61d48873688c9 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Sun, 7 Mar 2021 22:36:50 +0000 Subject: [PATCH 05/22] Display full base64 encoded API key --- .../security/public/components/code_field.tsx | 141 ++++++++++++++++ .../public/components/copy_code_field.tsx | 70 -------- .../api_keys_grid/api_keys_grid_page.test.tsx | 8 +- .../api_keys_grid/api_keys_grid_page.tsx | 150 ++++++++++++++---- 4 files changed, 262 insertions(+), 107 deletions(-) create mode 100644 x-pack/plugins/security/public/components/code_field.tsx delete mode 100644 x-pack/plugins/security/public/components/copy_code_field.tsx diff --git a/x-pack/plugins/security/public/components/code_field.tsx b/x-pack/plugins/security/public/components/code_field.tsx new file mode 100644 index 00000000000000..7e84965926fc82 --- /dev/null +++ b/x-pack/plugins/security/public/components/code_field.tsx @@ -0,0 +1,141 @@ +/* + * 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 { FunctionComponent, ReactElement } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { EuiFieldTextProps } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiCopy, + EuiFormControlLayout, + EuiHorizontalRule, + EuiPopover, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { useTheme } from './use_theme'; + +export interface CodeFieldProps extends Omit { + value: string; +} + +export const CodeField: FunctionComponent = (props) => { + const theme = useTheme(); + + return ( + + {(copyText) => ( + + )} + + } + style={{ backgroundColor: 'transparent' }} + readOnly + > + event.currentTarget.select()} + readOnly + /> + + ); +}; + +export interface SelectableCodeFieldOption { + key: string; + value: string; + icon?: string; + label: string; + description?: string; +} + +export interface SelectableCodeFieldProps extends Omit { + options: Array; +} + +export const SelectableCodeField: FunctionComponent = (props) => { + const { options, ...rest } = props; + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [selectedOption, setSelectedOption] = React.useState(options[0]); + const selectedIndex = options.findIndex((c) => c.key === selectedOption.key); + const closePopover = () => setIsPopoverOpen(false); + + return ( + { + console.log('toggle'); + setIsPopoverOpen(!isPopoverOpen); + }} + > + {selectedOption.label} + + } + isOpen={isPopoverOpen} + panelPaddingSize="none" + closePopover={closePopover} + > + >((accumulator, option, i) => { + accumulator.push( + { + closePopover(); + setSelectedOption(option); + }} + > + {option.label} + + +

{option.description}

+
+
+ ); + if (i < options.length - 1) { + accumulator.push( + + ); + } + return accumulator; + }, [])} + /> + + } + value={selectedOption.value} + /> + ); +}; diff --git a/x-pack/plugins/security/public/components/copy_code_field.tsx b/x-pack/plugins/security/public/components/copy_code_field.tsx deleted file mode 100644 index 87b4b97ca483af..00000000000000 --- a/x-pack/plugins/security/public/components/copy_code_field.tsx +++ /dev/null @@ -1,70 +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 { FunctionComponent } from 'react'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import type { EuiFieldTextProps } from '@elastic/eui'; -import { - EuiButtonIcon, - EuiCode, - EuiCopy, - EuiFlexGroup, - EuiFlexItem, - EuiFormControlLayout, -} from '@elastic/eui'; -import { useTheme } from './use_theme'; - -export interface CopyCodeFieldProps extends Omit { - value: string; -} - -export const CopyCodeField: FunctionComponent = (props) => { - const theme = useTheme(); - - return ( - - {(copyText) => ( - - )} - - } - readOnly - > - - - - {props.value} - - - - - ); -}; - -CopyCodeField.defaultProps = { - readOnly: true, -}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index c51e83458af46d..eb942a8656e144 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -140,12 +140,12 @@ describe('APIKeysGridPage', () => { getByText(/Could not load API keys/); }); - it('creates user when submitting form and redirects back', async () => { + it('creates API key when submitting form, redirects back and displays base64', async () => { const history = createMemoryHistory({ initialEntries: ['/create'] }); coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); - coreStart.http.post.mockResolvedValue({ api_key: 's3Cr3t_aP1-K3Y=' }); + coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' }); - const { findByRole, findByText } = render( + const { findByRole, findByDisplayValue } = render( { expect(history.location.pathname).toBe('/'); }); - await findByText('s3Cr3t_aP1-K3Y='); + await findByDisplayValue(btoa('1D:AP1_K3Y')); }); }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 0fc4561dd3ee42..52cf9562d02b68 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -7,21 +7,23 @@ import type { EuiBasicTableColumn, EuiInMemoryTableProps } from '@elastic/eui'; import { - EuiHealth, + EuiBadge, EuiButton, - EuiButtonIcon, EuiButtonEmpty, - EuiShowFor, - EuiHideFor, + EuiButtonIcon, EuiCallOut, + EuiCode, EuiFlexGroup, EuiFlexItem, + EuiHealth, + EuiHideFor, + EuiIcon, EuiInMemoryTable, EuiPageContent, - EuiIcon, EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, + EuiShowFor, EuiSpacer, EuiText, EuiTitle, @@ -41,7 +43,7 @@ import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/pu import type { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { Breadcrumb } from '../../../components/breadcrumb'; -import { CopyCodeField } from '../../../components/copy_code_field'; +import { SelectableCodeField } from '../../../components/code_field'; import type { APIKeysAPIClient, CreateApiKeyResponse } from '../api_keys_api_client'; import { PermissionDenied } from './permission_denied'; import { ApiKeysEmptyPrompt } from './api_keys_empty_prompt'; @@ -61,7 +63,7 @@ interface State { isAdmin: boolean; canManage: boolean; areApiKeysEnabled: boolean; - apiKeys: ApiKey[]; + apiKeys: Array; selectedItems: ApiKey[]; error: any; createdApiKey?: CreateApiKeyResponse; @@ -174,6 +176,8 @@ export class APIKeysGridPage extends Component { ); } + const concatenated = `${this.state.createdApiKey?.id}:${this.state.createdApiKey?.api_key}`; + return ( @@ -217,21 +221,77 @@ export class APIKeysGridPage extends Component {

- +
@@ -323,7 +383,16 @@ export class APIKeysGridPage extends Component { ).map((username) => { return { value: username, - view: username, + view: ( + + + + + + {username} + + + ), }; }), }, @@ -385,11 +454,19 @@ export class APIKeysGridPage extends Component { }; private getColumnConfig = () => { - const { isAdmin, isLoadingTable } = this.state; + const { isAdmin, isLoadingTable, createdApiKey } = this.state; let config: Array> = []; config = config.concat([ + { + field: 'base64', + name: i18n.translate('xpack.security.management.apiKeys.table.base64ColumnName', { + defaultMessage: 'API key', + }), + sortable: true, + render: (base64: String) => {base64}, + }, { field: 'name', name: i18n.translate('xpack.security.management.apiKeys.table.nameColumnName', { @@ -412,22 +489,12 @@ export class APIKeysGridPage extends Component { - + {username} ), }, - { - field: 'realm', - name: i18n.translate('xpack.security.management.apiKeys.table.realmColumnName', { - defaultMessage: 'Realm', - }), - sortable: true, - mobileOptions: { - show: false, - }, - }, ]); } @@ -438,9 +505,21 @@ export class APIKeysGridPage extends Component { defaultMessage: 'Created', }), sortable: true, - render: (expiration: number) => ( - - {moment(expiration).fromNow()} + mobileOptions: { + show: false, + }, + render: (creation: string, item: ApiKey) => ( + + {item.id === createdApiKey?.id ? ( + + + + ) : ( + {moment(creation).fromNow()} + )} ), }, @@ -602,7 +681,12 @@ export class APIKeysGridPage extends Component { try { const { isAdmin } = this.state; const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); - this.setState({ apiKeys }); + this.setState({ + apiKeys: apiKeys.map((apiKey) => ({ + ...apiKey, + base64: `${btoa(apiKey.id).substr(0, 8)}...`, + })), + }); } catch (e) { this.setState({ error: e }); } From 478cf74d201b561c4cfb7255f3ba2d5ec2acb8a1 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 8 Mar 2021 14:20:15 +0000 Subject: [PATCH 06/22] Fix linting errors --- .../security/public/components/breadcrumb.tsx | 2 +- .../security/public/components/code_field.tsx | 20 ++++++++--------- .../public/components/use_initial_focus.ts | 2 +- .../security/public/components/use_theme.ts | 1 + .../api_keys_grid/api_keys_empty_prompt.tsx | 4 +++- .../api_keys_grid/api_keys_grid_page.test.tsx | 11 +++++----- .../api_keys_grid/api_keys_grid_page.tsx | 15 +++++-------- .../api_keys_grid/create_api_key_flyout.tsx | 22 ++++++++++--------- .../api_keys/api_keys_management_app.test.tsx | 2 +- .../api_keys/api_keys_management_app.tsx | 12 +++++----- .../users/edit_user/confirm_delete_users.tsx | 4 ++-- .../users/edit_user/confirm_disable_users.tsx | 4 ++-- .../users/edit_user/confirm_enable_users.tsx | 4 ++-- .../management/users/users_management_app.tsx | 2 +- .../security/server/routes/api_keys/create.ts | 5 +++-- .../security/server/routes/api_keys/index.ts | 2 +- .../translations/translations/ja-JP.json | 9 -------- .../translations/translations/zh-CN.json | 9 -------- 18 files changed, 59 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx index eb890bcd46e4fd..796d41420eae7d 100644 --- a/x-pack/plugins/security/public/components/breadcrumb.tsx +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -9,8 +9,8 @@ import type { EuiBreadcrumb } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React, { createContext, useContext, useEffect, useRef } from 'react'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { ChromeStart } from '../../../../../src/core/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; interface BreadcrumbsContext { parents: BreadcrumbProps[]; diff --git a/x-pack/plugins/security/public/components/code_field.tsx b/x-pack/plugins/security/public/components/code_field.tsx index 7e84965926fc82..3cfbbab9511160 100644 --- a/x-pack/plugins/security/public/components/code_field.tsx +++ b/x-pack/plugins/security/public/components/code_field.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import type { FunctionComponent, ReactElement } from 'react'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { EuiFieldTextProps } from '@elastic/eui'; import { EuiButtonEmpty, @@ -21,6 +18,11 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import type { FunctionComponent, ReactElement } from 'react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + import { useTheme } from './use_theme'; export interface CodeFieldProps extends Omit { @@ -72,7 +74,7 @@ export interface SelectableCodeFieldOption { } export interface SelectableCodeFieldProps extends Omit { - options: Array; + options: SelectableCodeFieldOption[]; } export const SelectableCodeField: FunctionComponent = (props) => { @@ -107,8 +109,8 @@ export const SelectableCodeField: FunctionComponent = > >((accumulator, option, i) => { - accumulator.push( + items={options.reduce((items, option, i) => { + items.push( = ); if (i < options.length - 1) { - accumulator.push( - - ); + items.push(); } - return accumulator; + return items; }, [])} /> diff --git a/x-pack/plugins/security/public/components/use_initial_focus.ts b/x-pack/plugins/security/public/components/use_initial_focus.ts index 88b423ceefd5a7..d8dd57f81070f8 100644 --- a/x-pack/plugins/security/public/components/use_initial_focus.ts +++ b/x-pack/plugins/security/public/components/use_initial_focus.ts @@ -6,7 +6,7 @@ */ import type { DependencyList } from 'react'; -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; /** * Creates a ref for an HTML element, which will be focussed on mount. diff --git a/x-pack/plugins/security/public/components/use_theme.ts b/x-pack/plugins/security/public/components/use_theme.ts index 98a8c21e6f2df8..1eba956decf8ba 100644 --- a/x-pack/plugins/security/public/components/use_theme.ts +++ b/x-pack/plugins/security/public/components/use_theme.ts @@ -7,6 +7,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; + import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; /** diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx index c8925292467b71..a157ef5cd97913 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_empty_prompt.tsx @@ -5,10 +5,12 @@ * 2.0. */ +import { EuiEmptyPrompt } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; + import { FormattedMessage } from '@kbn/i18n/react'; + import { DocLink } from '../../../components/doc_link'; export interface ApiKeysEmptyPromptProps { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index eb942a8656e144..4e48bbccc24b60 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -5,21 +5,22 @@ * 2.0. */ -import React from 'react'; import { - render, fireEvent, + render, waitFor, - within, waitForElementToBeRemoved, + within, } from '@testing-library/react'; import { createMemoryHistory } from 'history'; +import React from 'react'; + import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; import { Providers } from '../api_keys_management_app'; -import { APIKeysGridPage } from './api_keys_grid_page'; import { apiKeysAPIClientMock } from '../index.mock'; -import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; +import { APIKeysGridPage } from './api_keys_grid_page'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 52cf9562d02b68..203c34b6c69b40 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -29,27 +29,27 @@ import { EuiTitle, EuiToolTip, } from '@elastic/eui'; +import type { History } from 'history'; import moment from 'moment-timezone'; import React, { Component } from 'react'; +import { Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Route } from 'react-router-dom'; -import type { History } from 'history'; import type { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; -import type { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import type { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { Breadcrumb } from '../../../components/breadcrumb'; import { SelectableCodeField } from '../../../components/code_field'; import type { APIKeysAPIClient, CreateApiKeyResponse } from '../api_keys_api_client'; -import { PermissionDenied } from './permission_denied'; import { ApiKeysEmptyPrompt } from './api_keys_empty_prompt'; import { CreateApiKeyFlyout } from './create_api_key_flyout'; -import { NotEnabled } from './not_enabled'; import { InvalidateProvider } from './invalidate_provider'; +import { NotEnabled } from './not_enabled'; +import { PermissionDenied } from './permission_denied'; interface Props { history: History; @@ -465,7 +465,7 @@ export class APIKeysGridPage extends Component { defaultMessage: 'API key', }), sortable: true, - render: (base64: String) => {base64}, + render: (base64: string) => {base64}, }, { field: 'name', @@ -545,9 +545,6 @@ export class APIKeysGridPage extends Component { ); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index 183572d82d2c36..ea2f5708148fdf 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import type { FunctionComponent } from 'react'; -import React, { useEffect } from 'react'; import { EuiCallOut, EuiFieldNumber, @@ -22,21 +20,25 @@ import { EuiSwitch, EuiText, } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React, { useEffect } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { useKibana, CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; -import type { CreateApiKeyRequest, CreateApiKeyResponse } from '../api_keys_api_client'; -import { APIKeysAPIClient } from '../api_keys_api_client'; -import type { ValidationErrors } from '../../../components/use_form'; -import { useForm } from '../../../components/use_form'; + +import { CodeEditor, useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import type { RoleDescriptors } from '../../../../common/model'; +import { DocLink } from '../../../components/doc_link'; import type { FormFlyoutProps } from '../../../components/form_flyout'; import { FormFlyout } from '../../../components/form_flyout'; -import { DocLink } from '../../../components/doc_link'; import { useCurrentUser } from '../../../components/use_current_user'; +import { useForm } from '../../../components/use_form'; +import type { ValidationErrors } from '../../../components/use_form'; import { useInitialFocus } from '../../../components/use_initial_focus'; import { RolesAPIClient } from '../../roles/roles_api_client'; -import type { RoleDescriptors } from '../../../../common/model'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import type { CreateApiKeyRequest, CreateApiKeyResponse } from '../api_keys_api_client'; export interface ApiKeyFormValues { name: string; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx index f5d39f95318399..e7fa8719629a7d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx @@ -9,9 +9,9 @@ jest.mock('./api_keys_grid', () => ({ APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, })); -import { apiKeysManagementApp } from './api_keys_management_app'; import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { securityMock } from '../../mocks'; +import { apiKeysManagementApp } from './api_keys_management_app'; describe('apiKeysManagementApp', () => { it('create() returns proper management app descriptor', () => { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index e23124b023c84e..68e06d38db4c86 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -5,25 +5,27 @@ * 2.0. */ +import type { History } from 'history'; import type { FunctionComponent } from 'react'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router } from 'react-router-dom'; -import type { History } from 'history'; + import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import type { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; -import type { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; + +import type { CoreStart, StartServicesAccessor } from '../../../../../../src/core/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import type { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import type { AuthenticationServiceSetup } from '../../authentication'; -import type { PluginStartDependencies } from '../../plugin'; import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb'; import { - BreadcrumbsProvider, Breadcrumb, + BreadcrumbsProvider, createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; import { AuthenticationProvider } from '../../components/use_current_user'; +import type { PluginStartDependencies } from '../../plugin'; interface CreateParams { authc: AuthenticationServiceSetup; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx index 83f2af421bd454..3ccbf36faf3152 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -5,16 +5,16 @@ * 2.0. */ +import { EuiConfirmModal, EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { UserAPIClient } from '..'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export interface ConfirmDeleteUsersProps { usernames: string[]; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx index 3bd02f5539cf63..ea178e1683c9e8 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -5,16 +5,16 @@ * 2.0. */ +import { EuiConfirmModal, EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { UserAPIClient } from '..'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export interface ConfirmDisableUsersProps { usernames: string[]; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx index b95bd3f706f209..352d9d7228dfad 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -5,16 +5,16 @@ * 2.0. */ +import { EuiConfirmModal, EuiText } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { UserAPIClient } from '..'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export interface ConfirmEnableUsersProps { usernames: string[]; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index eb9a33c7f5f9bc..f6a2956c7ad43f 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -21,8 +21,8 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac import type { AuthenticationServiceSetup } from '../../authentication'; import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb'; import { - BreadcrumbsProvider, Breadcrumb, + BreadcrumbsProvider, createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; import { AuthenticationProvider } from '../../components/use_current_user'; diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts index 22ed8c73bb015c..a309d3a0e3edba 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.ts @@ -6,9 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import { createLicensedRouteHandler } from '../licensed_route_handler'; -import { wrapIntoCustomErrorResponse } from '../../errors'; + import type { RouteDefinitionParams } from '..'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; export function defineCreateApiKeyRoutes({ router, diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index f726e2c12cfe86..aa1e3b858ea582 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -6,9 +6,9 @@ */ import type { RouteDefinitionParams } from '../'; +import { defineCreateApiKeyRoutes } from './create'; import { defineEnabledApiKeysRoutes } from './enabled'; import { defineGetApiKeysRoutes } from './get'; -import { defineCreateApiKeyRoutes } from './create'; import { defineInvalidateApiKeysRoutes } from './invalidate'; import { defineCheckPrivilegesRoutes } from './privileges'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 413f0d54afca9e..f4cb791a1ea67b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16512,10 +16512,6 @@ "xpack.security.management.apiKeys.invalidateApiKey.successMultipleNotificationTitle": "無効な {count} API キー", "xpack.security.management.apiKeys.invalidateApiKey.successSingleNotificationTitle": "API キー「{name}」を無効にしました", "xpack.security.management.apiKeys.noPermissionToManageRolesDescription": "システム管理者にお問い合わせください。", - "xpack.security.management.apiKeys.table.actionDeleteAriaLabel": "「{name}」を無効にする", - "xpack.security.management.apiKeys.table.actionDeleteTooltip": "無効にする", - "xpack.security.management.apiKeys.table.actionsColumnName": "アクション", - "xpack.security.management.apiKeys.table.adminText": "あなたは API キー管理者です。", "xpack.security.management.apiKeys.table.apiKeysAllDescription": "API キーを表示して無効にします。API キーはユーザーの代わりにリクエストを送信します。", "xpack.security.management.apiKeys.table.apiKeysDisabledErrorDescription": "システム管理者に連絡し、{link}を参照して API キーを有効にしてください。", "xpack.security.management.apiKeys.table.apiKeysDisabledErrorLinkText": "ドキュメント", @@ -16529,15 +16525,10 @@ "xpack.security.management.apiKeys.table.emptyPromptDescription": "コンソールで {link} を作成できます。", "xpack.security.management.apiKeys.table.emptyPromptDocsLinkMessage": "API キー", "xpack.security.management.apiKeys.table.emptyPromptNonAdminTitle": "まだ API キーがありません", - "xpack.security.management.apiKeys.table.expirationDateColumnName": "有効期限", - "xpack.security.management.apiKeys.table.expirationDateNeverMessage": "なし", "xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage": "権限の確認エラー: {message}", "xpack.security.management.apiKeys.table.loadingApiKeysDescription": "API キーを読み込み中…", - "xpack.security.management.apiKeys.table.loadingApiKeysErrorTitle": "API キーを読み込み中にエラーが発生", "xpack.security.management.apiKeys.table.nameColumnName": "名前", - "xpack.security.management.apiKeys.table.realmColumnName": "レルム", "xpack.security.management.apiKeys.table.realmFilterLabel": "レルム", - "xpack.security.management.apiKeys.table.reloadApiKeysButton": "再読み込み", "xpack.security.management.apiKeys.table.statusColumnName": "ステータス", "xpack.security.management.apiKeys.table.userFilterLabel": "ユーザー", "xpack.security.management.apiKeys.table.userNameColumnName": "ユーザー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0b4428b5625749..1d058e9ae1bec6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16554,10 +16554,6 @@ "xpack.security.management.apiKeys.invalidateApiKey.successMultipleNotificationTitle": "已作废 {count} 个 API 密钥", "xpack.security.management.apiKeys.invalidateApiKey.successSingleNotificationTitle": "已作废 API 密钥“{name}”", "xpack.security.management.apiKeys.noPermissionToManageRolesDescription": "请联系您的系统管理员。", - "xpack.security.management.apiKeys.table.actionDeleteAriaLabel": "作废“{name}”", - "xpack.security.management.apiKeys.table.actionDeleteTooltip": "作废", - "xpack.security.management.apiKeys.table.actionsColumnName": "操作", - "xpack.security.management.apiKeys.table.adminText": "您是 API 密钥管理员。", "xpack.security.management.apiKeys.table.apiKeysAllDescription": "查看并作废 API 密钥。API 密钥代表用户发送请求。", "xpack.security.management.apiKeys.table.apiKeysDisabledErrorDescription": "请联系您的系统管理员并参阅{link}以启用 API 密钥。", "xpack.security.management.apiKeys.table.apiKeysDisabledErrorLinkText": "文档", @@ -16571,16 +16567,11 @@ "xpack.security.management.apiKeys.table.emptyPromptDescription": "您可以从 Console 创建 {link}。", "xpack.security.management.apiKeys.table.emptyPromptDocsLinkMessage": "API 密钥", "xpack.security.management.apiKeys.table.emptyPromptNonAdminTitle": "您未有任何 API 密钥", - "xpack.security.management.apiKeys.table.expirationDateColumnName": "过期", - "xpack.security.management.apiKeys.table.expirationDateNeverMessage": "永不", "xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage": "检查权限时出错:{message}", "xpack.security.management.apiKeys.table.invalidateApiKeyButton": "作废 {count, plural, other {API 密钥}}", "xpack.security.management.apiKeys.table.loadingApiKeysDescription": "正在加载 API 密钥……", - "xpack.security.management.apiKeys.table.loadingApiKeysErrorTitle": "加载 API 密钥时出错", "xpack.security.management.apiKeys.table.nameColumnName": "名称", - "xpack.security.management.apiKeys.table.realmColumnName": "Realm", "xpack.security.management.apiKeys.table.realmFilterLabel": "Realm", - "xpack.security.management.apiKeys.table.reloadApiKeysButton": "重新加载", "xpack.security.management.apiKeys.table.statusColumnName": "状态", "xpack.security.management.apiKeys.table.userFilterLabel": "用户", "xpack.security.management.apiKeys.table.userNameColumnName": "用户", From 883061296f36c681381be1b2a58df92a9de3df65 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 8 Mar 2021 16:39:55 +0000 Subject: [PATCH 07/22] Fix more linting error and unit tests --- .../__snapshots__/code_editor.test.tsx.snap | 16 ++++- .../security/public/components/code_field.tsx | 5 +- .../api_keys/api_keys_management_app.test.tsx | 62 ++++++++++++++----- .../management/management_service.test.ts | 2 +- .../edit_user/change_password_flyout.tsx | 2 +- 5 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index 2800c6cd7c198a..a779ef540d72ec 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -9,7 +9,21 @@ exports[`is rendered 1`] = ` height={250} language="loglang" onChange={[Function]} - options={Object {}} + options={ + Object { + "minimap": Object { + "enabled": false, + }, + "renderLineHighlight": "none", + "scrollBeyondLastLine": false, + "scrollbar": Object { + "useShadows": false, + }, + "wordBasedSuggestions": false, + "wordWrap": "on", + "wrappingIndent": "indent", + } + } overrideServices={Object {}} theme="euiColors" value=" diff --git a/x-pack/plugins/security/public/components/code_field.tsx b/x-pack/plugins/security/public/components/code_field.tsx index 3cfbbab9511160..293baa8d00debb 100644 --- a/x-pack/plugins/security/public/components/code_field.tsx +++ b/x-pack/plugins/security/public/components/code_field.tsx @@ -95,10 +95,7 @@ export const SelectableCodeField: FunctionComponent = iconType="arrowDown" iconSide="right" color="success" - onClick={() => { - console.log('toggle'); - setIsPopoverOpen(!isPopoverOpen); - }} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} > {selectedOption.label} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx index e7fa8719629a7d..d2611864e77a2d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx @@ -6,10 +6,14 @@ */ jest.mock('./api_keys_grid', () => ({ - APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, + APIKeysGridPage: (props: any) => JSON.stringify(props, null, 2), })); -import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; +import { act } from '@testing-library/react'; + +import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; +import type { Unmount } from 'src/plugins/management/public/types'; + import { securityMock } from '../../mocks'; import { apiKeysManagementApp } from './api_keys_management_app'; @@ -24,7 +28,7 @@ describe('apiKeysManagementApp', () => { "id": "api_keys", "mount": [Function], "order": 30, - "title": "API Keys", + "title": "API keys", } `); }); @@ -39,28 +43,54 @@ describe('apiKeysManagementApp', () => { const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); - const unmount = await apiKeysManagementApp - .create({ authc, getStartServices: () => Promise.resolve(startServices) as any }) - .mount({ - basePath: '/some-base-path', - element: container, - setBreadcrumbs, - history: scopedHistoryMock.create(), - }); + let unmount: Unmount; + await act(async () => { + unmount = await apiKeysManagementApp + .create({ authc, getStartServices: () => Promise.resolve(startServices) as any }) + .mount({ + basePath: '/some-base-path', + element: container, + setBreadcrumbs, + history: scopedHistoryMock.create(), + }); + }); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'API Keys' }]); - expect(docTitle.change).toHaveBeenCalledWith('API Keys'); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'API keys' }]); + expect(docTitle.change).toHaveBeenCalledWith(['API keys']); expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Page: {"notifications":{"toasts":{}},"apiKeysAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}}} + { + "history": { + "action": "PUSH", + "length": 1, + "location": { + "pathname": "/", + "search": "", + "hash": "" + } + }, + "notifications": { + "toasts": {} + }, + "apiKeysAPIClient": { + "http": { + "basePath": { + "basePath": "", + "serverBasePath": "" + }, + "anonymousPaths": {}, + "externalUrl": {} + } + } + }
`); - unmount(); - expect(docTitle.reset).toHaveBeenCalledTimes(1); + unmount!(); + expect(docTitle.reset).toHaveBeenCalledTimes(1); expect(container).toMatchInlineSnapshot(`
`); }); }); diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts index 694f3cc3880a21..b21897377d5eb2 100644 --- a/x-pack/plugins/security/public/management/management_service.test.ts +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -68,7 +68,7 @@ describe('ManagementService', () => { id: 'api_keys', mount: expect.any(Function), order: 30, - title: 'API Keys', + title: 'API keys', }); expect(mockSection.registerApp).toHaveBeenCalledWith({ id: 'role_mappings', diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx index 4412ed561691ee..445d424adb3882 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -24,11 +24,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { useInitialFocus } from '../../../components/use_initial_focus'; import { FormFlyout } from '../../../components/form_flyout'; import { useCurrentUser } from '../../../components/use_current_user'; import type { ValidationErrors } from '../../../components/use_form'; import { useForm } from '../../../components/use_form'; +import { useInitialFocus } from '../../../components/use_initial_focus'; import { UserAPIClient } from '../user_api_client'; export interface ChangePasswordFormValues { From fc4a3c8bd61c86a6a97a3344a885d0a250ba875a Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Tue, 16 Mar 2021 10:30:29 +0000 Subject: [PATCH 08/22] Added suggestions from code review --- .../plugins/security/common/model/api_key.ts | 2 +- x-pack/plugins/security/common/model/index.ts | 2 +- .../security/public/components/breadcrumb.tsx | 5 ++- .../{code_field.tsx => token_field.tsx} | 25 ++++++----- .../api_keys/api_keys_api_client.test.ts | 16 +++++++ .../api_keys/api_keys_api_client.ts | 6 +-- .../api_keys_grid/api_keys_grid_page.tsx | 44 +++++++++---------- .../api_keys_grid/create_api_key_flyout.tsx | 35 ++++++++++----- 8 files changed, 82 insertions(+), 53 deletions(-) rename x-pack/plugins/security/public/components/{code_field.tsx => token_field.tsx} (82%) diff --git a/x-pack/plugins/security/common/model/api_key.ts b/x-pack/plugins/security/common/model/api_key.ts index 65298bd15c0118..f2467468f8069b 100644 --- a/x-pack/plugins/security/common/model/api_key.ts +++ b/x-pack/plugins/security/common/model/api_key.ts @@ -22,4 +22,4 @@ export interface ApiKeyToInvalidate { name: string; } -export type RoleDescriptors = Record; +export type ApiKeyRoleDescriptors = Record; diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 221dc524c36956..8eb341ef9bd371 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { ApiKey, ApiKeyToInvalidate, RoleDescriptors } from './api_key'; +export { ApiKey, ApiKeyToInvalidate, ApiKeyRoleDescriptors } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { AuthenticationProvider, shouldProviderUseLoginForm } from './authentication_provider'; diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx index 796d41420eae7d..353f738501cbef 100644 --- a/x-pack/plugins/security/public/components/breadcrumb.tsx +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -9,7 +9,8 @@ import type { EuiBreadcrumb } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React, { createContext, useContext, useEffect, useRef } from 'react'; -import type { ChromeStart } from '../../../../../src/core/public'; +import type { ChromeStart } from 'src/core/public'; + import { useKibana } from '../../../../../src/plugins/kibana_react/public'; interface BreadcrumbsContext { @@ -141,7 +142,7 @@ export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) } export function createBreadcrumbsChangeHandler( - chrome: ChromeStart, + chrome: Pick, setBreadcrumbs = chrome.setBreadcrumbs ) { return (breadcrumbs: BreadcrumbProps[]) => { diff --git a/x-pack/plugins/security/public/components/code_field.tsx b/x-pack/plugins/security/public/components/token_field.tsx similarity index 82% rename from x-pack/plugins/security/public/components/code_field.tsx rename to x-pack/plugins/security/public/components/token_field.tsx index 293baa8d00debb..8ceabcfbcc8b11 100644 --- a/x-pack/plugins/security/public/components/code_field.tsx +++ b/x-pack/plugins/security/public/components/token_field.tsx @@ -25,11 +25,11 @@ import { i18n } from '@kbn/i18n'; import { useTheme } from './use_theme'; -export interface CodeFieldProps extends Omit { +export interface TokenFieldProps extends Omit { value: string; } -export const CodeField: FunctionComponent = (props) => { +export const TokenField: FunctionComponent = (props) => { const theme = useTheme(); return ( @@ -39,8 +39,8 @@ export const CodeField: FunctionComponent = (props) => { {(copyText) => ( = (props) => { > = (props) => { ); }; -export interface SelectableCodeFieldOption { +export interface SelectableTokenFieldOption { key: string; value: string; icon?: string; @@ -73,19 +76,21 @@ export interface SelectableCodeFieldOption { description?: string; } -export interface SelectableCodeFieldProps extends Omit { - options: SelectableCodeFieldOption[]; +export interface SelectableTokenFieldProps extends Omit { + options: SelectableTokenFieldOption[]; } -export const SelectableCodeField: FunctionComponent = (props) => { +export const SelectableTokenField: FunctionComponent = (props) => { const { options, ...rest } = props; const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); - const [selectedOption, setSelectedOption] = React.useState(options[0]); + const [selectedOption, setSelectedOption] = React.useState( + options[0] + ); const selectedIndex = options.findIndex((c) => c.key === selectedOption.key); const closePopover = () => setIsPopoverOpen(false); return ( - { body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: true }), }); }); + + it('createApiKey() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.post.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + const mockAPIKeys = { name: 'name', expiration: '7d' }; + + await expect(apiClient.createApiKey(mockAPIKeys)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key', { + body: JSON.stringify(mockAPIKeys), + }); + }); }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 83d3a0e576cfa5..65540fd7ebfc15 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -7,7 +7,7 @@ import type { HttpStart } from 'src/core/public'; -import type { ApiKey, ApiKeyToInvalidate, Role } from '../../../common/model'; +import type { ApiKey, ApiKeyRoleDescriptors, ApiKeyToInvalidate } from '../../../common/model'; export interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; @@ -27,9 +27,7 @@ export interface GetApiKeysResponse { export interface CreateApiKeyRequest { name: string; expiration?: string; - role_descriptors?: { - [key in string]: Role['elasticsearch']; - }; + role_descriptors?: ApiKeyRoleDescriptors; } export interface CreateApiKeyResponse { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 203c34b6c69b40..014030a89278e6 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -12,7 +12,6 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiCallOut, - EuiCode, EuiFlexGroup, EuiFlexItem, EuiHealth, @@ -43,7 +42,7 @@ import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/pu import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import type { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { Breadcrumb } from '../../../components/breadcrumb'; -import { SelectableCodeField } from '../../../components/code_field'; +import { SelectableTokenField } from '../../../components/token_field'; import type { APIKeysAPIClient, CreateApiKeyResponse } from '../api_keys_api_client'; import { ApiKeysEmptyPrompt } from './api_keys_empty_prompt'; import { CreateApiKeyFlyout } from './create_api_key_flyout'; @@ -63,7 +62,7 @@ interface State { isAdmin: boolean; canManage: boolean; areApiKeysEnabled: boolean; - apiKeys: Array; + apiKeys: ApiKey[]; selectedItems: ApiKey[]; error: any; createdApiKey?: CreateApiKeyResponse; @@ -94,7 +93,12 @@ export class APIKeysGridPage extends Component { return (
- + { this.props.history.push({ pathname: '/' }); @@ -183,12 +187,12 @@ export class APIKeysGridPage extends Component { -

+

-

+

@@ -229,10 +233,10 @@ export class APIKeysGridPage extends Component {

- { title={ } color="primary" @@ -459,14 +463,6 @@ export class APIKeysGridPage extends Component { let config: Array> = []; config = config.concat([ - { - field: 'base64', - name: i18n.translate('xpack.security.management.apiKeys.table.base64ColumnName', { - defaultMessage: 'API key', - }), - sortable: true, - render: (base64: string) => {base64}, - }, { field: 'name', name: i18n.translate('xpack.security.management.apiKeys.table.nameColumnName', { @@ -495,6 +491,13 @@ export class APIKeysGridPage extends Component { ), }, + { + field: 'realm', + name: i18n.translate('xpack.security.management.apiKeys.table.realmColumnName', { + defaultMessage: 'Realm', + }), + sortable: true, + }, ]); } @@ -678,12 +681,7 @@ export class APIKeysGridPage extends Component { try { const { isAdmin } = this.state; const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); - this.setState({ - apiKeys: apiKeys.map((apiKey) => ({ - ...apiKey, - base64: `${btoa(apiKey.id).substr(0, 8)}...`, - })), - }); + this.setState({ apiKeys }); } catch (e) { this.setState({ error: e }); } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index ea2f5708148fdf..b2fd282c2558f2 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -28,7 +28,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CodeEditor, useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import type { RoleDescriptors } from '../../../../common/model'; +import type { ApiKeyRoleDescriptors } from '../../../../common/model'; import { DocLink } from '../../../components/doc_link'; import type { FormFlyoutProps } from '../../../components/form_flyout'; import { FormFlyout } from '../../../components/form_flyout'; @@ -116,13 +116,16 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ useEffect(() => { if (currentUser && roles) { - const userPermissions = currentUser.roles.reduce((accumulator, roleName) => { - const role = roles.find((r) => r.name === roleName)!; - if (role) { - accumulator[role.name] = role.elasticsearch; - } - return accumulator; - }, {}); + const userPermissions = currentUser.roles.reduce( + (accumulator, roleName) => { + const role = roles.find((r) => r.name === roleName); + if (role) { + accumulator[role.name] = role.elasticsearch; + } + return accumulator; + }, + {} + ); if (!form.touched.role_descriptors) { form.setValue('role_descriptors', JSON.stringify(userPermissions, null, 2)); } @@ -186,9 +189,17 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ - + - {currentUser?.username} + + {currentUser?.username} + @@ -220,7 +231,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ label={i18n.translate( 'xpack.security.accountManagement.createApiKey.customPrivilegesLabel', { - defaultMessage: 'Restrict access and permissions', + defaultMessage: 'Restrict privileges', } )} checked={!!form.values.customPrivileges} @@ -259,7 +270,6 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ = ({ } )} name="expiration" + min={0} defaultValue={form.values.expiration} isInvalid={form.touched.expiration && !!form.errors.expiration} fullWidth From 59f874d8499649b5e3b4b28595d70f6f8e49e57a Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 17 Mar 2021 14:44:10 +0000 Subject: [PATCH 09/22] fix unit tests --- .../expression_input.stories.storyshot | 22 ++++++++++++++++++- .../users/edit_user/confirm_delete_users.tsx | 1 + .../users/edit_user/confirm_disable_users.tsx | 1 + .../users/edit_user/confirm_enable_users.tsx | 1 + .../users/edit_user/edit_user_page.test.tsx | 8 +++---- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot index 5c17eb2b681373..b6e12b9cfc156d 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot @@ -16,7 +16,27 @@ exports[`Storyshots components/ExpressionInput default 1`] = ` id="generated-id" onBlur={[Function]} onFocus={[Function]} - /> + > +
+
+ +
diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx index 3ccbf36faf3152..46861c773a60c2 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -54,6 +54,7 @@ export const ConfirmDeleteUsers: FunctionComponent = ({ return ( = return ( = ({ return ( { expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); - it('deletes user when confirming and redirects back', async () => { + it.only('deletes user when confirming and redirects back', async () => { const coreStart = coreMock.createStart(); const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); const authc = securityMock.createSetup().authc; @@ -427,15 +427,15 @@ describe('EditUserPage', () => { coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.delete.mockResolvedValueOnce({}); - const { getByRole, findByRole } = render( + const { getByRole, findByRole, debug } = render( ); fireEvent.click(await findByRole('button', { name: 'Delete user' })); - - const dialog = getByRole('dialog'); + // debug(); + const dialog = await findByRole('dialog'); fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4977a092ae3fa1..604fbb18e7fc5d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17479,7 +17479,6 @@ "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.components.sessionLifespanWarning.message": "セッションは最大時間制限{timeout}に達しました。もう一度ログインする必要があります。", "xpack.security.components.sessionLifespanWarning.title": "警告", - "xpack.security.confirmModal.cancelButton": "キャンセル", "xpack.security.conflictingSessionError": "申し訳ありません。すでに有効なKibanaセッションがあります。新しいセッションを開始する場合は、先に既存のセッションからログアウトしてください。", "xpack.security.formFlyout.cancelButton": "キャンセル", "xpack.security.loggedOut.login": "ログイン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ac4cf0af0f9841..d5916043f68b30 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17717,7 +17717,6 @@ "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.components.sessionLifespanWarning.message": "您的会话将达到最大时间限制 {timeout}。您将需要重新登录。", "xpack.security.components.sessionLifespanWarning.title": "警告", - "xpack.security.confirmModal.cancelButton": "取消", "xpack.security.conflictingSessionError": "抱歉,您已有活动的 Kibana 会话。如果希望开始新的会话,请首先从现有会话注销。", "xpack.security.formFlyout.cancelButton": "取消", "xpack.security.loggedOut.login": "登录", From 5fcfc28c79eb48fe8a5711b81c4ae13f5edc8c58 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 18 Mar 2021 10:09:25 +0000 Subject: [PATCH 10/22] move code editor field into separate component --- .../public/code_editor/code_editor.test.tsx | 4 +- .../public/code_editor/code_editor.tsx | 17 ++++++-- .../public/code_editor/editor_theme.ts | 9 ++-- .../kibana_react/public/code_editor/index.tsx | 42 ++++++++++++++----- .../api_keys_grid/create_api_key_flyout.tsx | 4 +- 5 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx index 33f0f311d3a4a3..0f279e3bfea325 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx @@ -89,8 +89,8 @@ test('editor mount setup', () => { // Verify our mount callback will be called expect(editorWillMount.mock.calls.length).toBe(1); - // Verify our theme will be setup - expect((monaco.editor.defineTheme as jest.Mock).mock.calls.length).toBe(1); + // Verify that both, default and transparent theme will be setup + expect((monaco.editor.defineTheme as jest.Mock).mock.calls.length).toBe(2); // Verify our language features have been registered expect((monaco.languages.onLanguage as jest.Mock).mock.calls.length).toBe(1); diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index dba31afabe9bc3..5c53d67beebc6d 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -11,7 +11,12 @@ import ReactResizeDetector from 'react-resize-detector'; import MonacoEditor from 'react-monaco-editor'; import { monaco } from '@kbn/monaco'; -import { LIGHT_THEME, DARK_THEME } from './editor_theme'; +import { + DARK_THEME, + LIGHT_THEME, + DARK_THEME_TRANSPARENT, + LIGHT_THEME_TRANSPARENT, +} from './editor_theme'; import './editor.scss'; @@ -85,6 +90,8 @@ export interface Props { * Should the editor use the dark theme */ useDarkTheme?: boolean; + + transparentBackground?: boolean; } export class CodeEditor extends React.Component { @@ -131,8 +138,12 @@ export class CodeEditor extends React.Component { } }); - // Register the theme + // Register themes monaco.editor.defineTheme('euiColors', this.props.useDarkTheme ? DARK_THEME : LIGHT_THEME); + monaco.editor.defineTheme( + 'euiColorsTransparent', + this.props.useDarkTheme ? DARK_THEME_TRANSPARENT : LIGHT_THEME_TRANSPARENT + ); }; _editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor, __monaco: unknown) => { @@ -153,7 +164,7 @@ export class CodeEditor extends React.Component { return ( <> import('./code_editor')); +const Fallback = () => ( + + + +); + +/** + * Renders a Monaco code editor with EUI color theme. + * + * @see CodeEditorField to render a code editor in the same style as other EUI form fields. + */ export const CodeEditor: React.FunctionComponent = (props) => { - const { width, height, options } = props; + const darkMode = useUiSetting('theme:darkMode'); + return ( + + }> + + + + ); +}; +/** + * Renders a Monaco code editor in the same style as other EUI form fields. + */ +export const CodeEditorField: React.FunctionComponent = (props) => { + const { width, height, options } = props; const darkMode = useUiSetting('theme:darkMode'); - const theme = darkMode ? euiThemeDark : euiThemeLight; const style = { width, height, backgroundColor: options?.readOnly - ? theme.euiFormBackgroundReadOnlyColor - : theme.euiFormBackgroundColor, + ? euiThemeVars.euiFormBackgroundReadOnlyColor + : euiThemeVars.euiFormBackgroundColor, }; return ( @@ -39,17 +61,15 @@ export const CodeEditor: React.FunctionComponent = (props) => { fallback={ } > diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index b2fd282c2558f2..d27b8e3ed07e20 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -27,7 +27,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CodeEditor, useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { CodeEditorField, useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import type { ApiKeyRoleDescriptors } from '../../../../common/model'; import { DocLink } from '../../../components/doc_link'; import type { FormFlyoutProps } from '../../../components/form_flyout'; @@ -255,7 +255,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ error={form.errors.role_descriptors} isInvalid={form.touched.role_descriptors && !!form.errors.role_descriptors} > - form.setValue('role_descriptors', value)} languageId="xjson" From 60c8894279fae70110b3cd761120e5a4793556fb Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 18 Mar 2021 13:46:11 +0000 Subject: [PATCH 11/22] fixed tests --- .../management/users/edit_user/edit_user_page.test.tsx | 8 ++++---- x-pack/plugins/translations/translations/ja-JP.json | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 7163c8d194cbed..fb01ea0e618651 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -418,7 +418,7 @@ describe('EditUserPage', () => { expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); - it.only('deletes user when confirming and redirects back', async () => { + it('deletes user when confirming and redirects back', async () => { const coreStart = coreMock.createStart(); const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); const authc = securityMock.createSetup().authc; @@ -427,15 +427,15 @@ describe('EditUserPage', () => { coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.delete.mockResolvedValueOnce({}); - const { getByRole, findByRole, debug } = render( + const { getByRole, findByRole } = render( ); fireEvent.click(await findByRole('button', { name: 'Delete user' })); - // debug(); - const dialog = await findByRole('dialog'); + + const dialog = getByRole('dialog'); fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 748bb52b00024a..7e342b29c163f3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17531,8 +17531,6 @@ "xpack.security.management.apiKeys.table.emptyPromptDescription": "コンソールで {link} を作成できます。", "xpack.security.management.apiKeys.table.emptyPromptDocsLinkMessage": "API キー", "xpack.security.management.apiKeys.table.emptyPromptNonAdminTitle": "まだ API キーがありません", - "xpack.security.management.apiKeys.table.expirationDateColumnName": "有効期限", - "xpack.security.management.apiKeys.table.expirationDateNeverMessage": "なし", "xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage": "権限の確認エラー:{message}", "xpack.security.management.apiKeys.table.loadingApiKeysDescription": "API キーを読み込み中…", "xpack.security.management.apiKeys.table.nameColumnName": "名前", From 0e501b17a6ccb65777c7e791c436f273a4331451 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 18 Mar 2021 15:16:53 +0000 Subject: [PATCH 12/22] fixed test --- .../expression_input.stories.storyshot | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot index b6e12b9cfc156d..5c17eb2b681373 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot @@ -16,27 +16,7 @@ exports[`Storyshots components/ExpressionInput default 1`] = ` id="generated-id" onBlur={[Function]} onFocus={[Function]} - > -
-
- -
+ />
From 6f3a4722bb462c1d73a847c9f0ae3e616ffcbe2f Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 22 Mar 2021 09:53:36 +0000 Subject: [PATCH 13/22] Fixed functional tests --- x-pack/test/functional/apps/api_keys/home_page.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) 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 6191a2b8dbcfc5..be8f128359345f 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -5,7 +5,6 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -13,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); const security = getService('security'); const testSubjects = getService('testSubjects'); + const find = getService('find'); describe('Home page', function () { before(async () => { @@ -31,17 +31,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Loads the app', async () => { await security.testUser.setRoles(['test_api_keys']); - log.debug('Checking for section header'); - const headers = await testSubjects.findAll('noApiKeysHeader'); - if (headers.length > 0) { - expect(await headers[0].getVisibleText()).to.be('No API keys'); - const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); - expect(await goToConsoleButton.isDisplayed()).to.be(true); - } else { - // page may already contain EiTable with data, then check API Key Admin text - const description = await pageObjects.apiKeys.getApiKeyAdminDesc(); - expect(description).to.be('You are an API Key administrator.'); - } + log.debug('Checking for create API key call to action'); + await find.existsByLinkText('Create API key'); }); }); }; From 5ed5e679e23faf2f67d4163f303cdb382688ed1e Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 22 Mar 2021 11:35:59 +0000 Subject: [PATCH 14/22] replaced theme hook with eui import --- .../public/components/token_field.tsx | 7 ++--- .../security/public/components/use_theme.ts | 28 ------------------- 2 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 x-pack/plugins/security/public/components/use_theme.ts diff --git a/x-pack/plugins/security/public/components/token_field.tsx b/x-pack/plugins/security/public/components/token_field.tsx index 8ceabcfbcc8b11..98eee9352937c0 100644 --- a/x-pack/plugins/security/public/components/token_field.tsx +++ b/x-pack/plugins/security/public/components/token_field.tsx @@ -22,16 +22,13 @@ import type { FunctionComponent, ReactElement } from 'react'; import React from 'react'; import { i18n } from '@kbn/i18n'; - -import { useTheme } from './use_theme'; +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; export interface TokenFieldProps extends Omit { value: string; } export const TokenField: FunctionComponent = (props) => { - const theme = useTheme(); - return ( = (props) => { })} className="euiFieldText euiFieldText--inGroup" value={props.value} - style={{ fontFamily: theme.euiCodeFontFamily, fontSize: theme.euiFontSizeXS }} + style={{ fontFamily: euiThemeVars.euiCodeFontFamily, fontSize: euiThemeVars.euiFontSizeXS }} onFocus={(event) => event.currentTarget.select()} readOnly /> diff --git a/x-pack/plugins/security/public/components/use_theme.ts b/x-pack/plugins/security/public/components/use_theme.ts deleted file mode 100644 index 1eba956decf8ba..00000000000000 --- a/x-pack/plugins/security/public/components/use_theme.ts +++ /dev/null @@ -1,28 +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 euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; - -import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; - -/** - * Returns correct EUI theme depending on dark mode setting. - * - * @example - * ```typescript - * const theme = useTheme(); - * - * - * {props.value} - * - * ``` - */ -export function useTheme() { - const darkMode = useUiSetting('theme:darkMode'); - return darkMode ? euiThemeDark : euiThemeLight; -} From 6857eb7fefaf46f5b4cc5049adbe2ea01c9a81e9 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 22 Mar 2021 16:33:07 +0000 Subject: [PATCH 15/22] Revert to manual theme detection --- src/plugins/kibana_react/public/code_editor/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index fe3d6fde6ef289..635e84b1d8c202 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -13,7 +13,8 @@ import { EuiLoadingContent, EuiFormControlLayout, } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { useUiSetting } from '../ui_settings'; import type { Props } from './code_editor'; @@ -47,12 +48,13 @@ export const CodeEditor: React.FunctionComponent = (props) => { export const CodeEditorField: React.FunctionComponent = (props) => { const { width, height, options } = props; const darkMode = useUiSetting('theme:darkMode'); + const theme = darkMode ? darkTheme : lightTheme; const style = { width, height, backgroundColor: options?.readOnly - ? euiThemeVars.euiFormBackgroundReadOnlyColor - : euiThemeVars.euiFormBackgroundColor, + ? theme.euiFormBackgroundReadOnlyColor + : theme.euiFormBackgroundColor, }; return ( @@ -61,7 +63,7 @@ export const CodeEditorField: React.FunctionComponent = (props) => { fallback={