[ML] Use Kibana's HttpHandler for HTTP requests (#59320)

* [ML] use kibana http

* [ML] fromHttpHandler

* [ML] remove __LEGACY, update asSystemRequest header

* [ML] transform with NP http
This commit is contained in:
Dima Arnautov 2020-03-06 11:34:19 +01:00 committed by GitHub
parent 2817d6e3a8
commit 3a53fe8e45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 107 additions and 95 deletions

View file

@ -25,9 +25,6 @@ export interface MlDependencies extends AppMountParameters {
data: DataPublicPluginStart;
security: SecurityPluginSetup;
licensing: LicensingPluginSetup;
__LEGACY: {
XSRF: string;
};
}
interface AppProps {
@ -49,7 +46,6 @@ const App: FC<AppProps> = ({ coreStart, deps }) => {
recentlyAccessed: coreStart.chrome!.recentlyAccessed,
basePath: coreStart.http.basePath,
savedObjectsClient: coreStart.savedObjects.client,
XSRF: deps.__LEGACY.XSRF,
application: coreStart.application,
http: coreStart.http,
security: deps.security,

View file

@ -39,8 +39,7 @@ function randomNumber(min, max) {
}
function saveWatch(watchModel) {
const basePath = getBasePath();
const path = basePath.prepend('/api/watcher');
const path = '/api/watcher';
const url = `${path}/watch/${watchModel.id}`;
return http({
@ -188,8 +187,7 @@ class CreateWatchService {
loadWatch(jobId) {
const id = `ml-${jobId}`;
const basePath = getBasePath();
const path = basePath.prepend('/api/watcher');
const path = '/api/watcher';
const url = `${path}/watch/${id}`;
return http({
url,

View file

@ -54,7 +54,6 @@ function initManagementSection() {
setDependencyCache({
docLinks: legacyDocLinks as any,
basePath: legacyBasePath as any,
XSRF: chrome.getXsrfToken(),
});
management.register('ml', {

View file

@ -4,68 +4,66 @@
* you may not use this file except in compliance with the Elastic License.
*/
// service for interacting with the server
import { Observable } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
import { from, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { getXSRF } from '../util/dependency_cache';
export interface HttpOptions {
url?: string;
}
import { getHttp } from '../util/dependency_cache';
function getResultHeaders(headers: HeadersInit): HeadersInit {
return {
asSystemRequest: false,
asSystemRequest: true,
'Content-Type': 'application/json',
'kbn-version': getXSRF(),
...headers,
} as HeadersInit;
}
export function http(options: any) {
return new Promise((resolve, reject) => {
if (options && options.url) {
let url = '';
url = url + (options.url || '');
const headers = getResultHeaders(options.headers ?? {});
interface HttpOptions {
url: string;
method: string;
headers?: any;
data?: any;
}
const allHeaders =
options.headers === undefined ? headers : { ...options.headers, ...headers };
const body = options.data === undefined ? null : JSON.stringify(options.data);
/**
* Function for making HTTP requests to Kibana's backend.
* Wrapper for Kibana's HttpHandler.
*/
export async function http(options: HttpOptions) {
if (!options?.url) {
throw new Error('URL is missing');
}
const payload: RequestInit = {
method: options.method || 'GET',
headers: allHeaders,
credentials: 'same-origin',
};
try {
let url = '';
url = url + (options.url || '');
const headers = getResultHeaders(options.headers ?? {});
if (body !== null) {
payload.body = body;
}
const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers };
const body = options.data === undefined ? null : JSON.stringify(options.data);
fetch(url, payload)
.then(resp => {
resp
.json()
.then(resp.ok === true ? resolve : reject)
.catch(resp.ok === true ? resolve : reject);
})
.catch(resp => {
reject(resp);
});
} else {
reject();
const payload: RequestInit = {
method: options.method || 'GET',
headers: allHeaders,
credentials: 'same-origin',
};
if (body !== null) {
payload.body = body;
}
});
return await getHttp().fetch(url, payload);
} catch (e) {
throw new Error(e);
}
}
interface RequestOptions extends RequestInit {
body: BodyInit | any;
}
/**
* Function for making HTTP requests to Kibana's backend which returns an Observable
* with request cancellation support.
*/
export function http$<T>(url: string, options: RequestOptions): Observable<T> {
const requestInit: RequestInit = {
...options,
@ -75,13 +73,56 @@ export function http$<T>(url: string, options: RequestOptions): Observable<T> {
headers: getResultHeaders(options.headers ?? {}),
};
return fromFetch(url, requestInit).pipe(
switchMap(response => {
if (response.ok) {
return from(response.json() as Promise<T>);
} else {
throw new Error(String(response.status));
}
})
);
return fromHttpHandler<T>(url, requestInit);
}
/**
* Creates an Observable from Kibana's HttpHandler.
*/
export function fromHttpHandler<T>(input: string, init?: RequestInit): Observable<T> {
return new Observable<T>(subscriber => {
const controller = new AbortController();
const signal = controller.signal;
let abortable = true;
let unsubscribed = false;
if (init?.signal) {
if (init.signal.aborted) {
controller.abort();
} else {
init.signal.addEventListener('abort', () => {
if (!signal.aborted) {
controller.abort();
}
});
}
}
const perSubscriberInit: RequestInit = {
...(init ? init : {}),
signal,
};
getHttp()
.fetch<T>(input, perSubscriberInit)
.then(response => {
abortable = false;
subscriber.next(response);
subscriber.complete();
})
.catch(err => {
abortable = false;
if (!unsubscribed) {
subscriber.error(err);
}
});
return () => {
unsubscribed = true;
if (abortable) {
controller.abort();
}
};
});
}

View file

@ -13,10 +13,9 @@ import { filters } from './filters';
import { results } from './results';
import { jobs } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { getBasePath } from '../../util/dependency_cache';
export function basePath() {
return getBasePath().prepend('/api/ml');
return '/api/ml';
}
export const ml = {
@ -452,7 +451,7 @@ export const ml = {
},
getIndices() {
const tempBasePath = getBasePath().prepend('/api');
const tempBasePath = '/api';
return http({
url: `${tempBasePath}/index_management/indices`,
method: 'GET',

View file

@ -35,7 +35,6 @@ export interface DependencyCache {
autocomplete: DataPublicPluginStart['autocomplete'] | null;
basePath: IBasePath | null;
savedObjectsClient: SavedObjectsClientContract | null;
XSRF: string | null;
application: ApplicationStart | null;
http: HttpStart | null;
security: SecurityPluginSetup | null;
@ -54,7 +53,6 @@ const cache: DependencyCache = {
autocomplete: null,
basePath: null,
savedObjectsClient: null,
XSRF: null,
application: null,
http: null,
security: null,
@ -73,7 +71,6 @@ export function setDependencyCache(deps: Partial<DependencyCache>) {
cache.autocomplete = deps.autocomplete || null;
cache.basePath = deps.basePath || null;
cache.savedObjectsClient = deps.savedObjectsClient || null;
cache.XSRF = deps.XSRF || null;
cache.application = deps.application || null;
cache.http = deps.http || null;
cache.security = deps.security || null;
@ -162,13 +159,6 @@ export function getSavedObjectsClient() {
return cache.savedObjectsClient;
}
export function getXSRF() {
if (cache.XSRF === null) {
throw new Error("xsrf hasn't been initialized");
}
return cache.XSRF;
}
export function getApplication() {
if (cache.application === null) {
throw new Error("application hasn't been initialized");

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import { npSetup, npStart } from 'ui/new_platform';
import { PluginInitializerContext } from 'src/core/public';
import { SecurityPluginSetup } from '../../../../plugins/security/public';
@ -26,8 +25,5 @@ export const setup = pluginInstance.setup(npSetup.core, {
data: npStart.plugins.data,
security: setupDependencies.security,
licensing: setupDependencies.licensing,
__LEGACY: {
XSRF: chrome.getXsrfToken(),
},
});
export const start = pluginInstance.start(npStart.core, npStart.plugins);

View file

@ -8,7 +8,7 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public';
import { MlDependencies } from './application/app';
export class MlPlugin implements Plugin<Setup, Start> {
setup(core: CoreSetup, { data, security, licensing, __LEGACY }: MlDependencies) {
setup(core: CoreSetup, { data, security, licensing }: MlDependencies) {
core.application.register({
id: 'ml',
title: 'Machine learning',
@ -21,7 +21,6 @@ export class MlPlugin implements Plugin<Setup, Start> {
onAppLeave: params.onAppLeave,
history: params.history,
data,
__LEGACY,
security,
licensing,
});

View file

@ -27,7 +27,6 @@ const setAppDependencies = (deps: AppDependencies) => {
autocomplete: deps.plugins.data.autocomplete,
docLinks: deps.core.docLinks,
basePath: legacyBasePath as any,
XSRF: deps.plugins.xsrfToken,
});
DependenciesContext = createContext<AppDependencies>(deps);
return DependenciesContext.Provider;

View file

@ -94,10 +94,9 @@ const apiFactory = (basePath: string, indicesBasePath: string, http: Http) => ({
export const useApi = () => {
const appDeps = useAppDependencies();
const basePath = appDeps.core.http.basePath.prepend('/api/transform');
const indicesBasePath = appDeps.core.http.basePath.prepend('/api');
const xsrfToken = appDeps.plugins.xsrfToken;
const http = httpFactory(xsrfToken);
const basePath = '/api/transform';
const indicesBasePath = '/api';
const http = httpFactory(appDeps.core.http);
return apiFactory(basePath, indicesBasePath, http);
};

View file

@ -4,21 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
// service for interacting with the server
import { Dictionary } from '../../../common/types/common';
export type Http = (options: Dictionary<any>) => Promise<unknown>;
export function httpFactory(xsrfToken: string) {
export function httpFactory(httpSetup: HttpSetup) {
return function http(options: Dictionary<any>) {
return new Promise((resolve, reject) => {
if (options && options.url) {
let url = '';
url = url + (options.url || '');
const headers = {
'kbn-system-request': true,
'Content-Type': 'application/json',
'kbn-version': xsrfToken,
...options.headers,
};
@ -36,9 +35,10 @@ export function httpFactory(xsrfToken: string) {
payload.body = body;
}
fetch(url, payload)
httpSetup
.fetch(url, payload)
.then(resp => {
resp.json().then(resp.ok === true ? resolve : reject);
resolve(resp);
})
.catch(resp => {
reject(resp);

View file

@ -26,7 +26,7 @@ export class Plugin {
savedObjects,
overlays,
} = core;
const { data, management, uiMetric, xsrfToken } = plugins;
const { data, management, uiMetric } = plugins;
// AppCore/AppPlugins to be passed on as React context
const appDependencies = {
@ -45,7 +45,6 @@ export class Plugin {
plugins: {
data,
management,
xsrfToken,
},
};

View file

@ -6,7 +6,6 @@
import { npStart } from 'ui/new_platform';
import chrome from 'ui/chrome';
import { docTitle } from 'ui/doc_title/doc_title';
// @ts-ignore: allow traversal to fail on x-pack build
@ -32,7 +31,7 @@ export type AppCore = Pick<
| 'overlays'
| 'notifications'
>;
export type AppPlugins = Pick<ShimPlugins, 'data' | 'management' | 'xsrfToken'>;
export type AppPlugins = Pick<ShimPlugins, 'data' | 'management'>;
export interface AppDependencies {
core: AppCore;
@ -60,7 +59,6 @@ export interface ShimPlugins extends NpPlugins {
uiMetric: {
createUiStatsReporter: typeof createUiStatsReporter;
};
xsrfToken: string;
}
export function createPublicShim(): { core: ShimCore; plugins: ShimPlugins } {
@ -88,7 +86,6 @@ export function createPublicShim(): { core: ShimCore; plugins: ShimPlugins } {
uiMetric: {
createUiStatsReporter,
},
xsrfToken: chrome.getXsrfToken(),
},
};
}