Skip to content

Commit

Permalink
Merge branch 'master' into ui-polish-qanotes
Browse files Browse the repository at this point in the history
  • Loading branch information
animehart committed Mar 28, 2022
2 parents 2085602 + ca81ce8 commit ab4a89b
Show file tree
Hide file tree
Showing 36 changed files with 926 additions and 305 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"@elastic/charts": "45.0.1",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1",
"@elastic/ems-client": "8.1.0",
"@elastic/ems-client": "8.2.0",
"@elastic/eui": "51.1.0",
"@elastic/filesaver": "1.1.2",
"@elastic/node-crypto": "1.2.1",
Expand Down
17 changes: 17 additions & 0 deletions packages/kbn-shared-ux-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,20 @@ export const LazyIconButtonGroup = React.lazy(() =>
* The IconButtonGroup component that is wrapped by the `withSuspence` HOC.
*/
export const IconButtonGroup = withSuspense(LazyIconButtonGroup);

/**
* The Lazily-loaded `KibanaSolutionAvatar` component. Consumers should use `React.Suspense` or
* the withSuspense` HOC to load this component.
*/
export const KibanaSolutionAvatarLazy = React.lazy(() =>
import('./solution_avatar').then(({ KibanaSolutionAvatar }) => ({
default: KibanaSolutionAvatar,
}))
);

/**
* A `KibanaSolutionAvatar` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `KibanaPageTemplateSolutionNavAvatarLazy` component lazily with
* a predefined fallback and error boundary.
*/
export const KibanaSolutionAvatar = withSuspense(KibanaSolutionAvatarLazy);

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

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export { KibanaSolutionAvatar } from './solution_avatar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.kbnSolutionAvatar {
@include euiBottomShadowSmall;

&--xxl {
@include euiBottomShadowMedium;
@include size(100px);
line-height: 100px;
border-radius: 100px;
display: inline-block;
background: $euiColorEmptyShade url('/assets/texture.svg') no-repeat;
background-size: cover, 125%;
text-align: center;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { KibanaSolutionAvatar, KibanaSolutionAvatarProps } from './solution_avatar';

export default {
title: 'Solution Avatar',
description: 'A wrapper around EuiAvatar, specifically to stylize Elastic Solutions',
};

type Params = Pick<KibanaSolutionAvatarProps, 'size' | 'name'>;

export const PureComponent = (params: Params) => {
return <KibanaSolutionAvatar {...params} />;
};

PureComponent.argTypes = {
name: {
control: 'text',
defaultValue: 'Kibana',
},
size: {
control: 'radio',
options: ['s', 'm', 'l', 'xl', 'xxl'],
defaultValue: 'xxl',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { shallow } from 'enzyme';
import { KibanaSolutionAvatar } from './solution_avatar';

describe('KibanaSolutionAvatar', () => {
test('renders', () => {
const component = shallow(<KibanaSolutionAvatar name="Solution" iconType="logoElastic" />);
expect(component).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import './solution_avatar.scss';

import React from 'react';
import classNames from 'classnames';

import { DistributiveOmit, EuiAvatar, EuiAvatarProps } from '@elastic/eui';

export type KibanaSolutionAvatarProps = DistributiveOmit<EuiAvatarProps, 'size'> & {
/**
* Any EuiAvatar size available, or `xxl` for custom large, brand-focused version
*/
size?: EuiAvatarProps['size'] | 'xxl';
};

/**
* Applies extra styling to a typical EuiAvatar;
* The `name` value will be appended to 'logo' to configure the `iconType` unless `iconType` is provided.
*/
export const KibanaSolutionAvatar = ({ className, size, ...rest }: KibanaSolutionAvatarProps) => {
return (
// @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine
<EuiAvatar
className={classNames(
'kbnSolutionAvatar',
{
[`kbnSolutionAvatar--${size}`]: size,
},
className
)}
color="plain"
size={size === 'xxl' ? 'xl' : size}
iconSize={size}
iconType={`logo${rest.name}`}
{...rest}
/>
);
};
2 changes: 1 addition & 1 deletion packages/kbn-test/jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
coverageDirectory: '<rootDir>/target/kibana-coverage/jest',

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'],
coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts', 'jest\\.config\\.js'],

// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: !!process.env.CODE_COVERAGE
Expand Down
50 changes: 50 additions & 0 deletions src/core/server/status/cached_plugins_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Observable } from 'rxjs';

import { type PluginName } from '../plugins';
import { type ServiceStatus } from './types';

import { type Deps, PluginsStatusService as BasePluginsStatusService } from './plugins_status';

export class PluginsStatusService extends BasePluginsStatusService {
private all$?: Observable<Record<PluginName, ServiceStatus>>;
private dependenciesStatuses$: Record<PluginName, Observable<Record<PluginName, ServiceStatus>>>;
private derivedStatuses$: Record<PluginName, Observable<ServiceStatus>>;

constructor(deps: Deps) {
super(deps);
this.dependenciesStatuses$ = {};
this.derivedStatuses$ = {};
}

public getAll$(): Observable<Record<PluginName, ServiceStatus>> {
if (!this.all$) {
this.all$ = super.getAll$();
}

return this.all$;
}

public getDependenciesStatus$(plugin: PluginName): Observable<Record<PluginName, ServiceStatus>> {
if (!this.dependenciesStatuses$[plugin]) {
this.dependenciesStatuses$[plugin] = super.getDependenciesStatus$(plugin);
}

return this.dependenciesStatuses$[plugin];
}

public getDerivedStatus$(plugin: PluginName): Observable<ServiceStatus> {
if (!this.derivedStatuses$[plugin]) {
this.derivedStatuses$[plugin] = super.getDerivedStatus$(plugin);
}

return this.derivedStatuses$[plugin];
}
}
41 changes: 23 additions & 18 deletions src/core/server/status/plugins_status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PluginName } from '../plugins';
import { PluginsStatusService } from './plugins_status';
import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs';
import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types';
import { first } from 'rxjs/operators';
import { first, skip } from 'rxjs/operators';
import { ServiceStatusLevelSnapshotSerializer } from './test_utils';

expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer);
Expand Down Expand Up @@ -215,7 +215,7 @@ describe('PluginStatusService', () => {
service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' }));

expect(await service.getAll$().pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded
a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available despite savedObjects being degraded
b: {
level: ServiceStatusLevels.degraded,
summary: '1 service is degraded: savedObjects',
Expand All @@ -239,6 +239,10 @@ describe('PluginStatusService', () => {
const statusUpdates: Array<Record<PluginName, ServiceStatus>> = [];
const subscription = service
.getAll$()
// If we subscribe to the $getAll() Observable BEFORE setting a custom status Observable
// for a given plugin ('a' in this test), then the first emission will happen
// right after core$ services Observable emits
.pipe(skip(1))
.subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));

service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' }));
Expand All @@ -261,6 +265,8 @@ describe('PluginStatusService', () => {
const statusUpdates: Array<Record<PluginName, ServiceStatus>> = [];
const subscription = service
.getAll$()
// the first emission happens right after core services emit (see explanation above)
.pipe(skip(1))
.subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));

const aStatus$ = new BehaviorSubject<ServiceStatus>({
Expand All @@ -280,19 +286,21 @@ describe('PluginStatusService', () => {
});

it('emits an unavailable status if first emission times out, then continues future emissions', async () => {
jest.useFakeTimers();
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies: new Map([
['a', []],
['b', ['a']],
]),
});
const service = new PluginsStatusService(
{
core$: coreAllAvailable$,
pluginDependencies: new Map([
['a', []],
['b', ['a']],
]),
},
10 // set a small timeout so that the registered status Observable for 'a' times out quickly
);

const pluginA$ = new ReplaySubject<ServiceStatus>(1);
service.set('a', pluginA$);
const firstEmission = service.getAll$().pipe(first()).toPromise();
jest.runAllTimers();
// the first emission happens right after core$ services emit
const firstEmission = service.getAll$().pipe(skip(1), first()).toPromise();

expect(await firstEmission).toEqual({
a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' },
Expand All @@ -308,16 +316,16 @@ describe('PluginStatusService', () => {

pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
const secondEmission = service.getAll$().pipe(first()).toPromise();
jest.runAllTimers();
expect(await secondEmission).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'a available' },
b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
});
jest.useRealTimers();
});
});

describe('getDependenciesStatus$', () => {
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

it('only includes dependencies of specified plugin', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
Expand Down Expand Up @@ -357,7 +365,7 @@ describe('PluginStatusService', () => {

it('debounces plugins custom status registration', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
core$: coreOneCriticalOneDegraded$,
pluginDependencies,
});
const available: ServiceStatus = {
Expand All @@ -375,8 +383,6 @@ describe('PluginStatusService', () => {

expect(statusUpdates).toStrictEqual([]);

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Waiting for the debounce timeout should cut a new update
await delay(25);
subscription.unsubscribe();
Expand Down Expand Up @@ -404,7 +410,6 @@ describe('PluginStatusService', () => {
const subscription = service
.getDependenciesStatus$('b')
.subscribe((status) => statusUpdates.push(status));
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

pluginA$.next(degraded);
pluginA$.next(available);
Expand Down
Loading

0 comments on commit ab4a89b

Please sign in to comment.