Skip to content

Commit 20799a3

Browse files
committed
refactor(chrome): migrate class components to function components
Rewrite LoadingIndicator, HeaderBadge, HeaderExtension, and HeaderHelpMenu as function components with hooks. Extract shared breadcrumb mapping into prepareBreadcrumbs utility and duplicate hasBeta$ observable logic into useHasAppMenuConfig hook. Narrow BehaviorSubject to Observable in GridLayoutProjectSideNav props. Closes elastic/kibana-team#2651
1 parent 79e44f3 commit 20799a3

10 files changed

Lines changed: 381 additions & 512 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import classNames from 'classnames';
11+
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
12+
13+
/** Maps raw ChromeBreadcrumb[] to EUI-compatible breadcrumb props */
14+
export function prepareBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]) {
15+
const crumbs = breadcrumbs.length === 0 ? [{ text: 'Kibana' } as ChromeBreadcrumb] : breadcrumbs;
16+
17+
return crumbs.map((breadcrumb, i) => {
18+
const isLast = i === crumbs.length - 1;
19+
const { deepLinkId, ...rest } = breadcrumb;
20+
21+
return {
22+
...rest,
23+
href: isLast ? undefined : breadcrumb.href,
24+
onClick: isLast ? undefined : breadcrumb.onClick,
25+
'data-test-subj': classNames(
26+
'breadcrumb',
27+
deepLinkId && `breadcrumb-deepLinkId-${deepLinkId}`,
28+
breadcrumb['data-test-subj'],
29+
i === 0 && 'first',
30+
isLast && 'last'
31+
),
32+
};
33+
});
34+
}

src/core/packages/chrome/browser-internal/src/ui/header/header.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ import {
1717
} from '@elastic/eui';
1818
import { i18n } from '@kbn/i18n';
1919
import classnames from 'classnames';
20-
import React, { createRef, useState, useMemo } from 'react';
20+
import React, { createRef, useState } from 'react';
2121
import type { Observable } from 'rxjs';
22-
import { map, EMPTY } from 'rxjs';
23-
import useObservable from 'react-use/lib/useObservable';
22+
2423
import type { HttpStart } from '@kbn/core-http-browser';
2524
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
2625
import type {
@@ -49,6 +48,7 @@ import { HeaderActionMenu, useHeaderActionMenuMounter } from './header_action_me
4948
import { BreadcrumbsWithExtensionsWrapper } from './breadcrumbs_with_extensions';
5049
import { HeaderMenuButton } from './header_menu_button';
5150
import { HeaderPageAnnouncer } from './header_page_announcer';
51+
import { useHasAppMenuConfig } from '../use_has_app_menu_config';
5252

5353
export interface HeaderProps {
5454
kibanaVersion: string;
@@ -95,14 +95,7 @@ export function Header({
9595
const [navId] = useState(htmlIdGenerator()());
9696
const headerActionMenuMounter = useHeaderActionMenuMounter(application.currentActionMenu$);
9797

98-
const hasBeta$ = useMemo(
99-
() =>
100-
observables.appMenu$?.pipe(
101-
map((config) => !!config && !!config.items && config.items.length > 0)
102-
) ?? EMPTY,
103-
[observables.appMenu$]
104-
);
105-
const hasBetaConfig = useObservable(hasBeta$, false);
98+
const hasBetaConfig = useHasAppMenuConfig(observables.appMenu$);
10699

107100
const toggleCollapsibleNavRef = createRef<HTMLButtonElement & { euiAnimate: () => void }>();
108101
const className = classnames('hide-for-sharing', 'headerGlobalNav');

src/core/packages/chrome/browser-internal/src/ui/header/header_badge.tsx

Lines changed: 22 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,77 +7,34 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import React, { Component } from 'react';
11-
import type * as Rx from 'rxjs';
10+
import React from 'react';
11+
import useObservable from 'react-use/lib/useObservable';
12+
import type { Observable } from 'rxjs';
1213
import { EuiBetaBadge } from '@elastic/eui';
1314
import type { ChromeBadge } from '@kbn/core-chrome-browser';
1415

1516
interface Props {
16-
badge$: Rx.Observable<ChromeBadge | undefined>;
17+
badge$: Observable<ChromeBadge | undefined>;
1718
}
1819

19-
interface State {
20-
badge: ChromeBadge | undefined;
21-
}
22-
23-
export class HeaderBadge extends Component<Props, State> {
24-
private subscription?: Rx.Subscription;
25-
26-
constructor(props: Props) {
27-
super(props);
28-
29-
this.state = { badge: undefined };
30-
}
20+
export const HeaderBadge = ({ badge$ }: Props) => {
21+
const badge = useObservable(badge$, undefined);
3122

32-
public componentDidMount() {
33-
this.subscribe();
23+
if (badge == null) {
24+
return null;
3425
}
3526

36-
public componentDidUpdate(prevProps: Props) {
37-
if (prevProps.badge$ === this.props.badge$) {
38-
return;
39-
}
40-
41-
this.unsubscribe();
42-
this.subscribe();
43-
}
44-
45-
public componentWillUnmount() {
46-
this.unsubscribe();
47-
}
48-
49-
public render() {
50-
if (this.state.badge == null) {
51-
return null;
52-
}
53-
54-
return (
55-
<div css={({ euiTheme }) => ({ alignSelf: 'center', marginRight: euiTheme.size.s })}>
56-
<EuiBetaBadge
57-
alignment="middle"
58-
data-test-subj="headerBadge"
59-
data-test-badge-label={this.state.badge.text}
60-
tabIndex={0}
61-
label={this.state.badge.text}
62-
tooltipContent={this.state.badge.tooltip}
63-
iconType={this.state.badge.iconType}
64-
/>
65-
</div>
66-
);
67-
}
68-
69-
private subscribe() {
70-
this.subscription = this.props.badge$.subscribe((badge) => {
71-
this.setState({
72-
badge,
73-
});
74-
});
75-
}
76-
77-
private unsubscribe() {
78-
if (this.subscription) {
79-
this.subscription.unsubscribe();
80-
this.subscription = undefined;
81-
}
82-
}
83-
}
27+
return (
28+
<div css={({ euiTheme }) => ({ alignSelf: 'center', marginRight: euiTheme.size.s })}>
29+
<EuiBetaBadge
30+
alignment="middle"
31+
data-test-subj="headerBadge"
32+
data-test-badge-label={badge.text}
33+
tabIndex={0}
34+
label={badge.text}
35+
tooltipContent={badge.tooltip}
36+
iconType={badge.iconType}
37+
/>
38+
</div>
39+
);
40+
};

src/core/packages/chrome/browser-internal/src/ui/header/header_breadcrumbs.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,19 @@
88
*/
99

1010
import { EuiHeaderBreadcrumbs } from '@elastic/eui';
11-
import classNames from 'classnames';
1211
import React from 'react';
1312
import useObservable from 'react-use/lib/useObservable';
1413
import type { Observable } from 'rxjs';
1514
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
15+
import { prepareBreadcrumbs } from '../breadcrumb_utils';
1616

1717
interface Props {
1818
breadcrumbs$: Observable<ChromeBreadcrumb[]>;
1919
}
2020

2121
export function HeaderBreadcrumbs({ breadcrumbs$ }: Props) {
2222
const breadcrumbs = useObservable(breadcrumbs$, []);
23-
let crumbs = breadcrumbs;
24-
25-
if (breadcrumbs.length === 0) {
26-
crumbs = [{ text: 'Kibana' }];
27-
}
28-
29-
crumbs = crumbs.map((breadcrumb, i) => {
30-
const isLast = i === breadcrumbs.length - 1;
31-
const { deepLinkId, ...rest } = breadcrumb;
32-
33-
return {
34-
...rest,
35-
href: isLast ? undefined : breadcrumb.href,
36-
onClick: isLast ? undefined : breadcrumb.onClick,
37-
'data-test-subj': classNames(
38-
'breadcrumb',
39-
deepLinkId && `breadcrumb-deepLinkId-${deepLinkId}`,
40-
breadcrumb['data-test-subj'],
41-
i === 0 && 'first',
42-
isLast && 'last'
43-
),
44-
};
45-
});
23+
const crumbs = prepareBreadcrumbs(breadcrumbs);
4624

4725
return <EuiHeaderBreadcrumbs breadcrumbs={crumbs} max={10} data-test-subj="breadcrumbs" />;
4826
}

src/core/packages/chrome/browser-internal/src/ui/header/header_extension.tsx

Lines changed: 26 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import { css } from '@emotion/react';
11-
import React from 'react';
11+
import React, { useRef, useEffect } from 'react';
1212
import type { MountPoint } from '@kbn/core-mount-utils-browser';
1313

1414
interface Props {
@@ -17,57 +17,28 @@ interface Props {
1717
containerClassName?: string;
1818
}
1919

20-
export class HeaderExtension extends React.Component<Props> {
21-
private readonly ref = React.createRef<HTMLDivElement>();
22-
private unrender?: () => void;
23-
24-
public componentDidMount() {
25-
this.renderExtension();
26-
}
27-
28-
public componentDidUpdate(prevProps: Props) {
29-
if (this.props.extension === prevProps.extension) {
30-
return;
31-
}
32-
33-
this.unrenderExtension();
34-
this.renderExtension();
35-
}
36-
37-
public componentWillUnmount() {
38-
this.unrenderExtension();
39-
}
40-
41-
public render() {
42-
return (
43-
<div
44-
css={css`
45-
&:empty {
46-
// empty containers should be removed from the layout flow
47-
display: contents;
48-
}
49-
`}
50-
ref={this.ref}
51-
className={this.props.containerClassName}
52-
style={{ display: this.props.display === 'inlineBlock' ? 'inline-block' : undefined }}
53-
/>
54-
);
55-
}
56-
57-
private renderExtension() {
58-
if (!this.ref.current) {
59-
throw new Error('<HeaderExtension /> mounted without ref');
60-
}
61-
62-
if (this.props.extension) {
63-
this.unrender = this.props.extension(this.ref.current);
64-
}
65-
}
66-
67-
private unrenderExtension() {
68-
if (this.unrender) {
69-
this.unrender();
70-
this.unrender = undefined;
71-
}
72-
}
73-
}
20+
export const HeaderExtension = ({ extension, display, containerClassName }: Props) => {
21+
const ref = useRef<HTMLDivElement>(null);
22+
23+
useEffect(() => {
24+
if (!ref.current || !extension) return;
25+
const unrender = extension(ref.current);
26+
return () => {
27+
unrender?.();
28+
};
29+
}, [extension]);
30+
31+
return (
32+
<div
33+
css={css`
34+
&:empty {
35+
// empty containers should be removed from the layout flow
36+
display: contents;
37+
}
38+
`}
39+
ref={ref}
40+
className={containerClassName}
41+
style={{ display: display === 'inlineBlock' ? 'inline-block' : undefined }}
42+
/>
43+
);
44+
};

0 commit comments

Comments
 (0)