[HTTP] Add server/browser side http.staticAssets service (#171003)

## Summary

Part of https://github.com/elastic/kibana/issues/170421

### 1. Introduce the `http.staticAssets` service

Which can be used to generate hrefs to Kibana's static assets in a
CDN-friendly way (based on the CDN url if defined in the config, and the
Kibana's basePath otherwise)

The service is exposed both on the browser and server-side.

For now a single API is exposed: `getPluginAssetHref`

```ts
// returns "/plugins/{pluginId}/assets/some_folder/asset.png" when CDN isn't configured
core.http.statisAssets.getPluginAssetHref('some_folder/asset.png');
```

### 2. Plug it on some of the `home` plugin assets

Adapt the sample data sets and tutorial schemas to use the service for
links to the associated assets

## How to test

#### 1. Edit`/etc/hosts`

add a line `127.0.0.1       local.cdn`

#### 2. Edit `kibana.yaml`

Add `server.cdn.url: "http://local.cdn:5601"`

#### 3. Boot kibana and navigate to sample data set installation

(if started in dev mode, use `--no-base-path`)

Confirm that the sample data set presentation images are pointing to the
CDN url and properly displayed:

<img width="1565" alt="Screenshot 2023-11-13 at 09 28 51"
src="23a887af-00cb-400c-9ab1-511ba463495f">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pierre Gayvallet 2023-11-16 16:13:00 +01:00 committed by GitHub
parent 82d05036ac
commit c713b91e66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
163 changed files with 673 additions and 287 deletions

View file

@ -13,7 +13,7 @@ import { createBrowserHistory, History } from 'history';
import type { PluginOpaqueId } from '@kbn/core-base-common';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpSetup, InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { Capabilities } from '@kbn/core-capabilities-common';
import type { MountPoint } from '@kbn/core-mount-utils-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
@ -46,7 +46,7 @@ import {
import { registerAnalyticsContextProvider } from './register_analytics_context_provider';
export interface SetupDeps {
http: HttpSetup;
http: InternalHttpSetup;
analytics: AnalyticsServiceSetup;
history?: History<any>;
/** Used to redirect to external urls */
@ -54,7 +54,7 @@ export interface SetupDeps {
}
export interface StartDeps {
http: HttpStart;
http: InternalHttpStart;
analytics: AnalyticsServiceStart;
theme: ThemeServiceStart;
overlays: OverlayStart;

View file

@ -30,7 +30,7 @@ describe('parseAppUrl', () => {
beforeEach(() => {
apps = new Map();
basePath = new BasePath('/base-path');
basePath = new BasePath({ basePath: '/base-path' });
createApp({
id: 'foo',

View file

@ -10,7 +10,7 @@ import type { UnregisterCallback } from 'history';
import type { CoreContext } from '@kbn/core-base-browser-internal';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpSetup, InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { NotificationsSetup, NotificationsStart } from '@kbn/core-notifications-browser';
import { AppNavLinkStatus, type AppMountParameters } from '@kbn/core-application-browser';
@ -27,7 +27,7 @@ import { renderApp as renderStatusApp } from './status';
export interface CoreAppsServiceSetupDeps {
application: InternalApplicationSetup;
http: HttpSetup;
http: InternalHttpSetup;
injectedMetadata: InternalInjectedMetadataSetup;
notifications: NotificationsSetup;
}
@ -35,7 +35,7 @@ export interface CoreAppsServiceSetupDeps {
export interface CoreAppsServiceStartDeps {
application: InternalApplicationStart;
docLinks: DocLinksStart;
http: HttpStart;
http: InternalHttpStart;
notifications: NotificationsStart;
uiSettings: IUiSettingsClient;
}

View file

@ -22,7 +22,7 @@ describe('renderApp', () => {
let unmount: () => void;
beforeEach(() => {
basePath = new BasePath();
basePath = new BasePath({ basePath: '' });
element = document.createElement('div');
history = createMemoryHistory();
unmount = renderApp(

View file

@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
@ -21,7 +21,7 @@ export const MISSING_CONFIG_STORAGE_KEY = `core.warnings.publicBaseUrlMissingDis
interface Deps {
docLinks: DocLinksStart;
http: HttpStart;
http: InternalHttpStart;
notifications: NotificationsStart;
// Exposed for easier testing
storage?: Storage;

View file

@ -27,7 +27,7 @@ describe('url overflow detection', () => {
let unlisten: any;
beforeEach(() => {
basePath = new BasePath('/test-123');
basePath = new BasePath({ basePath: '/test-123' });
history = createMemoryHistory();
toasts = notificationServiceMock.createStartContract().toasts;
uiSettings = uiSettingsServiceMock.createStartContract();

View file

@ -189,7 +189,7 @@ export async function loadStatus({
http,
notifications,
}: {
http: HttpSetup;
http: Pick<HttpSetup, 'get'>;
notifications: NotificationsSetup;
}) {
let response: StatusResponse;

View file

@ -10,13 +10,13 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { CoreThemeProvider } from '@kbn/core-theme-browser-internal';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { NotificationsSetup } from '@kbn/core-notifications-browser';
import type { AppMountParameters } from '@kbn/core-application-browser';
import { StatusApp } from './status_app';
interface Deps {
http: HttpSetup;
http: InternalHttpSetup;
notifications: NotificationsSetup;
}

View file

@ -10,13 +10,13 @@ import React, { Component } from 'react';
import { EuiLoadingSpinner, EuiText, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { NotificationsSetup } from '@kbn/core-notifications-browser';
import { loadStatus, type ProcessedServerResponse } from './lib';
import { MetricTiles, ServerStatus, StatusSection, VersionHeader } from './components';
interface StatusAppProps {
http: HttpSetup;
http: InternalHttpSetup;
notifications: NotificationsSetup;
}

View file

@ -8,12 +8,12 @@
import type { RecursiveReadonly } from '@kbn/utility-types';
import { deepFreeze } from '@kbn/std';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { Capabilities } from '@kbn/core-capabilities-common';
interface StartDeps {
appIds: string[];
http: HttpStart;
http: InternalHttpStart;
}
/** @internal */

View file

@ -14,9 +14,9 @@
"kbn_references": [
"@kbn/utility-types",
"@kbn/std",
"@kbn/core-http-browser",
"@kbn/core-http-browser-mocks",
"@kbn/core-capabilities-common"
"@kbn/core-capabilities-common",
"@kbn/core-http-browser-internal"
],
"exclude": [
"target/**/*",

View file

@ -16,7 +16,7 @@ import useObservable from 'react-use/lib/useObservable';
import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal';
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import { type DocLinksStart } from '@kbn/core-doc-links-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
@ -63,7 +63,7 @@ export interface SetupDeps {
export interface StartDeps {
application: InternalApplicationStart;
docLinks: DocLinksStart;
http: HttpStart;
http: InternalHttpStart;
injectedMetadata: InternalInjectedMetadataStart;
notifications: NotificationsStart;
customBranding: CustomBrandingStart;

View file

@ -9,7 +9,8 @@
import { sortBy } from 'lodash';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import type { HttpStart, IBasePath } from '@kbn/core-http-browser';
import type { IBasePath } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { PublicAppDeepLinkInfo, PublicAppInfo } from '@kbn/core-application-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { ChromeNavLinks } from '@kbn/core-chrome-browser';
@ -18,7 +19,7 @@ import { toNavLink } from './to_nav_link';
interface StartDeps {
application: InternalApplicationStart;
http: HttpStart;
http: InternalHttpStart;
}
export class NavLinksService {

View file

@ -16,7 +16,7 @@ import {
ChromeSetProjectBreadcrumbsParams,
ChromeProjectNavigationNode,
} from '@kbn/core-chrome-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import {
BehaviorSubject,
Observable,
@ -37,7 +37,7 @@ import { buildBreadcrumbs } from './breadcrumbs';
interface StartDeps {
application: InternalApplicationStart;
navLinks: ChromeNavLinks;
http: HttpStart;
http: InternalHttpStart;
chromeBreadcrumbs$: Observable<ChromeBreadcrumb[]>;
}
@ -59,7 +59,7 @@ export class ProjectNavigationService {
}>({ breadcrumbs: [], params: { absolute: false } });
private readonly stop$ = new ReplaySubject<void>(1);
private application?: InternalApplicationStart;
private http?: HttpStart;
private http?: InternalHttpStart;
private unlistenHistory?: () => void;
public start({ application, navLinks, http, chromeBreadcrumbs$ }: StartDeps) {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type {
ChromeRecentlyAccessed,
ChromeRecentlyAccessedHistoryItem,
@ -15,7 +15,7 @@ import { PersistedLog } from './persisted_log';
import { createLogKey } from './create_log_key';
interface StartDeps {
http: HttpSetup;
http: InternalHttpStart;
}
/** @internal */

View file

@ -733,6 +733,7 @@ exports[`CollapsibleNav renders the default nav 1`] = `
}
basePath={
BasePath {
"assetsHrefBase": "/test",
"basePath": "/test",
"get": [Function],
"prepend": [Function],
@ -918,6 +919,7 @@ exports[`CollapsibleNav renders the default nav 2`] = `
}
basePath={
BasePath {
"assetsHrefBase": "/test",
"basePath": "/test",
"get": [Function],
"prepend": [Function],

View file

@ -43,6 +43,7 @@
"@kbn/core-analytics-browser",
"@kbn/shared-ux-router",
"@kbn/shared-ux-link-redirect-app",
"@kbn/core-http-browser-internal",
],
"exclude": [
"target/**/*",

View file

@ -8,13 +8,13 @@
import type { CoreService } from '@kbn/core-base-browser-internal';
import type { DeprecationsServiceStart } from '@kbn/core-deprecations-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import { DeprecationsClient } from './deprecations_client';
export class DeprecationsService implements CoreService<void, DeprecationsServiceStart> {
public setup(): void {}
public start({ http }: { http: HttpStart }): DeprecationsServiceStart {
public start({ http }: { http: InternalHttpStart }): DeprecationsServiceStart {
const deprecationsClient = new DeprecationsClient({ http });
return {

View file

@ -16,7 +16,8 @@
"@kbn/core-http-browser",
"@kbn/core-http-browser-mocks",
"@kbn/core-deprecations-common",
"@kbn/core-deprecations-browser"
"@kbn/core-deprecations-browser",
"@kbn/core-http-browser-internal"
],
"exclude": [
"target/**/*",

View file

@ -8,3 +8,4 @@
export { BasePath } from './src/base_path';
export { HttpService } from './src/http_service';
export type { InternalHttpSetup, InternalHttpStart } from './src/types';

View file

@ -12,13 +12,13 @@ import { BasePath } from './base_path';
describe('#setup()', () => {
describe('#register', () => {
it(`allows paths that don't start with /`, () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('bar');
});
it(`allows paths that end with '/'`, () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar/');
});
@ -26,70 +26,70 @@ describe('#setup()', () => {
describe('#isAnonymous', () => {
it('returns true for registered paths', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar/');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true);
});
it('returns true for paths registered without a starting slash', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/bar')).toBe(true);
});
it('returns true for paths whose capitalization is different', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/BAR');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns false for other paths', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false);
});
it('returns false for sub-paths of registered paths', () => {
const basePath = new BasePath('/foo');
const basePath = new BasePath({ basePath: '/foo' });
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false);

View file

@ -10,35 +10,31 @@ import { BasePath } from './base_path';
describe('BasePath', () => {
describe('#get()', () => {
it('returns an empty string if no basePath not provided', () => {
expect(new BasePath().get()).toBe('');
});
it('returns basePath value if provided', () => {
expect(new BasePath('/foo').get()).toBe('/foo');
expect(new BasePath({ basePath: '/foo' }).get()).toBe('/foo');
});
describe('#prepend()', () => {
it('adds the base path to the path if it is relative and starts with a slash', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.prepend('/a/b')).toBe('/foo/bar/a/b');
});
it('leaves the query string and hash of path unchanged', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.prepend('/a/b?x=y#c/d/e')).toBe('/foo/bar/a/b?x=y#c/d/e');
});
it('returns the path unchanged if it does not start with a slash', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.prepend('a/b')).toBe('a/b');
});
it('returns the path unchanged it it has a hostname', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.prepend('http://localhost:5601/a/b')).toBe('http://localhost:5601/a/b');
});
@ -46,19 +42,19 @@ describe('BasePath', () => {
describe('#remove()', () => {
it('removes the basePath if relative path starts with it', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.remove('/foo/bar/a/b')).toBe('/a/b');
});
it('leaves query string and hash intact', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.remove('/foo/bar/a/b?c=y#1234')).toBe('/a/b?c=y#1234');
});
it('ignores urls with hostnames', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.remove('http://localhost:5601/foo/bar/a/b')).toBe(
'http://localhost:5601/foo/bar/a/b'
@ -66,13 +62,13 @@ describe('BasePath', () => {
});
it('returns slash if path is just basePath', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.remove('/foo/bar')).toBe('/');
});
it('returns full path if basePath is not its own segment', () => {
const basePath = new BasePath('/foo/bar');
const basePath = new BasePath({ basePath: '/foo/bar' });
expect(basePath.remove('/foo/barhop')).toBe('/foo/barhop');
});
@ -81,20 +77,39 @@ describe('BasePath', () => {
describe('serverBasePath', () => {
it('defaults to basePath', () => {
expect(new BasePath('/foo/bar').serverBasePath).toEqual('/foo/bar');
expect(new BasePath({ basePath: '/foo/bar' }).serverBasePath).toEqual('/foo/bar');
});
it('returns value when passed into constructor', () => {
expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo');
expect(new BasePath({ basePath: '/foo/bar', serverBasePath: '/foo' }).serverBasePath).toEqual(
'/foo'
);
});
});
describe('publicBaseUrl', () => {
it('returns value passed into construtor', () => {
expect(new BasePath('/foo/bar', '/foo').publicBaseUrl).toEqual(undefined);
expect(new BasePath('/foo/bar', '/foo', 'http://myhost.com/foo').publicBaseUrl).toEqual(
'http://myhost.com/foo'
expect(new BasePath({ basePath: '/foo/bar' }).publicBaseUrl).toEqual(undefined);
expect(
new BasePath({ basePath: '/foo/bar', publicBaseUrl: 'http://myhost.com/foo' }).publicBaseUrl
).toEqual('http://myhost.com/foo');
});
});
describe('assetsHrefBase', () => {
it('default to the serverBasePath if unspecified', () => {
expect(new BasePath({ basePath: '/foo/bar', serverBasePath: '/foo' }).assetsHrefBase).toEqual(
'/foo'
);
});
it('returns the correct value when explicitly set', () => {
expect(
new BasePath({
basePath: '/foo/bar',
serverBasePath: '/foo',
assetsHrefBase: 'http://cdn/foo',
}).assetsHrefBase
).toEqual('http://cdn/foo');
});
});
});

View file

@ -10,18 +10,36 @@ import { IBasePath } from '@kbn/core-http-browser';
import { modifyUrl } from '@kbn/std';
export class BasePath implements IBasePath {
constructor(
private readonly basePath: string = '',
public readonly serverBasePath: string = basePath,
public readonly publicBaseUrl?: string
) {}
private readonly basePath: string;
public readonly serverBasePath: string;
public readonly assetsHrefBase: string;
public readonly publicBaseUrl?: string;
constructor({
basePath,
serverBasePath,
assetsHrefBase,
publicBaseUrl,
}: {
basePath: string;
serverBasePath?: string;
assetsHrefBase?: string;
publicBaseUrl?: string;
}) {
this.basePath = basePath;
this.serverBasePath = serverBasePath ?? this.basePath;
this.assetsHrefBase = assetsHrefBase ?? this.serverBasePath;
this.publicBaseUrl = publicBaseUrl;
}
public get = () => {
return this.basePath;
};
public prepend = (path: string): string => {
if (!this.basePath) return path;
if (!this.basePath) {
return path;
}
return modifyUrl(path, (parts) => {
if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) {
parts.pathname = `${this.basePath}${parts.pathname}`;

View file

@ -27,7 +27,7 @@ const BASE_PATH = 'http://localhost/myBase';
describe('Fetch', () => {
const executionContextMock = executionContextServiceMock.createSetupContract();
const fetchInstance = new Fetch({
basePath: new BasePath(BASE_PATH),
basePath: new BasePath({ basePath: BASE_PATH }),
kibanaVersion: 'VERSION',
buildNumber: 1234,
executionContext: executionContextMock,

View file

@ -10,8 +10,9 @@ import type { CoreService } from '@kbn/core-base-browser-internal';
import type { ExecutionContextSetup } from '@kbn/core-execution-context-browser';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser';
import type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpSetup, InternalHttpStart } from './types';
import { BasePath } from './base_path';
import { StaticAssets } from './static_assets';
import { AnonymousPathsService } from './anonymous_paths_service';
import { LoadingCountService } from './loading_count_service';
import { Fetch } from './fetch';
@ -24,19 +25,23 @@ interface HttpDeps {
}
/** @internal */
export class HttpService implements CoreService<HttpSetup, HttpStart> {
export class HttpService implements CoreService<InternalHttpSetup, InternalHttpStart> {
private readonly anonymousPaths = new AnonymousPathsService();
private readonly loadingCount = new LoadingCountService();
private service?: HttpSetup;
private service?: InternalHttpSetup;
public setup({ injectedMetadata, fatalErrors, executionContext }: HttpDeps): HttpSetup {
public setup({ injectedMetadata, fatalErrors, executionContext }: HttpDeps): InternalHttpSetup {
const kibanaVersion = injectedMetadata.getKibanaVersion();
const buildNumber = injectedMetadata.getKibanaBuildNumber();
const basePath = new BasePath(
injectedMetadata.getBasePath(),
injectedMetadata.getServerBasePath(),
injectedMetadata.getPublicBaseUrl()
);
const basePath = new BasePath({
basePath: injectedMetadata.getBasePath(),
serverBasePath: injectedMetadata.getServerBasePath(),
publicBaseUrl: injectedMetadata.getPublicBaseUrl(),
assetsHrefBase: injectedMetadata.getAssetsHrefBase(),
});
const staticAssets = new StaticAssets({
assetsHrefBase: injectedMetadata.getAssetsHrefBase(),
});
const fetchService = new Fetch({ basePath, kibanaVersion, buildNumber, executionContext });
const loadingCount = this.loadingCount.setup({ fatalErrors });
@ -44,6 +49,7 @@ export class HttpService implements CoreService<HttpSetup, HttpStart> {
this.service = {
basePath,
staticAssets,
anonymousPaths: this.anonymousPaths.setup({ basePath }),
externalUrl: new ExternalUrlService().setup({ injectedMetadata, location: window.location }),
intercept: fetchService.intercept.bind(fetchService),

View file

@ -0,0 +1,34 @@
/*
* 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 { StaticAssets } from './static_assets';
describe('StaticAssets', () => {
describe('#getPluginAssetHref()', () => {
it('returns the expected value when the base is a path', () => {
const staticAssets = new StaticAssets({ assetsHrefBase: '/base-path' });
expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual(
'/base-path/plugins/foo/assets/path/to/img.gif'
);
});
it('returns the expected value when the base is a full url', () => {
const staticAssets = new StaticAssets({ assetsHrefBase: 'http://cdn/cdn-base-path' });
expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual(
'http://cdn/cdn-base-path/plugins/bar/assets/path/to/img.gif'
);
});
it('removes leading slash from the', () => {
const staticAssets = new StaticAssets({ assetsHrefBase: '/base-path' });
expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg')).toEqual(
'/base-path/plugins/dolly/assets/path/for/something.svg'
);
});
});
});

View file

@ -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
* 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 { InternalStaticAssets } from './types';
export class StaticAssets implements InternalStaticAssets {
public readonly assetsHrefBase: string;
constructor({ assetsHrefBase }: { assetsHrefBase: string }) {
this.assetsHrefBase = assetsHrefBase.endsWith('/')
? assetsHrefBase.slice(0, -1)
: assetsHrefBase;
}
getPluginAssetHref(pluginName: string, assetPath: string): string {
if (assetPath.startsWith('/')) {
assetPath = assetPath.slice(1);
}
return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${assetPath}`;
}
}

View file

@ -0,0 +1,21 @@
/*
* 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 type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
export type InternalHttpSetup = Omit<HttpSetup, 'staticAssets'> & {
staticAssets: InternalStaticAssets;
};
export type InternalHttpStart = Omit<HttpStart, 'staticAssets'> & {
staticAssets: InternalStaticAssets;
};
export interface InternalStaticAssets {
getPluginAssetHref(pluginId: string, assetPath: string): string;
}

View file

@ -11,13 +11,15 @@ import type { IBasePath } from '@kbn/core-http-browser';
const createBasePathMock = ({
publicBaseUrl = '/',
serverBasePath = '/',
}: { publicBaseUrl?: string; serverBasePath?: string } = {}) => {
assetsHrefBase = '/',
}: { publicBaseUrl?: string; serverBasePath?: string; assetsHrefBase?: string } = {}) => {
const mock: jest.Mocked<IBasePath> = {
prepend: jest.fn(),
get: jest.fn(),
remove: jest.fn(),
publicBaseUrl,
serverBasePath,
assetsHrefBase,
};
return mock;

View file

@ -29,7 +29,10 @@ const createServiceMock = ({
patch: jest.fn(),
delete: jest.fn(),
options: jest.fn(),
basePath: new BasePath(basePath, undefined, publicBaseUrl),
basePath: new BasePath({
basePath,
publicBaseUrl,
}),
anonymousPaths: {
register: jest.fn(),
isAnonymous: jest.fn(),
@ -38,6 +41,9 @@ const createServiceMock = ({
isInternalUrl: jest.fn(),
validateUrl: jest.fn(),
},
staticAssets: {
getPluginAssetHref: jest.fn(),
},
addLoadingCountSource: jest.fn(),
getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)),
intercept: jest.fn(),

View file

@ -12,6 +12,7 @@ export type {
IBasePath,
IExternalUrl,
IAnonymousPaths,
IStaticAssets,
HttpHeadersInit,
HttpRequestInit,
HttpFetchQuery,

View file

@ -19,6 +19,12 @@ export interface HttpSetup {
*/
basePath: IBasePath;
/**
* APIs for creating hrefs to static assets.
* See {@link IStaticAssets}
*/
staticAssets: IStaticAssets;
/**
* APIs for denoting certain paths for not requiring authentication
*/
@ -96,6 +102,12 @@ export interface IBasePath {
*/
readonly serverBasePath: string;
/**
* Href (hypertext reference) intended to be used as the base for constructing
* other hrefs to static assets.
*/
readonly assetsHrefBase: string;
/**
* The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the
* {@link IBasePath.serverBasePath}.
@ -105,6 +117,7 @@ export interface IBasePath {
*/
readonly publicBaseUrl?: string;
}
/**
* APIs for working with external URLs.
*
@ -130,6 +143,25 @@ export interface IExternalUrl {
validateUrl(relativeOrAbsoluteUrl: string): URL | null;
}
/**
* APIs for creating hrefs to static assets.
*
* @public
*/
export interface IStaticAssets {
/**
* Gets the full href to the current plugin's asset,
* given its path relative to the plugin's `public/assets` folder.
*
* @example
* ```ts
* // I want to retrieve the href for the asset stored under `my_plugin/public/assets/some_folder/asset.png`:
* const assetHref = core.http.statisAssets.getPluginAssetHref('some_folder/asset.png');
* ```
*/
getPluginAssetHref(assetPath: string): string;
}
/**
* APIs for denoting paths as not requiring authentication
*/
@ -318,10 +350,13 @@ export interface HttpHandler {
path: string,
options: HttpFetchOptions & { asResponse: true }
): Promise<HttpResponse<TResponseBody>>;
<TResponseBody = unknown>(options: HttpFetchOptionsWithPath & { asResponse: true }): Promise<
HttpResponse<TResponseBody>
>;
<TResponseBody = unknown>(path: string, options?: HttpFetchOptions): Promise<TResponseBody>;
<TResponseBody = unknown>(options: HttpFetchOptionsWithPath): Promise<TResponseBody>;
}
@ -368,6 +403,7 @@ export interface HttpInterceptorResponseError extends HttpResponse {
request: Readonly<Request>;
error: Error | IHttpFetchError;
}
/** @public */
export interface HttpInterceptorRequestError {
fetchOptions: Readonly<HttpFetchOptionsWithPath>;
@ -429,6 +465,7 @@ export interface HttpInterceptor {
export interface IHttpInterceptController {
/** Whether or not this chain has been halted. */
halted: boolean;
/** Halt the request Promise chain and do not process further interceptors or response handlers. */
halt(): void;
}

View file

@ -82,6 +82,7 @@ beforeEach(() => {
cors: {
enabled: false,
},
cdn: {},
shutdownTimeout: moment.duration(500, 'ms'),
} as any;

View file

@ -58,7 +58,7 @@ import { AuthStateStorage } from './auth_state_storage';
import { AuthHeadersStorage } from './auth_headers_storage';
import { BasePath } from './base_path_service';
import { getEcsResponseLog } from './logging';
import { StaticAssets, type IStaticAssets } from './static_assets';
import { StaticAssets, type InternalStaticAssets } from './static_assets';
/**
* Adds ELU timings for the executed function to the current's context transaction
@ -136,7 +136,7 @@ export interface HttpServerSetup {
* @note Static assets may be served over CDN
*/
registerStaticDir: (path: string, dirPath: string) => void;
staticAssets: IStaticAssets;
staticAssets: InternalStaticAssets;
basePath: HttpServiceSetup['basePath'];
csp: HttpServiceSetup['csp'];
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];

View file

@ -199,7 +199,7 @@ export class HttpService
// the `plugin` and `legacy` services.
public getStartContract(): InternalHttpServiceStart {
return {
...pick(this.internalSetup!, ['auth', 'basePath', 'getServerInfo']),
...pick(this.internalSetup!, ['auth', 'basePath', 'getServerInfo', 'staticAssets']),
isListening: () => this.httpServer.isListening(),
};
}

View file

@ -14,18 +14,48 @@ describe('StaticAssets', () => {
let basePath: BasePath;
let cdnConfig: CdnConfig;
let staticAssets: StaticAssets;
beforeEach(() => {
basePath = new BasePath('/test');
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
});
it('provides fallsback to server base path', () => {
expect(staticAssets.getHrefBase()).toEqual('/test');
basePath = new BasePath('/base-path');
});
it('provides the correct HREF given a CDN is configured', () => {
cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test');
describe('#getHrefBase()', () => {
it('provides fallback to server base path', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getHrefBase()).toEqual('/base-path');
});
it('provides the correct HREF given a CDN is configured', () => {
cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test');
});
});
describe('#getPluginAssetHref()', () => {
it('returns the expected value when CDN config is not set', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual(
'/base-path/plugins/foo/assets/path/to/img.gif'
);
});
it('returns the expected value when CDN config is set', () => {
cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual(
'https://cdn.example.com/test/plugins/bar/assets/path/to/img.gif'
);
});
it('removes leading slash from the', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg')).toEqual(
'/base-path/plugins/dolly/assets/path/for/something.svg'
);
});
});
});

View file

@ -9,20 +9,31 @@
import type { BasePath } from './base_path_service';
import { CdnConfig } from './cdn';
export interface IStaticAssets {
export interface InternalStaticAssets {
getHrefBase(): string;
getPluginAssetHref(pluginName: string, assetPath: string): string;
}
export class StaticAssets implements IStaticAssets {
constructor(private readonly basePath: BasePath, private readonly cdnConfig: CdnConfig) {}
export class StaticAssets implements InternalStaticAssets {
private readonly assetsHrefBase: string;
constructor(basePath: BasePath, cdnConfig: CdnConfig) {
const hrefToUse = cdnConfig.baseHref ?? basePath.serverBasePath;
this.assetsHrefBase = hrefToUse.endsWith('/') ? hrefToUse.slice(0, -1) : hrefToUse;
}
/**
* Returns a href (hypertext reference) intended to be used as the base for constructing
* other hrefs to static assets.
*/
getHrefBase(): string {
if (this.cdnConfig.baseHref) {
return this.cdnConfig.baseHref;
return this.assetsHrefBase;
}
getPluginAssetHref(pluginName: string, assetPath: string): string {
if (assetPath.startsWith('/')) {
assetPath = assetPath.slice(1);
}
return this.basePath.serverBasePath;
return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${assetPath}`;
}
}

View file

@ -16,8 +16,9 @@ import type {
HttpServiceSetup,
HttpServiceStart,
} from '@kbn/core-http-server';
import { HttpServerSetup } from './http_server';
import { ExternalUrlConfig } from './external_url';
import type { HttpServerSetup } from './http_server';
import type { ExternalUrlConfig } from './external_url';
import type { InternalStaticAssets } from './static_assets';
/** @internal */
export interface InternalHttpServicePreboot
@ -43,10 +44,10 @@ export interface InternalHttpServicePreboot
/** @internal */
export interface InternalHttpServiceSetup
extends Omit<HttpServiceSetup, 'createRouter' | 'registerRouteHandlerContext'> {
extends Omit<HttpServiceSetup, 'createRouter' | 'registerRouteHandlerContext' | 'staticAssets'> {
auth: HttpServerSetup['auth'];
server: HttpServerSetup['server'];
staticAssets: HttpServerSetup['staticAssets'];
staticAssets: InternalStaticAssets;
externalUrl: ExternalUrlConfig;
createRouter: <Context extends RequestHandlerContextBase = RequestHandlerContextBase>(
path: string,
@ -66,7 +67,8 @@ export interface InternalHttpServiceSetup
}
/** @internal */
export interface InternalHttpServiceStart extends HttpServiceStart {
export interface InternalHttpServiceStart extends Omit<HttpServiceStart, 'staticAssets'> {
staticAssets: InternalStaticAssets;
/** Indicates if the http server is listening on the configured port */
isListening: () => boolean;
}

View file

@ -20,6 +20,7 @@ import type {
HttpServicePreboot,
HttpServiceSetup,
HttpServiceStart,
IStaticAssets,
} from '@kbn/core-http-server';
import { AuthStatus } from '@kbn/core-http-server';
import { mockRouter, RouterMock } from '@kbn/core-http-router-server-mocks';
@ -34,17 +35,19 @@ import type {
import { sessionStorageMock } from './cookie_session_storage.mocks';
type BasePathMocked = jest.Mocked<InternalHttpServiceSetup['basePath']>;
type StaticAssetsMocked = jest.Mocked<InternalHttpServiceSetup['staticAssets']>;
type InternalStaticAssetsMocked = jest.Mocked<InternalHttpServiceSetup['staticAssets']>;
type StaticAssetsMocked = jest.Mocked<IStaticAssets>;
type AuthMocked = jest.Mocked<InternalHttpServiceSetup['auth']>;
export type HttpServicePrebootMock = jest.Mocked<HttpServicePreboot>;
export type InternalHttpServicePrebootMock = jest.Mocked<
Omit<InternalHttpServicePreboot, 'basePath' | 'staticAssets'>
> & { basePath: BasePathMocked; staticAssets: StaticAssetsMocked };
> & { basePath: BasePathMocked; staticAssets: InternalStaticAssetsMocked };
export type HttpServiceSetupMock<
ContextType extends RequestHandlerContextBase = RequestHandlerContextBase
> = jest.Mocked<Omit<HttpServiceSetup<ContextType>, 'basePath' | 'createRouter'>> & {
basePath: BasePathMocked;
staticAssets: StaticAssetsMocked;
createRouter: jest.MockedFunction<() => RouterMock>;
};
export type InternalHttpServiceSetupMock = jest.Mocked<
@ -55,15 +58,17 @@ export type InternalHttpServiceSetupMock = jest.Mocked<
> & {
auth: AuthMocked;
basePath: BasePathMocked;
staticAssets: StaticAssetsMocked;
staticAssets: InternalStaticAssetsMocked;
createRouter: jest.MockedFunction<(path: string) => RouterMock>;
authRequestHeaders: jest.Mocked<IAuthHeadersStorage>;
};
export type HttpServiceStartMock = jest.Mocked<HttpServiceStart> & {
basePath: BasePathMocked;
staticAssets: StaticAssetsMocked;
};
export type InternalHttpServiceStartMock = jest.Mocked<InternalHttpServiceStart> & {
basePath: BasePathMocked;
staticAssets: InternalStaticAssetsMocked;
};
const createBasePathMock = (
@ -78,11 +83,12 @@ const createBasePathMock = (
remove: jest.fn(),
});
const createStaticAssetsMock = (
const createInternalStaticAssetsMock = (
basePath: BasePathMocked,
cdnUrl: undefined | string = undefined
): StaticAssetsMocked => ({
): InternalStaticAssetsMocked => ({
getHrefBase: jest.fn(() => cdnUrl ?? basePath.serverBasePath),
getPluginAssetHref: jest.fn().mockReturnValue(cdnUrl ?? basePath.serverBasePath),
});
const createAuthMock = () => {
@ -106,6 +112,7 @@ const createAuthHeaderStorageMock = () => {
interface CreateMockArgs {
cdnUrl?: string;
}
const createInternalPrebootContractMock = (args: CreateMockArgs = {}) => {
const basePath = createBasePathMock();
const mock: InternalHttpServicePrebootMock = {
@ -113,7 +120,7 @@ const createInternalPrebootContractMock = (args: CreateMockArgs = {}) => {
registerRouteHandlerContext: jest.fn(),
registerStaticDir: jest.fn(),
basePath,
staticAssets: createStaticAssetsMock(basePath, args.cdnUrl),
staticAssets: createInternalStaticAssetsMock(basePath, args.cdnUrl),
csp: CspConfig.DEFAULT,
externalUrl: ExternalUrlConfig.DEFAULT,
auth: createAuthMock(),
@ -144,6 +151,7 @@ const createPrebootContractMock = () => {
};
const createInternalSetupContractMock = () => {
const basePath = createBasePathMock();
const mock: InternalHttpServiceSetupMock = {
// we can mock other hapi server methods when we need it
server: {
@ -164,9 +172,9 @@ const createInternalSetupContractMock = () => {
registerOnPreResponse: jest.fn(),
createRouter: jest.fn().mockImplementation(() => mockRouter.create({})),
registerStaticDir: jest.fn(),
basePath: createBasePathMock(),
basePath,
csp: CspConfig.DEFAULT,
staticAssets: { getHrefBase: jest.fn(() => mock.basePath.serverBasePath) },
staticAssets: createInternalStaticAssetsMock(basePath),
externalUrl: ExternalUrlConfig.DEFAULT,
auth: createAuthMock(),
authRequestHeaders: createAuthHeaderStorageMock(),
@ -202,6 +210,9 @@ const createSetupContractMock = <
createRouter: jest.fn(),
registerRouteHandlerContext: jest.fn(),
getServerInfo: internalMock.getServerInfo,
staticAssets: {
getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath),
},
};
mock.createRouter.mockImplementation(() => internalMock.createRouter(''));
@ -214,14 +225,19 @@ const createStartContractMock = () => {
auth: createAuthMock(),
basePath: createBasePathMock(),
getServerInfo: jest.fn(),
staticAssets: {
getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath),
},
};
return mock;
};
const createInternalStartContractMock = () => {
const basePath = createBasePathMock();
const mock: InternalHttpServiceStartMock = {
...createStartContractMock(),
staticAssets: createInternalStaticAssetsMock(basePath),
isListening: jest.fn(),
};

View file

@ -142,3 +142,5 @@ export type {
VersionedRouteRegistrar,
VersionedRouter,
} from './src/versioning';
export type { IStaticAssets } from './src/static_assets';

View file

@ -20,6 +20,7 @@ import type {
OnPreRoutingHandler,
} from './lifecycle';
import type { IBasePath } from './base_path';
import type { IStaticAssets } from './static_assets';
import type { ICspConfig } from './csp';
import type { GetAuthState, IsAuthenticated } from './auth_state';
import type { SessionStorageCookieOptions, SessionStorageFactory } from './session_storage';
@ -287,6 +288,12 @@ export interface HttpServiceSetup<
*/
basePath: IBasePath;
/**
* APIs for creating hrefs to static assets.
* See {@link IStaticAssets}
*/
staticAssets: IStaticAssets;
/**
* The CSP config used for Kibana.
*/
@ -361,6 +368,12 @@ export interface HttpServiceStart {
*/
basePath: IBasePath;
/**
* APIs for creating hrefs to static assets.
* See {@link IStaticAssets}
*/
staticAssets: IStaticAssets;
/**
* Auth status.
* See {@link HttpAuth}

View file

@ -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
* 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.
*/
/**
* APIs for creating hrefs to static assets.
*
* @public
*/
export interface IStaticAssets {
/**
* Gets the full href to the current plugin's asset,
* given its path relative to the plugin's `public/assets` folder.
*
* @example
* ```ts
* // I want to retrieve the href for the asset stored under `my_plugin/public/assets/some_folder/asset.png`:
* const assetHref = core.http.statisAssets.getPluginAssetHref('some_folder/asset.png');
* ```
*/
getPluginAssetHref(assetPath: string): string;
}

View file

@ -48,6 +48,10 @@ export class InjectedMetadataService {
return this.state.publicBaseUrl;
},
getAssetsHrefBase: () => {
return this.state.assetsHrefBase;
},
getAnonymousStatusPage: () => {
return this.state.anonymousStatusPage;
},

View file

@ -29,6 +29,7 @@ export interface InternalInjectedMetadataSetup {
getBasePath: () => string;
getServerBasePath: () => string;
getPublicBaseUrl: () => string | undefined;
getAssetsHrefBase: () => string;
getKibanaBuildNumber: () => number;
getKibanaBranch: () => string;
getKibanaVersion: () => string;

View file

@ -16,6 +16,7 @@ const createSetupContractMock = () => {
const setupContract: jest.Mocked<InternalInjectedMetadataSetup> = {
getBasePath: jest.fn(),
getServerBasePath: jest.fn(),
getAssetsHrefBase: jest.fn(),
getPublicBaseUrl: jest.fn(),
getKibanaVersion: jest.fn(),
getKibanaBranch: jest.fn(),
@ -31,6 +32,9 @@ const createSetupContractMock = () => {
getKibanaBuildNumber: jest.fn(),
getCustomBranding: jest.fn(),
};
setupContract.getBasePath.mockReturnValue('/base-path');
setupContract.getServerBasePath.mockReturnValue('/server-base-path');
setupContract.getAssetsHrefBase.mockReturnValue('/assets-base-path');
setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
setupContract.getExternalUrlConfig.mockReturnValue({ policy: [] });
setupContract.getKibanaVersion.mockReturnValue('kibanaVersion');

View file

@ -42,6 +42,7 @@ export interface InjectedMetadata {
basePath: string;
serverBasePath: string;
publicBaseUrl?: string;
assetsHrefBase: string;
clusterInfo: InjectedMetadataClusterInfo;
env: {
mode: EnvironmentMode;

View file

@ -9,10 +9,12 @@
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { InternalApplicationSetup } from '@kbn/core-application-browser-internal';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
/** @internal */
export interface InternalCoreSetup
extends Omit<CoreSetup, 'application' | 'plugins' | 'getStartServices'> {
extends Omit<CoreSetup, 'application' | 'plugins' | 'getStartServices' | 'http'> {
application: InternalApplicationSetup;
injectedMetadata: InternalInjectedMetadataSetup;
http: InternalHttpSetup;
}

View file

@ -9,9 +9,11 @@
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
/** @internal */
export interface InternalCoreStart extends Omit<CoreStart, 'application' | 'plugins'> {
export interface InternalCoreStart extends Omit<CoreStart, 'application' | 'plugins' | 'http'> {
application: InternalApplicationStart;
injectedMetadata: InternalInjectedMetadataStart;
http: InternalHttpStart;
}

View file

@ -14,7 +14,8 @@
"kbn_references": [
"@kbn/core-lifecycle-browser",
"@kbn/core-application-browser-internal",
"@kbn/core-injected-metadata-browser-internal"
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-http-browser-internal"
],
"exclude": [
"target/**/*",

View file

@ -82,7 +82,13 @@ export function createPluginSetupContext<
customBranding: deps.customBranding,
fatalErrors: deps.fatalErrors,
executionContext: deps.executionContext,
http: deps.http,
http: {
...deps.http,
staticAssets: {
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},
},
notifications: deps.notifications,
uiSettings: deps.uiSettings,
settings: deps.settings,
@ -133,7 +139,13 @@ export function createPluginStartContext<
customBranding: deps.customBranding,
docLinks: deps.docLinks,
executionContext: deps.executionContext,
http: deps.http,
http: {
...deps.http,
staticAssets: {
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},
},
chrome: omit(deps.chrome, 'getComponent'),
i18n: deps.i18n,
notifications: deps.notifications,

View file

@ -104,6 +104,10 @@ describe('PluginsService', () => {
application: expect.any(Object),
plugins: expect.any(Object),
getStartServices: expect.any(Function),
http: {
...mockSetupDeps.http,
staticAssets: expect.any(Object),
},
};
// @ts-expect-error this file was not being type checked properly in the past, error is legit
mockStartDeps = {
@ -128,6 +132,10 @@ describe('PluginsService', () => {
application: expect.any(Object),
plugins: expect.any(Object),
chrome: omit(mockStartDeps.chrome, 'getComponent'),
http: {
...mockStartDeps.http,
staticAssets: expect.any(Object),
},
};
// Reset these for each test.

View file

@ -235,6 +235,10 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
registerOnPostAuth: deps.http.registerOnPostAuth,
registerOnPreResponse: deps.http.registerOnPreResponse,
basePath: deps.http.basePath,
staticAssets: {
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},
csp: deps.http.csp,
getServerInfo: deps.http.getServerInfo,
},
@ -324,6 +328,10 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>({
auth: deps.http.auth,
basePath: deps.http.basePath,
getServerInfo: deps.http.getServerInfo,
staticAssets: {
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},
},
savedObjects: {
getScopedClient: deps.savedObjects.getScopedClient,

View file

@ -3,6 +3,7 @@
exports[`RenderingService preboot() render() renders "core" CDN url injected 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "http://foo.bar:1773",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -70,6 +71,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" page 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -133,6 +135,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" page driven by settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -200,6 +203,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" page for blank basepath 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -263,6 +267,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" page for unauthenticated requests 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -326,6 +331,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" page with global settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -393,6 +399,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" with excluded global user settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -456,6 +463,7 @@ Object {
exports[`RenderingService preboot() render() renders "core" with excluded user settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -519,6 +527,7 @@ Object {
exports[`RenderingService setup() render() renders "core" CDN url injected 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -591,6 +600,7 @@ Object {
exports[`RenderingService setup() render() renders "core" page 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -654,6 +664,7 @@ Object {
exports[`RenderingService setup() render() renders "core" page driven by settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -726,6 +737,7 @@ Object {
exports[`RenderingService setup() render() renders "core" page for blank basepath 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -794,6 +806,7 @@ Object {
exports[`RenderingService setup() render() renders "core" page for unauthenticated requests 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -857,6 +870,7 @@ Object {
exports[`RenderingService setup() render() renders "core" page with global settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -929,6 +943,7 @@ Object {
exports[`RenderingService setup() render() renders "core" with excluded global user settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
@ -997,6 +1012,7 @@ Object {
exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = `
Object {
"anonymousStatusPage": false,
"assetsHrefBase": "/mock-server-basepath",
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,

View file

@ -209,6 +209,7 @@ export class RenderingService {
basePath,
serverBasePath,
publicBaseUrl,
assetsHrefBase: staticAssetsHrefBase,
env,
clusterInfo,
anonymousStatusPage: status?.isStatusPageAnonymous() ?? false,

View file

@ -7,7 +7,8 @@
*/
import { pick, throttle, cloneDeep } from 'lodash';
import type { HttpSetup, HttpFetchOptions } from '@kbn/core-http-browser';
import type { HttpFetchOptions } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { SavedObject, SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
import type {
SavedObjectsBulkResolveResponse as SavedObjectsBulkResolveResponseServer,
@ -106,7 +107,7 @@ const getObjectsToResolve = (queue: BatchResolveQueueEntry[]) => {
* @deprecated See https://github.com/elastic/kibana/issues/149098
*/
export class SavedObjectsClient implements SavedObjectsClientContract {
private http: HttpSetup;
private http: InternalHttpSetup;
private batchGetQueue: BatchGetQueueEntry[];
private batchResolveQueue: BatchResolveQueueEntry[];
@ -180,7 +181,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
);
/** @internal */
constructor(http: HttpSetup) {
constructor(http: InternalHttpSetup) {
this.http = http;
this.batchGetQueue = [];
this.batchResolveQueue = [];

View file

@ -7,7 +7,7 @@
*/
import type { CoreService } from '@kbn/core-base-browser-internal';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { SavedObjectsStart } from '@kbn/core-saved-objects-browser';
import { SavedObjectsClient } from './saved_objects_client';
@ -17,7 +17,7 @@ import { SavedObjectsClient } from './saved_objects_client';
export class SavedObjectsService implements CoreService<void, SavedObjectsStart> {
public async setup() {}
public async start({ http }: { http: HttpStart }): Promise<SavedObjectsStart> {
public async start({ http }: { http: InternalHttpStart }): Promise<SavedObjectsStart> {
return { client: new SavedObjectsClient(http) };
}

View file

@ -19,6 +19,7 @@
"@kbn/core-saved-objects-api-server",
"@kbn/core-saved-objects-api-browser",
"@kbn/core-http-browser-mocks",
"@kbn/core-http-browser-internal",
],
"exclude": [
"target/**/*",

View file

@ -9,14 +9,14 @@
import { Subject } from 'rxjs';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { SettingsStart, SettingsSetup } from '@kbn/core-ui-settings-browser';
import { UiSettingsApi } from './ui_settings_api';
import { UiSettingsClient } from './ui_settings_client';
import { UiSettingsGlobalClient } from './ui_settings_global_client';
export interface SettingsServiceDeps {
http: HttpSetup;
http: InternalHttpSetup;
injectedMetadata: InternalInjectedMetadataSetup;
}

View file

@ -7,7 +7,7 @@
*/
import { BehaviorSubject } from 'rxjs';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { UiSettingsState } from '@kbn/core-ui-settings-browser';
import { UiSettingsScope } from '@kbn/core-ui-settings-common';
@ -37,7 +37,7 @@ export class UiSettingsApi {
private readonly loadingCount$ = new BehaviorSubject(0);
constructor(private readonly http: HttpSetup) {}
constructor(private readonly http: InternalHttpSetup) {}
/**
* Adds a key+value that will be sent to the server ASAP. If a request is

View file

@ -9,14 +9,14 @@
import { Subject } from 'rxjs';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { UiSettingsApi } from './ui_settings_api';
import { UiSettingsClient } from './ui_settings_client';
export interface UiSettingsServiceDeps {
http: HttpSetup;
http: InternalHttpSetup;
injectedMetadata: InternalInjectedMetadataSetup;
}

View file

@ -12,12 +12,12 @@
],
"kbn_references": [
"@kbn/core-test-helpers-http-setup-browser",
"@kbn/core-http-browser",
"@kbn/core-ui-settings-browser",
"@kbn/core-ui-settings-common",
"@kbn/core-http-browser-mocks",
"@kbn/core-injected-metadata-browser-mocks",
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-http-browser-internal",
],
"exclude": [
"target/**/*",

View file

@ -6,11 +6,10 @@
* Side Public License, v 1.
*/
import React, { useMemo } from 'react';
import React from 'react';
import { useEuiTheme } from '@elastic/eui';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useServices } from './services';
import { SampleDataCard as Component, Props as ComponentProps } from './sample_data_card.component';
/**
@ -27,12 +26,10 @@ export interface Props extends Pick<ComponentProps, 'onStatusChange'> {
* function.
*/
export const SampleDataCard = ({ sampleDataSet, onStatusChange }: Props) => {
const { addBasePath } = useServices();
const { colorMode } = useEuiTheme();
const { darkPreviewImagePath, previewImagePath } = sampleDataSet;
const path =
const imagePath =
colorMode === 'DARK' && darkPreviewImagePath ? darkPreviewImagePath : previewImagePath;
const imagePath = useMemo(() => addBasePath(path), [addBasePath, path]);
return <Component {...{ sampleDataSet, imagePath, onStatusChange }} />;
};

View file

@ -187,6 +187,7 @@ export type {
HttpResponse,
HttpHandler,
IBasePath,
IStaticAssets,
IAnonymousPaths,
IExternalUrl,
IHttpInterceptController,

View file

@ -184,6 +184,7 @@ export type {
ICspConfig,
IExternalUrlConfig,
IBasePath,
IStaticAssets,
SessionStorage,
SessionStorageCookieOptions,
SessionCookieValidationResult,

View file

@ -36,6 +36,7 @@ describe('Http server', () => {
allowFromAnyIp: true,
ipAllowlist: [],
},
cdn: {},
cors: {
enabled: false,
},

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { HttpSetup } from '@kbn/core/public';
import { http } from './data_views_api_client.test.mock';
import { DataViewsApiClient } from './data_views_api_client';
import { FIELDS_FOR_WILDCARD_PATH as expectedPath } from '../../common/constants';
@ -16,7 +17,7 @@ describe('IndexPatternsApiClient', () => {
beforeEach(() => {
fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => Promise.resolve({}));
indexPatternsApiClient = new DataViewsApiClient(http);
indexPatternsApiClient = new DataViewsApiClient(http as HttpSetup);
});
test('uses the right URI to fetch fields for wildcard', async function () {

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetProvider } from '../../lib/sample_dataset_registry_types';
const ecommerceName = i18n.translate('home.sampleData.ecommerceSpecTitle', {
defaultMessage: 'Sample eCommerce orders',
@ -19,14 +19,17 @@ const ecommerceDescription = i18n.translate('home.sampleData.ecommerceSpecDescri
defaultMessage: 'Sample data, visualizations, and dashboards for tracking eCommerce orders.',
});
export const ecommerceSpecProvider = function (): SampleDatasetSchema {
export const ecommerceSpecProvider: SampleDatasetProvider = ({ staticAssets }) => {
return {
id: 'ecommerce',
name: ecommerceName,
description: ecommerceDescription,
previewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard.webp',
darkPreviewImagePath:
'/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.webp',
previewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/ecommerce/dashboard.webp'
),
darkPreviewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/ecommerce/dashboard_dark.webp'
),
overviewDashboard: '722b74f0-b882-11e8-a6d9-e546fe2bba5f',
defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
savedObjects: getSavedObjects(),
@ -41,6 +44,6 @@ export const ecommerceSpecProvider = function (): SampleDatasetSchema {
},
],
status: 'not_installed',
iconPath: '/plugins/home/assets/sample_data_resources/ecommerce/icon.svg',
iconPath: staticAssets.getPluginAssetHref('/sample_data_resources/ecommerce/icon.svg'),
};
};

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetProvider } from '../../lib/sample_dataset_registry_types';
const flightsName = i18n.translate('home.sampleData.flightsSpecTitle', {
defaultMessage: 'Sample flight data',
@ -19,13 +19,17 @@ const flightsDescription = i18n.translate('home.sampleData.flightsSpecDescriptio
defaultMessage: 'Sample data, visualizations, and dashboards for monitoring flight routes.',
});
export const flightsSpecProvider = function (): SampleDatasetSchema {
export const flightsSpecProvider: SampleDatasetProvider = ({ staticAssets }) => {
return {
id: 'flights',
name: flightsName,
description: flightsDescription,
previewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard.webp',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.webp',
previewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/flights/dashboard.webp'
),
darkPreviewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/flights/dashboard_dark.webp'
),
overviewDashboard: '7adfa750-4c81-11e8-b3d7-01146121b73d',
defaultIndex: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
savedObjects: getSavedObjects(),
@ -40,6 +44,6 @@ export const flightsSpecProvider = function (): SampleDatasetSchema {
},
],
status: 'not_installed',
iconPath: '/plugins/home/assets/sample_data_resources/flights/icon.svg',
iconPath: staticAssets.getPluginAssetHref('/sample_data_resources/flights/icon.svg'),
};
};

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetProvider } from '../../lib/sample_dataset_registry_types';
const logsName = i18n.translate('home.sampleData.logsSpecTitle', {
defaultMessage: 'Sample web logs',
@ -20,13 +20,15 @@ const logsDescription = i18n.translate('home.sampleData.logsSpecDescription', {
});
export const GLOBE_ICON_PATH = '/plugins/home/assets/sample_data_resources/logs/icon.svg';
export const logsSpecProvider = function (): SampleDatasetSchema {
export const logsSpecProvider: SampleDatasetProvider = ({ staticAssets }) => {
return {
id: 'logs',
name: logsName,
description: logsDescription,
previewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard.webp',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.webp',
previewImagePath: staticAssets.getPluginAssetHref('/sample_data_resources/logs/dashboard.webp'),
darkPreviewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/logs/dashboard_dark.webp'
),
overviewDashboard: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b',
defaultIndex: '90943e30-9a47-11e8-b64d-95841ca0b247',
savedObjects: getSavedObjects(),
@ -42,6 +44,6 @@ export const logsSpecProvider = function (): SampleDatasetSchema {
},
],
status: 'not_installed',
iconPath: GLOBE_ICON_PATH,
iconPath: staticAssets.getPluginAssetHref('/sample_data_resources/logs/icon.svg'),
};
};

View file

@ -10,7 +10,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import { getSavedObjects } from './saved_objects';
import { fieldMappings } from './field_mappings';
import { SampleDatasetSchema } from '../../lib/sample_dataset_registry_types';
import { SampleDatasetProvider } from '../../lib/sample_dataset_registry_types';
const logsName = i18n.translate('home.sampleData.logsTsdbSpecTitle', {
defaultMessage: 'Sample web logs (TSDB)',
@ -20,7 +20,7 @@ const logsDescription = i18n.translate('home.sampleData.logsTsdbSpecDescription'
});
export const GLOBE_ICON_PATH = '/plugins/home/assets/sample_data_resources/logs/icon.svg';
export const logsTSDBSpecProvider = function (): SampleDatasetSchema {
export const logsTSDBSpecProvider: SampleDatasetProvider = ({ staticAssets }) => {
const startDate = new Date();
const endDate = new Date();
startDate.setMonth(startDate.getMonth() - 1);
@ -29,8 +29,10 @@ export const logsTSDBSpecProvider = function (): SampleDatasetSchema {
id: 'logstsdb',
name: logsName,
description: logsDescription,
previewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard.webp',
darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.webp',
previewImagePath: staticAssets.getPluginAssetHref('/sample_data_resources/logs/dashboard.webp'),
darkPreviewImagePath: staticAssets.getPluginAssetHref(
'/sample_data_resources/logs/dashboard_dark.webp'
),
overviewDashboard: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef8f5b',
defaultIndex: '90943e30-9a47-11e8-b64d-95841ca0c247',
savedObjects: getSavedObjects(),
@ -53,6 +55,6 @@ export const logsTSDBSpecProvider = function (): SampleDatasetSchema {
},
],
status: 'not_installed',
iconPath: GLOBE_ICON_PATH,
iconPath: staticAssets.getPluginAssetHref('/sample_data_resources/logs/icon.svg'),
};
};

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { IStaticAssets } from '@kbn/core/server';
import type { SampleDatasetSchema } from './sample_dataset_schema';
export type { SampleDatasetSchema, DataIndexSchema } from './sample_dataset_schema';
@ -27,7 +28,11 @@ export enum EmbeddableTypes {
SEARCH_EMBEDDABLE_TYPE = 'search',
VISUALIZE_EMBEDDABLE_TYPE = 'visualization',
}
export type SampleDatasetProvider = () => SampleDatasetSchema;
export interface SampleDatasetProviderContext {
staticAssets: IStaticAssets;
}
export type SampleDatasetProvider = (context: SampleDatasetProviderContext) => SampleDatasetSchema;
/** This type is used to identify an object in a sample dataset. */
export interface SampleObject {

View file

@ -15,6 +15,7 @@ import {
SampleDatasetSchema,
SampleDatasetDashboardPanel,
AppLinkData,
SampleDatasetProviderContext,
} from './lib/sample_dataset_registry_types';
import { sampleDataSchema } from './lib/sample_dataset_schema';
@ -34,11 +35,15 @@ export class SampleDataRegistry {
private readonly sampleDatasets: SampleDatasetSchema[] = [];
private readonly appLinksMap = new Map<string, AppLinkData[]>();
private sampleDataProviderContext?: SampleDatasetProviderContext;
private registerSampleDataSet(specProvider: SampleDatasetProvider) {
if (!this.sampleDataProviderContext) {
throw new Error('#registerSampleDataSet called before #setup');
}
let value: SampleDatasetSchema;
try {
value = sampleDataSchema.validate(specProvider());
value = sampleDataSchema.validate(specProvider(this.sampleDataProviderContext));
} catch (error) {
throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`);
}
@ -83,6 +88,10 @@ export class SampleDataRegistry {
createInstallRoute(router, this.sampleDatasets, logger, usageTracker, core.analytics);
createUninstallRoute(router, this.sampleDatasets, logger, usageTracker, core.analytics);
this.sampleDataProviderContext = {
staticAssets: core.http.staticAssets,
};
this.registerSampleDataSet(flightsSpecProvider);
this.registerSampleDataSet(logsSpecProvider);
this.registerSampleDataSet(ecommerceSpecProvider);

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { KibanaRequest } from '@kbn/core/server';
import type { KibanaRequest, IStaticAssets } from '@kbn/core/server';
import type { TutorialSchema } from './tutorial_schema';
export { TutorialsCategory } from '../../../../common/constants';
@ -25,6 +25,7 @@ export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM';
export interface TutorialContext {
kibanaBranch: string;
staticAssets: IStaticAssets;
[key: string]: unknown;
}
export type TutorialProvider = (context: TutorialContext) => TutorialSchema;

View file

@ -166,10 +166,9 @@ describe('TutorialsRegistry', () => {
describe('start', () => {
test('exposes proper contract', () => {
const start = new TutorialsRegistry(mockInitContext).start(
coreMock.createStart(),
mockCustomIntegrationsPluginSetup
);
const registry = new TutorialsRegistry(mockInitContext);
registry.setup(mockCoreSetup, mockCustomIntegrationsPluginSetup);
const start = registry.start(coreMock.createStart(), mockCustomIntegrationsPluginSetup);
expect(start).toBeDefined();
});
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import { CoreSetup, CoreStart, PluginInitializerContext, IStaticAssets } from '@kbn/core/server';
import { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server';
import { IntegrationCategory } from '@kbn/custom-integrations-plugin/common';
import {
@ -71,10 +71,13 @@ function registerBeatsTutorialsWithCustomIntegrations(
export class TutorialsRegistry {
private tutorialProviders: TutorialProvider[] = []; // pre-register all the tutorials we know we want in here
private readonly scopedTutorialContextFactories: TutorialContextFactory[] = [];
private staticAssets!: IStaticAssets;
constructor(private readonly initContext: PluginInitializerContext) {}
public setup(core: CoreSetup, customIntegrations?: CustomIntegrationsPluginSetup) {
this.staticAssets = core.http.staticAssets;
const router = core.http.createRouter();
router.get(
{ path: '/api/kibana/home/tutorials', validate: false },
@ -143,7 +146,10 @@ export class TutorialsRegistry {
}
private get baseTutorialContext(): TutorialContext {
return { kibanaBranch: this.initContext.env.packageInfo.branch };
return {
kibanaBranch: this.initContext.env.packageInfo.branch,
staticAssets: this.staticAssets,
};
}
}

View file

@ -38,7 +38,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-activemq.html',
},
}),
euiIconType: '/plugins/home/assets/logos/activemq.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/activemq.svg'),
artifacts: {
dashboards: [
{
@ -54,7 +54,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/activemq_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/activemq_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -38,7 +38,7 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html',
},
}),
euiIconType: '/plugins/home/assets/logos/activemq.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/activemq.svg'),
isBeta: true,
artifacts: {
application: {

View file

@ -55,7 +55,7 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/apache_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/apache_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -54,7 +54,7 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/apache_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/apache_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -54,7 +54,7 @@ processes, users, logins, sockets information, file accesses, and more. \
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/auditbeat/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/auditbeat/screenshot.webp'),
onPrem: onPremInstructions(platforms, context),
elasticCloud: cloudInstructions(platforms, context),
onPremElasticCloud: onPremCloudInstructions(platforms, context),

View file

@ -39,7 +39,7 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-auditd.html',
},
}),
euiIconType: '/plugins/home/assets/logos/linux.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/linux.svg'),
artifacts: {
dashboards: [
{
@ -55,7 +55,7 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/auditd_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/auditd_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -55,7 +55,7 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema {
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/aws_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/aws_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -56,7 +56,7 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/aws_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/aws_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -56,7 +56,7 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/azure_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/azure_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -55,7 +55,7 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/azure_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/azure_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -40,7 +40,7 @@ export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSch
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-barracuda.html',
},
}),
euiIconType: '/plugins/home/assets/logos/barracuda.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/barracuda.svg'),
artifacts: {
dashboards: [],
application: {

View file

@ -39,7 +39,7 @@ export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSc
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-checkpoint.html',
},
}),
euiIconType: '/plugins/home/assets/logos/checkpoint.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/checkpoint.svg'),
artifacts: {
dashboards: [],
application: {

View file

@ -39,7 +39,7 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cisco.html',
},
}),
euiIconType: '/plugins/home/assets/logos/cisco.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/cisco.svg'),
artifacts: {
dashboards: [
{
@ -55,7 +55,7 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/cisco_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/cisco_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -38,7 +38,7 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-cockroachdb.html',
},
}),
euiIconType: '/plugins/home/assets/logos/cockroachdb.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/cockroachdb.svg'),
artifacts: {
dashboards: [
{
@ -57,7 +57,9 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/cockroachdb_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref(
'/cockroachdb_metrics/screenshot.webp'
),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -38,7 +38,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-consul.html',
},
}),
euiIconType: '/plugins/home/assets/logos/consul.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/consul.svg'),
artifacts: {
dashboards: [
{
@ -54,7 +54,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/consul_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/consul_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -39,7 +39,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-coredns.html',
},
}),
euiIconType: '/plugins/home/assets/logos/coredns.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/coredns.svg'),
artifacts: {
dashboards: [
{
@ -55,7 +55,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/coredns_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -38,7 +38,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-coredns.html',
},
}),
euiIconType: '/plugins/home/assets/logos/coredns.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/coredns.svg'),
artifacts: {
application: {
label: i18n.translate('home.tutorials.corednsMetrics.artifacts.application.label', {
@ -52,7 +52,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/coredns_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/coredns_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -38,7 +38,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchdb.html',
},
}),
euiIconType: '/plugins/home/assets/logos/couchdb.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/couchdb.svg'),
artifacts: {
dashboards: [
{
@ -57,7 +57,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/couchdb_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/couchdb_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -43,7 +43,7 @@ export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialS
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-crowdstrike.html',
},
}),
euiIconType: '/plugins/home/assets/logos/crowdstrike.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/crowdstrike.svg'),
artifacts: {
dashboards: [],
application: {

View file

@ -39,7 +39,7 @@ export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchem
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cylance.html',
},
}),
euiIconType: '/plugins/home/assets/logos/cylance.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/cylance.svg'),
artifacts: {
dashboards: [],
application: {

View file

@ -54,7 +54,7 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/docker_metrics/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/docker_metrics/screenshot.webp'),
onPrem: onPremInstructions(moduleName, context),
elasticCloud: cloudInstructions(moduleName, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, context),

View file

@ -54,7 +54,9 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/elasticsearch_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref(
'/elasticsearch_logs/screenshot.webp'
),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -39,7 +39,7 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-envoyproxy.html',
},
}),
euiIconType: '/plugins/home/assets/logos/envoyproxy.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/envoyproxy.svg'),
artifacts: {
dashboards: [
{
@ -58,7 +58,7 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/envoyproxy_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/envoyproxy_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

View file

@ -38,7 +38,7 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria
learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-envoyproxy.html',
},
}),
euiIconType: '/plugins/home/assets/logos/envoyproxy.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/envoyproxy.svg'),
artifacts: {
dashboards: [],
exportedFields: {

View file

@ -39,7 +39,7 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema {
learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-f5.html',
},
}),
euiIconType: '/plugins/home/assets/logos/f5.svg',
euiIconType: context.staticAssets.getPluginAssetHref('/logos/f5.svg'),
artifacts: {
dashboards: [],
application: {
@ -53,7 +53,7 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema {
},
},
completionTimeMinutes: 10,
previewImagePath: '/plugins/home/assets/f5_logs/screenshot.webp',
previewImagePath: context.staticAssets.getPluginAssetHref('/f5_logs/screenshot.webp'),
onPrem: onPremInstructions(moduleName, platforms, context),
elasticCloud: cloudInstructions(moduleName, platforms, context),
onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context),

Some files were not shown because too many files have changed in this diff Show more