=
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 (
}
- style={{
- width,
- height,
- backgroundColor: theme.euiFormBackgroundDisabledColor,
- padding: theme.paddingSizes.m,
- }}
- isDisabled
+ style={{ ...style, padding: theme.paddingSizes.m }}
+ readOnly={options?.readOnly}
>
-
+
+
+
}
>
- }
- style={{
- width,
- height,
- backgroundColor: options?.readOnly
- ? theme.euiFormBackgroundReadOnlyColor
- : theme.euiFormBackgroundColor,
- }}
- readOnly={options?.readOnly}
- >
+ } style={style} readOnly={options?.readOnly}>
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
-
-
-
-
-
-
-
-
-`;
-
-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={
}
- style={{ ...style, padding: theme.paddingSizes.m }}
+ style={{ ...style, padding: euiThemeVars.paddingSizes.m }}
readOnly={options?.readOnly}
>
-
-
-
+
}
>
} style={style} readOnly={options?.readOnly}>
-
+
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={
}
- style={{ ...style, padding: euiThemeVars.paddingSizes.m }}
+ style={{ ...style, padding: theme.paddingSizes.m }}
readOnly={options?.readOnly}
>
From 0d1a7a7094690b3946c0ffd83df183caf3ba798b Mon Sep 17 00:00:00 2001
From: Thom Heymann
Date: Mon, 22 Mar 2021 17:02:12 +0000
Subject: [PATCH 16/22] added storybook
---
.../code_editor/code_editor.stories.tsx | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx
index a5fdfe773a2f8d..09c46bf7a327e0 100644
--- a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx
+++ b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx
@@ -78,6 +78,25 @@ storiesOf('CodeEditor', module)
},
}
)
+ .add(
+ 'transparent background',
+ () => (
+
+
+
+ ),
+ {
+ info: {
+ text: 'Plaintext Monaco Editor',
+ },
+ }
+ )
.add(
'custom log language',
() => (
From d044e096a2ecf48e76f139ef986739243f2e35a0 Mon Sep 17 00:00:00 2001
From: Larry Gregory
Date: Mon, 29 Mar 2021 12:35:08 -0400
Subject: [PATCH 17/22] Additional unit and functional tests
---
.../public/code_editor/code_editor.tsx | 3 +
.../api_keys_grid/api_keys_grid_page.test.tsx | 47 +++++++
.../api_keys_grid/create_api_key_flyout.tsx | 23 ++-
.../server/routes/api_keys/create.test.ts | 133 ++++++++++++++++++
.../api_integration/apis/security/api_keys.ts | 22 +++
5 files changed, 221 insertions(+), 7 deletions(-)
create mode 100644 x-pack/plugins/security/server/routes/api_keys/create.test.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 5c53d67beebc6d..51344e2d28ab67 100644
--- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx
+++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx
@@ -91,6 +91,9 @@ export interface Props {
*/
useDarkTheme?: boolean;
+ /**
+ * Should the editor use a transparent background
+ */
transparentBackground?: boolean;
}
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 4e48bbccc24b60..2261e3f62170c5 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
@@ -26,6 +26,8 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
+jest.setTimeout(15000);
+
const coreStart = coreMock.createStart();
const apiClientMock = apiKeysAPIClientMock.create();
@@ -179,4 +181,49 @@ describe('APIKeysGridPage', () => {
await findByDisplayValue(btoa('1D:AP1_K3Y'));
});
+
+ it('creates API key with optional expiration, redirects back and displays base64', async () => {
+ const history = createMemoryHistory({ initialEntries: ['/create'] });
+ coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]);
+ coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' });
+
+ const { findByRole, findByDisplayValue } = render(
+
+
+
+ );
+ expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role');
+
+ const dialog = await findByRole('dialog');
+
+ fireEvent.change(await within(dialog).findByLabelText('Name'), {
+ target: { value: 'Test' },
+ });
+
+ fireEvent.click(await within(dialog).findByLabelText('Expire after time'));
+
+ fireEvent.click(await findByRole('button', { name: 'Create API key' }));
+
+ const alert = await findByRole('alert');
+ within(alert).getByText(/Enter a valid duration or disable this option\./i);
+
+ fireEvent.change(await within(dialog).findByLabelText('Lifetime (days)'), {
+ target: { value: '12' },
+ });
+
+ fireEvent.click(await findByRole('button', { name: 'Create API key' }));
+
+ await waitFor(() => {
+ expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', {
+ body: JSON.stringify({ name: 'Test', expiration: '12d' }),
+ });
+ expect(history.location.pathname).toBe('/');
+ });
+
+ await findByDisplayValue(btoa('1D:AP1_K3Y'));
+ });
});
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 d27b8e3ed07e20..27385e4b29b009 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
@@ -286,6 +286,12 @@ export const CreateApiKeyFlyout: FunctionComponent = ({
{
+ function getMockContext(
+ licenseCheckResult: { state: string; message?: string } = { state: 'valid' }
+ ) {
+ return ({
+ licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } },
+ } as unknown) as SecurityRequestHandlerContext;
+ }
+
+ let routeHandler: RequestHandler;
+ let authc: DeeplyMockedKeys;
+ beforeEach(() => {
+ authc = authenticationServiceMock.createStart();
+ const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
+ mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
+
+ defineCreateApiKeyRoutes(mockRouteDefinitionParams);
+
+ const [, apiKeyRouteHandler] = mockRouteDefinitionParams.router.post.mock.calls.find(
+ ([{ path }]) => path === '/internal/security/api_key'
+ )!;
+ routeHandler = apiKeyRouteHandler;
+ });
+
+ describe('failure', () => {
+ test('returns result of license checker', async () => {
+ const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' });
+ const response = await routeHandler(
+ mockContext,
+ httpServerMock.createKibanaRequest(),
+ kibanaResponseFactory
+ );
+
+ expect(response.status).toBe(403);
+ expect(response.payload).toEqual({ message: 'test forbidden message' });
+ expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic');
+ });
+
+ test('returns error from cluster client', async () => {
+ const error = Boom.notAcceptable('test not acceptable message');
+ authc.apiKeys.create.mockRejectedValue(error);
+
+ const response = await routeHandler(
+ getMockContext(),
+ httpServerMock.createKibanaRequest(),
+ kibanaResponseFactory
+ );
+
+ expect(response.status).toBe(406);
+ expect(response.payload).toEqual(error);
+ });
+ });
+
+ describe('success', () => {
+ test('allows an API Key to be created', async () => {
+ authc.apiKeys.create.mockResolvedValue({
+ api_key: 'abc123',
+ id: 'key_id',
+ name: 'my api key',
+ });
+
+ const payload = {
+ name: 'my api key',
+ expires: '12d',
+ role_descriptors: {
+ role_1: {},
+ },
+ };
+
+ const request = httpServerMock.createKibanaRequest({
+ body: {
+ ...payload,
+ },
+ });
+
+ const response = await routeHandler(getMockContext(), request, kibanaResponseFactory);
+ expect(authc.apiKeys.create).toHaveBeenCalledWith(request, payload);
+
+ expect(response.status).toBe(200);
+ expect(response.payload).toEqual({
+ api_key: 'abc123',
+ id: 'key_id',
+ name: 'my api key',
+ });
+ });
+
+ test('returns a message if API Keys are disabled', async () => {
+ authc.apiKeys.create.mockResolvedValue(null);
+
+ const payload = {
+ name: 'my api key',
+ expires: '12d',
+ role_descriptors: {
+ role_1: {},
+ },
+ };
+
+ const request = httpServerMock.createKibanaRequest({
+ body: {
+ ...payload,
+ },
+ });
+
+ const response = await routeHandler(getMockContext(), request, kibanaResponseFactory);
+ expect(authc.apiKeys.create).toHaveBeenCalledWith(request, payload);
+
+ expect(response.status).toBe(400);
+ expect(response.payload).toEqual({
+ message: 'API Keys are not available',
+ });
+ });
+ });
+});
diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts
index 596a0b038cfb3f..c6513fa800c1c2 100644
--- a/x-pack/test/api_integration/apis/security/api_keys.ts
+++ b/x-pack/test/api_integration/apis/security/api_keys.ts
@@ -25,5 +25,27 @@ export default function ({ getService }: FtrProviderContext) {
});
});
});
+
+ describe('POST /internal/security/api_key', () => {
+ it('should allow an API Key to be created', async () => {
+ await supertest
+ .post('/internal/security/api_key')
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ name: 'test_api_key',
+ expiration: '12d',
+ role_descriptors: {
+ role_1: {
+ cluster: ['monitor'],
+ },
+ },
+ })
+ .expect(200)
+ .then((response: Record) => {
+ const { name } = response.body;
+ expect(name).to.eql('test_api_key');
+ });
+ });
+ });
});
}
From d7556c393f5533a6cfb2b171a61e0fe1afc0c811 Mon Sep 17 00:00:00 2001
From: Thom Heymann
Date: Wed, 7 Apr 2021 10:11:37 +0100
Subject: [PATCH 18/22] Added suggestions from code review
---
.../api_keys_grid/api_keys_grid_page.test.tsx | 72 +++++++++++++++++-
.../empty_prompt/empty_prompt.tsx | 76 -------------------
.../api_keys_grid/empty_prompt/index.ts | 8 --
.../invalidate_provider.tsx | 1 +
.../authentication/api_keys/api_keys.ts | 3 +
5 files changed, 74 insertions(+), 86 deletions(-)
delete mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
delete mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/index.ts
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 2261e3f62170c5..5ccaeacc7558fd 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
@@ -43,7 +43,16 @@ apiClientMock.getApiKeys.mockResolvedValue({
expiration: 1571408582082,
id: '0QQZ2m0BO2XZwgJFuWTT',
invalidated: false,
- name: 'my-api-key',
+ name: 'first-api-key',
+ realm: 'reserved',
+ username: 'elastic',
+ },
+ {
+ creation: 1571322182082,
+ expiration: 1571408582082,
+ id: 'BO2XZwgJFuWTT0QQZ2m0',
+ invalidated: false,
+ name: 'second-api-key',
realm: 'reserved',
username: 'elastic',
},
@@ -76,7 +85,8 @@ describe('APIKeysGridPage', () => {
);
await waitForElementToBeRemoved(() => getByText(/Loading API keys/));
- getByText(/my-api-key/);
+ getByText(/first-api-key/);
+ getByText(/second-api-key/);
});
it('displays callout when API keys are disabled', async () => {
@@ -226,4 +236,62 @@ describe('APIKeysGridPage', () => {
await findByDisplayValue(btoa('1D:AP1_K3Y'));
});
+
+ it('invalidates api key using cta button', async () => {
+ const history = createMemoryHistory({ initialEntries: ['/'] });
+
+ const { getByText, findByRole, findAllByLabelText } = render(
+
+
+
+ );
+
+ const [invalidateButton] = await findAllByLabelText(/Invalidate/i);
+ fireEvent.click(invalidateButton);
+
+ const dialog = await findByRole('dialog');
+ fireEvent.click(await within(dialog).findByRole('button', { name: 'Invalidate API key' }));
+
+ await waitFor(() => {
+ expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith(
+ [{ id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }],
+ true
+ );
+ });
+ });
+
+ it('invalidates multiple api keys using bulk select', async () => {
+ const history = createMemoryHistory({ initialEntries: ['/'] });
+
+ const { getByText, findByRole, findAllByLabelText, debug, findAllByRole } = render(
+
+
+
+ );
+
+ const invalidateCheckboxes = await findAllByRole('checkbox', { name: 'Select this row' });
+ invalidateCheckboxes.forEach((checkbox) => fireEvent.click(checkbox));
+ fireEvent.click(await findByRole('button', { name: 'Invalidate API keys' }));
+
+ const dialog = await findByRole('dialog');
+ fireEvent.click(await within(dialog).findByRole('button', { name: 'Invalidate API keys' }));
+
+ await waitFor(() => {
+ expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith(
+ [
+ { id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' },
+ { id: 'BO2XZwgJFuWTT0QQZ2m0', name: 'second-api-key' },
+ ],
+ true
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
deleted file mode 100644
index 0987f43a3d14d7..00000000000000
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx
+++ /dev/null
@@ -1,76 +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 { EuiButton, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
-import React, { Fragment } from 'react';
-
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
-
-interface Props {
- isAdmin: boolean;
-}
-
-export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => {
- const { services } = useKibana();
- const application = services.application!;
- const docLinks = services.docLinks!;
- return (
-
- {isAdmin ? (
-
- ) : (
-
- )}
-
- }
- body={
-
-
-
-
-
- ),
- }}
- />
-
-
- }
- actions={
- application.navigateToApp('dev_tools')}
- data-test-subj="goToConsoleButton"
- >
-
-
- }
- data-test-subj="emptyPrompt"
- />
- );
-};
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/index.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/index.ts
deleted file mode 100644
index c68b2c170df5b1..00000000000000
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/index.ts
+++ /dev/null
@@ -1,8 +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.
- */
-
-export { EmptyPrompt } from './empty_prompt';
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx
index a68534db4fd85a..b6e6bb5246a3f3 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx
@@ -130,6 +130,7 @@ export const InvalidateProvider: React.FunctionComponent = ({
return (
Date: Wed, 7 Apr 2021 11:00:18 +0100
Subject: [PATCH 19/22] Remove unused translations
---
.../api_keys/api_keys_grid/api_keys_grid_page.test.tsx | 4 ++--
x-pack/plugins/translations/translations/ja-JP.json | 5 -----
x-pack/plugins/translations/translations/zh-CN.json | 5 -----
3 files changed, 2 insertions(+), 12 deletions(-)
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 5ccaeacc7558fd..b0c63862534637 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
@@ -240,7 +240,7 @@ describe('APIKeysGridPage', () => {
it('invalidates api key using cta button', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
- const { getByText, findByRole, findAllByLabelText } = render(
+ const { findByRole, findAllByLabelText } = render(
{
it('invalidates multiple api keys using bulk select', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
- const { getByText, findByRole, findAllByLabelText, debug, findAllByRole } = render(
+ const { findByRole, findAllByRole } = render(
Date: Thu, 8 Apr 2021 19:25:11 +0100
Subject: [PATCH 20/22] Updated docs and added detailed error description
---
.../api-keys/images/api-key-invalidate.png | Bin 129223 -> 0 bytes
.../security/api-keys/images/api-keys.png | Bin 111824 -> 158901 bytes
.../api-keys/images/create-api-key.png | Bin 0 -> 377920 bytes
docs/user/security/api-keys/index.asciidoc | 57 ++---
.../api_keys_grid/api_keys_empty_prompt.tsx | 38 +++-
.../api_keys_grid/api_keys_grid_page.tsx | 215 ++++++++----------
.../invalidate_provider/index.ts | 2 +-
.../invalidate_provider.tsx | 32 +--
8 files changed, 160 insertions(+), 184 deletions(-)
delete mode 100755 docs/user/security/api-keys/images/api-key-invalidate.png
mode change 100755 => 100644 docs/user/security/api-keys/images/api-keys.png
create mode 100644 docs/user/security/api-keys/images/create-api-key.png
diff --git a/docs/user/security/api-keys/images/api-key-invalidate.png b/docs/user/security/api-keys/images/api-key-invalidate.png
deleted file mode 100755
index c925679ab24bc64565b780203a05bf0d1183ee24..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 129223
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 sKd%Fpjr^FqMxD*?
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 |