From 32e5360afc823d1762865e28c95e1c9218ea71cc Mon Sep 17 00:00:00 2001 From: elena-shostak <165678770+elena-shostak@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:45:38 +0200 Subject: [PATCH 1/4] a11y fixes for user profile input labels (#186471) ## Summary a11y fixes for user profile input labels. Screen readers announce "Change username, optional" or "Change email address, optional" when inputs receive focus. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) __Fixes: https://github.com/elastic/kibana/issues/151934__ ### Release note a11y fixes for user profile input labels. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../user_profile/user_profile.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx index c9ae85dcda6345..af3b22901d12e8 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx @@ -29,6 +29,7 @@ import { useEuiTheme, useGeneratedHtmlId, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { Form, FormikProvider, useFormik, useFormikContext } from 'formik'; import type { FunctionComponent } from 'react'; import React, { useRef, useState } from 'react'; @@ -64,6 +65,12 @@ import { FormRow, OptionalText } from '../../components/form_row'; import { ChangePasswordModal } from '../../management/users/edit_user/change_password_modal'; import { isUserReserved } from '../../management/users/user_utils'; +const formRowCSS = css` + .euiFormRow__label { + flex: 1; + } +`; + export interface UserProfileProps { user: AuthenticatedUser; data?: UserProfileData; @@ -128,30 +135,36 @@ const UserDetailsEditor: FunctionComponent = ({ user }) } > - + + + + } - labelAppend={} fullWidth > - + + + + } - labelAppend={} fullWidth > From 9ed2ad9faffa95c26503ce2933c06dbec48a706e Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 21 Jun 2024 10:22:28 +0200 Subject: [PATCH 2/4] Refactor Roles Grid page from class component to functional component (#186278) Closes https://github.com/elastic/kibana/issues/186388 ## Summary This PR covers some of the pre-work required to modernize role management in Kibana. It convert the older Class component to a functional component. It also breaks up the EUI in-memory table into it's component parts of EUI Search Bar, Filters and EUI Basic table. ### Checklist - [x] Since there's no change to the functionality, tests are expected to continue passing --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../roles/roles_grid/roles_grid_page.test.tsx | 139 +++- .../roles/roles_grid/roles_grid_page.tsx | 684 +++++++++--------- 2 files changed, 448 insertions(+), 375 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 6b666cfd378f48..8951fd3e5c2023 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -44,6 +44,7 @@ const waitForRender = async ( describe('', () => { let apiClientMock: jest.Mocked>; let history: ReturnType; + const { theme, i18n, analytics, notifications } = coreMock.createStart(); beforeEach(() => { history = scopedHistoryMock.create(); @@ -87,7 +88,11 @@ describe('', () => { ); const initialIconCount = wrapper.find(EuiIcon).length; @@ -105,7 +110,11 @@ describe('', () => { ); const initialIconCount = wrapper.find(EuiIcon).length; @@ -125,7 +134,11 @@ describe('', () => { ); await waitForRender(wrapper, (updatedWrapper) => { @@ -139,7 +152,11 @@ describe('', () => { ); const initialIconCount = wrapper.find(EuiIcon).length; @@ -179,7 +196,11 @@ describe('', () => { ); const initialIconCount = wrapper.find(EuiIcon).length; @@ -190,32 +211,86 @@ describe('', () => { expect(wrapper.find(EuiBasicTable).props().items).toEqual([ { - name: 'disabled-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [{ base: [], spaces: [], feature: {} }], - transient_metadata: { enabled: false }, + name: 'test-role-1', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], }, { - name: 'reserved-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [{ base: [], spaces: [], feature: {} }], - metadata: { _reserved: true }, + name: 'test-role-with-description', + description: 'role-description', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], }, { - name: 'special%chars%role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [{ base: [], spaces: [], feature: {} }], + name: 'reserved-role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + metadata: { + _reserved: true, + }, }, { - name: 'test-role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [{ base: [], spaces: [], feature: {} }], + name: 'disabled-role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + transient_metadata: { + enabled: false, + }, }, { - name: 'test-role-with-description', - description: 'role-description', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [{ base: [], spaces: [], feature: {} }], + name: 'special%chars%role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], }, ]); @@ -223,24 +298,24 @@ describe('', () => { expect(wrapper.find(EuiBasicTable).props().items).toEqual([ { - name: 'disabled-role', + name: 'test-role-1', elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], spaces: [], feature: {} }], - transient_metadata: { enabled: false }, }, { - name: 'special%chars%role', + name: 'test-role-with-description', + description: 'role-description', elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], spaces: [], feature: {} }], }, { - name: 'test-role-1', + name: 'disabled-role', elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, }, { - name: 'test-role-with-description', - description: 'role-description', + name: 'special%chars%role', elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], spaces: [], feature: {} }], }, @@ -252,7 +327,11 @@ describe('', () => { ); diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index c753a7cc8a6a6a..7c16c4179f1936 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -5,28 +5,35 @@ * 2.0. */ -import type { EuiBasicTableColumn, EuiSwitchEvent } from '@elastic/eui'; +import type { + CriteriaWithPagination, + EuiBasicTableColumn, + EuiSwitchEvent, + Query, +} from '@elastic/eui'; import { + EuiBasicTable, EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, EuiLink, - EuiPageHeader, + EuiSearchBar, EuiSpacer, EuiSwitch, EuiText, EuiToolTip, } from '@elastic/eui'; import _ from 'lodash'; -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; +import type { FC } from 'react'; import type { BuildFlavor } from '@kbn/config'; import type { NotificationsStart, ScopedHistory } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { ConfirmDelete } from './confirm_delete'; @@ -52,190 +59,192 @@ export interface Props extends StartServices { cloudOrgUrl?: string; } -interface State { - roles: Role[]; - visibleRoles: Role[]; - selection: Role[]; - filter: string; - showDeleteConfirmation: boolean; - permissionDenied: boolean; - includeReservedRoles: boolean; - isLoading: boolean; +interface RolesTableState { + query: Query; + from: number; + size: number; } const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`; }; -export class RolesGridPage extends Component { - static defaultProps: Partial = { - readOnly: false, - }; - - constructor(props: Props) { - super(props); - this.state = { - roles: [], - visibleRoles: [], - selection: [], - filter: '', - showDeleteConfirmation: false, - permissionDenied: false, - includeReservedRoles: true, - isLoading: false, - }; - } +const getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => { + return roles.filter((role) => { + const normalized = `${role.name}`.toLowerCase(); + const normalizedQuery = filter.toLowerCase(); + return ( + normalized.indexOf(normalizedQuery) !== -1 && (includeReservedRoles || !isRoleReserved(role)) + ); + }); +}; - public componentDidMount() { - this.loadRoles(); - } +const DEFAULT_TABLE_STATE = { + query: EuiSearchBar.Query.MATCH_ALL, + sort: { + field: 'creation' as const, + direction: 'desc' as const, + }, + from: 0, + size: 25, + filters: {}, +}; - public render() { - const { permissionDenied } = this.state; +export const RolesGridPage: FC = ({ + notifications, + rolesAPIClient, + history, + readOnly, + buildFlavor, + cloudOrgUrl, + analytics, + theme, + i18n: i18nStart, +}) => { + const [roles, setRoles] = useState([]); + const [visibleRoles, setVisibleRoles] = useState([]); + const [selection, setSelection] = useState([]); + const [filter, setFilter] = useState(''); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [permissionDenied, setPermissionDenied] = useState(false); + const [includeReservedRoles, setIncludeReservedRoles] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [tableState, setTableState] = useState(DEFAULT_TABLE_STATE); + + useEffect(() => { + loadRoles(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const loadRoles = async () => { + try { + setIsLoading(true); + const rolesFromApi = await rolesAPIClient.getRoles(); + setRoles(rolesFromApi); + setVisibleRoles(getVisibleRoles(rolesFromApi, filter, includeReservedRoles)); + } catch (e) { + if (_.get(e, 'body.statusCode') === 403) { + setPermissionDenied(true); + } else { + notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) + ); + } + } finally { + setIsLoading(false); + } + }; - return permissionDenied ? : this.getPageContent(); - } + const onIncludeReservedRolesChange = (e: EuiSwitchEvent) => { + setIncludeReservedRoles(e.target.checked); + setVisibleRoles(getVisibleRoles(roles, filter, e.target.checked)); + }; - private getPageContent = () => { - const { isLoading } = this.state; + const getRoleStatusBadges = (role: Role) => { + const enabled = isRoleEnabled(role); + const deprecated = isRoleDeprecated(role); + const reserved = isRoleReserved(role); - const customRolesEnabled = this.props.buildFlavor === 'serverless'; + const badges: JSX.Element[] = []; + if (!enabled) { + badges.push(); + } + if (reserved) { + badges.push( + + } + /> + ); + } + if (deprecated) { + badges.push( + + ); + } - const rolesTitle = customRolesEnabled ? ( - - ) : ( - + return ( + + {badges.map((badge, index) => ( + + {badge} + + ))} + ); + }; - const rolesDescription = customRolesEnabled ? ( - - ) : ( - - ); + const handleDelete = () => { + setSelection([]); + setShowDeleteConfirmation(false); + loadRoles(); + }; - const emptyResultsMessage = customRolesEnabled ? ( - - ) : ( - - ); - const pageRightSideItems = [ + const deleteOneRole = (roleToDelete: Role) => { + setSelection([roleToDelete]); + setShowDeleteConfirmation(true); + }; + + const renderToolsLeft = () => { + if (selection.length === 0) { + return; + } + const numSelected = selection.length; + return ( setShowDeleteConfirmation(true)} > - , - ]; - if (customRolesEnabled) { - pageRightSideItems.push( - - - - ); - } - return ( - <> - + + ); + }; - - - {this.state.showDeleteConfirmation ? ( - role.name)} - callback={this.handleDelete} - cloudOrgUrl={this.props.cloudOrgUrl} - {...this.props} - /> - ) : null} - - !role.metadata || !role.metadata._reserved, - selectableMessage: (selectable: boolean) => - !selectable ? 'Role is reserved' : '', - onSelectionChange: (selection: Role[]) => this.setState({ selection }), - selected: this.state.selection, - } + const renderToolsRight = () => { + if (buildFlavor !== 'serverless') { + return ( + } - pagination={{ - initialPageSize: 20, - pageSizeOptions: [10, 20, 30, 50, 100], - }} - message={emptyResultsMessage} - items={this.state.visibleRoles} - loading={isLoading} - search={{ - toolsLeft: this.renderToolsLeft(), - toolsRight: this.renderToolsRight(), - box: { - incremental: true, - 'data-test-subj': 'searchRoles', - }, - onChange: (query: Record) => { - this.setState({ - filter: query.queryText, - visibleRoles: this.getVisibleRoles( - this.state.roles, - query.queryText, - this.state.includeReservedRoles - ), - }); - }, - }} - sorting={{ - sort: { - field: 'name', - direction: 'asc', - }, - }} - rowProps={{ 'data-test-subj': 'roleRow' }} + checked={includeReservedRoles} + onChange={onIncludeReservedRolesChange} /> - - ); + ); + } }; - private getColumnConfig = () => { + const onTableChange = ({ page, sort }: CriteriaWithPagination) => { + const newState = { + ...tableState, + from: page?.index! * page?.size!, + size: page?.size!, + }; + setTableState(newState); + }; + + const getColumnConfig = (): Array> => { const config: Array> = [ { field: 'name', @@ -243,18 +252,16 @@ export class RolesGridPage extends Component { defaultMessage: 'Role', }), sortable: true, - render: (name: string) => { - return ( - - - {name} - - - ); - }, + render: (name: string) => ( + + + {name} + + + ), }, { field: 'description', @@ -263,35 +270,27 @@ export class RolesGridPage extends Component { }), sortable: true, truncateText: { lines: 3 }, - render: (description: string, record: Role) => { - return ( - - - {description} - - - ); - }, + render: (description: string, record: Role) => ( + + + {description} + + + ), }, ]; - if (this.props.buildFlavor !== 'serverless') { + if (buildFlavor !== 'serverless') { config.push({ field: 'metadata', name: i18n.translate('xpack.security.management.roles.statusColumnName', { defaultMessage: 'Status', }), sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role), - render: (_metadata: Role['metadata'], record: Role) => { - return this.getRoleStatusBadges(record); - }, + render: (_metadata: Role['metadata'], record: Role) => getRoleStatusBadges(record), }); } - if (!this.props.readOnly) { + if (!readOnly) { config.push({ name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', @@ -312,13 +311,11 @@ export class RolesGridPage extends Component { values: { roleName: role.name }, }), href: (role: Role) => - reactRouterNavigate(this.props.history, getRoleManagementHref('clone', role.name)) - .href, + reactRouterNavigate(history, getRoleManagementHref('clone', role.name)).href, onClick: (role: Role, event: React.MouseEvent) => - reactRouterNavigate( - this.props.history, - getRoleManagementHref('clone', role.name) - ).onClick(event), + reactRouterNavigate(history, getRoleManagementHref('clone', role.name)).onClick( + event + ), 'data-test-subj': (role: Role) => `clone-role-action-${role.name}`, }, { @@ -334,7 +331,7 @@ export class RolesGridPage extends Component { values: { roleName: role.name }, }), 'data-test-subj': (role: Role) => `delete-role-action-${role.name}`, - onClick: (role: Role) => this.deleteOneRole(role), + onClick: (role: Role) => deleteOneRole(role), available: (role: Role) => !role.metadata || !role.metadata._reserved, }, { @@ -351,15 +348,11 @@ export class RolesGridPage extends Component { }), 'data-test-subj': (role: Role) => `edit-role-action-${role.name}`, href: (role: Role) => - reactRouterNavigate(this.props.history, getRoleManagementHref('edit', role.name)) - .href, + reactRouterNavigate(history, getRoleManagementHref('edit', role.name)).href, onClick: (role: Role, event: React.MouseEvent) => - reactRouterNavigate( - this.props.history, - getRoleManagementHref('edit', role.name) - ).onClick(event), + reactRouterNavigate(history, getRoleManagementHref('edit', role.name)).onClick(event), available: (role: Role) => !isRoleReadOnly(role), - enabled: () => this.state.selection.length === 0, + enabled: () => selection.length === 0, }, ], }); @@ -368,151 +361,152 @@ export class RolesGridPage extends Component { return config; }; - private getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => { - return roles.filter((role) => { - const normalized = `${role.name}`.toLowerCase(); - const normalizedQuery = filter.toLowerCase(); - return ( - normalized.indexOf(normalizedQuery) !== -1 && - (includeReservedRoles || !isRoleReserved(role)) - ); - }); + const onCancelDelete = () => { + setShowDeleteConfirmation(false); }; - private onIncludeReservedRolesChange = (e: EuiSwitchEvent) => { - this.setState({ - includeReservedRoles: e.target.checked, - visibleRoles: this.getVisibleRoles(this.state.roles, this.state.filter, e.target.checked), - }); + const pagination = { + pageIndex: tableState.from / tableState.size, + pageSize: tableState.size, + totalItemCount: visibleRoles.length, + pageSizeOptions: [25, 50, 100], }; - - private getRoleStatusBadges = (role: Role) => { - const enabled = isRoleEnabled(role); - const deprecated = isRoleDeprecated(role); - const reserved = isRoleReserved(role); - - const badges: JSX.Element[] = []; - if (!enabled) { - badges.push(); - } - if (reserved) { - badges.push( - + ) : ( + <> + - } - /> - ); - } - if (deprecated) { - badges.push( - - ); - } - - return ( - - {badges.map((badge, index) => ( - - {badge} - - ))} - - ); - }; - - private handleDelete = () => { - this.setState({ - selection: [], - showDeleteConfirmation: false, - }); - this.loadRoles(); - }; - - private deleteOneRole = (roleToDelete: Role) => { - this.setState({ - selection: [roleToDelete], - showDeleteConfirmation: true, - }); - }; - - private async loadRoles() { - try { - this.setState({ isLoading: true }); - const roles = await this.props.rolesAPIClient.getRoles(); + ) : ( + + ) + } + description={ + buildFlavor === 'serverless' ? ( + + ) : ( + + ) + } + rightSideItems={ + readOnly + ? undefined + : [ + + + , + buildFlavor === 'serverless' && ( + + + + ), + ] + } + /> - this.setState({ - roles, - visibleRoles: this.getVisibleRoles( - roles, - this.state.filter, - this.state.includeReservedRoles - ), - }); - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - this.setState({ permissionDenied: true }); - } else { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { - defaultMessage: 'Error fetching roles: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); - } - } finally { - this.setState({ isLoading: false }); - } - } + + + {showDeleteConfirmation ? ( + role.name)} + callback={handleDelete} + cloudOrgUrl={cloudOrgUrl} + notifications={notifications} + rolesAPIClient={rolesAPIClient} + buildFlavor={buildFlavor} + theme={theme} + analytics={analytics} + i18n={i18nStart} + /> + ) : null} - private renderToolsLeft() { - const { selection } = this.state; - if (selection.length === 0) { - return; - } - const numSelected = selection.length; - return ( - this.setState({ showDeleteConfirmation: true })} - > - ) => { + setFilter(query.queryText); + setVisibleRoles(getVisibleRoles(roles, query.queryText, includeReservedRoles)); + }} + toolsLeft={renderToolsLeft()} + toolsRight={renderToolsRight()} /> - - ); - } - - private renderToolsRight() { - if (this.props.buildFlavor !== 'serverless') { - return ( - + + !role.metadata || !role.metadata._reserved, + selectableMessage: (selectable: boolean) => + !selectable ? 'Role is reserved' : '', + onSelectionChange: (value: Role[]) => setSelection(value), + selected: selection, + } + } + onChange={onTableChange} + pagination={pagination} + noItemsMessage={ + buildFlavor === 'serverless' ? ( + + ) : ( + + ) } - checked={this.state.includeReservedRoles} - onChange={this.onIncludeReservedRolesChange} + items={visibleRoles} + loading={isLoading} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + rowProps={{ 'data-test-subj': 'roleRow' }} /> - ); - } - } - private onCancelDelete = () => { - this.setState({ showDeleteConfirmation: false }); - }; -} + + + ); +}; From aaf891406e02f2359b218798d5b551068379dc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 21 Jun 2024 10:54:48 +0200 Subject: [PATCH 3/4] fix(apm): flatten `globalLabels` object (#186579) --- .../kbn-apm-config-loader/src/config.test.ts | 29 +++++++++++++++++++ packages/kbn-apm-config-loader/src/config.ts | 10 +++++++ 2 files changed, 39 insertions(+) diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index e955b175de1295..01f83d43f160d0 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -146,6 +146,35 @@ describe('ApmConfiguration', () => { ); }); + it('flattens the `globalLabels` object', () => { + const kibanaConfig = { + elastic: { + apm: { + globalLabels: { + keyOne: 'k1', + objectOne: { + objectOneKeyOne: 'o1k1', + objectOneKeyTwo: { + objectOneKeyTwoSubkeyOne: 'o1k2s1', + }, + }, + }, + }, + }, + }; + const config = new ApmConfiguration(mockedRootDir, kibanaConfig, true); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + globalLabels: { + git_rev: 'sha', + keyOne: 'k1', + 'objectOne.objectOneKeyOne': 'o1k1', + 'objectOne.objectOneKeyTwo.objectOneKeyTwoSubkeyOne': 'o1k2s1', + }, + }) + ); + }); + describe('env vars', () => { beforeEach(() => { delete process.env.ELASTIC_APM_ENVIRONMENT; diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 8771eb042dc750..ef5f41bb8398e1 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -13,6 +13,7 @@ import { getDataPath } from '@kbn/utils'; import { readFileSync } from 'fs'; import type { AgentConfigOptions } from 'elastic-apm-node'; import type { AgentConfigOptions as RUMAgentConfigOptions } from '@elastic/apm-rum'; +import { getFlattenedObject } from '@kbn/std'; import type { ApmConfigSchema } from './apm_config'; // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html @@ -129,6 +130,15 @@ export class ApmConfiguration { ) { this.baseConfig = merge(this.baseConfig, centralizedConfig); } + + if (this.baseConfig?.globalLabels) { + // Global Labels need to be a key/value pair... + // Dotted names will be renamed to underscored ones by the agent, but we need to provide key/value pairs + // https://github.com/elastic/apm-agent-nodejs/issues/4096#issuecomment-2181621221 + this.baseConfig.globalLabels = getFlattenedObject( + this.baseConfig.globalLabels as Record + ); + } } return this.baseConfig; From 1ef0793245d342cb9744143aac8c13b72a810bb0 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 21 Jun 2024 02:04:52 -0700 Subject: [PATCH 4/4] [OAS] Add deployment GitHub action (#186487) ## Summary This PR adds a new GitHub workflow to publish an [OpenAPI document](https://github.com/elastic/kibana/blob/main/oas_docs/kibana.serverless.yaml) to https://www.elastic.co/docs/api/doc/serverless, per https://docs.bump.sh/help/continuous-integration/github-actions/ --- .github/workflows/bump.yml | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/bump.yml diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml new file mode 100644 index 00000000000000..4e8bd45fd86908 --- /dev/null +++ b/.github/workflows/bump.yml @@ -0,0 +1,60 @@ +name: Check & deploy API documentation + +on: + push: + branches: + - main + paths: + - 'oas_docs/kibana.serverless.yaml' + + pull_request: + branches: + - main + paths: + - 'oas_docs/kibana.serverless.yaml' + +permissions: + contents: read + pull-requests: write + +jobs: + deploy-doc: + if: ${{ github.event_name == 'push' }} + name: Deploy API documentation on Bump.sh + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Deploy API documentation + uses: bump-sh/github-action@v1 + with: + doc: serverless + token: ${{secrets.BUMP_TOKEN}} + file: oas_docs/kibana.serverless.yaml + + api-diff: + if: ${{ github.event_name == 'pull_request' }} + name: Check API diff on Bump.sh + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Create Preview + uses: bump-sh/github-action@v1 + with: + doc: serverless + token: ${{secrets.BUMP_TOKEN}} + file: oas_docs/kibana.serverless.yaml + command: preview + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Comment pull request with API diff + uses: bump-sh/github-action@v1 + with: + doc: serverless + token: ${{secrets.BUMP_TOKEN}} + file: oas_docs/kibana.serverless.yaml + command: diff + fail_on_breaking: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}