Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving loader to logo in header, add a slight 250ms pause #78879

Merged
merged 14 commits into from
Oct 16, 2020

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

268 changes: 161 additions & 107 deletions src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions src/core/public/chrome/ui/header/elastic_mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React, { HTMLAttributes } from 'react';

export const ElasticMark = ({ ...props }: HTMLAttributes<SVGElement>) => (
<svg
width="64"
height="19"
viewBox="0 0 64 19"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="elasticMark"
{...props}
>
<title id="elasticMark">Elastic</title>
<path d="M9.73938 16.8818L10.4515 16.8061L10.497 18.2606C8.61817 18.5182 7.01211 18.6545 5.67878 18.6545C3.90605 18.6545 2.64847 18.1394 1.90605 17.1091C1.16362 16.0788 0.799988 14.4727 0.799988 12.3061C0.799988 7.97272 2.52726 5.80606 5.96665 5.80606C7.63332 5.80606 8.87575 6.27575 9.69393 7.2C10.5121 8.12424 10.9212 9.59394 10.9212 11.5788L10.8151 12.9879H2.66362C2.66362 14.3515 2.90605 15.3667 3.40605 16.0182C3.90605 16.6697 4.75453 17.003 5.98181 17.003C7.22423 17.0333 8.46666 16.9879 9.73938 16.8818ZM9.07271 11.5333C9.07271 10.0182 8.83029 8.94242 8.34544 8.32121C7.8606 7.7 7.07272 7.38182 5.98181 7.38182C4.8909 7.38182 4.05756 7.71515 3.51211 8.36666C2.96665 9.01818 2.67878 10.0788 2.66362 11.5333H9.07271Z" />
snide marked this conversation as resolved.
Show resolved Hide resolved
<path d="M13.497 18.4273V0.699997H15.3454V18.4273H13.497Z" />
<path d="M27.0273 9.80606V15.8818C27.0273 16.503 28.5576 16.6242 28.5576 16.6242L28.4667 18.2606C27.1636 18.2606 26.0879 18.3667 25.4364 17.7455C23.9515 18.397 22.4818 18.6697 20.997 18.6697C19.8606 18.6697 18.997 18.3515 18.4061 17.7C17.8151 17.0636 17.5121 16.1394 17.5121 14.9273C17.5121 13.7303 17.8151 12.8364 18.4212 12.2758C19.0273 11.7152 19.9818 11.3515 21.2848 11.2303L25.1636 10.8667V9.80606C25.1636 8.97273 24.9818 8.36667 24.6182 8.00303C24.2545 7.63939 23.7545 7.45757 23.1333 7.45757H18.2697V5.82121H23.0121C24.4061 5.82121 25.4212 6.13939 26.0576 6.79091C26.7091 7.42727 27.0273 8.44242 27.0273 9.80606ZM19.4212 14.8364C19.4212 16.3515 20.0424 17.1091 21.3 17.1091C22.4212 17.1091 23.5273 16.9273 24.603 16.5485L25.1636 16.3515V12.2758L21.5121 12.6242C20.7697 12.6848 20.2394 12.897 19.9061 13.2606C19.5727 13.6242 19.4212 14.1545 19.4212 14.8364Z" />
<path d="M34.2545 7.47273C32.4667 7.47273 31.5576 8.09394 31.5576 9.35151C31.5576 9.92727 31.7697 10.3364 32.1788 10.5788C32.5879 10.8212 33.5273 11.0636 34.997 11.3212C36.4667 11.5788 37.497 11.9273 38.103 12.397C38.7091 12.8515 39.0121 13.7151 39.0121 14.9879C39.0121 16.2606 38.603 17.1848 37.7848 17.7758C36.9667 18.3667 35.7848 18.6697 34.2091 18.6697C33.1939 18.6697 29.7848 18.2909 29.7848 18.2909L29.8909 16.6848C31.8454 16.8667 33.2697 17.0182 34.2242 17.0182C35.1788 17.0182 35.9061 16.8667 36.4061 16.5636C36.9061 16.2606 37.1636 15.7454 37.1636 15.0333C37.1636 14.3212 36.9515 13.8364 36.5273 13.5788C36.103 13.3212 35.1636 13.0788 33.7091 12.8515C32.2545 12.6242 31.2242 12.2909 30.6182 11.8364C30.0121 11.397 29.7091 10.5636 29.7091 9.36667C29.7091 8.1697 30.1333 7.27576 30.9818 6.7C31.8303 6.12424 32.8909 5.83636 34.1485 5.83636C35.1485 5.83636 38.6333 6.09394 38.6333 6.09394V7.71515C36.8 7.60909 35.3 7.47273 34.2545 7.47273Z" />
<path d="M47.9515 7.68485H44.0273V13.5939C44.0273 15.003 44.1333 15.9424 44.3303 16.3818C44.5424 16.8212 45.0273 17.0485 45.8 17.0485L47.997 16.897L48.1182 18.4273C47.0121 18.6091 46.1788 18.7 45.603 18.7C44.3152 18.7 43.4364 18.3818 42.9364 17.7606C42.4364 17.1394 42.1939 15.9424 42.1939 14.1848V7.68485H40.4364V6.07878H42.1939V2.29091H44.0273V6.06363H47.9515V7.68485Z" />
<path d="M50.5273 3.27575V1.13939H52.3758V3.29091L50.5273 3.27575ZM50.5273 18.4273V6.07879H52.3758V18.4273H50.5273Z" />
<path d="M60.4061 5.82121C60.9515 5.82121 61.8758 5.92727 63.1788 6.12424L63.7697 6.2L63.6939 7.7C62.3758 7.54848 61.4061 7.47273 60.7848 7.47273C59.3909 7.47273 58.4364 7.80606 57.9364 8.47272C57.4364 9.13939 57.1788 10.3818 57.1788 12.1848C57.1788 13.9879 57.4061 15.2455 57.8758 15.9576C58.3455 16.6697 59.3151 17.0182 60.8 17.0182L63.7091 16.7909L63.7848 18.3212C62.2545 18.5485 61.103 18.6697 60.3454 18.6697C58.4212 18.6697 57.0879 18.1697 56.3606 17.1848C55.6333 16.2 55.2545 14.5333 55.2545 12.1848C55.2545 9.83636 55.6485 8.18485 56.4364 7.24545C57.2394 6.30606 58.5576 5.82121 60.4061 5.82121Z" />
</svg>
);
2 changes: 1 addition & 1 deletion src/core/public/chrome/ui/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export function Header({
forceNavigation$={observables.forceAppSwitcherNavigation$}
navLinks$={observables.navLinks$}
navigateToApp={application.navigateToApp}
loadingCount$={observables.loadingCount$}
/>,
<LoadingIndicator loadingCount$={observables.loadingCount$} />,
],
borders: 'none',
},
Expand Down
4 changes: 4 additions & 0 deletions src/core/public/chrome/ui/header/header_logo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.chrHeaderLogo__mark {
margin-left: $euiSizeS;
fill: $euiColorGhost;
}
19 changes: 12 additions & 7 deletions src/core/public/chrome/ui/header/header_logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
* under the License.
*/

import { EuiHeaderLogo } from '@elastic/eui';
import './header_logo.scss';
import { i18n } from '@kbn/i18n';
import React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import Url from 'url';
import { ChromeNavLink } from '../..';
import { ElasticMark } from './elastic_mark';
import { HttpStart } from '../../../http';
import { LoadingIndicator } from '../loading_indicator';

function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void {
let current = element;
Expand Down Expand Up @@ -90,23 +93,25 @@ interface Props {
navLinks$: Observable<ChromeNavLink[]>;
forceNavigation$: Observable<boolean>;
navigateToApp: (appId: string) => void;
loadingCount$?: ReturnType<HttpStart['getLoadingCount$']>;
}

export function HeaderLogo({ href, navigateToApp, ...observables }: Props) {
export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables }: Props) {
const forceNavigation = useObservable(observables.forceNavigation$, false);
const navLinks = useObservable(observables.navLinks$, []);

return (
<EuiHeaderLogo
data-test-subj="logo"
iconType="logoElastic"
<a
onClick={(e) => onClick(e, forceNavigation, navLinks, navigateToApp)}
className="euiHeaderLogo"
href={href}
data-test-subj="logo"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', {
defaultMessage: 'Go to home page',
snide marked this conversation as resolved.
Show resolved Hide resolved
})}
>
Elastic
</EuiHeaderLogo>
<LoadingIndicator loadingCount$={loadingCount$!} />
<ElasticMark className="chrHeaderLogo__mark" aria-hidden={true} />
</a>
);
}
5 changes: 4 additions & 1 deletion src/core/public/chrome/ui/loading_indicator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ describe('kbnLoadingIndicator', () => {

it('is visible when loadingCount is > 0', () => {
const wrapper = shallow(<LoadingIndicator loadingCount$={new BehaviorSubject(1)} />);
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator');
// Pause the check beyond the 250ms delay that it has
setTimeout(() => {
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator');
}, 300);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: It could be a source of flakiness on CI. We might implement a function polling the element presence (as in FTR TestSubjects.exists or remove the time factor in tests https://jestjs.io/docs/en/timer-mocks#run-all-timers

expect(wrapper).toMatchSnapshot();
});
});
38 changes: 31 additions & 7 deletions src/core/public/chrome/ui/loading_indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui';
import { EuiLoadingSpinner, EuiProgress, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import classNames from 'classnames';
Expand All @@ -39,16 +39,26 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
visible: false,
};

private timer: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: private timer?: number;

Copy link
Contributor Author

@snide snide Sep 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a number. It relates to the setTimeout function. TS complains.

Copy link
Contributor

@pgayvallet pgayvallet Oct 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout ids are numbers

setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
clearTimeout(handle?: number): void;

What does TS complains about exactly?

private increment = 1;

componentDidMount() {
this.loadingCountSubscription = this.props.loadingCount$.subscribe((count) => {
this.setState({
visible: count > 0,
});
if (this.increment > 1) {
snide marked this conversation as resolved.
Show resolved Hide resolved
clearTimeout(this.timer);
}
this.increment += this.increment;
Comment on lines +47 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As increment is initialized to 1, I'm ensure to understand what exactly this block does. Both the if (this.increment > 1) (that would be always true) and this.increment += this.increment; seems wrong to me?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sure that the 250 pause only starts the first time the cycle begins. You can see that it does print initially and only increases when the subscription does.

The subscription code isn't the most approachable, but this method seemed to do what was needed: only show a loading indicator if a new "loading event" existed for more than 250ms.

image

this.timer = setTimeout(() => {
this.setState({
visible: count > 0,
});
}, 250);
Comment on lines +47 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the delay is reset every time loadingCount emits a new value, meaning that if there are consecutive requests firing at an interval < 250, the loader will never be displayed until 250ms after the last one. Is that the expected behavior?

Shouldn't we start the timer the first time loadingCount$ is greater than 0, but not on the next emissions until loadingCount is back to 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest, if you have experience with this code, feel free to commit on top of it. The way the subscription numbers shifted felt weird.

In any case the problem I was trying to solve from a UI perspective with the current functionality:

The loading action "blips" for minor events. Basically, ANY time the subscription changed (which is often) it would signal. Less than a second of loading, and the user won't even notice the load, but they would notice an annoying animation on the page. We'd prefer not to show it in those cases, and only show it for sustained loads. I think what your'e saying is the correct way to handle it. Look for it to go back to zero, then start the count, and see if the number is larger than zero.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to Pierre's comments

I can add this to my queue if you're not sure how to implement it but I don't think I'll get to it for at least a few days (if someone else has time before me, feel free to jump in)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna give this a shot tonight. I'll bug you all tomorrow for a review.

});
}

componentWillUnmount() {
if (this.loadingCountSubscription) {
clearTimeout(this.timer);
this.loadingCountSubscription.unsubscribe();
this.loadingCountSubscription = undefined;
}
Expand All @@ -67,13 +77,27 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
defaultMessage: 'Loading content',
});

return !this.props.showAsBar ? (
const logo = this.state.visible ? (
<EuiLoadingSpinner
className={className}
size="l"
data-test-subj={testSubj}
aria-hidden={ariaHidden}
aria-hidden={false}
aria-label={ariaLabel}
/>
) : (
<EuiIcon
type="logoElastic"
size="l"
data-test-subj={testSubj}
className="chrHeaderLogo__cluster"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
defaultMessage: 'Elastic Logo',
})}
/>
);

return !this.props.showAsBar ? (
logo
) : (
<EuiProgress
className={className}
Expand Down